diff --git a/.github/actions/build-dotnet-project/action.yml b/.github/actions/build-dotnet-project/action.yml new file mode 100644 index 0000000..921b72c --- /dev/null +++ b/.github/actions/build-dotnet-project/action.yml @@ -0,0 +1,26 @@ +name: Build Dotnet Project +description: Hello! + +inputs: + project-dir: + description: 'Yay, project directory' + required: true + type: string + dotnet-version: + description: 'Yay, dotnet version' + required: false + type: string + default: 6.0.x + +runs: + using: "composite" + steps: + - name: Setup .NET + uses: actions/setup-dotnet@v2 + with: + dotnet-version: ${{ inputs.dotnet-version }} + + - name: Restore dependencies + run: dotnet restore ${{ inputs.project-dir }} + shell: bash + \ No newline at end of file diff --git a/.github/actions/run-tests/action.yml b/.github/actions/run-tests/action.yml new file mode 100644 index 0000000..0da0b12 --- /dev/null +++ b/.github/actions/run-tests/action.yml @@ -0,0 +1,22 @@ +name: Run Tests +description: This project run project that has an assemly with an specified Category + +inputs: + project-dir: + description: 'project directory' + required: true + category: + description: 'Type of the test (Unit or Integration)' + default: Unit + +runs: + using: "composite" + steps: + - name: Run Tests + shell: bash + continue-on-error: true + run: dotnet test ${{ inputs.project-dir }} + --filter "TestCategory=${{ inputs.category }}" + --logger:"trx;logfilename=Results-${{ inputs.category }}.xml" + --configuration release + \ No newline at end of file diff --git a/.github/workflows/api_releases.yml b/.github/workflows/api_releases.yml new file mode 100644 index 0000000..ab698eb --- /dev/null +++ b/.github/workflows/api_releases.yml @@ -0,0 +1,60 @@ +name: "API Release" + +on: + workflow_dispatch: + pull_request: + types: + - closed + branches: + - 'release/**' + +jobs: + releases: + name: Release + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Get version + run: echo "value=${BRANCH##*/}" >> $GITHUB_OUTPUT + id: version + env: + BRANCH: ${{ github.event.pull_request.base.ref }} + + - name: Create tag + uses: actions/github-script@v5 + with: + script: | + github.rest.git.createRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: 'refs/tags/api/${{ steps.version.outputs.value }}', + sha: context.sha + }) + + - name: Create release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: api/${{ steps.version.outputs.value }} + release_name: Release ${{ steps.version.outputs.value }} + body: Hello World + draft: true + prerelease: true + + # TODO: zip + #- name: Upload Release Asset + # id: upload-release-asset + # uses: actions/upload-release-asset@v1 + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # with: + # upload_url: ${{ steps.create_release.outputs.upload_url }} + # asset_path: ./my-artifact.zip + # asset_name: my-artifact.zip + # asset_content_type: application/zip \ No newline at end of file diff --git a/.github/workflows/archived/api_basic_flow.yml b/.github/workflows/archived/api_basic_flow.yml new file mode 100644 index 0000000..d334223 --- /dev/null +++ b/.github/workflows/archived/api_basic_flow.yml @@ -0,0 +1,50 @@ +name: "API Build & Tests" + +on: + push: + branches: + - 'main' + paths: + - 'src/FinancialHub/*' + - 'src/FinancialHub/**' + pull_request: + branches: + - 'main' + - 'release/**' + paths: + - 'src/FinancialHub/*' + - 'src/FinancialHub/**' + +jobs: + build: + name: Build and Test + runs-on: ubuntu-latest + + services: + sql-database: + image: mcr.microsoft.com/mssql/server:2019-latest + env: + SA_PASSWORD: "P@ssw0rd!" + ACCEPT_EULA: "Y" + ports: + - 1450:1433 + + steps: + - uses: actions/checkout@v2 + + - name: Setup .NET + uses: actions/setup-dotnet@v2 + with: + dotnet-version: 6.0.x + + - name: Restore dependencies + run: dotnet restore + working-directory: './src/FinancialHub/' + + - name: Build + run: dotnet build --no-restore + working-directory: './src/FinancialHub/' + + - name: Test + run: dotnet test --no-build --verbosity normal + working-directory: './src/FinancialHub/' diff --git a/.github/workflows/archived/api_clean_cache.yml b/.github/workflows/archived/api_clean_cache.yml new file mode 100644 index 0000000..0fe5e22 --- /dev/null +++ b/.github/workflows/archived/api_clean_cache.yml @@ -0,0 +1,34 @@ +name: Cleanup caches by branch +on: + pull_request: + types: + - closed + +jobs: + cleanup: + runs-on: ubuntu-latest + steps: + - name: Install Github Action Cache Manager + run: gh extension install actions/gh-actions-cache + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Get current merge + run: echo "value=refs/pull/${{ github.event.pull_request.number }}/merge" >> $GITHUB_OUTPUT + id: merge + + - name: Fetching list of cache key + run: | + cacheKeysForPR=$(gh actions-cache list -R ${{ github.repository }} -B ${{ steps.merge.outputs.value }} | cut -f 1 ) + set +e + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Deleting caches + run: | + for cacheKey in $cacheKeysForPR + do + gh actions-cache delete $cacheKey -R ${{ github.repository }} -B ${{ steps.merge.outputs.value }} --confirm + done + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/archived/api_workflow.yml b/.github/workflows/archived/api_workflow.yml new file mode 100644 index 0000000..c5ba7bc --- /dev/null +++ b/.github/workflows/archived/api_workflow.yml @@ -0,0 +1,113 @@ +name: "API Basic Workflow" + +on: + push: + branches: + - 'main' + paths: + - 'src/FinancialHub/*' + - 'src/FinancialHub/**' + pull_request: + branches: + - 'main' + - 'release/**' + #paths: + # - 'src/FinancialHub/*' + # - 'src/FinancialHub/**' + + +env: + project-dir: ./src/FinancialHub/ + project-tests-dir: ./src/FinancialHub/ # for now it has no distiction + project-tests-results-dir: ./src/FinancialHub/TestResult # for now it has no distiction + +permissions: + checks: write + pull-requests: write + +jobs: + tests: + name: Build & Test + runs-on: ubuntu-latest + + defaults: + run: + working-directory: ${{ env.project-dir }} + + services: + sql-database: + image: mcr.microsoft.com/mssql/server:2019-latest + env: + SA_PASSWORD: "P@ssw0rd!" + ACCEPT_EULA: "Y" + ports: + - 1450:1433 + + steps: + - name: Setup .NET + uses: actions/setup-dotnet@v2 + with: + dotnet-version: 6.0.x + + - uses: actions/checkout@v2 + + - name: Restore dependencies + run: dotnet restore + + - name: Run Tests + continue-on-error: true + run: dotnet test + --logger:"trx;logfilename=Results.xml" + --configuration release + + - name: Publish Test Results + uses: EnricoMi/publish-unit-test-result-action/composite@v2 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + action_fail: true + files: "**/TestResults/Results.xml" + + - name: Restore Dotnet tests + run: dotnet tool restore + + - name: Cache SonarCloud packages + uses: actions/cache@v1 + with: + path: ~\sonar\cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + + - name: Cache SonarCloud scanner + id: cache-sonar-scanner + uses: actions/cache@v1 + with: + path: .\.sonar\scanner + key: ${{ runner.os }}-sonar-scanner + restore-keys: ${{ runner.os }}-sonar-scanner + + - name: Begin Coverage Report + run: + dotnet tool run dotnet-sonarscanner begin + /k:"Chingling152_my-financial-hub" + /o:"chingling-152" + /d:sonar.token="${{ secrets.SONAR_TOKEN }}" + /d:sonar.host.url="https://sonarcloud.io" + /d:sonar.coverage.exclusions="**/Migrations/." + /d:sonar.cs.opencover.reportsPaths="/TestResults/**/coverage.opencover.xml" + + - name: Build Project + run: dotnet build + --configuration Release + --no-restore + + - name: Run Coverage Report + continue-on-error: true + run: dotnet test + --collect:"XPlat Code Coverage;Format=opencover" + --results-directory TestResults/ + --configuration Release + + - name: Send Coverage Report + run: + dotnet tool run dotnet-sonarscanner end + /d:sonar.token="${{ secrets.SONAR_TOKEN }}" \ No newline at end of file diff --git a/.github/workflows/branch_main_ci.yml b/.github/workflows/branch_main_ci.yml new file mode 100644 index 0000000..6013de1 --- /dev/null +++ b/.github/workflows/branch_main_ci.yml @@ -0,0 +1,28 @@ +name: "Main Tests & Analysis" + +on: + push: + branches: + - 'main' + paths: + - 'src/api/*' + - 'src/api/**' + pull_request: + branches: + - 'main' + paths: + - 'src/api/*' + - 'src/api/**' + +permissions: + checks: write + pull-requests: write + +jobs: + tests: + name: Tests + uses: ./.github/workflows/matrix_test_result.yml + secrets: inherit + with: + dir: ./src/api/ + dotnet-version: 6.0.x \ No newline at end of file diff --git a/.github/workflows/branch_release_ci.yml b/.github/workflows/branch_release_ci.yml new file mode 100644 index 0000000..13f1c63 --- /dev/null +++ b/.github/workflows/branch_release_ci.yml @@ -0,0 +1,22 @@ +name: "Release Tests & Analysis" + +on: + pull_request: + branches: + - 'release/**' + paths: + - 'src/api/*' + - 'src/api/**' + +permissions: + checks: write + pull-requests: write + +jobs: + tests: + name: Tests + uses: ./.github/workflows/matrix_test_result.yml + secrets: inherit + with: + dir: ./src/api/ + dotnet-version: 6.0.x \ No newline at end of file diff --git a/.github/workflows/matrix_test_result.yml b/.github/workflows/matrix_test_result.yml new file mode 100644 index 0000000..e21fcb1 --- /dev/null +++ b/.github/workflows/matrix_test_result.yml @@ -0,0 +1,64 @@ +name: Api Test Result +on: + workflow_call: + inputs: + dir: + type: string + required: true + dotnet-version: + type: string + default: 6.0.x + +permissions: + checks: write + pull-requests: write + +jobs: + test_results: + name: Code Tests + runs-on: ubuntu-latest + + strategy: + matrix: + project: + - auth + env: + project-dir: ${{ inputs.dir }}/${{ matrix.project }} + + services: + sql-database: + image: mcr.microsoft.com/mssql/server:2019-latest + env: + SA_PASSWORD: "P@ssw0rd!" + ACCEPT_EULA: "Y" + ports: + - 1450:1433 + + steps: + - uses: actions/checkout@v2 + + - name: Setup Project + uses: ./.github/actions/build-dotnet-project + with: + project-dir: ${{ inputs.dir }} + dotnet-version: 6.0.x + + - name: Run Unit Tests + uses: ./.github/actions/run-tests + with: + project-dir: ${{ inputs.dir }} + category: Unit + + - name: Run Integration Tests + uses: ./.github/actions/run-tests + with: + project-dir: ${{ inputs.dir }} + category: Integration + + - name: Publish Test Results + uses: EnricoMi/publish-unit-test-result-action/composite@v2 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + check_name: ${{ matrix.project }} project test results + action_fail: true + files: "./tests/${{ matrix.project }}/**/TestResults/Results-*.xml" \ No newline at end of file diff --git a/.github/workflows/test_result.yml b/.github/workflows/test_result.yml new file mode 100644 index 0000000..85c04df --- /dev/null +++ b/.github/workflows/test_result.yml @@ -0,0 +1,60 @@ +name: Api Test Result +on: + workflow_call: + inputs: + dir: + type: string + required: true + dotnet-version: + type: string + default: 6.0.x + +permissions: + checks: write + pull-requests: write + +jobs: + test_results: + name: Code Tests + runs-on: ubuntu-latest + + defaults: + run: + working-directory: ${{ inputs.dir }} + + services: + sql-database: + image: mcr.microsoft.com/mssql/server:2019-latest + env: + SA_PASSWORD: "P@ssw0rd!" + ACCEPT_EULA: "Y" + ports: + - 1450:1433 + + steps: + - uses: actions/checkout@v2 + + - name: Setup Project + uses: ./.github/actions/build-dotnet-project + with: + project-dir: ${{ inputs.dir }} + dotnet-version: ${{ inputs.dotnet-version }} + + - name: Run Unit Tests + uses: ./.github/actions/run-tests + with: + project-dir: ${{ inputs.dir }} + category: Unit + + - name: Run Integration Tests + uses: ./.github/actions/run-tests + with: + project-dir: ${{ inputs.dir }} + category: Integration + + - name: Publish Test Results + uses: EnricoMi/publish-unit-test-result-action/composite@v2 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + action_fail: true + files: "**/TestResults/Results.xml" \ No newline at end of file diff --git a/.github/workflows/tests_workflow.yml b/.github/workflows/tests_workflow.yml new file mode 100644 index 0000000..cb219f5 --- /dev/null +++ b/.github/workflows/tests_workflow.yml @@ -0,0 +1,21 @@ +name: Api Tests CI + +on: + workflow_call: + inputs: + dir: + type: string + description: "..." + required: true + dotnet-version: + type: string + description: "..." + default: 6.0.x + +jobs: + test-result: + uses: ./.github/workflows/test_result.yml + secrets: inherit + with: + dir: ${{ inputs.dir }} + dotnet-version: ${{ inputs.dotnet-version }} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/README.md b/README.md index f8af120..45f454d 100644 --- a/README.md +++ b/README.md @@ -1 +1 @@ -# my-financial-hub \ No newline at end of file +# My Financial Hub Auth Api diff --git a/src/api/.config/dotnet-tools.json b/src/api/.config/dotnet-tools.json new file mode 100644 index 0000000..ea50492 --- /dev/null +++ b/src/api/.config/dotnet-tools.json @@ -0,0 +1,18 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "6.0.1", + "commands": [ + "dotnet-ef" + ] + }, + "dotnet-sonarscanner": { + "version": "5.13.0", + "commands": [ + "dotnet-sonarscanner" + ] + } + } +} \ No newline at end of file diff --git a/src/api/.editorconfig b/src/api/.editorconfig new file mode 100644 index 0000000..5e94c65 --- /dev/null +++ b/src/api/.editorconfig @@ -0,0 +1,238 @@ +# Remova a linha abaixo se quiser herdar as configurações do .editorconfig de diretórios superiores +root = true + +# Arquivos C# +[*.cs] + +#### Opções Principais do EditorConfig #### + +# Recuo e espaçamento +indent_size = 4 +indent_style = space +tab_width = 4 + +# Preferências de nova linha +end_of_line = crlf +insert_final_newline = false + +#### Convenções de codificação .NET #### + +# Organizar Usos +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false +file_header_template = unset + +# Preferências +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Preferências de tipos BCL contra palavras-chave do idioma +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Preferências de parênteses +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Preferências de modificador +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Preferências de nível de expressão +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Preferências de campo +dotnet_style_readonly_field = true + +# Preferências de parâmetro +dotnet_code_quality_unused_parameters = all + +# Preferências de supressão +dotnet_remove_unnecessary_suppression_exclusions = none + +# Preferências de nova linha +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true + +#### Convenções de Codificação em C# #### + +# preferências de var +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = false + +# Membros aptos para expressão +csharp_style_expression_bodied_accessors = true +csharp_style_expression_bodied_constructors = false +csharp_style_expression_bodied_indexers = true +csharp_style_expression_bodied_lambdas = true +csharp_style_expression_bodied_local_functions = false +csharp_style_expression_bodied_methods = false +csharp_style_expression_bodied_operators = false +csharp_style_expression_bodied_properties = true + +# Preferências de correspondência de padrões +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_switch_expression = true + +# Preferências de verificação nula +csharp_style_conditional_delegate_call = true + +# Preferências de modificador +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async +csharp_style_prefer_readonly_struct = true +csharp_style_prefer_readonly_struct_member = true + +# Preferências do bloco de código +csharp_prefer_braces = true +csharp_prefer_simple_using_statement = true +csharp_style_namespace_declarations = block_scoped +csharp_style_prefer_method_group_conversion = true +csharp_style_prefer_primary_constructors = true +csharp_style_prefer_top_level_statements = true + +# Preferências de nível de expressão +csharp_prefer_simple_default_expression = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_local_over_anonymous_function = true +csharp_style_prefer_null_check_over_type_check = true +csharp_style_prefer_range_operator = true +csharp_style_prefer_tuple_swap = true +csharp_style_prefer_utf8_string_literals = true +csharp_style_throw_expression = true +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable + +# Preferências da diretiva 'using' +csharp_using_directive_placement = outside_namespace + +# Preferências de nova linha +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true + +#### Regras de Formatação de C# #### + +# Preferências de nova linha +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Preferências de recuo +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Preferências de espaço +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Preferências de quebra de linha +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Estilos de nomenclatura #### + +# Regras de nomenclatura + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Especificações de símbolo + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Estilos de nomenclatura + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +# Code Smells + +# CS8618: O campo não anulável precisa conter um valor não nulo ao sair do construtor. Considere declará-lo como anulável. +dotnet_diagnostic.CS8618.severity = none + +# CS8632: A anotação para tipos de referência anuláveis deve ser usada apenas em código em um contexto de anotações '#nullable'. +dotnet_diagnostic.CS8632.severity = none diff --git a/src/api/Dockerfile b/src/api/Dockerfile new file mode 100644 index 0000000..fe64382 --- /dev/null +++ b/src/api/Dockerfile @@ -0,0 +1,8 @@ +# database +FROM mcr.microsoft.com/mssql/server:2019-latest + +ENV SA_PASSWORD=P@ssw0rd! +ENV ACCEPT_EULA=Y + +EXPOSE 1450:1433 +VOLUME ../database:/var/opt/mssql/data diff --git a/src/api/FinancialHub.sln b/src/api/FinancialHub.sln new file mode 100644 index 0000000..78556da --- /dev/null +++ b/src/api/FinancialHub.sln @@ -0,0 +1,150 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31912.275 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Common", "Common", "{B00013E6-DB9F-415A-B542-6D32DA258EB2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinancialHub.Common", "common\FinancialHub.Common\FinancialHub.Common.csproj", "{C24C6373-0DB4-4ECC-A902-692267EBD922}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinancialHub.Common.Tests", "..\..\tests\common\FinancialHub.Common.Tests\FinancialHub.Common.Tests.csproj", "{785E774A-9D67-4EAB-AB71-D60EBE8144DD}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Auth", "Auth", "{A93CC4E0-F468-49C7-8429-9384A124BB2D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Application", "Application", "{698C287C-20C9-45E1-A6B2-15141C74C3E2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Domain", "Domain", "{2F11BC84-BA3C-43A0-BAB8-FC65E6E4566C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infra", "Infra", "{B7C0B348-3E2C-40BA-850D-BDA93FEA6B44}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Data", "Data", "{022A9AE7-CA3B-4DD1-934B-FBBF680312B1}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Presentation", "Presentation", "{4C3B217C-32D6-4995-BC05-08BC7490FF89}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinancialHub.Auth.Application.Tests", "..\..\tests\auth\FinancialHub.Auth.Application.Tests\FinancialHub.Auth.Application.Tests.csproj", "{CCADF7E9-2D5E-433C-8376-1C4F32BEDED4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinancialHub.Auth.Common.Tests", "..\..\tests\auth\FinancialHub.Auth.Common.Tests\FinancialHub.Auth.Common.Tests.csproj", "{37DC7DFC-2806-4E22-9A9B-9A690AD95091}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinancialHub.Auth.Infra.Data.Tests", "..\..\tests\auth\FinancialHub.Auth.Infra.Data.Tests\FinancialHub.Auth.Infra.Data.Tests.csproj", "{E9266D91-AF86-49E5-A4CA-08721FE01DB0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinancialHub.Auth.Infra.Tests", "..\..\tests\auth\FinancialHub.Auth.Infra.Tests\FinancialHub.Auth.Infra.Tests.csproj", "{21A094AE-3A26-4E37-B76E-C213C84BEAFC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinancialHub.Auth.Presentation.Tests", "..\..\tests\auth\FinancialHub.Auth.Presentation.Tests\FinancialHub.Auth.Presentation.Tests.csproj", "{B8858D6E-FEDA-45CA-A895-4FCDD45C4856}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinancialHub.Auth.IntegrationTests", "..\..\tests\auth\FinancialHub.Auth.IntegrationTests\FinancialHub.Auth.IntegrationTests.csproj", "{5CB8F10A-96A8-4854-A990-13818B034F24}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinancialHub.Auth.Presentation", "auth\FinancialHub.Auth.Presentation\FinancialHub.Auth.Presentation.csproj", "{B63EB0FB-B23B-43EB-A700-46F2F9D9CB39}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinancialHub.Auth.Application", "auth\FinancialHub.Auth.Application\FinancialHub.Auth.Application.csproj", "{84793402-7ED4-4390-B7C7-87C11475B342}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinancialHub.Auth.Domain", "auth\FinancialHub.Auth.Domain\FinancialHub.Auth.Domain.csproj", "{AAD9189B-299E-4492-9B3E-4073D177E5FF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinancialHub.Auth.Infra", "auth\FinancialHub.Auth.Infra\FinancialHub.Auth.Infra.csproj", "{8D1218F9-926A-4EAB-8C53-1A83A8AAFF8F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinancialHub.Auth.Infra.Data", "auth\FinancialHub.Auth.Infra.Data\FinancialHub.Auth.Infra.Data.csproj", "{551319E1-1415-449B-A9B7-B49AA182D6C4}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinancialHub.Auth.Resources", "auth\FinancialHub.Auth.Resources\FinancialHub.Auth.Resources.csproj", "{F24D2732-A777-4246-A5E2-633FB552B999}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinancialHub.Auth.WebApi", "auth\FinancialHub.Auth.WebApi\FinancialHub.Auth.WebApi.csproj", "{A1CE8588-9FE3-4ADB-AEB9-E34680091A78}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D36A889D-A7ED-45B4-91DD-0039FC4211FB}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C24C6373-0DB4-4ECC-A902-692267EBD922}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C24C6373-0DB4-4ECC-A902-692267EBD922}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C24C6373-0DB4-4ECC-A902-692267EBD922}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C24C6373-0DB4-4ECC-A902-692267EBD922}.Release|Any CPU.Build.0 = Release|Any CPU + {785E774A-9D67-4EAB-AB71-D60EBE8144DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {785E774A-9D67-4EAB-AB71-D60EBE8144DD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {785E774A-9D67-4EAB-AB71-D60EBE8144DD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {785E774A-9D67-4EAB-AB71-D60EBE8144DD}.Release|Any CPU.Build.0 = Release|Any CPU + {CCADF7E9-2D5E-433C-8376-1C4F32BEDED4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CCADF7E9-2D5E-433C-8376-1C4F32BEDED4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CCADF7E9-2D5E-433C-8376-1C4F32BEDED4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CCADF7E9-2D5E-433C-8376-1C4F32BEDED4}.Release|Any CPU.Build.0 = Release|Any CPU + {37DC7DFC-2806-4E22-9A9B-9A690AD95091}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {37DC7DFC-2806-4E22-9A9B-9A690AD95091}.Debug|Any CPU.Build.0 = Debug|Any CPU + {37DC7DFC-2806-4E22-9A9B-9A690AD95091}.Release|Any CPU.ActiveCfg = Release|Any CPU + {37DC7DFC-2806-4E22-9A9B-9A690AD95091}.Release|Any CPU.Build.0 = Release|Any CPU + {E9266D91-AF86-49E5-A4CA-08721FE01DB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E9266D91-AF86-49E5-A4CA-08721FE01DB0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E9266D91-AF86-49E5-A4CA-08721FE01DB0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E9266D91-AF86-49E5-A4CA-08721FE01DB0}.Release|Any CPU.Build.0 = Release|Any CPU + {21A094AE-3A26-4E37-B76E-C213C84BEAFC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {21A094AE-3A26-4E37-B76E-C213C84BEAFC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {21A094AE-3A26-4E37-B76E-C213C84BEAFC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {21A094AE-3A26-4E37-B76E-C213C84BEAFC}.Release|Any CPU.Build.0 = Release|Any CPU + {B8858D6E-FEDA-45CA-A895-4FCDD45C4856}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B8858D6E-FEDA-45CA-A895-4FCDD45C4856}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B8858D6E-FEDA-45CA-A895-4FCDD45C4856}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B8858D6E-FEDA-45CA-A895-4FCDD45C4856}.Release|Any CPU.Build.0 = Release|Any CPU + {5CB8F10A-96A8-4854-A990-13818B034F24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5CB8F10A-96A8-4854-A990-13818B034F24}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5CB8F10A-96A8-4854-A990-13818B034F24}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5CB8F10A-96A8-4854-A990-13818B034F24}.Release|Any CPU.Build.0 = Release|Any CPU + {B63EB0FB-B23B-43EB-A700-46F2F9D9CB39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B63EB0FB-B23B-43EB-A700-46F2F9D9CB39}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B63EB0FB-B23B-43EB-A700-46F2F9D9CB39}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B63EB0FB-B23B-43EB-A700-46F2F9D9CB39}.Release|Any CPU.Build.0 = Release|Any CPU + {84793402-7ED4-4390-B7C7-87C11475B342}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {84793402-7ED4-4390-B7C7-87C11475B342}.Debug|Any CPU.Build.0 = Debug|Any CPU + {84793402-7ED4-4390-B7C7-87C11475B342}.Release|Any CPU.ActiveCfg = Release|Any CPU + {84793402-7ED4-4390-B7C7-87C11475B342}.Release|Any CPU.Build.0 = Release|Any CPU + {AAD9189B-299E-4492-9B3E-4073D177E5FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AAD9189B-299E-4492-9B3E-4073D177E5FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AAD9189B-299E-4492-9B3E-4073D177E5FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AAD9189B-299E-4492-9B3E-4073D177E5FF}.Release|Any CPU.Build.0 = Release|Any CPU + {8D1218F9-926A-4EAB-8C53-1A83A8AAFF8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8D1218F9-926A-4EAB-8C53-1A83A8AAFF8F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8D1218F9-926A-4EAB-8C53-1A83A8AAFF8F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8D1218F9-926A-4EAB-8C53-1A83A8AAFF8F}.Release|Any CPU.Build.0 = Release|Any CPU + {551319E1-1415-449B-A9B7-B49AA182D6C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {551319E1-1415-449B-A9B7-B49AA182D6C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {551319E1-1415-449B-A9B7-B49AA182D6C4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {551319E1-1415-449B-A9B7-B49AA182D6C4}.Release|Any CPU.Build.0 = Release|Any CPU + {F24D2732-A777-4246-A5E2-633FB552B999}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F24D2732-A777-4246-A5E2-633FB552B999}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F24D2732-A777-4246-A5E2-633FB552B999}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F24D2732-A777-4246-A5E2-633FB552B999}.Release|Any CPU.Build.0 = Release|Any CPU + {A1CE8588-9FE3-4ADB-AEB9-E34680091A78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1CE8588-9FE3-4ADB-AEB9-E34680091A78}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1CE8588-9FE3-4ADB-AEB9-E34680091A78}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1CE8588-9FE3-4ADB-AEB9-E34680091A78}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {C24C6373-0DB4-4ECC-A902-692267EBD922} = {B00013E6-DB9F-415A-B542-6D32DA258EB2} + {785E774A-9D67-4EAB-AB71-D60EBE8144DD} = {B00013E6-DB9F-415A-B542-6D32DA258EB2} + {698C287C-20C9-45E1-A6B2-15141C74C3E2} = {A93CC4E0-F468-49C7-8429-9384A124BB2D} + {2F11BC84-BA3C-43A0-BAB8-FC65E6E4566C} = {A93CC4E0-F468-49C7-8429-9384A124BB2D} + {B7C0B348-3E2C-40BA-850D-BDA93FEA6B44} = {A93CC4E0-F468-49C7-8429-9384A124BB2D} + {022A9AE7-CA3B-4DD1-934B-FBBF680312B1} = {B7C0B348-3E2C-40BA-850D-BDA93FEA6B44} + {4C3B217C-32D6-4995-BC05-08BC7490FF89} = {A93CC4E0-F468-49C7-8429-9384A124BB2D} + {CCADF7E9-2D5E-433C-8376-1C4F32BEDED4} = {698C287C-20C9-45E1-A6B2-15141C74C3E2} + {37DC7DFC-2806-4E22-9A9B-9A690AD95091} = {2F11BC84-BA3C-43A0-BAB8-FC65E6E4566C} + {E9266D91-AF86-49E5-A4CA-08721FE01DB0} = {022A9AE7-CA3B-4DD1-934B-FBBF680312B1} + {21A094AE-3A26-4E37-B76E-C213C84BEAFC} = {B7C0B348-3E2C-40BA-850D-BDA93FEA6B44} + {B8858D6E-FEDA-45CA-A895-4FCDD45C4856} = {4C3B217C-32D6-4995-BC05-08BC7490FF89} + {5CB8F10A-96A8-4854-A990-13818B034F24} = {A93CC4E0-F468-49C7-8429-9384A124BB2D} + {B63EB0FB-B23B-43EB-A700-46F2F9D9CB39} = {4C3B217C-32D6-4995-BC05-08BC7490FF89} + {84793402-7ED4-4390-B7C7-87C11475B342} = {698C287C-20C9-45E1-A6B2-15141C74C3E2} + {AAD9189B-299E-4492-9B3E-4073D177E5FF} = {2F11BC84-BA3C-43A0-BAB8-FC65E6E4566C} + {8D1218F9-926A-4EAB-8C53-1A83A8AAFF8F} = {B7C0B348-3E2C-40BA-850D-BDA93FEA6B44} + {551319E1-1415-449B-A9B7-B49AA182D6C4} = {022A9AE7-CA3B-4DD1-934B-FBBF680312B1} + {F24D2732-A777-4246-A5E2-633FB552B999} = {2F11BC84-BA3C-43A0-BAB8-FC65E6E4566C} + {A1CE8588-9FE3-4ADB-AEB9-E34680091A78} = {A93CC4E0-F468-49C7-8429-9384A124BB2D} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {9A4C4A77-82DC-47E2-AA98-E741B329A3E8} + EndGlobalSection +EndGlobal diff --git a/src/api/auth/FinancialHub.Auth.Application/Configurations/TokenServiceSettings.cs b/src/api/auth/FinancialHub.Auth.Application/Configurations/TokenServiceSettings.cs new file mode 100644 index 0000000..bb484c7 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Application/Configurations/TokenServiceSettings.cs @@ -0,0 +1,10 @@ +namespace FinancialHub.Auth.Application.Configurations +{ + public class TokenServiceSettings + { + public string Audience { get; set ; } + public string Issuer { get; set ; } + public string SecurityKey { get; set; } + public int Expires { get; set; } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Application/Extensions/IServiceCollectionExtensions.Auth.cs b/src/api/auth/FinancialHub.Auth.Application/Extensions/IServiceCollectionExtensions.Auth.cs new file mode 100644 index 0000000..5795f5e --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Application/Extensions/IServiceCollectionExtensions.Auth.cs @@ -0,0 +1,75 @@ +using FinancialHub.Auth.Application.Configurations; +using FinancialHub.Auth.Application.Services; +using FinancialHub.Auth.Application.Validators; +using FluentValidation; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; +using System.Text; + +namespace FinancialHub.Auth.Application.Extensions +{ + public static partial class IServiceCollectionExtensions + { + public static IServiceCollection AddAuthentication(this IServiceCollection services, IConfiguration configuration) + { + services.AddSettings(configuration); + + services.AddScoped(); + + services.AddScoped, SigninModelValidator>(); + services.AddScoped, SignupModelValidator>(); + + services.AddScoped(); + services.AddScoped(); + + services.AddAuthConfiguration(configuration); + + return services; + } + + private static IServiceCollection AddAuthConfiguration(this IServiceCollection services, IConfiguration configuration) + { + var settings = configuration.GetRequiredSection("TokenServiceSettings").Get(); + services + .AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer( + options => + { + var key = Encoding.ASCII.GetBytes(settings.SecurityKey); + + options.RequireHttpsMetadata = false; + options.SaveToken = true; + options.TokenValidationParameters = new() + { + //ValidAudience = settings.Audience, + ValidateAudience = false, + + //ValidAlgorithms = new[] { SecurityAlgorithms.HmacSha256Signature }, + IssuerSigningKey = new SymmetricSecurityKey(key), + ValidateIssuerSigningKey = true, + + //ValidIssuer = settings.Issuer, + ValidateIssuer = false, + + ValidateLifetime = true, + RequireExpirationTime = true, + ClockSkew = TimeSpan.FromMinutes(settings.Expires), + }; + } + ); + return services; + } + + private static IServiceCollection AddSettings(this IServiceCollection services, IConfiguration configuration) + { + services.Configure(configuration.GetSection("TokenServiceSettings")); + return services; + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Application/Extensions/IServiceCollectionExtensions.Docs.cs b/src/api/auth/FinancialHub.Auth.Application/Extensions/IServiceCollectionExtensions.Docs.cs new file mode 100644 index 0000000..39a46a1 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Application/Extensions/IServiceCollectionExtensions.Docs.cs @@ -0,0 +1,30 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OpenApi.Models; + +namespace FinancialHub.Auth.Application.Extensions +{ + public static partial class IServiceCollectionExtensions + { + public static IEnumerable GetAuthSecuritySchemes(this IServiceCollection _) + { + return new[] + { + new OpenApiSecurityScheme + { + BearerFormat = "JWT", + Name = "JWT Authentication", + In = ParameterLocation.Header, + Type = SecuritySchemeType.Http, + Scheme = JwtBearerDefaults.AuthenticationScheme, + + Reference = new OpenApiReference + { + Id = JwtBearerDefaults.AuthenticationScheme, + Type = ReferenceType.SecurityScheme + } + } + }; + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Application/Extensions/IServiceCollectionExtensions.cs b/src/api/auth/FinancialHub.Auth.Application/Extensions/IServiceCollectionExtensions.cs new file mode 100644 index 0000000..bc48e6b --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Application/Extensions/IServiceCollectionExtensions.cs @@ -0,0 +1,45 @@ +using FinancialHub.Auth.Application.Services; +using FinancialHub.Auth.Application.Validators; +using FluentValidation; +using FluentValidation.AspNetCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using System.Diagnostics.CodeAnalysis; + +namespace FinancialHub.Auth.Application.Extensions +{ + [ExcludeFromCodeCoverage] + public static partial class IServiceCollectionExtensions + { + public static IServiceCollection AddAuthServices(this IServiceCollection services, IConfiguration configuration) + { + services.AddAuthServices(); + + services.AddAuthentication(configuration); + + services.AddAuthValidators(); + + return services; + } + + private static IServiceCollection AddAuthServices(this IServiceCollection services) + { + services.AddScoped(); + + return services; + } + + private static IServiceCollection AddAuthValidators(this IServiceCollection services) + { + services.AddFluentValidation(x => + { + x.AutomaticValidationEnabled = true; + x.DisableDataAnnotationsValidation = true; + x.ValidatorOptions.DefaultRuleLevelCascadeMode = CascadeMode.Stop; + }); + services.AddScoped, UserValidator>(); + + return services; + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Application/FinancialHub.Auth.Application.csproj b/src/api/auth/FinancialHub.Auth.Application/FinancialHub.Auth.Application.csproj new file mode 100644 index 0000000..13acf3c --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Application/FinancialHub.Auth.Application.csproj @@ -0,0 +1,28 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/api/auth/FinancialHub.Auth.Application/Services/SigninService.cs b/src/api/auth/FinancialHub.Auth.Application/Services/SigninService.cs new file mode 100644 index 0000000..a4b4014 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Application/Services/SigninService.cs @@ -0,0 +1,24 @@ +namespace FinancialHub.Auth.Application.Services +{ + public class SigninService : ISigninService + { + private readonly ITokenService tokenService; + private readonly ISigninProvider signinProvider; + + public SigninService(ITokenService tokenService, ISigninProvider signinProvider) + { + this.tokenService = tokenService; + this.signinProvider = signinProvider; + } + + public async Task> AuthenticateAsync(SigninModel login) + { + var user = await this.signinProvider.GetAccountAsync(login); + + if (user == null) + return new ServiceError(401, "Wrong e-mail or password"); + + return this.tokenService.GenerateToken(user); + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Application/Services/SignupService.cs b/src/api/auth/FinancialHub.Auth.Application/Services/SignupService.cs new file mode 100644 index 0000000..ed9d986 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Application/Services/SignupService.cs @@ -0,0 +1,27 @@ +namespace FinancialHub.Auth.Application.Services +{ + public class SignupService : ISignupService + { + private readonly ISignupProvider signupProvider; + private readonly ICredentialProvider credentialProvider; + + public SignupService(ISignupProvider signupProvider, ICredentialProvider credentialProvider) + { + this.signupProvider = signupProvider; + this.credentialProvider = credentialProvider; + } + + public async Task> CreateAccountAsync(SignupModel signup) + { + var credential = await credentialProvider.GetAsync(signup.Email); + if(credential != null) + return new ServiceError(400, "Credential already exists"); + + var createdUser = await signupProvider.CreateAccountAsync(signup); + if(createdUser == null) + return new ServiceError(400, "Failed to create user"); + + return createdUser; + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Application/Services/TokenService.cs b/src/api/auth/FinancialHub.Auth.Application/Services/TokenService.cs new file mode 100644 index 0000000..9b041c0 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Application/Services/TokenService.cs @@ -0,0 +1,57 @@ +using System.Text; +using System.Security.Claims; +using System.IdentityModel.Tokens.Jwt; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using FinancialHub.Auth.Application.Configurations; + +namespace FinancialHub.Auth.Application.Services +{ + public class TokenService : ITokenService + { + private readonly TokenServiceSettings settings; + public TokenService(IOptions settings) + { + this.settings = settings.Value; + } + + private SigningCredentials Credentials + { + get + { + var key = Encoding.ASCII.GetBytes(this.settings.SecurityKey); + var securityKey = new SymmetricSecurityKey(key); + return new SigningCredentials( + key: securityKey, + algorithm: SecurityAlgorithms.HmacSha256Signature + ); + } + } + + public TokenModel GenerateToken(UserModel user) + { + var expires = DateTime.UtcNow.AddMinutes(this.settings.Expires); + var handler = new JwtSecurityTokenHandler(); + var tokenDescriptor = new SecurityTokenDescriptor() + { + Expires = expires, + Issuer = this.settings.Issuer, + Audience = this.settings.Audience, + SigningCredentials = this.Credentials, + Subject = new ClaimsIdentity( + new[] + { + new Claim(ClaimTypes.Email, user.Email), + new Claim(JwtRegisteredClaimNames.Jti, user.Id.ToString()!), + new Claim(JwtRegisteredClaimNames.Name, user.FirstName) + } + ), + }; + + var securityToken = handler.CreateToken(tokenDescriptor); + var token = handler.WriteToken(securityToken); + + return new TokenModel(token, expires); + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Application/Services/UserService.cs b/src/api/auth/FinancialHub.Auth.Application/Services/UserService.cs new file mode 100644 index 0000000..9c7972f --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Application/Services/UserService.cs @@ -0,0 +1,43 @@ +namespace FinancialHub.Auth.Application.Services +{ + public class UserService : IUserService + { + private readonly IUserProvider provider; + + public UserService(IUserProvider provider) + { + this.provider = provider; + } + + public async Task> CreateAsync(UserModel user) + { + return await provider.CreateAsync(user); + } + + public async Task> GetAsync(Guid id) + { + var user = await provider.GetAsync(id); + + if(user == null) + { + return new ServiceError(404, "User not found"); + } + + return user; + } + + public async Task> UpdateAsync(Guid id,UserModel user) + { + var getByIdResult = await GetAsync(id); + if (getByIdResult.HasError) + { + return getByIdResult; + } + + user.Id = id; + var updatedUser = await provider.UpdateAsync(user); + + return updatedUser; + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Application/Usings.cs b/src/api/auth/FinancialHub.Auth.Application/Usings.cs new file mode 100644 index 0000000..218d136 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Application/Usings.cs @@ -0,0 +1,6 @@ +global using FinancialHub.Auth.Domain.Models; +global using FinancialHub.Auth.Domain.Interfaces.Services; +global using FinancialHub.Auth.Domain.Interfaces.Providers; + +global using FinancialHub.Common.Results; +global using FinancialHub.Common.Results.Errors; \ No newline at end of file diff --git a/src/api/auth/FinancialHub.Auth.Application/Validators/Rules/ValidatorRulesExtensions.cs b/src/api/auth/FinancialHub.Auth.Application/Validators/Rules/ValidatorRulesExtensions.cs new file mode 100644 index 0000000..2a5a3c9 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Application/Validators/Rules/ValidatorRulesExtensions.cs @@ -0,0 +1,46 @@ +using FinancialHub.Auth.Domain.Interfaces.Resources; +using FluentValidation; + +namespace FinancialHub.Auth.Application.Validators.Rules +{ + public static class ValidatorRulesExtensions + { + public static IRuleBuilderOptions ValidEmail( + this IRuleBuilderInitial builder, IErrorMessageProvider provider + ) + { + return builder + .NotEmpty() + .WithMessage(provider.Required) + .EmailAddress() + .WithMessage(provider.Invalid) + .MaximumLength(300) + .WithMessage(provider.MaxLength); + } + + public static IRuleBuilderOptions ValidName( + this IRuleBuilderInitial builder, IErrorMessageProvider provider + ) + { + return builder + .NotEmpty() + .WithMessage(provider.Required) + .MaximumLength(300) + .WithMessage(provider.MaxLength); + } + + public static IRuleBuilderOptions ValidPassword( + this IRuleBuilderInitial builder, IErrorMessageProvider provider + ) + { + return builder + .NotNull() + .NotEmpty() + .WithMessage(provider.Required) + .MinimumLength(8) + .WithMessage(provider.MinLength) + .MaximumLength(80) + .WithMessage(provider.MaxLength); + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Application/Validators/SigninModelValidator.cs b/src/api/auth/FinancialHub.Auth.Application/Validators/SigninModelValidator.cs new file mode 100644 index 0000000..c2ef4ab --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Application/Validators/SigninModelValidator.cs @@ -0,0 +1,18 @@ +using FinancialHub.Auth.Domain.Interfaces.Resources; +using FinancialHub.Auth.Application.Validators.Rules; +using FluentValidation; + +namespace FinancialHub.Auth.Application.Validators +{ + public class SigninModelValidator : AbstractValidator + { + public SigninModelValidator(IErrorMessageProvider provider) + { + RuleFor(x => x.Email) + .ValidEmail(provider); + + RuleFor(x => x.Password) + .ValidPassword(provider); + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Application/Validators/SignupModelValidator.cs b/src/api/auth/FinancialHub.Auth.Application/Validators/SignupModelValidator.cs new file mode 100644 index 0000000..70a1205 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Application/Validators/SignupModelValidator.cs @@ -0,0 +1,32 @@ +using FluentValidation; +using FinancialHub.Auth.Domain.Interfaces.Resources; +using FinancialHub.Auth.Application.Validators.Rules; + +namespace FinancialHub.Auth.Application.Validators +{ + public class SignupModelValidator : AbstractValidator + { + public SignupModelValidator(IErrorMessageProvider provider) + { + RuleFor(x => x.Email) + .ValidEmail(provider); + + RuleFor(x => x.FirstName) + .ValidName(provider); + + RuleFor(x => x.LastName) + .ValidName(provider); + + RuleFor(x => x.BirthDate) + .NotNull() + .WithMessage(provider.Required); + + RuleFor(x => x.Password) + .ValidPassword(provider); + + RuleFor(x => x.ConfirmPassword) + .Equal(x => x.Password) + .WithMessage(provider.ConfirmPassword); + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Application/Validators/UserValidator.cs b/src/api/auth/FinancialHub.Auth.Application/Validators/UserValidator.cs new file mode 100644 index 0000000..69e7498 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Application/Validators/UserValidator.cs @@ -0,0 +1,25 @@ +using FluentValidation; +using FinancialHub.Auth.Domain.Interfaces.Resources; +using FinancialHub.Auth.Application.Validators.Rules; + +namespace FinancialHub.Auth.Application.Validators +{ + public class UserValidator : AbstractValidator + { + public UserValidator(IErrorMessageProvider provider) + { + RuleFor(x => x.Email) + .ValidEmail(provider); + + RuleFor(x => x.FirstName) + .ValidName(provider); + + RuleFor(x => x.LastName) + .ValidName(provider); + + RuleFor(x => x.BirthDate) + .NotEmpty() + .WithMessage(provider.Required); + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Domain/Entities/CredentialEntity.cs b/src/api/auth/FinancialHub.Auth.Domain/Entities/CredentialEntity.cs new file mode 100644 index 0000000..5f3b7f8 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Domain/Entities/CredentialEntity.cs @@ -0,0 +1,10 @@ +namespace FinancialHub.Auth.Domain.Entities +{ + public class CredentialEntity : BaseEntity + { + public string Login { get; set; } + public string Password { get; set; } + public Guid UserId { get; set; } + public UserEntity User { get; set; } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Domain/Entities/UserEntity.cs b/src/api/auth/FinancialHub.Auth.Domain/Entities/UserEntity.cs new file mode 100644 index 0000000..fad1aec --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Domain/Entities/UserEntity.cs @@ -0,0 +1,10 @@ +namespace FinancialHub.Auth.Domain.Entities +{ + public class UserEntity : BaseEntity + { + public string FirstName { get; set; } + public string LastName { get; set; } + public DateTime? BirthDate { get; set; } + public string Email { get; set; } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Domain/FinancialHub.Auth.Domain.csproj b/src/api/auth/FinancialHub.Auth.Domain/FinancialHub.Auth.Domain.csproj new file mode 100644 index 0000000..f378188 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Domain/FinancialHub.Auth.Domain.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Helpers/IPasswordHelper.cs b/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Helpers/IPasswordHelper.cs new file mode 100644 index 0000000..f8e9559 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Helpers/IPasswordHelper.cs @@ -0,0 +1,9 @@ +namespace FinancialHub.Auth.Domain.Interfaces.Helpers +{ + //TODO: use IPasswordHasher and IPasswordValidator + public interface IPasswordHelper + { + string Encrypt(string value); + bool Verify(string password, string encryptedPassword); + } +} diff --git a/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Providers/ICredentialProvider.cs b/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Providers/ICredentialProvider.cs new file mode 100644 index 0000000..2af4218 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Providers/ICredentialProvider.cs @@ -0,0 +1,11 @@ +using FinancialHub.Auth.Domain.Models; + +namespace FinancialHub.Auth.Domain.Interfaces.Providers +{ + public interface ICredentialProvider + { + Task CreateAsync(CredentialModel signup); + Task GetAsync(string email); + Task GetAsync(CredentialModel signup); + } +} diff --git a/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Providers/ISigninProvider.cs b/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Providers/ISigninProvider.cs new file mode 100644 index 0000000..9085a26 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Providers/ISigninProvider.cs @@ -0,0 +1,9 @@ +using FinancialHub.Auth.Domain.Models; + +namespace FinancialHub.Auth.Domain.Interfaces.Providers +{ + public interface ISigninProvider + { + Task GetAccountAsync(SigninModel signin); + } +} diff --git a/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Providers/ISignupProvider.cs b/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Providers/ISignupProvider.cs new file mode 100644 index 0000000..34f3438 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Providers/ISignupProvider.cs @@ -0,0 +1,9 @@ +using FinancialHub.Auth.Domain.Models; + +namespace FinancialHub.Auth.Domain.Interfaces.Providers +{ + public interface ISignupProvider + { + Task CreateAccountAsync(SignupModel signup); + } +} diff --git a/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Providers/IUserProvider.cs b/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Providers/IUserProvider.cs new file mode 100644 index 0000000..69911ca --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Providers/IUserProvider.cs @@ -0,0 +1,11 @@ +using FinancialHub.Auth.Domain.Models; + +namespace FinancialHub.Auth.Domain.Interfaces.Providers +{ + public interface IUserProvider + { + Task GetAsync(Guid id); + Task CreateAsync(UserModel user); + Task UpdateAsync(UserModel user); + } +} diff --git a/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Repositories/ICredentialRepository.cs b/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Repositories/ICredentialRepository.cs new file mode 100644 index 0000000..e85d569 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Repositories/ICredentialRepository.cs @@ -0,0 +1,12 @@ +using FinancialHub.Auth.Domain.Entities; + +namespace FinancialHub.Auth.Domain.Interfaces.Repositories +{ + public interface ICredentialRepository + { + Task GetAsync(string username); + Task GetAsync(string username, string password); + Task> GetAsync(Guid userId); + Task CreateAsync(CredentialEntity credential); + } +} diff --git a/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Repositories/IUserRepository.cs b/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Repositories/IUserRepository.cs new file mode 100644 index 0000000..384b173 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Repositories/IUserRepository.cs @@ -0,0 +1,11 @@ +using FinancialHub.Auth.Domain.Entities; + +namespace FinancialHub.Auth.Domain.Interfaces.Repositories +{ + public interface IUserRepository + { + Task GetAsync(Guid id); + Task CreateAsync(UserEntity user); + Task UpdateAsync(UserEntity user); + } +} diff --git a/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Resources/IErrorMessageProvider.cs b/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Resources/IErrorMessageProvider.cs new file mode 100644 index 0000000..64e78f9 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Resources/IErrorMessageProvider.cs @@ -0,0 +1,11 @@ +namespace FinancialHub.Auth.Domain.Interfaces.Resources +{ + public interface IErrorMessageProvider + { + string? Required { get; } + string? Invalid { get; } + string? MinLength { get; } + string? ConfirmPassword { get; } + string? MaxLength { get; } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Services/ISigninService.cs b/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Services/ISigninService.cs new file mode 100644 index 0000000..6df6fae --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Services/ISigninService.cs @@ -0,0 +1,9 @@ +using FinancialHub.Auth.Domain.Models; + +namespace FinancialHub.Auth.Domain.Interfaces.Services +{ + public interface ISigninService + { + Task> AuthenticateAsync(SigninModel login); + } +} diff --git a/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Services/ISignupService.cs b/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Services/ISignupService.cs new file mode 100644 index 0000000..62add36 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Services/ISignupService.cs @@ -0,0 +1,9 @@ +using FinancialHub.Auth.Domain.Models; + +namespace FinancialHub.Auth.Domain.Interfaces.Services +{ + public interface ISignupService + { + Task> CreateAccountAsync(SignupModel signup); + } +} diff --git a/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Services/ITokenService.cs b/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Services/ITokenService.cs new file mode 100644 index 0000000..658f0af --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Services/ITokenService.cs @@ -0,0 +1,9 @@ +using FinancialHub.Auth.Domain.Models; + +namespace FinancialHub.Auth.Domain.Interfaces.Services +{ + public interface ITokenService + { + TokenModel GenerateToken(UserModel user); + } +} diff --git a/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Services/IUserService.cs b/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Services/IUserService.cs new file mode 100644 index 0000000..6ad7d7a --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Domain/Interfaces/Services/IUserService.cs @@ -0,0 +1,11 @@ +using FinancialHub.Auth.Domain.Models; + +namespace FinancialHub.Auth.Domain.Interfaces.Services +{ + public interface IUserService + { + Task> GetAsync(Guid id); + Task> CreateAsync(UserModel user); + Task> UpdateAsync(Guid id, UserModel user); + } +} diff --git a/src/api/auth/FinancialHub.Auth.Domain/Models/CredentialModel.cs b/src/api/auth/FinancialHub.Auth.Domain/Models/CredentialModel.cs new file mode 100644 index 0000000..f67eafd --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Domain/Models/CredentialModel.cs @@ -0,0 +1,9 @@ +namespace FinancialHub.Auth.Domain.Models +{ + public class CredentialModel + { + public string Login { get; set; } + public string Password { get; set; } + public Guid UserId { get; set; } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Domain/Models/SigninModel.cs b/src/api/auth/FinancialHub.Auth.Domain/Models/SigninModel.cs new file mode 100644 index 0000000..6510a37 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Domain/Models/SigninModel.cs @@ -0,0 +1,8 @@ +namespace FinancialHub.Auth.Domain.Models +{ + public class SigninModel + { + public string Email { get; set; } + public string Password { get; set; } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Domain/Models/SignupModel.cs b/src/api/auth/FinancialHub.Auth.Domain/Models/SignupModel.cs new file mode 100644 index 0000000..d099aa0 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Domain/Models/SignupModel.cs @@ -0,0 +1,12 @@ +namespace FinancialHub.Auth.Domain.Models +{ + public class SignupModel + { + public string FirstName { get; set; } + public string LastName { get; set; } + public DateTime? BirthDate { get; set; } + public string Email { get; set; } + public string Password { get; set; } + public string ConfirmPassword { get; set; } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Domain/Models/TokenModel.cs b/src/api/auth/FinancialHub.Auth.Domain/Models/TokenModel.cs new file mode 100644 index 0000000..f31e40c --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Domain/Models/TokenModel.cs @@ -0,0 +1,14 @@ +namespace FinancialHub.Auth.Domain.Models +{ + public class TokenModel + { + public string Token { get; } + public DateTime ExpiresIn { get; } + + public TokenModel(string token, DateTime expiresIn) + { + Token = token; + ExpiresIn = expiresIn; + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Domain/Models/UserModel.cs b/src/api/auth/FinancialHub.Auth.Domain/Models/UserModel.cs new file mode 100644 index 0000000..5ebb9db --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Domain/Models/UserModel.cs @@ -0,0 +1,10 @@ +namespace FinancialHub.Auth.Domain.Models +{ + public class UserModel : BaseModel + { + public string FirstName { get; set; } + public string LastName { get; set; } + public DateTime? BirthDate { get; set; } + public string Email { get; set; } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Domain/Usings.cs b/src/api/auth/FinancialHub.Auth.Domain/Usings.cs new file mode 100644 index 0000000..8c5e002 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Domain/Usings.cs @@ -0,0 +1,3 @@ +global using FinancialHub.Common.Entities; +global using FinancialHub.Common.Results; +global using FinancialHub.Common.Models; \ No newline at end of file diff --git a/src/api/auth/FinancialHub.Auth.Infra.Data/Contexts/FinancialHubAuthContext.cs b/src/api/auth/FinancialHub.Auth.Infra.Data/Contexts/FinancialHubAuthContext.cs new file mode 100644 index 0000000..7a0c3bb --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Infra.Data/Contexts/FinancialHubAuthContext.cs @@ -0,0 +1,16 @@ +namespace FinancialHub.Auth.Infra.Data.Contexts +{ + public class FinancialHubAuthContext : DbContext + { + public FinancialHubAuthContext(DbContextOptions options) : base(options){} + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.ApplyConfigurationsFromAssembly(typeof(FinancialHubAuthContext).Assembly); + base.OnModelCreating(modelBuilder); + } + + public DbSet Users { get; set; } + public DbSet Credentials { get; set; } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Infra.Data/Extensions/IServiceCollectionExtensions.cs b/src/api/auth/FinancialHub.Auth.Infra.Data/Extensions/IServiceCollectionExtensions.cs new file mode 100644 index 0000000..c10349c --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Infra.Data/Extensions/IServiceCollectionExtensions.cs @@ -0,0 +1,34 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using FinancialHub.Auth.Infra.Data.Repositories; +using System.Diagnostics.CodeAnalysis; + +namespace FinancialHub.Auth.Infra.Data.Extensions +{ + [ExcludeFromCodeCoverage] + public static class IServiceCollectionExtensions + { + private static IServiceCollection AddAuthDatabase(this IServiceCollection services, IConfiguration configuration) + { + services.AddDbContext( + provider => + provider.UseSqlServer( + configuration.GetConnectionString("auth"), + x => x.MigrationsHistoryTable("auth_migrations") + ) + ); + + return services; + } + + public static IServiceCollection AddAuthRepositories(this IServiceCollection services, IConfiguration configuration) + { + services.AddAuthDatabase(configuration); + + services.AddScoped(); + services.AddScoped(); + + return services; + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Infra.Data/FinancialHub.Auth.Infra.Data.csproj b/src/api/auth/FinancialHub.Auth.Infra.Data/FinancialHub.Auth.Infra.Data.csproj new file mode 100644 index 0000000..c53923b --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Infra.Data/FinancialHub.Auth.Infra.Data.csproj @@ -0,0 +1,23 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + + + + diff --git a/src/api/auth/FinancialHub.Auth.Infra.Data/Mapping/UserCredentialEntityMapping.cs b/src/api/auth/FinancialHub.Auth.Infra.Data/Mapping/UserCredentialEntityMapping.cs new file mode 100644 index 0000000..8dec29d --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Infra.Data/Mapping/UserCredentialEntityMapping.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace FinancialHub.Auth.Infra.Data.Mapping +{ + internal class UserCredentialEntityMapping : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.Property(x => x.Login) + .HasColumnName("login") + .HasMaxLength(200) + .IsRequired(); + + builder.Property(x => x.Password) + .HasColumnName("password") + .HasMaxLength(300) + .IsRequired(); + + builder.Property(x => x.UserId) + .HasColumnName("user_id") + .IsRequired(); + builder.HasOne(x => x.User); + + builder.ToTable("user_credential"); + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Infra.Data/Mapping/UserEntityMapping.cs b/src/api/auth/FinancialHub.Auth.Infra.Data/Mapping/UserEntityMapping.cs new file mode 100644 index 0000000..9115841 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Infra.Data/Mapping/UserEntityMapping.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace FinancialHub.Auth.Infra.Data.Mapping +{ + internal class UserEntityMapping : IEntityTypeConfiguration + { + public void Configure(EntityTypeBuilder builder) + { + builder.Property(t => t.FirstName) + .HasColumnName("first_name") + .HasMaxLength(300) + .IsRequired(); + + builder.Property(t => t.LastName) + .HasColumnName("last_name") + .HasMaxLength(300); + + builder.Property(t => t.BirthDate) + .HasColumnName("birth_name"); + + builder.Property(t => t.Email) + .HasColumnName("email") + .HasMaxLength(300) + .IsRequired(); + + builder.ToTable("users"); + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Infra.Data/Migrations/20230503220050_Adds_User_Table.Designer.cs b/src/api/auth/FinancialHub.Auth.Infra.Data/Migrations/20230503220050_Adds_User_Table.Designer.cs new file mode 100644 index 0000000..4fde734 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Infra.Data/Migrations/20230503220050_Adds_User_Table.Designer.cs @@ -0,0 +1,73 @@ +// +using System; +using System.Diagnostics.CodeAnalysis; +using FinancialHub.Auth.Infra.Data.Contexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace FinancialHub.Auth.Infra.Data.Migrations +{ + [DbContext(typeof(FinancialHubAuthContext))] + [Migration("20230503220050_Adds_User_Table")] + [ExcludeFromCodeCoverage] + partial class Adds_User_Table + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("FinancialHub.Auth.Domain.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("BirthDate") + .HasColumnType("datetime2") + .HasColumnName("birth_name"); + + b.Property("CreationTime") + .HasColumnType("datetimeoffset") + .HasColumnName("creation_time"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)") + .HasColumnName("email"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)") + .HasColumnName("first_name"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)") + .HasColumnName("last_name"); + + b.Property("UpdateTime") + .HasColumnType("datetimeoffset") + .HasColumnName("update_time"); + + b.HasKey("Id"); + + b.ToTable("users", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Infra.Data/Migrations/20230503220050_Adds_User_Table.cs b/src/api/auth/FinancialHub.Auth.Infra.Data/Migrations/20230503220050_Adds_User_Table.cs new file mode 100644 index 0000000..c157839 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Infra.Data/Migrations/20230503220050_Adds_User_Table.cs @@ -0,0 +1,36 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace FinancialHub.Auth.Infra.Data.Migrations +{ + public partial class Adds_User_Table : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "users", + columns: table => new + { + id = table.Column(type: "uniqueidentifier", nullable: false), + first_name = table.Column(type: "nvarchar(300)", maxLength: 300, nullable: false), + last_name = table.Column(type: "nvarchar(300)", maxLength: 300, nullable: false), + birth_name = table.Column(type: "datetime2", nullable: true), + email = table.Column(type: "nvarchar(300)", maxLength: 300, nullable: false), + creation_time = table.Column(type: "datetimeoffset", nullable: true), + update_time = table.Column(type: "datetimeoffset", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_users", x => x.id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "users"); + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Infra.Data/Migrations/20230615215715_Add_Credential_Table.Designer.cs b/src/api/auth/FinancialHub.Auth.Infra.Data/Migrations/20230615215715_Add_Credential_Table.Designer.cs new file mode 100644 index 0000000..e729730 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Infra.Data/Migrations/20230615215715_Add_Credential_Table.Designer.cs @@ -0,0 +1,122 @@ +// +using System; +using System.Diagnostics.CodeAnalysis; +using FinancialHub.Auth.Infra.Data.Contexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace FinancialHub.Auth.Infra.Data.Migrations +{ + [DbContext(typeof(FinancialHubAuthContext))] + [Migration("20230615215715_Add_Credential_Table")] + [ExcludeFromCodeCoverage] + partial class Add_Credential_Table + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("FinancialHub.Auth.Domain.Entities.CredentialEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreationTime") + .HasColumnType("datetimeoffset") + .HasColumnName("creation_time"); + + b.Property("Login") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("login"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)") + .HasColumnName("password"); + + b.Property("UpdateTime") + .HasColumnType("datetimeoffset") + .HasColumnName("update_time"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("user_credential", (string)null); + }); + + modelBuilder.Entity("FinancialHub.Auth.Domain.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("BirthDate") + .HasColumnType("datetime2") + .HasColumnName("birth_name"); + + b.Property("CreationTime") + .HasColumnType("datetimeoffset") + .HasColumnName("creation_time"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)") + .HasColumnName("email"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)") + .HasColumnName("first_name"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)") + .HasColumnName("last_name"); + + b.Property("UpdateTime") + .HasColumnType("datetimeoffset") + .HasColumnName("update_time"); + + b.HasKey("Id"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("FinancialHub.Auth.Domain.Entities.CredentialEntity", b => + { + b.HasOne("FinancialHub.Auth.Domain.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Infra.Data/Migrations/20230615215715_Add_Credential_Table.cs b/src/api/auth/FinancialHub.Auth.Infra.Data/Migrations/20230615215715_Add_Credential_Table.cs new file mode 100644 index 0000000..fd702aa --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Infra.Data/Migrations/20230615215715_Add_Credential_Table.cs @@ -0,0 +1,46 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace FinancialHub.Auth.Infra.Data.Migrations +{ + public partial class Add_Credential_Table : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "user_credential", + columns: table => new + { + id = table.Column(type: "uniqueidentifier", nullable: false), + login = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + password = table.Column(type: "nvarchar(300)", maxLength: 300, nullable: false), + user_id = table.Column(type: "uniqueidentifier", nullable: false), + creation_time = table.Column(type: "datetimeoffset", nullable: true), + update_time = table.Column(type: "datetimeoffset", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_user_credential", x => x.id); + table.ForeignKey( + name: "FK_user_credential_users_user_id", + column: x => x.user_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_user_credential_user_id", + table: "user_credential", + column: "user_id"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "user_credential"); + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Infra.Data/Migrations/FinancialHubAuthContextModelSnapshot.cs b/src/api/auth/FinancialHub.Auth.Infra.Data/Migrations/FinancialHubAuthContextModelSnapshot.cs new file mode 100644 index 0000000..beeda6a --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Infra.Data/Migrations/FinancialHubAuthContextModelSnapshot.cs @@ -0,0 +1,120 @@ +// +using System; +using System.Diagnostics.CodeAnalysis; +using FinancialHub.Auth.Infra.Data.Contexts; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace FinancialHub.Auth.Infra.Data.Migrations +{ + [ExcludeFromCodeCoverage] + [DbContext(typeof(FinancialHubAuthContext))] + partial class FinancialHubAuthContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("FinancialHub.Auth.Domain.Entities.CredentialEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("CreationTime") + .HasColumnType("datetimeoffset") + .HasColumnName("creation_time"); + + b.Property("Login") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)") + .HasColumnName("login"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)") + .HasColumnName("password"); + + b.Property("UpdateTime") + .HasColumnType("datetimeoffset") + .HasColumnName("update_time"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("user_credential", (string)null); + }); + + modelBuilder.Entity("FinancialHub.Auth.Domain.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasColumnName("id"); + + b.Property("BirthDate") + .HasColumnType("datetime2") + .HasColumnName("birth_name"); + + b.Property("CreationTime") + .HasColumnType("datetimeoffset") + .HasColumnName("creation_time"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)") + .HasColumnName("email"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)") + .HasColumnName("first_name"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(300) + .HasColumnType("nvarchar(300)") + .HasColumnName("last_name"); + + b.Property("UpdateTime") + .HasColumnType("datetimeoffset") + .HasColumnName("update_time"); + + b.HasKey("Id"); + + b.ToTable("users", (string)null); + }); + + modelBuilder.Entity("FinancialHub.Auth.Domain.Entities.CredentialEntity", b => + { + b.HasOne("FinancialHub.Auth.Domain.Entities.UserEntity", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Infra.Data/Repositories/CredentialRepository.cs b/src/api/auth/FinancialHub.Auth.Infra.Data/Repositories/CredentialRepository.cs new file mode 100644 index 0000000..9eebe9b --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Infra.Data/Repositories/CredentialRepository.cs @@ -0,0 +1,39 @@ +namespace FinancialHub.Auth.Infra.Data.Repositories +{ + public class CredentialRepository : ICredentialRepository + { + private readonly FinancialHubAuthContext context; + + public CredentialRepository(FinancialHubAuthContext context) + { + this.context = context; + } + + public async Task CreateAsync(CredentialEntity credential) + { + credential.Id = null; + credential.CreationTime = DateTimeOffset.Now; + credential.UpdateTime = DateTimeOffset.Now; + credential.User = default!; + + var entry = await this.context.Credentials.AddAsync(credential); + await this.context.SaveChangesAsync(); + return entry.Entity; + } + + public async Task GetAsync(string username) + { + return await this.context.Credentials.FirstOrDefaultAsync(x => x.Login == username); + } + + public async Task GetAsync(string username, string password) + { + return await this.context.Credentials.FirstOrDefaultAsync(x => x.Login == username && x.Password == password); + } + + public async Task> GetAsync(Guid userId) + { + return await this.context.Credentials.Where(x => x.UserId == userId).ToArrayAsync(); + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Infra.Data/Repositories/UserRepository.cs b/src/api/auth/FinancialHub.Auth.Infra.Data/Repositories/UserRepository.cs new file mode 100644 index 0000000..321e756 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Infra.Data/Repositories/UserRepository.cs @@ -0,0 +1,43 @@ +namespace FinancialHub.Auth.Infra.Data.Repositories +{ + public class UserRepository : IUserRepository + { + private readonly FinancialHubAuthContext context; + + public UserRepository(FinancialHubAuthContext context) + { + this.context = context; + } + + public async Task CreateAsync(UserEntity user) + { + user.Id = null; + user.CreationTime = DateTimeOffset.Now; + user.UpdateTime = DateTimeOffset.Now; + + var entry = this.context.Users.Add(user); + await this.context.SaveChangesAsync(); + + return entry.Entity; + } + + public async Task GetAsync(Guid id) + { + return await this.context.Users + .AsNoTracking() + .FirstOrDefaultAsync(x => x.Id == id); + } + + public async Task UpdateAsync(UserEntity user) + { + user.UpdateTime = DateTimeOffset.Now; + + var res = context.Users.Update(user); + this.context.Entry(res.Entity).Property(x => x.CreationTime).IsModified = false; + + await context.SaveChangesAsync(); + + return res.Entity; + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Infra.Data/Usings.cs b/src/api/auth/FinancialHub.Auth.Infra.Data/Usings.cs new file mode 100644 index 0000000..cb594b8 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Infra.Data/Usings.cs @@ -0,0 +1,4 @@ +global using Microsoft.EntityFrameworkCore; +global using FinancialHub.Auth.Domain.Entities; +global using FinancialHub.Auth.Domain.Interfaces.Repositories; +global using FinancialHub.Auth.Infra.Data.Contexts; \ No newline at end of file diff --git a/src/api/auth/FinancialHub.Auth.Infra/Extensions/IServiceCollectionExtensions.cs b/src/api/auth/FinancialHub.Auth.Infra/Extensions/IServiceCollectionExtensions.cs new file mode 100644 index 0000000..3e5b504 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Infra/Extensions/IServiceCollectionExtensions.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using FinancialHub.Auth.Domain.Interfaces.Helpers; +using FinancialHub.Auth.Infra.Mappers; +using FinancialHub.Auth.Infra.Providers; +using FinancialHub.Auth.Infra.Helpers; + +namespace FinancialHub.Auth.Infra.Extensions +{ + public static class IServiceCollectionExtensions + { + public static IServiceCollection AddAuthProviders(this IServiceCollection services, IConfiguration configuration) + { + services.AddScoped(); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddSingleton(provider => + new MapperConfiguration(cfg => + { + cfg.AddProfile(new FinancialHubAuthProviderProfile()); + using var scope = provider.CreateScope(); + cfg.AddProfile(new FinancialHubAuthCredentialProfile(scope.ServiceProvider.GetService()!)); + } + ).CreateMapper()); + + return services; + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Infra/FinancialHub.Auth.Infra.csproj b/src/api/auth/FinancialHub.Auth.Infra/FinancialHub.Auth.Infra.csproj new file mode 100644 index 0000000..0b99a09 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Infra/FinancialHub.Auth.Infra.csproj @@ -0,0 +1,19 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + diff --git a/src/api/auth/FinancialHub.Auth.Infra/Helpers/PasswordHelper.cs b/src/api/auth/FinancialHub.Auth.Infra/Helpers/PasswordHelper.cs new file mode 100644 index 0000000..0db4253 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Infra/Helpers/PasswordHelper.cs @@ -0,0 +1,26 @@ +using System.Text; +using System.Security.Cryptography; +using FinancialHub.Auth.Domain.Interfaces.Helpers; + +namespace FinancialHub.Auth.Infra.Helpers +{ + public class PasswordHelper : IPasswordHelper + { + public string Encrypt(string value) + { + var dataArray = Encoding.UTF8.GetBytes(value); + + var bytes = SHA256.Create().ComputeHash(dataArray); + + return new string( + Convert.ToBase64String(bytes).Reverse().ToArray() + ); + } + + public bool Verify(string password, string encryptedPassword) + { + var encrypted = Encrypt(password); + return encrypted == password; + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Infra/Mappers/FinancialHubAuthCredentialProfile.cs b/src/api/auth/FinancialHub.Auth.Infra/Mappers/FinancialHubAuthCredentialProfile.cs new file mode 100644 index 0000000..9844e44 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Infra/Mappers/FinancialHubAuthCredentialProfile.cs @@ -0,0 +1,21 @@ +using FinancialHub.Auth.Domain.Interfaces.Helpers; + +namespace FinancialHub.Auth.Infra.Mappers +{ + public class FinancialHubAuthCredentialProfile : Profile + { + public FinancialHubAuthCredentialProfile(IPasswordHelper helper) : base() + { + this.CreateMap().ReverseMap(); + + this.CreateMap(); + this.CreateMap() + .ForMember(x => x.Login, y => y.MapFrom(z => z.Email)) + .ForMember(x => x.Password, y => y.MapFrom(z => helper.Encrypt(z.Password))); + + this.CreateMap() + .ForMember(x => x.Login, y => y.MapFrom(z => z.Email)) + .ForMember(x => x.Password, y => y.MapFrom(z => helper.Encrypt(z.Password))); + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Infra/Mappers/FinancialHubAuthProfile.cs b/src/api/auth/FinancialHub.Auth.Infra/Mappers/FinancialHubAuthProfile.cs new file mode 100644 index 0000000..cbb28f4 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Infra/Mappers/FinancialHubAuthProfile.cs @@ -0,0 +1,10 @@ +namespace FinancialHub.Auth.Infra.Mappers +{ + public class FinancialHubAuthProviderProfile : Profile + { + public FinancialHubAuthProviderProfile() : base() + { + this.CreateMap().ReverseMap(); + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Infra/Providers/CredentialProvider.cs b/src/api/auth/FinancialHub.Auth.Infra/Providers/CredentialProvider.cs new file mode 100644 index 0000000..b48070f --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Infra/Providers/CredentialProvider.cs @@ -0,0 +1,45 @@ +namespace FinancialHub.Auth.Infra.Providers +{ + public class CredentialProvider : ICredentialProvider + { + private readonly IMapper mapper; + private readonly ICredentialRepository credentialRepository; + + public CredentialProvider(ICredentialRepository credentialRepository, IMapper mapper) + { + this.credentialRepository = credentialRepository; + this.mapper = mapper; + } + + public async Task CreateAsync(CredentialModel signup) + { + var entity = this.mapper.Map(signup); + + var result = await this.credentialRepository.CreateAsync(entity); + + return this.mapper.Map(result); + } + + public async Task GetAsync(string email) + { + var result = await this.credentialRepository.GetAsync(email); + if (result == null) + { + return null; + } + + return this.mapper.Map(result); + } + + public async Task GetAsync(CredentialModel signup) + { + var result = await this.credentialRepository.GetAsync(signup.Login, signup.Password); + if (result == null) + { + return null; + } + + return this.mapper.Map(result); + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Infra/Providers/SigninProvider.cs b/src/api/auth/FinancialHub.Auth.Infra/Providers/SigninProvider.cs new file mode 100644 index 0000000..8f26a42 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Infra/Providers/SigninProvider.cs @@ -0,0 +1,29 @@ +namespace FinancialHub.Auth.Infra.Providers +{ + public class SigninProvider : ISigninProvider + { + private readonly IUserProvider userProvider; + private readonly ICredentialProvider credentialProvider; + private readonly IMapper mapper; + + public SigninProvider(ICredentialProvider credentialProvider, IUserProvider userProvider, IMapper mapper) + { + this.credentialProvider = credentialProvider; + this.userProvider = userProvider; + this.mapper = mapper; + } + + public async Task GetAccountAsync(SigninModel signin) + { + var credential = mapper.Map(signin); + var existingCredential = await credentialProvider.GetAsync(credential); + + if(existingCredential == null) + { + return null; + } + + return await userProvider.GetAsync(existingCredential.UserId); + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Infra/Providers/SignupProvider.cs b/src/api/auth/FinancialHub.Auth.Infra/Providers/SignupProvider.cs new file mode 100644 index 0000000..82ebeb7 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Infra/Providers/SignupProvider.cs @@ -0,0 +1,38 @@ +namespace FinancialHub.Auth.Infra.Providers +{ + public class SignupProvider : ISignupProvider + { + private readonly ICredentialProvider credentialProvider; + private readonly IUserProvider userProvider; + private readonly IMapper mapper; + + public SignupProvider( + ICredentialProvider credentialProvider, + IUserProvider userProvider, + IMapper mapper + ) + { + this.credentialProvider = credentialProvider; + this.userProvider = userProvider; + this.mapper = mapper; + } + + public async Task CreateAccountAsync(SignupModel signup) + { + var createdUser = await userProvider.CreateAsync( + mapper.Map(signup) + ); + if(createdUser == null) + return null; + + var userCredential = mapper.Map(signup); + userCredential.UserId = createdUser.Id.GetValueOrDefault(); + + var createdCredential = await credentialProvider.CreateAsync(userCredential); + if (createdCredential == null) + return null; + + return createdUser; + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Infra/Providers/UserProvider.cs b/src/api/auth/FinancialHub.Auth.Infra/Providers/UserProvider.cs new file mode 100644 index 0000000..1793487 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Infra/Providers/UserProvider.cs @@ -0,0 +1,43 @@ +namespace FinancialHub.Auth.Infra.Providers +{ + public class UserProvider : IUserProvider + { + private readonly IUserRepository repository; + private readonly IMapper mapper; + + public UserProvider(IUserRepository repository, IMapper mapper) + { + this.repository = repository; + this.mapper = mapper; + } + + public async Task CreateAsync(UserModel user) + { + var entity = this.mapper.Map(user); + + var createdEntity = await this.repository.CreateAsync(entity); + + return this.mapper.Map(createdEntity); + } + + public async Task GetAsync(Guid id) + { + var user = await this.repository.GetAsync(id); + if(user == null) + { + return null; + } + + return this.mapper.Map(user); + } + + public async Task UpdateAsync(UserModel user) + { + var entity = this.mapper.Map(user); + + var updatedEntity = await this.repository.UpdateAsync(entity); + + return this.mapper.Map(updatedEntity); + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Infra/Usings.cs b/src/api/auth/FinancialHub.Auth.Infra/Usings.cs new file mode 100644 index 0000000..e7a288c --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Infra/Usings.cs @@ -0,0 +1,5 @@ +global using AutoMapper; +global using FinancialHub.Auth.Domain.Models; +global using FinancialHub.Auth.Domain.Entities; +global using FinancialHub.Auth.Domain.Interfaces.Providers; +global using FinancialHub.Auth.Domain.Interfaces.Repositories; \ No newline at end of file diff --git a/src/api/auth/FinancialHub.Auth.Presentation/.editorconfig b/src/api/auth/FinancialHub.Auth.Presentation/.editorconfig new file mode 100644 index 0000000..6377fa0 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Presentation/.editorconfig @@ -0,0 +1,7 @@ +[*.cs] + +# CS8618: O campo não anulável precisa conter um valor não nulo ao sair do construtor. Considere declará-lo como anulável. +dotnet_diagnostic.CS8618.severity = none + +# CS1591: O comentário XML ausente não foi encontrado para o tipo ou membro visível publicamente +dotnet_diagnostic.CS1591.severity = none diff --git a/src/api/auth/FinancialHub.Auth.Presentation/Controllers/SigninController.cs b/src/api/auth/FinancialHub.Auth.Presentation/Controllers/SigninController.cs new file mode 100644 index 0000000..7f198dc --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Presentation/Controllers/SigninController.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Authorization; + +namespace FinancialHub.Auth.Presentation.Controllers +{ + [AllowAnonymous] + [ApiController] + [Route("[controller]")] + [Produces("application/json")] + public class SigninController : ControllerBase + { + private readonly ISigninService signinService; + + public SigninController(ISigninService signinService) + { + this.signinService = signinService; + } + + /// + /// Makes an Attempt to login an user + /// + /// An user credential to login + /// User Token after a successful login + /// Login fields Invalids + /// Email or Password Invalid + [HttpPost] + [ProducesResponseType(typeof(ItemResponse), 200)] + [ProducesResponseType(typeof(ValidationErrorResponse), 400)] + [ProducesResponseType(typeof(ValidationErrorResponse), 401)] + public async Task SigninAsync([FromBody]SigninModel login) + { + var tokenResult = await this.signinService.AuthenticateAsync(login); + + if (tokenResult.HasError) + { + return Unauthorized( + new ValidationErrorResponse(tokenResult.Error.Message) + ); + } + + return Ok( + new ItemResponse(tokenResult.Data) + ); + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Presentation/Controllers/SignupController.cs b/src/api/auth/FinancialHub.Auth.Presentation/Controllers/SignupController.cs new file mode 100644 index 0000000..864266a --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Presentation/Controllers/SignupController.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNetCore.Authorization; + +namespace FinancialHub.Auth.Presentation.Controllers +{ + [AllowAnonymous] + [ApiController] + [Route("[controller]")] + [Produces("application/json")] + public class SignupController : ControllerBase + { + private readonly ISignupService signupService; + + public SignupController(ISignupService signupService) + { + this.signupService = signupService; + } + + /// + /// Creates an User and a Credential + /// + /// User and Credential informations + /// User and Credential Successfully created + /// User or Credential Validation error + [HttpPost] + [ProducesResponseType(typeof(ItemResponse), 200)] + [ProducesResponseType(typeof(ValidationErrorResponse), 400)] + public async Task SignupAsync([FromBody] SignupModel signup) + { + var result = await signupService.CreateAccountAsync(signup); + + if(result.HasError) + { + return BadRequest( + error: new ValidationErrorResponse(result.Error.Message) + ); + } + + return Ok(new ItemResponse(result.Data)); + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Presentation/Controllers/UsersController.cs b/src/api/auth/FinancialHub.Auth.Presentation/Controllers/UsersController.cs new file mode 100644 index 0000000..de545e4 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Presentation/Controllers/UsersController.cs @@ -0,0 +1,106 @@ +using Microsoft.AspNetCore.Authorization; + +namespace FinancialHub.Auth.Presentation.Controllers +{ + [Authorize] + [ApiController] + [Route("[controller]")] + [Produces("application/json")] + public class UsersController : ControllerBase + { + private readonly IUserService service; + + public UsersController(IUserService service) + { + this.service = service; + } + + /// + /// Gets an user by id + /// + /// id of the User + /// Existing user response + /// Non-Existing user response + [HttpGet("{id}")] + [ProducesResponseType(typeof(ItemResponse), 200)] + [ProducesResponseType(typeof(NotFoundErrorResponse), 404)] + [ProducesResponseType(typeof(ValidationErrorResponse), 401)] + public async Task GetUserAsync([FromRoute]Guid id) + { + var userResult = await service.GetAsync(id); + + if(userResult.HasError) + { + return StatusCode( + userResult.Error.Code, + new NotFoundErrorResponse(userResult.Error.Message) + ); + } + + return Ok( + new ItemResponse(userResult.Data) + ); + } + + /// + /// Creates an user + /// + /// User data to be saved + /// Successful user creation + /// Failed user creation + [HttpPost] + [Obsolete("removed : use /sign-up")] + [ProducesResponseType(typeof(SaveResponse), 200)] + [ProducesResponseType(typeof(ValidationErrorResponse), 400)] + [ProducesResponseType(typeof(ValidationErrorResponse), 401)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Info Code Smell", "S1133:Deprecated code should be removed", Justification = "In Progress")] + public async Task CreateUserAsync([FromBody] UserModel user) + { + var userResult = await service.CreateAsync(user); + + if (userResult.HasError) + { + return StatusCode( + userResult.Error.Code, + new ValidationErrorResponse(userResult.Error.Message) + ); + } + + return Ok(new SaveResponse(userResult.Data)); + } + + /// + /// Updates an user + /// + /// id of the User + /// User data to be updated (id field is ignored) + /// Successful update + /// Fail update + /// User not found response + [HttpPatch("{id}")] + [ProducesResponseType(typeof(SaveResponse), 200)] + [ProducesResponseType(typeof(ValidationErrorResponse), 400)] + [ProducesResponseType(typeof(NotFoundErrorResponse), 404)] + [ProducesResponseType(typeof(ValidationErrorResponse), 401)] + public async Task UpdateUserAsync([FromRoute] Guid id, [FromBody]UserModel user) + { + var userResult = await service.UpdateAsync(id, user); + + if (userResult.HasError) + { + if(userResult.Error.Code == 404) + { + return NotFound( + new NotFoundErrorResponse(userResult.Error.Message) + ); + } + + return BadRequest( + new ValidationErrorResponse(userResult.Error.Message) + ); + } + + return Ok(new SaveResponse(userResult.Data)); + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Presentation/Extensions/IServiceCollectionExtensions.cs b/src/api/auth/FinancialHub.Auth.Presentation/Extensions/IServiceCollectionExtensions.cs new file mode 100644 index 0000000..188960a --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Presentation/Extensions/IServiceCollectionExtensions.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using FinancialHub.Auth.Application.Extensions; +using FinancialHub.Auth.Resources.Extensions; +using FinancialHub.Auth.Infra.Extensions; +using FinancialHub.Auth.Infra.Data.Extensions; + +namespace FinancialHub.Auth.Presentation.Extensions +{ + public static class IServiceCollectionExtensions + { + public static IServiceCollection AddAuthApplication(this IServiceCollection services, IConfiguration configuration) + { + services + .AddAuthServices(configuration) + .AddAuthResources() + .AddAuthProviders(configuration) + .AddAuthRepositories(configuration); + + return services; + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Presentation/Extensions/SwaggerExtensions.cs b/src/api/auth/FinancialHub.Auth.Presentation/Extensions/SwaggerExtensions.cs new file mode 100644 index 0000000..9b1b382 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Presentation/Extensions/SwaggerExtensions.cs @@ -0,0 +1,48 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using FinancialHub.Auth.Application.Extensions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.OpenApi.Models; + +namespace FinancialHub.Auth.Presentation.Extensions +{ + [ExcludeFromCodeCoverage] + public static class SwaggerExtensions + { + public static IServiceCollection AddAuthDocs(this IServiceCollection services) + { + services.AddEndpointsApiExplorer(); + services.AddSwaggerGen(c => + { + c.IncludeXmlComments( + Path.Combine( + AppContext.BaseDirectory, + $"{Assembly.GetExecutingAssembly().GetName().Name}.xml" + ) + ); + c.SwaggerDoc( + "v1", + new OpenApiInfo + { + Title = "Financial Hub Auth Api", + Version = "v1" + } + ); + + var schemes = services.GetAuthSecuritySchemes(); + + var requeriments = new OpenApiSecurityRequirement(); + + foreach (var scheme in schemes) + { + c.AddSecurityDefinition(scheme.Reference.Id, scheme); + + requeriments.Add(scheme, Array.Empty()); + } + + c.AddSecurityRequirement(requeriments); + }); + return services; + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Presentation/FinancialHub.Auth.Presentation.csproj b/src/api/auth/FinancialHub.Auth.Presentation/FinancialHub.Auth.Presentation.csproj new file mode 100644 index 0000000..cd6f5a1 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Presentation/FinancialHub.Auth.Presentation.csproj @@ -0,0 +1,24 @@ + + + + net6.0 + enable + enable + True + + + + + + + + + + + + + + + + + diff --git a/src/api/auth/FinancialHub.Auth.Presentation/Usings.cs b/src/api/auth/FinancialHub.Auth.Presentation/Usings.cs new file mode 100644 index 0000000..4cf0875 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Presentation/Usings.cs @@ -0,0 +1,8 @@ +global using Microsoft.AspNetCore.Mvc; + +global using FinancialHub.Auth.Domain.Models; + +global using FinancialHub.Auth.Domain.Interfaces.Services; + +global using FinancialHub.Common.Responses.Errors; +global using FinancialHub.Common.Responses.Success; \ No newline at end of file diff --git a/src/api/auth/FinancialHub.Auth.Resources/Extensions/IServiceCollectionExtensions.cs b/src/api/auth/FinancialHub.Auth.Resources/Extensions/IServiceCollectionExtensions.cs new file mode 100644 index 0000000..2aeba1f --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Resources/Extensions/IServiceCollectionExtensions.cs @@ -0,0 +1,18 @@ +using FinancialHub.Auth.Domain.Interfaces.Resources; +using FinancialHub.Auth.Resources.Providers; +using Microsoft.Extensions.DependencyInjection; +using System.Globalization; + +namespace FinancialHub.Auth.Resources.Extensions +{ + public static class IServiceCollectionExtensions + { + public static IServiceCollection AddAuthResources(this IServiceCollection services) + { + services.AddSingleton( + new ErrorMessageProvider(CultureInfo.CurrentCulture) + ); + return services; + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Resources/FinancialHub.Auth.Resources.csproj b/src/api/auth/FinancialHub.Auth.Resources/FinancialHub.Auth.Resources.csproj new file mode 100644 index 0000000..6e5b45b --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Resources/FinancialHub.Auth.Resources.csproj @@ -0,0 +1,35 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + True + True + ErrorMessages.resx + + + + + + ResXFileCodeGenerator + + + ResXFileCodeGenerator + ErrorMessages.Designer.cs + + + + diff --git a/src/api/auth/FinancialHub.Auth.Resources/Providers/ErrorMessageProvider.cs b/src/api/auth/FinancialHub.Auth.Resources/Providers/ErrorMessageProvider.cs new file mode 100644 index 0000000..c72a3d5 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Resources/Providers/ErrorMessageProvider.cs @@ -0,0 +1,22 @@ +using FinancialHub.Auth.Domain.Interfaces.Resources; +using FinancialHub.Auth.Resources.Resources.Errors; +using System.Globalization; + +namespace FinancialHub.Auth.Resources.Providers +{ + public class ErrorMessageProvider : IErrorMessageProvider + { + private readonly CultureInfo cultureInfo; + + public ErrorMessageProvider(CultureInfo cultureInfo) + { + this.cultureInfo = cultureInfo; + } + + public string? Required => ErrorMessages.ResourceManager.GetString("Required", this.cultureInfo); + public string? MinLength => ErrorMessages.ResourceManager.GetString("MinLength", this.cultureInfo); + public string? MaxLength => ErrorMessages.ResourceManager.GetString("MaxLength", this.cultureInfo); + public string? ConfirmPassword => ErrorMessages.ResourceManager.GetString("ConfirmPassword", this.cultureInfo); + public string? Invalid => ErrorMessages.ResourceManager.GetString("Invalid", this.cultureInfo); + } +} diff --git a/src/api/auth/FinancialHub.Auth.Resources/Resources/Errors/ErrorMessages.Designer.cs b/src/api/auth/FinancialHub.Auth.Resources/Resources/Errors/ErrorMessages.Designer.cs new file mode 100644 index 0000000..ee3a74e --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Resources/Resources/Errors/ErrorMessages.Designer.cs @@ -0,0 +1,108 @@ +//------------------------------------------------------------------------------ +// +// O código foi gerado por uma ferramenta. +// Versão de Tempo de Execução:4.0.30319.42000 +// +// As alterações ao arquivo poderão causar comportamento incorreto e serão perdidas se +// o código for gerado novamente. +// +//------------------------------------------------------------------------------ + +namespace FinancialHub.Auth.Resources.Resources.Errors { + using System; + + + /// + /// Uma classe de recurso de tipo de alta segurança, para pesquisar cadeias de caracteres localizadas etc. + /// + // Essa classe foi gerada automaticamente pela classe StronglyTypedResourceBuilder + // através de uma ferramenta como ResGen ou Visual Studio. + // Para adicionar ou remover um associado, edite o arquivo .ResX e execute ResGen novamente + // com a opção /str, ou recrie o projeto do VS. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class ErrorMessages { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal ErrorMessages() { + } + + /// + /// Retorna a instância de ResourceManager armazenada em cache usada por essa classe. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("FinancialHub.Auth.Resources.Resources.Errors.ErrorMessages", typeof(ErrorMessages).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Substitui a propriedade CurrentUICulture do thread atual para todas as + /// pesquisas de recursos que usam essa classe de recurso de tipo de alta segurança. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Consulta uma cadeia de caracteres localizada semelhante a {PropertyName} needs to be equal to Password. + /// + internal static string ConfirmPassword { + get { + return ResourceManager.GetString("ConfirmPassword", resourceCulture); + } + } + + /// + /// Consulta uma cadeia de caracteres localizada semelhante a {PropertyName} is invalid. + /// + internal static string Invalid { + get { + return ResourceManager.GetString("Invalid", resourceCulture); + } + } + + /// + /// Consulta uma cadeia de caracteres localizada semelhante a {PropertyName} exceeds the max length of {MaxLength}. + /// + internal static string MaxLength { + get { + return ResourceManager.GetString("MaxLength", resourceCulture); + } + } + + /// + /// Consulta uma cadeia de caracteres localizada semelhante a {PropertyName} needs to have at least the lenght of {MinLength}. + /// + internal static string MinLength { + get { + return ResourceManager.GetString("MinLength", resourceCulture); + } + } + + /// + /// Consulta uma cadeia de caracteres localizada semelhante a {PropertyName} is required. + /// + internal static string Required { + get { + return ResourceManager.GetString("Required", resourceCulture); + } + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.Resources/Resources/Errors/ErrorMessages.pt-BR.resx b/src/api/auth/FinancialHub.Auth.Resources/Resources/Errors/ErrorMessages.pt-BR.resx new file mode 100644 index 0000000..e02b94a --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Resources/Resources/Errors/ErrorMessages.pt-BR.resx @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + {PropertyName} precisa ser igual ao valor do campo Password + + + {PropertyName} é invalido + + + {PropertyName} excede o limite de {MaxLength} caracteres + + + {PropertyName} precisa ter o tamanho minimo de {MinLength} + + + {PropertyName} é obrigatório + + \ No newline at end of file diff --git a/src/api/auth/FinancialHub.Auth.Resources/Resources/Errors/ErrorMessages.resx b/src/api/auth/FinancialHub.Auth.Resources/Resources/Errors/ErrorMessages.resx new file mode 100644 index 0000000..5e62d10 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.Resources/Resources/Errors/ErrorMessages.resx @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + {PropertyName} needs to be equal to Password + + + {PropertyName} is invalid + + + {PropertyName} exceeds the max length of {MaxLength} + + + {PropertyName} needs to have at least the lenght of {MinLength} + + + {PropertyName} is required + + \ No newline at end of file diff --git a/src/api/auth/FinancialHub.Auth.WebApi/FinancialHub.Auth.WebApi.csproj b/src/api/auth/FinancialHub.Auth.WebApi/FinancialHub.Auth.WebApi.csproj new file mode 100644 index 0000000..bfe63e3 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.WebApi/FinancialHub.Auth.WebApi.csproj @@ -0,0 +1,34 @@ + + + + net6.0 + enable + enable + aspnet-FinancialHub.Auth.WebApi-4a49b595-70f4-4997-b0b7-134dc53e490e + False + + + + 6 + + + + 6 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + diff --git a/src/api/auth/FinancialHub.Auth.WebApi/Program.cs b/src/api/auth/FinancialHub.Auth.WebApi/Program.cs new file mode 100644 index 0000000..0ccc028 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.WebApi/Program.cs @@ -0,0 +1,41 @@ +using FinancialHub.Auth.Presentation.Extensions; +using System.Diagnostics.CodeAnalysis; + +namespace FinancialHub.Auth.WebApi; + +[ExcludeFromCodeCoverage] +public partial class Program +{ + protected Program() + { + + } + + public static void Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + // Add services to the container. + builder.Services.AddAuthApplication(builder.Configuration); + builder.Services.AddControllers(); + builder.Services.AddAuthDocs(); + + var app = builder.Build(); + + // Configure the HTTP request pipeline. + if (app.Environment.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + } + + app.UseHttpsRedirection(); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.MapControllers(); + + app.Run(); + } +} \ No newline at end of file diff --git a/src/api/auth/FinancialHub.Auth.WebApi/Properties/launchSettings.json b/src/api/auth/FinancialHub.Auth.WebApi/Properties/launchSettings.json new file mode 100644 index 0000000..a949367 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.WebApi/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:64755", + "sslPort": 44306 + } + }, + "profiles": { + "FinancialHub.Auth.WebApi": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7198;http://localhost:5072", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/api/auth/FinancialHub.Auth.WebApi/appsettings.Development.json b/src/api/auth/FinancialHub.Auth.WebApi/appsettings.Development.json new file mode 100644 index 0000000..8c49dfd --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.WebApi/appsettings.Development.json @@ -0,0 +1,17 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "TokenServiceSettings": { + "Issuer": "https://localhost:7198", + "Audience": "https://localhost:7198", + "SecurityKey": "A_Big_And_Safe_Security-Key_Çç", + "Expires": 60 + }, + "ConnectionStrings": { + "auth": "Server=localhost,1450;Database=financial_hub;user=sa;pwd=P@ssw0rd!;" + } +} diff --git a/src/api/auth/FinancialHub.Auth.WebApi/appsettings.Testing.json b/src/api/auth/FinancialHub.Auth.WebApi/appsettings.Testing.json new file mode 100644 index 0000000..a16aa05 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.WebApi/appsettings.Testing.json @@ -0,0 +1,17 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "TokenServiceSettings": { + "Issuer": "https://localhost:7198", + "Audience": "https://localhost:7198", + "SecurityKey": "A_Big_And_Safe_Security-Key_Çç", + "Expires": 60 + }, + "ConnectionStrings": { + "auth": "Server=localhost,1450;Database=financial_hub_test;user=sa;pwd=P@ssw0rd!;" + } +} diff --git a/src/api/auth/FinancialHub.Auth.WebApi/appsettings.json b/src/api/auth/FinancialHub.Auth.WebApi/appsettings.json new file mode 100644 index 0000000..6fe1d08 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.WebApi/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "ConnectionStrings": { + "default": "" + }, + "AllowedHosts": "*" +} diff --git a/src/api/auth/FinancialHub.Auth.sln b/src/api/auth/FinancialHub.Auth.sln new file mode 100644 index 0000000..c18dcb9 --- /dev/null +++ b/src/api/auth/FinancialHub.Auth.sln @@ -0,0 +1,145 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31912.275 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Common", "Common", "{B00013E6-DB9F-415A-B542-6D32DA258EB2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Auth", "Auth", "{BEE32E30-21CF-489B-B208-3D053E4966A8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Application", "Application", "{FB841C40-3211-443F-AC95-CD5796CF4FB3}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Domain", "Domain", "{0F151C31-117F-438E-A5B5-9EE1D669BE27}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Presentation", "Presentation", "{3B96E644-4E1A-4C2F-8F3C-E4843C921B92}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Infra", "Infra", "{15C0C14F-DA06-46BB-A8B0-4BA5D6372777}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Data", "Data", "{C0D9D883-C3CC-4A74-B373-CAFD6FC4A32F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinancialHub.Common.Tests", "..\..\..\tests\common\FinancialHub.Common.Tests\FinancialHub.Common.Tests.csproj", "{EDB047FA-1B3C-4060-8327-EDA6EB3D201B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinancialHub.Auth.Application.Tests", "..\..\..\tests\auth\FinancialHub.Auth.Application.Tests\FinancialHub.Auth.Application.Tests.csproj", "{B66A8B1B-4123-4FC1-A0A8-FAA0A9BAC7A9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinancialHub.Auth.Common.Tests", "..\..\..\tests\auth\FinancialHub.Auth.Common.Tests\FinancialHub.Auth.Common.Tests.csproj", "{C9329ED1-DACA-4DD2-9BB6-75C7083FF8B5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinancialHub.Auth.Infra.Data.Tests", "..\..\..\tests\auth\FinancialHub.Auth.Infra.Data.Tests\FinancialHub.Auth.Infra.Data.Tests.csproj", "{5F63B3FF-A42B-496E-A3C7-DC4EB601750F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinancialHub.Auth.Infra.Tests", "..\..\..\tests\auth\FinancialHub.Auth.Infra.Tests\FinancialHub.Auth.Infra.Tests.csproj", "{EF50702A-4F37-4467-9B23-CD52BE6E56BE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinancialHub.Auth.Presentation.Tests", "..\..\..\tests\auth\FinancialHub.Auth.Presentation.Tests\FinancialHub.Auth.Presentation.Tests.csproj", "{F93EE692-EFCD-4729-A98F-C832D927D914}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinancialHub.Auth.IntegrationTests", "..\..\..\tests\auth\FinancialHub.Auth.IntegrationTests\FinancialHub.Auth.IntegrationTests.csproj", "{F87C32B0-8B5C-410C-9A07-2FF4C83CC657}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinancialHub.Common", "..\common\FinancialHub.Common\FinancialHub.Common.csproj", "{7483C5F0-3F6F-4C2F-ADA0-B013CB9A502D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinancialHub.Auth.Application", "FinancialHub.Auth.Application\FinancialHub.Auth.Application.csproj", "{0084DFA0-A5BD-4F53-9652-C3EE50F0703D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinancialHub.Auth.Domain", "FinancialHub.Auth.Domain\FinancialHub.Auth.Domain.csproj", "{DFFD9C3A-E127-43C3-93C1-C8221E9E4B21}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinancialHub.Auth.Infra.Data", "FinancialHub.Auth.Infra.Data\FinancialHub.Auth.Infra.Data.csproj", "{FA8CC50A-AAD4-4F22-9FCD-A2151AC040A0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinancialHub.Auth.Infra", "FinancialHub.Auth.Infra\FinancialHub.Auth.Infra.csproj", "{0329A416-4B7E-4969-9844-32E45258F3D2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinancialHub.Auth.Presentation", "FinancialHub.Auth.Presentation\FinancialHub.Auth.Presentation.csproj", "{E7F77ED2-25CE-4F7D-8909-122076EAF5D2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinancialHub.Auth.WebApi", "FinancialHub.Auth.WebApi\FinancialHub.Auth.WebApi.csproj", "{1D72A253-61A8-4AE4-9DD3-C3DA350CAAB7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FinancialHub.Auth.Resources", "FinancialHub.Auth.Resources\FinancialHub.Auth.Resources.csproj", "{338427B6-0CD8-452B-BA8C-87AADB1F1757}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {EDB047FA-1B3C-4060-8327-EDA6EB3D201B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EDB047FA-1B3C-4060-8327-EDA6EB3D201B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EDB047FA-1B3C-4060-8327-EDA6EB3D201B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EDB047FA-1B3C-4060-8327-EDA6EB3D201B}.Release|Any CPU.Build.0 = Release|Any CPU + {B66A8B1B-4123-4FC1-A0A8-FAA0A9BAC7A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B66A8B1B-4123-4FC1-A0A8-FAA0A9BAC7A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B66A8B1B-4123-4FC1-A0A8-FAA0A9BAC7A9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B66A8B1B-4123-4FC1-A0A8-FAA0A9BAC7A9}.Release|Any CPU.Build.0 = Release|Any CPU + {C9329ED1-DACA-4DD2-9BB6-75C7083FF8B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C9329ED1-DACA-4DD2-9BB6-75C7083FF8B5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C9329ED1-DACA-4DD2-9BB6-75C7083FF8B5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C9329ED1-DACA-4DD2-9BB6-75C7083FF8B5}.Release|Any CPU.Build.0 = Release|Any CPU + {5F63B3FF-A42B-496E-A3C7-DC4EB601750F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5F63B3FF-A42B-496E-A3C7-DC4EB601750F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5F63B3FF-A42B-496E-A3C7-DC4EB601750F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5F63B3FF-A42B-496E-A3C7-DC4EB601750F}.Release|Any CPU.Build.0 = Release|Any CPU + {EF50702A-4F37-4467-9B23-CD52BE6E56BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EF50702A-4F37-4467-9B23-CD52BE6E56BE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EF50702A-4F37-4467-9B23-CD52BE6E56BE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EF50702A-4F37-4467-9B23-CD52BE6E56BE}.Release|Any CPU.Build.0 = Release|Any CPU + {F93EE692-EFCD-4729-A98F-C832D927D914}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F93EE692-EFCD-4729-A98F-C832D927D914}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F93EE692-EFCD-4729-A98F-C832D927D914}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F93EE692-EFCD-4729-A98F-C832D927D914}.Release|Any CPU.Build.0 = Release|Any CPU + {F87C32B0-8B5C-410C-9A07-2FF4C83CC657}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F87C32B0-8B5C-410C-9A07-2FF4C83CC657}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F87C32B0-8B5C-410C-9A07-2FF4C83CC657}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F87C32B0-8B5C-410C-9A07-2FF4C83CC657}.Release|Any CPU.Build.0 = Release|Any CPU + {7483C5F0-3F6F-4C2F-ADA0-B013CB9A502D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7483C5F0-3F6F-4C2F-ADA0-B013CB9A502D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7483C5F0-3F6F-4C2F-ADA0-B013CB9A502D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7483C5F0-3F6F-4C2F-ADA0-B013CB9A502D}.Release|Any CPU.Build.0 = Release|Any CPU + {0084DFA0-A5BD-4F53-9652-C3EE50F0703D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0084DFA0-A5BD-4F53-9652-C3EE50F0703D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0084DFA0-A5BD-4F53-9652-C3EE50F0703D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0084DFA0-A5BD-4F53-9652-C3EE50F0703D}.Release|Any CPU.Build.0 = Release|Any CPU + {DFFD9C3A-E127-43C3-93C1-C8221E9E4B21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DFFD9C3A-E127-43C3-93C1-C8221E9E4B21}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DFFD9C3A-E127-43C3-93C1-C8221E9E4B21}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DFFD9C3A-E127-43C3-93C1-C8221E9E4B21}.Release|Any CPU.Build.0 = Release|Any CPU + {FA8CC50A-AAD4-4F22-9FCD-A2151AC040A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FA8CC50A-AAD4-4F22-9FCD-A2151AC040A0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FA8CC50A-AAD4-4F22-9FCD-A2151AC040A0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FA8CC50A-AAD4-4F22-9FCD-A2151AC040A0}.Release|Any CPU.Build.0 = Release|Any CPU + {0329A416-4B7E-4969-9844-32E45258F3D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0329A416-4B7E-4969-9844-32E45258F3D2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0329A416-4B7E-4969-9844-32E45258F3D2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0329A416-4B7E-4969-9844-32E45258F3D2}.Release|Any CPU.Build.0 = Release|Any CPU + {E7F77ED2-25CE-4F7D-8909-122076EAF5D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E7F77ED2-25CE-4F7D-8909-122076EAF5D2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E7F77ED2-25CE-4F7D-8909-122076EAF5D2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E7F77ED2-25CE-4F7D-8909-122076EAF5D2}.Release|Any CPU.Build.0 = Release|Any CPU + {1D72A253-61A8-4AE4-9DD3-C3DA350CAAB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1D72A253-61A8-4AE4-9DD3-C3DA350CAAB7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1D72A253-61A8-4AE4-9DD3-C3DA350CAAB7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1D72A253-61A8-4AE4-9DD3-C3DA350CAAB7}.Release|Any CPU.Build.0 = Release|Any CPU + {338427B6-0CD8-452B-BA8C-87AADB1F1757}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {338427B6-0CD8-452B-BA8C-87AADB1F1757}.Debug|Any CPU.Build.0 = Debug|Any CPU + {338427B6-0CD8-452B-BA8C-87AADB1F1757}.Release|Any CPU.ActiveCfg = Release|Any CPU + {338427B6-0CD8-452B-BA8C-87AADB1F1757}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {FB841C40-3211-443F-AC95-CD5796CF4FB3} = {BEE32E30-21CF-489B-B208-3D053E4966A8} + {0F151C31-117F-438E-A5B5-9EE1D669BE27} = {BEE32E30-21CF-489B-B208-3D053E4966A8} + {3B96E644-4E1A-4C2F-8F3C-E4843C921B92} = {BEE32E30-21CF-489B-B208-3D053E4966A8} + {15C0C14F-DA06-46BB-A8B0-4BA5D6372777} = {BEE32E30-21CF-489B-B208-3D053E4966A8} + {C0D9D883-C3CC-4A74-B373-CAFD6FC4A32F} = {15C0C14F-DA06-46BB-A8B0-4BA5D6372777} + {EDB047FA-1B3C-4060-8327-EDA6EB3D201B} = {B00013E6-DB9F-415A-B542-6D32DA258EB2} + {B66A8B1B-4123-4FC1-A0A8-FAA0A9BAC7A9} = {FB841C40-3211-443F-AC95-CD5796CF4FB3} + {C9329ED1-DACA-4DD2-9BB6-75C7083FF8B5} = {0F151C31-117F-438E-A5B5-9EE1D669BE27} + {5F63B3FF-A42B-496E-A3C7-DC4EB601750F} = {C0D9D883-C3CC-4A74-B373-CAFD6FC4A32F} + {EF50702A-4F37-4467-9B23-CD52BE6E56BE} = {15C0C14F-DA06-46BB-A8B0-4BA5D6372777} + {F93EE692-EFCD-4729-A98F-C832D927D914} = {3B96E644-4E1A-4C2F-8F3C-E4843C921B92} + {F87C32B0-8B5C-410C-9A07-2FF4C83CC657} = {BEE32E30-21CF-489B-B208-3D053E4966A8} + {7483C5F0-3F6F-4C2F-ADA0-B013CB9A502D} = {B00013E6-DB9F-415A-B542-6D32DA258EB2} + {0084DFA0-A5BD-4F53-9652-C3EE50F0703D} = {FB841C40-3211-443F-AC95-CD5796CF4FB3} + {DFFD9C3A-E127-43C3-93C1-C8221E9E4B21} = {0F151C31-117F-438E-A5B5-9EE1D669BE27} + {FA8CC50A-AAD4-4F22-9FCD-A2151AC040A0} = {C0D9D883-C3CC-4A74-B373-CAFD6FC4A32F} + {0329A416-4B7E-4969-9844-32E45258F3D2} = {15C0C14F-DA06-46BB-A8B0-4BA5D6372777} + {E7F77ED2-25CE-4F7D-8909-122076EAF5D2} = {3B96E644-4E1A-4C2F-8F3C-E4843C921B92} + {1D72A253-61A8-4AE4-9DD3-C3DA350CAAB7} = {BEE32E30-21CF-489B-B208-3D053E4966A8} + {338427B6-0CD8-452B-BA8C-87AADB1F1757} = {0F151C31-117F-438E-A5B5-9EE1D669BE27} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {9A4C4A77-82DC-47E2-AA98-E741B329A3E8} + EndGlobalSection +EndGlobal diff --git a/src/api/common/FinancialHub.Common/Entities/BaseEntity.cs b/src/api/common/FinancialHub.Common/Entities/BaseEntity.cs new file mode 100644 index 0000000..37d7e6b --- /dev/null +++ b/src/api/common/FinancialHub.Common/Entities/BaseEntity.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace FinancialHub.Common.Entities +{ + public abstract class BaseEntity + { + [Column("id")] + public Guid? Id { get; set; } + + [Column("creation_time")] + public DateTimeOffset? CreationTime { get; set; } + + [Column("update_time")] + public DateTimeOffset? UpdateTime { get; set; } + } +} diff --git a/src/api/common/FinancialHub.Common/FinancialHub.Common.csproj b/src/api/common/FinancialHub.Common/FinancialHub.Common.csproj new file mode 100644 index 0000000..132c02c --- /dev/null +++ b/src/api/common/FinancialHub.Common/FinancialHub.Common.csproj @@ -0,0 +1,9 @@ + + + + net6.0 + enable + enable + + + diff --git a/src/api/common/FinancialHub.Common/Interfaces/Repositories/IBaseRepository.cs b/src/api/common/FinancialHub.Common/Interfaces/Repositories/IBaseRepository.cs new file mode 100644 index 0000000..4cbc2c7 --- /dev/null +++ b/src/api/common/FinancialHub.Common/Interfaces/Repositories/IBaseRepository.cs @@ -0,0 +1,41 @@ +using FinancialHub.Common.Entities; + +namespace FinancialHub.Common.Interfaces.Repositories +{ + /// + /// Base repository with basic CRUD methods + /// + /// Any Entity that inherits + public interface IBaseRepository + where T : BaseEntity + { + /// + /// Adds an entity to the database + /// + /// Entity to be added + Task CreateAsync(T obj); + /// + /// Updates a created entity on the database + /// + /// Entity on the database + Task UpdateAsync(T obj); + /// + /// Deletes an entity from the database + /// + /// Id of the entity to be removed + Task DeleteAsync(Guid id); + /// + /// Get All entities from the database + /// + Task> GetAllAsync(); + /// + /// Get All entities from the database based on a filter + /// + Task> GetAsync(Func predicate); + /// + /// Gets an entity by id + /// + /// Id of the choosen entity + Task GetByIdAsync(Guid id); + } +} diff --git a/src/api/common/FinancialHub.Common/Models/BaseModel.cs b/src/api/common/FinancialHub.Common/Models/BaseModel.cs new file mode 100644 index 0000000..e15488f --- /dev/null +++ b/src/api/common/FinancialHub.Common/Models/BaseModel.cs @@ -0,0 +1,7 @@ +namespace FinancialHub.Common.Models +{ + public abstract class BaseModel + { + public Guid? Id { get; set; } + } +} diff --git a/src/api/common/FinancialHub.Common/Responses/Errors/BaseErrorResponse.cs b/src/api/common/FinancialHub.Common/Responses/Errors/BaseErrorResponse.cs new file mode 100644 index 0000000..450edef --- /dev/null +++ b/src/api/common/FinancialHub.Common/Responses/Errors/BaseErrorResponse.cs @@ -0,0 +1,14 @@ +namespace FinancialHub.Common.Responses.Errors +{ + public abstract class BaseErrorResponse + { + public int Code { get; protected set; } + public string Message { get; protected set; } + + protected BaseErrorResponse(int code, string message) + { + Code = code; + Message = message; + } + } +} diff --git a/src/api/common/FinancialHub.Common/Responses/Errors/NotFoundErrorResponse.cs b/src/api/common/FinancialHub.Common/Responses/Errors/NotFoundErrorResponse.cs new file mode 100644 index 0000000..436249e --- /dev/null +++ b/src/api/common/FinancialHub.Common/Responses/Errors/NotFoundErrorResponse.cs @@ -0,0 +1,9 @@ +namespace FinancialHub.Common.Responses.Errors +{ + public class NotFoundErrorResponse : BaseErrorResponse + { + public NotFoundErrorResponse(string message) : base(404, message) + { + } + } +} diff --git a/src/api/common/FinancialHub.Common/Responses/Errors/ValidationErrorResponse.cs b/src/api/common/FinancialHub.Common/Responses/Errors/ValidationErrorResponse.cs new file mode 100644 index 0000000..08cefe3 --- /dev/null +++ b/src/api/common/FinancialHub.Common/Responses/Errors/ValidationErrorResponse.cs @@ -0,0 +1,9 @@ +namespace FinancialHub.Common.Responses.Errors +{ + public class ValidationErrorResponse : BaseErrorResponse + { + public ValidationErrorResponse(string message) : base(400, message) + { + } + } +} diff --git a/src/api/common/FinancialHub.Common/Responses/Errors/ValidationsErrorResponse.cs b/src/api/common/FinancialHub.Common/Responses/Errors/ValidationsErrorResponse.cs new file mode 100644 index 0000000..ac215e3 --- /dev/null +++ b/src/api/common/FinancialHub.Common/Responses/Errors/ValidationsErrorResponse.cs @@ -0,0 +1,13 @@ +namespace FinancialHub.Common.Responses.Errors +{ + public class ValidationsErrorResponse : BaseErrorResponse + { + public Dictionary? Errors { get; set; } + + public ValidationsErrorResponse(string message, Dictionary? errors = null) + : base(400, message) + { + this.Errors = errors; + } + } +} diff --git a/src/api/common/FinancialHub.Common/Responses/Success/BaseResponse.cs b/src/api/common/FinancialHub.Common/Responses/Success/BaseResponse.cs new file mode 100644 index 0000000..d4924aa --- /dev/null +++ b/src/api/common/FinancialHub.Common/Responses/Success/BaseResponse.cs @@ -0,0 +1,11 @@ +namespace FinancialHub.Common.Responses.Success +{ + public abstract class BaseResponse + { + public T Data { get; } + protected BaseResponse(T data) + { + this.Data = data; + } + } +} diff --git a/src/api/common/FinancialHub.Common/Responses/Success/ItemResponse.cs b/src/api/common/FinancialHub.Common/Responses/Success/ItemResponse.cs new file mode 100644 index 0000000..8f577e0 --- /dev/null +++ b/src/api/common/FinancialHub.Common/Responses/Success/ItemResponse.cs @@ -0,0 +1,9 @@ +namespace FinancialHub.Common.Responses.Success +{ + public class ItemResponse : BaseResponse + { + public ItemResponse(T data) : base(data) + { + } + } +} diff --git a/src/api/common/FinancialHub.Common/Responses/Success/ListResponse.cs b/src/api/common/FinancialHub.Common/Responses/Success/ListResponse.cs new file mode 100644 index 0000000..78687df --- /dev/null +++ b/src/api/common/FinancialHub.Common/Responses/Success/ListResponse.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace FinancialHub.Common.Responses.Success +{ + public class ListResponse : BaseResponse> + { + public ListResponse(ICollection data) : base(data) + { + } + } +} diff --git a/src/api/common/FinancialHub.Common/Responses/Success/PaginatedListResponse.cs b/src/api/common/FinancialHub.Common/Responses/Success/PaginatedListResponse.cs new file mode 100644 index 0000000..67c0b3e --- /dev/null +++ b/src/api/common/FinancialHub.Common/Responses/Success/PaginatedListResponse.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace FinancialHub.Common.Responses.Success +{ + public class PaginatedListResponse : BaseResponse> + { + public int Page { get; } + public int PageSize { get; } + public int LastPage { get; } + + public PaginatedListResponse(IEnumerable data, int page, int pageSize, int lastPage) : base(data) + { + Page = page; + PageSize = pageSize; + LastPage = lastPage; + } + } +} diff --git a/src/api/common/FinancialHub.Common/Responses/Success/SaveResponse.cs b/src/api/common/FinancialHub.Common/Responses/Success/SaveResponse.cs new file mode 100644 index 0000000..74c4c65 --- /dev/null +++ b/src/api/common/FinancialHub.Common/Responses/Success/SaveResponse.cs @@ -0,0 +1,9 @@ +namespace FinancialHub.Common.Responses.Success +{ + public class SaveResponse : BaseResponse + { + public SaveResponse(T data) : base(data) + { + } + } +} diff --git a/src/api/common/FinancialHub.Common/Results/Errors/InvalidDataError.cs b/src/api/common/FinancialHub.Common/Results/Errors/InvalidDataError.cs new file mode 100644 index 0000000..3ca575b --- /dev/null +++ b/src/api/common/FinancialHub.Common/Results/Errors/InvalidDataError.cs @@ -0,0 +1,12 @@ +using System; + +namespace FinancialHub.Common.Results.Errors +{ + public class InvalidDataError : ServiceError + { + private const int code = 400; + public InvalidDataError(string message) : base(code, message) + { + } + } +} diff --git a/src/api/common/FinancialHub.Common/Results/Errors/NotFoundError.cs b/src/api/common/FinancialHub.Common/Results/Errors/NotFoundError.cs new file mode 100644 index 0000000..e990e1d --- /dev/null +++ b/src/api/common/FinancialHub.Common/Results/Errors/NotFoundError.cs @@ -0,0 +1,12 @@ +using System; + +namespace FinancialHub.Common.Results.Errors +{ + public class NotFoundError : ServiceError + { + private const int code = 404; + public NotFoundError(string message) : base(code, message) + { + } + } +} diff --git a/src/api/common/FinancialHub.Common/Results/Errors/ServiceError.cs b/src/api/common/FinancialHub.Common/Results/Errors/ServiceError.cs new file mode 100644 index 0000000..c424d94 --- /dev/null +++ b/src/api/common/FinancialHub.Common/Results/Errors/ServiceError.cs @@ -0,0 +1,17 @@ +using System; + +namespace FinancialHub.Common.Results.Errors +{ + public class ServiceError + { + #warning it can be changed to an enum or use a different value from https codes + public int Code { get; protected set; } + public string Message { get; protected set; } + + public ServiceError(int code,string message) + { + this.Code = code; + this.Message = message; + } + } +} diff --git a/src/api/common/FinancialHub.Common/Results/ServiceResult.cs b/src/api/common/FinancialHub.Common/Results/ServiceResult.cs new file mode 100644 index 0000000..4d85ab7 --- /dev/null +++ b/src/api/common/FinancialHub.Common/Results/ServiceResult.cs @@ -0,0 +1,43 @@ +using FinancialHub.Common.Results.Errors; + +namespace FinancialHub.Common.Results +{ + public class ServiceResult + { + public bool HasError => this.Error != null; + public ServiceError? Error { get; protected set; } + public T? Data { get; protected set; } + + public ServiceResult(T? data = default, ServiceError? error = null) + { + this.Data = data; + this.Error = error; + } + + public static implicit operator ServiceResult(T result) + { + return new ServiceResult(result); + } + + public static implicit operator ServiceResult(ServiceError? error) + { + return new ServiceResult(error: error); + } + } + + public class ServiceResult + { + public bool HasError => this.Error != null; + public ServiceError? Error { get; protected set; } + + public ServiceResult(ServiceError? error = null) + { + this.Error = error; + } + + public static implicit operator ServiceResult(ServiceError? error) + { + return new ServiceResult(error: error); + } + } +} diff --git a/src/api/docker-compose.yml b/src/api/docker-compose.yml new file mode 100644 index 0000000..edcaa54 --- /dev/null +++ b/src/api/docker-compose.yml @@ -0,0 +1,12 @@ +version: '3.4' +services: + sql-database: + image: mcr.microsoft.com/mssql/server:2019-latest + tty: true + environment: + - SA_PASSWORD=P@ssw0rd! + - ACCEPT_EULA=Y + ports: + - "1450:1433" + volumes: + - ../database:/var/opt/mssql/data \ No newline at end of file diff --git a/tests/.editorconfig b/tests/.editorconfig new file mode 100644 index 0000000..5e94c65 --- /dev/null +++ b/tests/.editorconfig @@ -0,0 +1,238 @@ +# Remova a linha abaixo se quiser herdar as configurações do .editorconfig de diretórios superiores +root = true + +# Arquivos C# +[*.cs] + +#### Opções Principais do EditorConfig #### + +# Recuo e espaçamento +indent_size = 4 +indent_style = space +tab_width = 4 + +# Preferências de nova linha +end_of_line = crlf +insert_final_newline = false + +#### Convenções de codificação .NET #### + +# Organizar Usos +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false +file_header_template = unset + +# Preferências +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Preferências de tipos BCL contra palavras-chave do idioma +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Preferências de parênteses +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Preferências de modificador +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Preferências de nível de expressão +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Preferências de campo +dotnet_style_readonly_field = true + +# Preferências de parâmetro +dotnet_code_quality_unused_parameters = all + +# Preferências de supressão +dotnet_remove_unnecessary_suppression_exclusions = none + +# Preferências de nova linha +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true + +#### Convenções de Codificação em C# #### + +# preferências de var +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = false + +# Membros aptos para expressão +csharp_style_expression_bodied_accessors = true +csharp_style_expression_bodied_constructors = false +csharp_style_expression_bodied_indexers = true +csharp_style_expression_bodied_lambdas = true +csharp_style_expression_bodied_local_functions = false +csharp_style_expression_bodied_methods = false +csharp_style_expression_bodied_operators = false +csharp_style_expression_bodied_properties = true + +# Preferências de correspondência de padrões +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_switch_expression = true + +# Preferências de verificação nula +csharp_style_conditional_delegate_call = true + +# Preferências de modificador +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async +csharp_style_prefer_readonly_struct = true +csharp_style_prefer_readonly_struct_member = true + +# Preferências do bloco de código +csharp_prefer_braces = true +csharp_prefer_simple_using_statement = true +csharp_style_namespace_declarations = block_scoped +csharp_style_prefer_method_group_conversion = true +csharp_style_prefer_primary_constructors = true +csharp_style_prefer_top_level_statements = true + +# Preferências de nível de expressão +csharp_prefer_simple_default_expression = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_local_over_anonymous_function = true +csharp_style_prefer_null_check_over_type_check = true +csharp_style_prefer_range_operator = true +csharp_style_prefer_tuple_swap = true +csharp_style_prefer_utf8_string_literals = true +csharp_style_throw_expression = true +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable + +# Preferências da diretiva 'using' +csharp_using_directive_placement = outside_namespace + +# Preferências de nova linha +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true + +#### Regras de Formatação de C# #### + +# Preferências de nova linha +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Preferências de recuo +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Preferências de espaço +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Preferências de quebra de linha +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Estilos de nomenclatura #### + +# Regras de nomenclatura + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Especificações de símbolo + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Estilos de nomenclatura + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +# Code Smells + +# CS8618: O campo não anulável precisa conter um valor não nulo ao sair do construtor. Considere declará-lo como anulável. +dotnet_diagnostic.CS8618.severity = none + +# CS8632: A anotação para tipos de referência anuláveis deve ser usada apenas em código em um contexto de anotações '#nullable'. +dotnet_diagnostic.CS8632.severity = none diff --git a/tests/auth/FinancialHub.Auth.Application.Tests/FinancialHub.Auth.Application.Tests.csproj b/tests/auth/FinancialHub.Auth.Application.Tests/FinancialHub.Auth.Application.Tests.csproj new file mode 100644 index 0000000..32fd9da --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Application.Tests/FinancialHub.Auth.Application.Tests.csproj @@ -0,0 +1,26 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + + + + + + diff --git a/tests/auth/FinancialHub.Auth.Application.Tests/Services/Signin/SigninServiceTests.cs b/tests/auth/FinancialHub.Auth.Application.Tests/Services/Signin/SigninServiceTests.cs new file mode 100644 index 0000000..ee47797 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Application.Tests/Services/Signin/SigninServiceTests.cs @@ -0,0 +1,30 @@ +using FinancialHub.Auth.Domain.Interfaces.Providers; +using FinancialHub.Auth.Domain.Interfaces.Services; +using FinancialHub.Auth.Application.Services; + +namespace FinancialHub.Auth.Application.Tests.Services +{ + public partial class SigninServiceTests + { + private ISigninService service; + private Mock mockTokenProvider; + private Mock mockSigninProvider; + + private UserModelBuilder userBuilder; + private SigninModelBuilder signinModelBuilder; + private TokenModelBuilder tokenModelBuilder; + + [SetUp] + public void SetUp() + { + this.userBuilder = new UserModelBuilder(); + this.signinModelBuilder = new SigninModelBuilder(); + this.tokenModelBuilder = new TokenModelBuilder(); + + this.mockTokenProvider = new Mock(); + this.mockSigninProvider = new Mock(); + + this.service = new SigninService(mockTokenProvider.Object, mockSigninProvider.Object); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Application.Tests/Services/Signin/SigninServiceTests.get.cs b/tests/auth/FinancialHub.Auth.Application.Tests/Services/Signin/SigninServiceTests.get.cs new file mode 100644 index 0000000..aa62313 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Application.Tests/Services/Signin/SigninServiceTests.get.cs @@ -0,0 +1,70 @@ +namespace FinancialHub.Auth.Application.Tests.Services +{ + public partial class SigninServiceTests + { + [Test] + public async Task AuthenticateAsync_ExistingUser_ReturnsToken() + { + var login = signinModelBuilder.Generate(); + var user = userBuilder + .WithEmail(login.Email) + .Generate(); + var tokenModel = tokenModelBuilder.Generate(); + + mockSigninProvider + .Setup(x => x.GetAccountAsync(login)) + .ReturnsAsync(user); + mockTokenProvider + .Setup(x => x.GenerateToken(user)) + .Returns(tokenModel); + + var tokenResult = await this.service.AuthenticateAsync(login); + + Assert.Multiple(() => + { + Assert.That(tokenResult.HasError, Is.False); + Assert.That(tokenResult.Data, Is.EqualTo(tokenModel)); + }); + } + + [Test] + public async Task AuthenticateAsync_CallsGetAccountAsync() + { + var login = signinModelBuilder.Generate(); + var user = userBuilder + .WithEmail(login.Email) + .Generate(); + var tokenModel = tokenModelBuilder.Generate(); + + mockSigninProvider + .Setup(x => x.GetAccountAsync(login)) + .ReturnsAsync(user) + .Verifiable(); + mockTokenProvider + .Setup(x => x.GenerateToken(user)) + .Returns(tokenModel) + .Verifiable(); + + await this.service.AuthenticateAsync(login); + + mockSigninProvider.Verify(x => x.GetAccountAsync(login), Times.Once); + } + + [Test] + public async Task AuthenticateAsync_NotExistingUser_ReturnsNull() + { + var login = signinModelBuilder.Generate(); + + mockSigninProvider + .Setup(x => x.GetAccountAsync(login)) + .ReturnsAsync(default(UserModel?)); + var tokenResult = await this.service.AuthenticateAsync(login); + + Assert.Multiple(() => + { + Assert.That(tokenResult.HasError, Is.True); + Assert.That(tokenResult.Error!.Message, Is.EqualTo("Wrong e-mail or password")); + }); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Application.Tests/Services/Signup/SignupServiceTests.create.cs b/tests/auth/FinancialHub.Auth.Application.Tests/Services/Signup/SignupServiceTests.create.cs new file mode 100644 index 0000000..4e1c97a --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Application.Tests/Services/Signup/SignupServiceTests.create.cs @@ -0,0 +1,78 @@ +namespace FinancialHub.Auth.Application.Tests.Services +{ + public partial class SignupServiceTests + { + [Test] + public async Task CreateAccountAsync_ValidSignup_ReturnsCreatedUser() + { + var signup = signupBuilder.Generate(); + var user = userBuilder + .WithEmail(signup.Email) + .WithName(signup.FirstName) + .WithLastName(signup.LastName) + .WithBirthDate(signup.BirthDate) + .Generate(); + + this.mockCredentialProvider + .Setup(x => x.GetAsync(signup.Email)) + .ReturnsAsync((CredentialModel?)default); + this.mockSignupProvider + .Setup(x => x.CreateAccountAsync(signup)) + .ReturnsAsync(user); + + var result = await this.service.CreateAccountAsync(signup); + + Assert.That(result, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(result.HasError, Is.False); + Assert.That(result.Data, Is.EqualTo(user)); + }); + } + + [Test] + public async Task CreateAccountAsync_ExistingCredential_ReturnsErrror() + { + var signup = signupBuilder.Generate(); + var credential = userCredentialBuilder + .WithLogin(signup.Email) + .WithPassword(signup.Password) + .Generate(); + + this.mockCredentialProvider + .Setup(x => x.GetAsync(signup.Email)) + .ReturnsAsync(credential); + + var result = await this.service.CreateAccountAsync(signup); + + Assert.That(result, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(result.HasError, Is.True); + Assert.That(result.Error!.Message, Is.EqualTo("Credential already exists")); + }); + } + + [Test] + public async Task CreateAccountAsync_FailedToCreateUser_ReturnsError() + { + var signup = signupBuilder.Generate(); + + this.mockCredentialProvider + .Setup(x => x.GetAsync(signup.Email)) + .ReturnsAsync((CredentialModel?)default); + this.mockSignupProvider + .Setup(x => x.CreateAccountAsync(signup)) + .ReturnsAsync((UserModel?)default); + + var result = await this.service.CreateAccountAsync(signup); + + Assert.That(result, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(result.HasError, Is.True); + Assert.That(result.Error!.Message, Is.EqualTo("Failed to create user")); + }); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Application.Tests/Services/Signup/SignupServiceTests.cs b/tests/auth/FinancialHub.Auth.Application.Tests/Services/Signup/SignupServiceTests.cs new file mode 100644 index 0000000..d590ee1 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Application.Tests/Services/Signup/SignupServiceTests.cs @@ -0,0 +1,30 @@ +using FinancialHub.Auth.Domain.Interfaces.Providers; +using FinancialHub.Auth.Domain.Interfaces.Services; +using FinancialHub.Auth.Application.Services; + +namespace FinancialHub.Auth.Application.Tests.Services +{ + public partial class SignupServiceTests + { + private ISignupService service; + private Mock mockCredentialProvider; + private Mock mockSignupProvider; + + private UserModelBuilder userBuilder; + private UserCredentialModelBuilder userCredentialBuilder; + private SignupModelBuilder signupBuilder; + + [SetUp] + public void SetUp() + { + this.userBuilder = new UserModelBuilder(); + this.signupBuilder = new SignupModelBuilder(); + this.userCredentialBuilder = new UserCredentialModelBuilder(); + + this.mockCredentialProvider = new Mock(); + this.mockSignupProvider = new Mock(); + + this.service = new SignupService(mockSignupProvider.Object, mockCredentialProvider.Object); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Application.Tests/Services/Token/TokenServiceTests.cs b/tests/auth/FinancialHub.Auth.Application.Tests/Services/Token/TokenServiceTests.cs new file mode 100644 index 0000000..b3cc055 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Application.Tests/Services/Token/TokenServiceTests.cs @@ -0,0 +1,34 @@ +using FinancialHub.Auth.Domain.Interfaces.Services; +using FinancialHub.Auth.Application.Configurations; +using FinancialHub.Auth.Application.Services; +using Microsoft.Extensions.Options; + +namespace FinancialHub.Auth.Application.Tests.Services +{ + public partial class TokenServiceTests + { + private ITokenService service; + private UserModelBuilder userModelBuilder; + + [SetUp] + public void SetUp() + { + this.userModelBuilder = new UserModelBuilder(); + this.service = new TokenService(Options.Create(DefaultOption)); + } + + private static TokenServiceSettings DefaultOption + { + get + { + return new TokenServiceSettings() + { + Audience = "https://localhost:5000", + Issuer = "https://localhost:5000", + SecurityKey = "SecurityKeyVeryBigAndComplexNoOneWouldGuess", + Expires = 30 + }; + } + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Application.Tests/Services/Token/TokenServiceTests.generate.cs b/tests/auth/FinancialHub.Auth.Application.Tests/Services/Token/TokenServiceTests.generate.cs new file mode 100644 index 0000000..d8cb62e --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Application.Tests/Services/Token/TokenServiceTests.generate.cs @@ -0,0 +1,77 @@ +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Text; + +namespace FinancialHub.Auth.Application.Tests.Services +{ + public partial class TokenServiceTests + { + [Test] + public void GenerateToken_ValidUser_ReturnsValidToken() + { + var userModel = userModelBuilder.Generate(); + + var tokenModel = service.GenerateToken(userModel); + + Assert.That(new JwtSecurityTokenHandler().CanReadToken(tokenModel.Token), Is.True); + } + + [Test] + public void GenerateToken_ValidUser_ReturnsTokenWithValidExpirationDate() + { + var expectedExpirationDate = DefaultOption.Expires; + var minimumExpected = DateTime.UtcNow.AddMinutes(expectedExpirationDate - 1); + var maximumExpected = DateTime.UtcNow.AddMinutes(expectedExpirationDate + 1); + var userModel = userModelBuilder.Generate(); + + var tokenModel = service.GenerateToken(userModel); + + var token = new JwtSecurityTokenHandler().ReadJwtToken(tokenModel.Token); + Assert.That(token.ValidTo, Is.InRange(minimumExpected, maximumExpected)); + } + + [Test] + public void GenerateToken_ValidUser_ReturnsTokenWithUserData() + { + var userModel = userModelBuilder.Generate(); + + var tokenModel = service.GenerateToken(userModel); + + var token = new JwtSecurityTokenHandler().ReadJwtToken(tokenModel.Token); + Assert.Multiple(() => + { + Assert.That(token.Payload.Jti, Is.EqualTo(userModel.Id.ToString())); + Assert.That(token.Payload["email"], Is.EqualTo(userModel.Email)); + Assert.That(token.Payload["name"], Is.EqualTo(userModel.FirstName)); + }); + } + + [Test] + public void GenerateToken_ValidUser_ReturnsTokenWithValidSignature() + { + var userModel = userModelBuilder.Generate(); + + var tokenModel = service.GenerateToken(userModel); + + var key = Encoding.ASCII.GetBytes(DefaultOption.SecurityKey); + var handler = new JwtSecurityTokenHandler(); + handler.ValidateToken( + tokenModel.Token, + new TokenValidationParameters() + { + IssuerSigningKey = new SymmetricSecurityKey(key), + ValidateIssuer = false, + ValidateAudience = false, + }, + out var token + ); + + Assert.Multiple(() => + { + var signinKey = (SymmetricSecurityKey)token.SigningKey; + Assert.That(signinKey.IsSupportedAlgorithm(SecurityAlgorithms.HmacSha256Signature)); + Assert.That(signinKey.Key, Is.EqualTo(key)); + }); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Application.Tests/Services/Users/UserServiceTests.create.cs b/tests/auth/FinancialHub.Auth.Application.Tests/Services/Users/UserServiceTests.create.cs new file mode 100644 index 0000000..f932040 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Application.Tests/Services/Users/UserServiceTests.create.cs @@ -0,0 +1,21 @@ +namespace FinancialHub.Auth.Application.Tests.Services +{ + public partial class UserServiceTests + { + [Test] + public async Task CreateAsync_ValidUser_ReturnsCreatedUser() + { + var user = this.builder.Generate(); + + mockProvider + .Setup(x => x.CreateAsync(user)) + .ReturnsAsync(user) + .Verifiable(); + var createdUserResult = await this.service.CreateAsync(user); + + Assert.That(createdUserResult.HasError, Is.False); + ModelAssert.Equal(user, createdUserResult.Data); + mockProvider.Verify(x => x.CreateAsync(It.IsAny()), Times.Once()); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Application.Tests/Services/Users/UserServiceTests.cs b/tests/auth/FinancialHub.Auth.Application.Tests/Services/Users/UserServiceTests.cs new file mode 100644 index 0000000..93bc5a0 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Application.Tests/Services/Users/UserServiceTests.cs @@ -0,0 +1,22 @@ +using FinancialHub.Auth.Domain.Interfaces.Providers; +using FinancialHub.Auth.Domain.Interfaces.Services; +using FinancialHub.Auth.Application.Services; + +namespace FinancialHub.Auth.Application.Tests.Services +{ + public partial class UserServiceTests + { + private IUserService service; + private Mock mockProvider; + private UserModelBuilder builder; + + [SetUp] + public void SetUp() + { + this.builder = new UserModelBuilder(); + + this.mockProvider = new Mock(); + this.service = new UserService(this.mockProvider.Object); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Application.Tests/Services/Users/UserServiceTests.get.cs b/tests/auth/FinancialHub.Auth.Application.Tests/Services/Users/UserServiceTests.get.cs new file mode 100644 index 0000000..9bf24b0 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Application.Tests/Services/Users/UserServiceTests.get.cs @@ -0,0 +1,42 @@ +namespace FinancialHub.Auth.Application.Tests.Services +{ + public partial class UserServiceTests + { + [Test] + public async Task GetAsync_ExistingUser_ReturnsUser() + { + var id = Guid.NewGuid(); + var user = this.builder.Generate(); + + mockProvider + .Setup(x => x.GetAsync(id)) + .ReturnsAsync(user) + .Verifiable(); + + var userResult = await this.service.GetAsync(id); + + Assert.That(userResult.HasError, Is.False); + ModelAssert.Equal(user, userResult.Data); + } + + [Test] + public async Task GetAsync_NotExistingUser_ReturnsNotFoundUser() + { + var id = Guid.NewGuid(); + var user = this.builder.Generate(); + + mockProvider + .Setup(x => x.GetAsync(id)) + .ReturnsAsync(default(UserModel)) + .Verifiable(); + + var userResult = await this.service.GetAsync(id); + Assert.Multiple(() => + { + Assert.That(userResult.HasError, Is.True); + Assert.That(userResult.Error!.Message, Is.EqualTo("User not found")); + }); + } + + } +} diff --git a/tests/auth/FinancialHub.Auth.Application.Tests/Services/Users/UserServiceTests.update.cs b/tests/auth/FinancialHub.Auth.Application.Tests/Services/Users/UserServiceTests.update.cs new file mode 100644 index 0000000..879d956 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Application.Tests/Services/Users/UserServiceTests.update.cs @@ -0,0 +1,46 @@ +namespace FinancialHub.Auth.Application.Tests.Services +{ + public partial class UserServiceTests + { + [Test] + public async Task UpdateAsync_ExistingUser_ReturnsUpdatedUser() + { + var id = Guid.NewGuid(); + var user = this.builder + .WithId(id) + .Generate(); + + mockProvider + .Setup(x => x.GetAsync(id)) + .ReturnsAsync(user); + mockProvider + .Setup(x => x.UpdateAsync(user)) + .ReturnsAsync(user); + + var updatedUserResult = await this.service.UpdateAsync(id, user); + + Assert.That(updatedUserResult.HasError, Is.False); + ModelAssert.Equal(user, updatedUserResult.Data); + } + + [Test] + public async Task UpdateAsync_NonExistingUser_ReturnsNotFoundServiceError() + { + var id = Guid.NewGuid(); + var user = this.builder + .WithId(id) + .Generate(); + + mockProvider + .Setup(x => x.UpdateAsync(user)) + .ReturnsAsync(user); + + var updatedUserResult = await this.service.UpdateAsync(id, user); + Assert.Multiple(() => + { + Assert.That(updatedUserResult.HasError, Is.True); + Assert.That(updatedUserResult.Error!.Message, Is.EqualTo("User not found")); + }); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Application.Tests/Usings.cs b/tests/auth/FinancialHub.Auth.Application.Tests/Usings.cs new file mode 100644 index 0000000..42a2b57 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Application.Tests/Usings.cs @@ -0,0 +1,10 @@ +global using NUnit.Framework; +global using Moq; + +global using FinancialHub.Auth.Domain.Models; + +global using FinancialHub.Auth.Common.Tests.Assertions; + +global using FinancialHub.Auth.Common.Tests.Builders.Models; + +[assembly: Category("Unit")] \ No newline at end of file diff --git a/tests/auth/FinancialHub.Auth.Application.Tests/Validators/SigninModelValidatorTests.cs b/tests/auth/FinancialHub.Auth.Application.Tests/Validators/SigninModelValidatorTests.cs new file mode 100644 index 0000000..9be1eca --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Application.Tests/Validators/SigninModelValidatorTests.cs @@ -0,0 +1,125 @@ +using FinancialHub.Auth.Domain.Interfaces.Resources; +using FinancialHub.Auth.Resources.Providers; +using FinancialHub.Auth.Application.Validators; +using System.Globalization; + +namespace FinancialHub.Auth.Application.Tests.Validators +{ + public class SigninModelValidatorTests + { + private IErrorMessageProvider errorMessageProvider; + private SigninModelValidator validator; + private SigninModelBuilder builder; + + [SetUp] + public void SetUp() + { + errorMessageProvider = new ErrorMessageProvider(CultureInfo.InvariantCulture); + validator = new SigninModelValidator(errorMessageProvider); + builder = new SigninModelBuilder(); + } + + [Test] + public void Validate_ValidSignupModel_ReturnsTrue() + { + var signup = builder.Generate(); + + var result = validator.Validate(signup); + + Assert.That(result.IsValid, Is.True); + } + + [Test] + public void Validate_EmailEmpty_ReturnsFalse() + { + var user = builder + .WithEmail(string.Empty) + .Generate(); + + var expectedMessage = "Email is required"; + + var result = validator.Validate(user); + Assert.Multiple(() => + { + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(expectedMessage)); + }); + } + + [Test] + public void Validate_InvalidEmail_ReturnsFalse() + { + var user = builder + .WithEmail(new string('a', 3)) + .Generate(); + + var expectedMessage = "Email is invalid"; + + var result = validator.Validate(user); + Assert.Multiple(() => + { + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(expectedMessage)); + }); + } + + + [TestCase(1)] + [TestCase(2)] + [TestCase(4)] + [TestCase(7)] + public void Validate_PasswordLessThan_ReturnsFalse(int size) + { + var user = builder + .WithPassword(new string('a', size)) + .Generate(); + + var expectedMessage = "Password needs to have at least the lenght of 8"; + + var result = validator.Validate(user); + + Assert.Multiple(() => + { + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(expectedMessage)); + }); + } + + [Test] + public void Validate_PasswordOverMaxLength_ReturnsFalse() + { + var user = builder + .WithPassword(new string('a', 301)) + .Generate(); + + var expectedMessage = "Password exceeds the max length of 80"; + + var result = validator.Validate(user); + + Assert.Multiple(() => + { + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(expectedMessage)); + }); + } + + [TestCase(8)] + [TestCase(10)] + [TestCase(50)] + [TestCase(80)] + public void Validate_ValidPassword_ReturnsTrue(int size) + { + var user = builder + .WithPassword(new string('a', size)) + .Generate(); + + var result = validator.Validate(user); + + Assert.Multiple(() => + { + Assert.That(result.IsValid, Is.True); + Assert.That(result.Errors, Is.Empty); + }); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Application.Tests/Validators/SignupModelValidatorTests.cs b/tests/auth/FinancialHub.Auth.Application.Tests/Validators/SignupModelValidatorTests.cs new file mode 100644 index 0000000..638ee06 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Application.Tests/Validators/SignupModelValidatorTests.cs @@ -0,0 +1,232 @@ +using FinancialHub.Auth.Domain.Interfaces.Resources; +using FinancialHub.Auth.Resources.Providers; +using FinancialHub.Auth.Application.Validators; +using System.Globalization; + +namespace FinancialHub.Auth.Application.Tests.Validators +{ + public class SignupModelValidatorTests + { + private IErrorMessageProvider errorMessageProvider; + private SignupModelValidator validator; + private SignupModelBuilder builder; + + [SetUp] + public void SetUp() + { + errorMessageProvider = new ErrorMessageProvider(CultureInfo.InvariantCulture); + validator = new SignupModelValidator(errorMessageProvider); + builder = new SignupModelBuilder(); + } + + [Test] + public void Validate_ValidSignupModel_ReturnsTrue() + { + var signup = builder.Generate(); + + var result = validator.Validate(signup); + + Assert.That(result.IsValid, Is.True); + } + + [Test] + public void Validate_EmailEmpty_ReturnsFalse() + { + var user = builder + .WithEmail(string.Empty) + .Generate(); + + var expectedMessage = "Email is required"; + + var result = validator.Validate(user); + Assert.Multiple(() => + { + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(expectedMessage)); + }); + } + + [Test] + public void Validate_InvalidEmail_ReturnsFalse() + { + var user = builder + .WithEmail(new string('a', 3)) + .Generate(); + + var expectedMessage = "Email is invalid"; + + var result = validator.Validate(user); + Assert.Multiple(() => + { + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(expectedMessage)); + }); + } + + [Test] + public void Validate_EmailOverMaxLength_ReturnsFalse() + { + var user = builder + .WithEmail(new string('a', 301) + "@test.com") + .Generate(); + + var expectedMessage = "Email exceeds the max length of 300"; + + var result = validator.Validate(user); + + Assert.Multiple(() => + { + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(expectedMessage)); + }); + } + + [Test] + public void Validate_FirstNameEmpty_ReturnsFalse() + { + var user = builder + .WithFirstName(string.Empty) + .Generate(); + + var expectedMessage = "First Name is required"; + + var result = validator.Validate(user); + + Assert.Multiple(() => + { + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(expectedMessage)); + }); + } + + [Test] + public void Validate_FirstNameOverMaxLength_ReturnsFalse() + { + var user = builder + .WithFirstName(new string('a', 301)) + .Generate(); + + var expectedMessage = "First Name exceeds the max length of 300"; + + var result = validator.Validate(user); + + Assert.Multiple(() => + { + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(expectedMessage)); + }); + } + + [Test] + public void Validate_LastNameEmpty_ReturnsFalse() + { + var user = builder + .WithLastName(string.Empty) + .Generate(); + + var expectedMessage = "Last Name is required"; + + var result = validator.Validate(user); + + Assert.Multiple(() => + { + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(expectedMessage)); + }); + } + + [Test] + public void Validate_LastNameOverMaxLength_ReturnsFalse() + { + var user = builder + .WithLastName(new string('a', 301)) + .Generate(); + + var expectedMessage = "Last Name exceeds the max length of 300"; + + var result = validator.Validate(user); + + Assert.Multiple(() => + { + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(expectedMessage)); + }); + } + + [TestCase(1)] + [TestCase(2)] + [TestCase(4)] + [TestCase(7)] + public void Validate_PasswordLessThan_ReturnsFalse(int size) + { + var user = builder + .WithValidPassword(new string('a', size)) + .Generate(); + + var expectedMessage = "Password needs to have at least the lenght of 8"; + + var result = validator.Validate(user); + + Assert.Multiple(() => + { + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(expectedMessage)); + }); + } + + [Test] + public void Validate_PasswordOverMaxLength_ReturnsFalse() + { + var user = builder + .WithValidPassword(new string('a', 301)) + .Generate(); + + var expectedMessage = "Password exceeds the max length of 80"; + + var result = validator.Validate(user); + + Assert.Multiple(() => + { + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(expectedMessage)); + }); + } + + [TestCase(8)] + [TestCase(10)] + [TestCase(50)] + [TestCase(80)] + public void Validate_ValidPassword_ReturnsTrue(int size) + { + var user = builder + .WithValidPassword(new string('a', size)) + .Generate(); + + var result = validator.Validate(user); + + Assert.Multiple(() => + { + Assert.That(result.IsValid, Is.True); + Assert.That(result.Errors, Is.Empty); + }); + } + + [Test] + public void Validate_ConfirmPasswordNotEqual_ReturnsFalse() + { + var user = builder + .WithConfirmPassword(new string('a', 30)) + .Generate(); + + var expectedMessage = "Confirm Password needs to be equal to Password"; + + var result = validator.Validate(user); + + Assert.Multiple(() => + { + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(expectedMessage)); + }); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Application.Tests/Validators/UserValidatorTests.cs b/tests/auth/FinancialHub.Auth.Application.Tests/Validators/UserValidatorTests.cs new file mode 100644 index 0000000..5cde0fd --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Application.Tests/Validators/UserValidatorTests.cs @@ -0,0 +1,175 @@ +using FinancialHub.Auth.Domain.Interfaces.Resources; +using FinancialHub.Auth.Resources.Providers; +using FinancialHub.Auth.Application.Validators; +using System.Globalization; + +namespace FinancialHub.Auth.Application.Tests.Validators +{ + public class UserValidatorTests + { + private IErrorMessageProvider errorMessageProvider; + + private UserValidator validator; + private UserModelBuilder builder; + + [SetUp] + public void SetUp() + { + errorMessageProvider = new ErrorMessageProvider(CultureInfo.InvariantCulture); + validator = new UserValidator(errorMessageProvider); + builder = new UserModelBuilder(); + } + + [Test] + public void Validate_ValidUser_ReturnsTrue() + { + var user = builder.Generate(); + + var result = validator.Validate(user); + + Assert.That(result.IsValid, Is.True); + } + + [Test] + public void Validate_EmailEmpty_ReturnsFalse() + { + var user = builder + .WithEmail(string.Empty) + .Generate(); + + var expectedMessage = "Email is required"; + + var result = validator.Validate(user); + Assert.Multiple(() => + { + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(expectedMessage)); + }); + } + + [Test] + public void Validate_InvalidEmail_ReturnsFalse() + { + var user = builder + .WithEmail(new string('a', 3)) + .Generate(); + + var expectedMessage = "Email is invalid"; + + var result = validator.Validate(user); + Assert.Multiple(() => + { + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(expectedMessage)); + }); + } + + [Test] + public void Validate_EmailOverMaxLength_ReturnsFalse() + { + var user = builder + .WithEmail(new string('a', 301) + "@test.com") + .Generate(); + + var expectedMessage = "Email exceeds the max length of 300"; + + var result = validator.Validate(user); + + Assert.Multiple(() => + { + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(expectedMessage)); + }); + } + + [Test] + public void Validate_FirstNameEmpty_ReturnsFalse() + { + var user = builder + .WithName(string.Empty) + .Generate(); + + var expectedMessage = "First Name is required"; + + var result = validator.Validate(user); + + Assert.Multiple(() => + { + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(expectedMessage)); + }); + } + + [Test] + public void Validate_FirstNameOverMaxLength_ReturnsFalse() + { + var user = builder + .WithName(new string('a', 301)) + .Generate(); + + var expectedMessage = "First Name exceeds the max length of 300"; + + var result = validator.Validate(user); + + Assert.Multiple(() => + { + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(expectedMessage)); + }); + } + + [Test] + public void Validate_LastNameEmpty_ReturnsFalse() + { + var user = builder + .WithLastName(string.Empty) + .Generate(); + + var expectedMessage = "Last Name is required"; + + var result = validator.Validate(user); + + Assert.Multiple(() => + { + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(expectedMessage)); + }); + } + + [Test] + public void Validate_LastNameOverMaxLength_ReturnsFalse() + { + var user = builder + .WithLastName(new string('a', 301)) + .Generate(); + + var expectedMessage = "Last Name exceeds the max length of 300"; + + var result = validator.Validate(user); + + Assert.Multiple(() => + { + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(expectedMessage)); + }); + } + + [Test] + public void Validate_BirthDateEmpty_ReturnsFalse() + { + var user = builder + .WithBirthDate(default) + .Generate(); + + var expectedMessage = "Birth Date is required"; + + var result = validator.Validate(user); + + Assert.Multiple(() => + { + Assert.That(result.IsValid, Is.False); + Assert.That(result.Errors[0].ErrorMessage, Is.EqualTo(expectedMessage)); + }); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Common.Tests/Assertions/EntityAssert.cs b/tests/auth/FinancialHub.Auth.Common.Tests/Assertions/EntityAssert.cs new file mode 100644 index 0000000..8ce3667 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Common.Tests/Assertions/EntityAssert.cs @@ -0,0 +1,40 @@ +namespace FinancialHub.Auth.Common.Tests.Assertions +{ + public static class EntityAssert + { + public static void Equal(UserEntity expected, UserEntity result) + { + Assert.Multiple(() => + { + Assert.That(result.Id, Is.EqualTo(expected.Id)); + Assert.That(result.FirstName, Is.EqualTo(expected.FirstName)); + Assert.That(result.LastName, Is.EqualTo(expected.LastName)); + Assert.That(result.Email, Is.EqualTo(expected.Email)); + Assert.That(result.BirthDate, Is.EqualTo(expected.BirthDate)); + }); + } + + public static void Equal(UserEntity expected, UserModel result) + { + Assert.Multiple(() => + { + Assert.That(result.Id, Is.EqualTo(expected.Id)); + Assert.That(result.FirstName, Is.EqualTo(expected.FirstName)); + Assert.That(result.LastName, Is.EqualTo(expected.LastName)); + Assert.That(result.Email, Is.EqualTo(expected.Email)); + Assert.That(result.BirthDate, Is.EqualTo(expected.BirthDate)); + }); + } + + public static void Equal(CredentialEntity expected, CredentialEntity result) + { + Assert.Multiple(() => + { + Assert.That(result.Id, Is.EqualTo(expected.Id)); + Assert.That(result.Password, Is.EqualTo(expected.Password)); + Assert.That(result.Login, Is.EqualTo(expected.Login)); + Assert.That(result.UserId, Is.EqualTo(expected.UserId)); + }); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Common.Tests/Assertions/ModelAssert.cs b/tests/auth/FinancialHub.Auth.Common.Tests/Assertions/ModelAssert.cs new file mode 100644 index 0000000..00f85ac --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Common.Tests/Assertions/ModelAssert.cs @@ -0,0 +1,50 @@ +namespace FinancialHub.Auth.Common.Tests.Assertions +{ + public static class ModelAssert + { + public static void Equal(UserModel expected, UserModel? actual) + { + Assert.Multiple(() => + { + Assert.That(actual, Is.Not.Null); + Assert.That(actual!.Id, Is.EqualTo(expected.Id)); + Assert.That(actual!.FirstName, Is.EqualTo(expected.FirstName)); + Assert.That(actual!.LastName, Is.EqualTo(expected.LastName)); + Assert.That(actual!.Email, Is.EqualTo(expected.Email)); + Assert.That(actual!.BirthDate, Is.EqualTo(expected.BirthDate)); + }); + } + + public static void Equal(UserEntity expected, UserModel actual) + { + Assert.Multiple(() => + { + Assert.That(actual.Id, Is.EqualTo(expected.Id)); + Assert.That(actual.FirstName, Is.EqualTo(expected.FirstName)); + Assert.That(actual.LastName, Is.EqualTo(expected.LastName)); + Assert.That(actual.Email, Is.EqualTo(expected.Email)); + Assert.That(actual.BirthDate, Is.EqualTo(expected.BirthDate)); + }); + } + + public static void Equal(CredentialModel expected, CredentialModel result) + { + Assert.Multiple(() => + { + Assert.That(result.Password, Is.EqualTo(expected.Password)); + Assert.That(result.Login, Is.EqualTo(expected.Login)); + Assert.That(result.UserId, Is.EqualTo(expected.UserId)); + }); + } + + public static void Equal(CredentialModel expected, CredentialEntity result) + { + Assert.Multiple(() => + { + Assert.That(result.Password, Is.EqualTo(expected.Password)); + Assert.That(result.Login, Is.EqualTo(expected.Login)); + Assert.That(result.UserId, Is.EqualTo(expected.UserId)); + }); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Common.Tests/Builders/Entities/UserCredentialEntityBuilder.cs b/tests/auth/FinancialHub.Auth.Common.Tests/Builders/Entities/UserCredentialEntityBuilder.cs new file mode 100644 index 0000000..49c0cfc --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Common.Tests/Builders/Entities/UserCredentialEntityBuilder.cs @@ -0,0 +1,42 @@ +namespace FinancialHub.Auth.Common.Tests.Builders.Entities +{ + public class UserCredentialEntityBuilder : BaseEntityBuilder + { + private readonly UserEntityBuilder userBuilder; + public UserCredentialEntityBuilder() + { + this.userBuilder = new UserEntityBuilder(); + + var user = userBuilder.Generate(); + RuleFor(x => x.Login, x => x.Person.Email); + RuleFor(x => x.Password, x => x.Hashids.Encode(x.Random.Digits(10))); + RuleFor(x => x.User, user); + RuleFor(x => x.UserId, user.Id); + } + + public UserCredentialEntityBuilder WithLogin(string? login) + { + RuleFor(x => x.Login, login); + return this; + } + + public UserCredentialEntityBuilder WithPassword(string? password) + { + RuleFor(x => x.Password, password); + return this; + } + + public UserCredentialEntityBuilder WithUserId(Guid? id) + { + RuleFor(x => x.UserId, id); + this.WithUser(default); + return this; + } + + public UserCredentialEntityBuilder WithUser(UserEntity? user) + { + RuleFor(x => x.User, user); + return this; + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Common.Tests/Builders/Entities/UserEntityBuilder.cs b/tests/auth/FinancialHub.Auth.Common.Tests/Builders/Entities/UserEntityBuilder.cs new file mode 100644 index 0000000..bb042f2 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Common.Tests/Builders/Entities/UserEntityBuilder.cs @@ -0,0 +1,37 @@ +namespace FinancialHub.Auth.Common.Tests.Builders.Entities +{ + public class UserEntityBuilder : BaseEntityBuilder + { + public UserEntityBuilder() + { + this.RuleFor(x => x.FirstName, x => x.Person.FirstName); + this.RuleFor(x => x.LastName, x => x.Person.LastName); + this.RuleFor(x => x.BirthDate, x => x.Person.DateOfBirth); + this.RuleFor(x => x.Email, x => x.Person.Email); + } + + public UserEntityBuilder WithName(string name) + { + this.RuleFor(x => x.FirstName, name); + return this; + } + + public UserEntityBuilder WithLastName(string name) + { + this.RuleFor(x => x.LastName, name); + return this; + } + + public UserEntityBuilder WithBirthDate(DateTime date) + { + this.RuleFor(x => x.BirthDate, date); + return this; + } + + public UserEntityBuilder WithEmail(string email) + { + this.RuleFor(x => x.Email, email); + return this; + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Common.Tests/Builders/Models/SigninModelBuilder.cs b/tests/auth/FinancialHub.Auth.Common.Tests/Builders/Models/SigninModelBuilder.cs new file mode 100644 index 0000000..ec31716 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Common.Tests/Builders/Models/SigninModelBuilder.cs @@ -0,0 +1,23 @@ +namespace FinancialHub.Auth.Common.Tests.Builders.Models +{ + public class SigninModelBuilder : AutoFaker + { + public SigninModelBuilder() + { + RuleFor(x => x.Email, x => x.Person.Email); + RuleFor(x => x.Password, x=> x.Hashids.Encode(x.Random.Digits(10))); + } + + public SigninModelBuilder WithEmail(string email) + { + RuleFor(x => x.Email, email); + return this; + } + + public SigninModelBuilder WithPassword(string password) + { + RuleFor(x => x.Password, password); + return this; + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Common.Tests/Builders/Models/SignupModelBuilder.cs b/tests/auth/FinancialHub.Auth.Common.Tests/Builders/Models/SignupModelBuilder.cs new file mode 100644 index 0000000..18f0da2 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Common.Tests/Builders/Models/SignupModelBuilder.cs @@ -0,0 +1,58 @@ +namespace FinancialHub.Auth.Common.Tests.Builders.Models +{ + public class SignupModelBuilder : AutoFaker + { + public SignupModelBuilder() + { + RuleFor(x => x.FirstName, x=> x.Person.FirstName); + RuleFor(x => x.LastName, x=> x.Person.LastName); + RuleFor(x => x.Email, x=> x.Person.Email); + RuleFor(x => x.Password, x=> x.Hashids.Encode(x.Random.Digits(10))); + RuleFor(x => x.ConfirmPassword, (x, sign)=> sign.Password); + RuleFor(x => x.BirthDate, x=> x.Person.DateOfBirth); + } + + public SignupModelBuilder WithFirstName(string firstName) + { + RuleFor(x => x.FirstName, firstName); + return this; + } + + public SignupModelBuilder WithLastName(string lastName) + { + RuleFor(x => x.LastName, lastName); + return this; + } + + public SignupModelBuilder WithEmail(string email) + { + RuleFor(x => x.Email, email); + return this; + } + + public SignupModelBuilder WithValidPassword(string password) + { + RuleFor(x => x.Password, password); + RuleFor(x => x.ConfirmPassword, password); + return this; + } + + public SignupModelBuilder WithPassword(string password) + { + RuleFor(x => x.Password, password); + return this; + } + + public SignupModelBuilder WithConfirmPassword(string password) + { + RuleFor(x => x.ConfirmPassword, password); + return this; + } + + public SignupModelBuilder WithBirthDate(DateTime birthDate) + { + RuleFor(x => x.BirthDate, birthDate); + return this; + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Common.Tests/Builders/Models/TokenModelBuilder.cs b/tests/auth/FinancialHub.Auth.Common.Tests/Builders/Models/TokenModelBuilder.cs new file mode 100644 index 0000000..6ab364e --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Common.Tests/Builders/Models/TokenModelBuilder.cs @@ -0,0 +1,11 @@ +namespace FinancialHub.Auth.Common.Tests.Builders.Models +{ + public class TokenModelBuilder : AutoFaker + { + public TokenModelBuilder() + { + RuleFor(x => x.ExpiresIn, x => x.Date.Soon()); + RuleFor(x => x.Token, x => x.Hashids.Encode(x.Random.Digits(10))); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Common.Tests/Builders/Models/UserCredentialModelBuilder.cs b/tests/auth/FinancialHub.Auth.Common.Tests/Builders/Models/UserCredentialModelBuilder.cs new file mode 100644 index 0000000..c679282 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Common.Tests/Builders/Models/UserCredentialModelBuilder.cs @@ -0,0 +1,29 @@ +namespace FinancialHub.Auth.Common.Tests.Builders.Models +{ + public class UserCredentialModelBuilder : AutoFaker + { + public UserCredentialModelBuilder() + { + RuleFor(x => x.Login, X => X.Person.Email); + RuleFor(x => x.Password, x => x.Hashids.Encode(x.Random.Digits(10))); + } + + public UserCredentialModelBuilder WithLogin(string? login) + { + RuleFor(x => x.Login, login); + return this; + } + + public UserCredentialModelBuilder WithPassword(string? password) + { + RuleFor(x => x.Password, password); + return this; + } + + public UserCredentialModelBuilder WithUserId(Guid? id) + { + RuleFor(x => x.UserId, id); + return this; + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Common.Tests/Builders/Models/UserModelBuilder.cs b/tests/auth/FinancialHub.Auth.Common.Tests/Builders/Models/UserModelBuilder.cs new file mode 100644 index 0000000..6246635 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Common.Tests/Builders/Models/UserModelBuilder.cs @@ -0,0 +1,37 @@ +namespace FinancialHub.Auth.Common.Tests.Builders.Models +{ + public class UserModelBuilder : BaseModelBuilder + { + public UserModelBuilder() + { + this.RuleFor(x => x.FirstName, x => x.Person.FirstName); + this.RuleFor(x => x.LastName, x => x.Person.LastName); + this.RuleFor(x => x.BirthDate, x => x.Person.DateOfBirth); + this.RuleFor(x => x.Email, x => x.Person.Email); + } + + public UserModelBuilder WithName(string name) + { + this.RuleFor(x => x.FirstName, name); + return this; + } + + public UserModelBuilder WithLastName(string name) + { + this.RuleFor(x => x.LastName, name); + return this; + } + + public UserModelBuilder WithBirthDate(DateTime? date) + { + this.RuleFor(x => x.BirthDate, date); + return this; + } + + public UserModelBuilder WithEmail(string email) + { + this.RuleFor(x => x.Email, email); + return this; + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Common.Tests/FinancialHub.Auth.Common.Tests.csproj b/tests/auth/FinancialHub.Auth.Common.Tests/FinancialHub.Auth.Common.Tests.csproj new file mode 100644 index 0000000..261d7c9 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Common.Tests/FinancialHub.Auth.Common.Tests.csproj @@ -0,0 +1,19 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + diff --git a/tests/auth/FinancialHub.Auth.Common.Tests/Usings.cs b/tests/auth/FinancialHub.Auth.Common.Tests/Usings.cs new file mode 100644 index 0000000..d110713 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Common.Tests/Usings.cs @@ -0,0 +1,8 @@ +global using NUnit.Framework; +global using AutoBogus; + +global using FinancialHub.Auth.Domain.Entities; +global using FinancialHub.Auth.Domain.Models; + +global using FinancialHub.Common.Tests.Builders.Entities; +global using FinancialHub.Common.Tests.Builders.Models; diff --git a/tests/auth/FinancialHub.Auth.Infra.Data.Tests/Assertions/DbContextAssert.cs b/tests/auth/FinancialHub.Auth.Infra.Data.Tests/Assertions/DbContextAssert.cs new file mode 100644 index 0000000..ccba77b --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Infra.Data.Tests/Assertions/DbContextAssert.cs @@ -0,0 +1,17 @@ +namespace FinancialHub.Auth.Infra.Data.Tests.Assertions +{ + internal static class DbContextAssert + { + internal static void AssertCreated(DbContext context,T createdItem) + where T : BaseEntity + { + Assert.Multiple(() => + { + Assert.That(context.Set().ToList(), Is.Not.Empty); + + var datebaseUser = context.Set().First(u => u.Id == createdItem.Id); + Assert.That(datebaseUser, Is.EqualTo(createdItem)); + }); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Infra.Data.Tests/FinancialHub.Auth.Infra.Data.Tests.csproj b/tests/auth/FinancialHub.Auth.Infra.Data.Tests/FinancialHub.Auth.Infra.Data.Tests.csproj new file mode 100644 index 0000000..8ddad64 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Infra.Data.Tests/FinancialHub.Auth.Infra.Data.Tests.csproj @@ -0,0 +1,26 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + + + + + + diff --git a/tests/auth/FinancialHub.Auth.Infra.Data.Tests/Fixtures/DatabaseFixture.cs b/tests/auth/FinancialHub.Auth.Infra.Data.Tests/Fixtures/DatabaseFixture.cs new file mode 100644 index 0000000..a6c8e32 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Infra.Data.Tests/Fixtures/DatabaseFixture.cs @@ -0,0 +1,69 @@ +using FinancialHub.Auth.Infra.Data.Contexts; +using Microsoft.Data.Sqlite; +using System.Collections; + +namespace FinancialHub.Auth.Infra.Data.Tests.Fixtures +{ + public class DatabaseFixture : IEnumerable, IDisposable + { + public void Dispose() + { + GC.SuppressFinalize(this); + } + + public IEnumerator GetEnumerator() + { + yield return this; + } + + protected FinancialHubAuthContext context; + public FinancialHubAuthContext Context => context; + + protected Random random; + public Random Random => random; + + protected static FinancialHubAuthContext GetContext() + { + var conn = new SqliteConnection("DataSource=:memory:"); + conn.Open(); + var cfg = new DbContextOptionsBuilder().UseSqlite(conn); + cfg.EnableSensitiveDataLogging(true); + + return new FinancialHubAuthContext( + cfg.Options + ); + } + + public async Task InsertData(T item) where T: BaseEntity + { + var res = await this.context.Set().AddAsync(item); + item.Id = res.Entity.Id; + await this.context.SaveChangesAsync(); + res.State = EntityState.Detached; + + return res.Entity; + } + + public async Task> InsertData(IEnumerable items) where T : BaseEntity + { + await this.context.Set().AddRangeAsync(items); + await this.context.SaveChangesAsync(); + + return items; + } + + public void SetUp() + { + this.context = GetContext(); + this.context.Database.EnsureCreated(); + this.random = new Random(); + } + + public void TearDown() + { + this.context.Database.EnsureDeleted(); + this.context.Database.CloseConnection(); + this.Dispose(); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Infra.Data.Tests/Repositories/Credentials/CredentialRepositoryTests.create.cs b/tests/auth/FinancialHub.Auth.Infra.Data.Tests/Repositories/Credentials/CredentialRepositoryTests.create.cs new file mode 100644 index 0000000..6f02071 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Infra.Data.Tests/Repositories/Credentials/CredentialRepositoryTests.create.cs @@ -0,0 +1,40 @@ +namespace FinancialHub.Auth.Infra.Data.Tests.Repositories.Credentials +{ + public partial class CredentialRepositoryTests + { + [Test] + public async Task CreateAsync_ValidCredeential_InsertsCredential() + { + var user = await this.fixture.InsertData(this.userBuilder.Generate()); + var credential = this.builder.WithUserId(user.Id).Generate(); + + await this.repository.CreateAsync(credential); + + DbContextAssert.AssertCreated(this.fixture.Context, credential); + } + + [Test] + public async Task CreateAsync_ValidCredential_ReturnsCreatedCredential() + { + var user = await this.fixture.InsertData(this.userBuilder.Generate()); + var credential = this.builder.WithUserId(user.Id).Generate(); + + var result = await this.repository.CreateAsync(credential); + + Assert.That(result, Is.Not.Null); + EntityAssert.Equal(credential, result); + } + + [Test] + public async Task CreateAsync_CredentialWithExistingId_InsertsIntoDatabaseWithDifferentId() + { + var user = await this.fixture.InsertData(this.userBuilder.Generate()); + var credential = this.builder.WithUserId(user.Id).Generate(); + + await this.repository.CreateAsync(credential); + await this.repository.CreateAsync(credential); + + DbContextAssert.AssertCreated(this.fixture.Context, credential); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Infra.Data.Tests/Repositories/Credentials/CredentialRepositoryTests.cs b/tests/auth/FinancialHub.Auth.Infra.Data.Tests/Repositories/Credentials/CredentialRepositoryTests.cs new file mode 100644 index 0000000..0e4026a --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Infra.Data.Tests/Repositories/Credentials/CredentialRepositoryTests.cs @@ -0,0 +1,31 @@ +namespace FinancialHub.Auth.Infra.Data.Tests.Repositories.Credentials +{ + [TestFixtureSource(typeof(DatabaseFixture))] + public partial class CredentialRepositoryTests + { + private readonly DatabaseFixture fixture; + private ICredentialRepository repository; + private UserCredentialEntityBuilder builder; + private UserEntityBuilder userBuilder; + + public CredentialRepositoryTests(DatabaseFixture fixture) + { + this.fixture = fixture; + } + + [SetUp] + public void SetUp() + { + this.fixture.SetUp(); + this.builder = new UserCredentialEntityBuilder(); + this.userBuilder = new UserEntityBuilder(); + this.repository = new CredentialRepository(this.fixture.Context); + } + + [TearDown] + public void TearDown() + { + this.fixture.TearDown(); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Infra.Data.Tests/Repositories/Credentials/CredentialRepositoryTests.get.cs b/tests/auth/FinancialHub.Auth.Infra.Data.Tests/Repositories/Credentials/CredentialRepositoryTests.get.cs new file mode 100644 index 0000000..4d93b30 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Infra.Data.Tests/Repositories/Credentials/CredentialRepositoryTests.get.cs @@ -0,0 +1,88 @@ +namespace FinancialHub.Auth.Infra.Data.Tests.Repositories.Credentials +{ + public partial class CredentialRepositoryTests + { + [Test] + public async Task GetAsyncByLogin_ExistingCredential_ReturnsCredential() + { + var user = await this.fixture.InsertData(userBuilder.Generate()); + var credential = builder + .WithUserId(user.Id) + .Generate(); + await this.fixture.InsertData(credential); + + var result = await this.repository.GetAsync(credential.Login); + + Assert.That(result, Is.Not.Null); + EntityAssert.Equal(credential, result); + } + + [Test] + public async Task GetAsyncByLogin_NotExistingCredential_ReturnsNull() + { + var user = await this.fixture.InsertData(userBuilder.Generate()); + var credential = builder + .WithUserId(user.Id) + .Generate(); + + var result = await this.repository.GetAsync(credential.Login); + + Assert.That(result, Is.Null); + } + + [Test] + public async Task GetAsyncByLoginAndPassword_ExistingCredential_ReturnsCredential() + { + var user = await this.fixture.InsertData(userBuilder.Generate()); + var credential = builder + .WithUserId(user.Id) + .Generate(); + await this.fixture.InsertData(credential); + + var result = await this.repository.GetAsync(credential.Login, credential.Password); + + Assert.That(result, Is.Not.Null); + EntityAssert.Equal(credential, result); + } + + [Test] + public async Task GetAsyncByLoginAndPassword_NotExistingCredential_ReturnsNull() + { + var user = await this.fixture.InsertData(userBuilder.Generate()); + var credential = builder + .WithUserId(user.Id) + .Generate(); + + var result = await this.repository.GetAsync(credential.Login, credential.Password); + + Assert.That(result, Is.Null); + } + + [Test] + public async Task GetAsyncByUserId_ExistingCredential_ReturnsCredential() + { + var expectedCount = fixture.Random.Next(1,10); + var user = await this.fixture.InsertData(userBuilder.Generate()); + + var credentials = builder + .WithUserId(user.Id) + .Generate(expectedCount); + await this.fixture.InsertData(credentials); + + var result = await this.repository.GetAsync(user.Id.GetValueOrDefault()); + + Assert.That(result, Is.Not.Empty); + Assert.That(result.Count(), Is.EqualTo(expectedCount)); + } + + [Test] + public async Task GetAsyncByUserId_NotExistingCredential_ReturnsEmptyArray() + { + var user = await this.fixture.InsertData(userBuilder.Generate()); + + var result = await this.repository.GetAsync(user.Id.GetValueOrDefault()); + + Assert.That(result, Is.Empty); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Infra.Data.Tests/Repositories/Users/UserRepositoryTests.create.cs b/tests/auth/FinancialHub.Auth.Infra.Data.Tests/Repositories/Users/UserRepositoryTests.create.cs new file mode 100644 index 0000000..ae3298b --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Infra.Data.Tests/Repositories/Users/UserRepositoryTests.create.cs @@ -0,0 +1,37 @@ +namespace FinancialHub.Auth.Infra.Data.Tests.Repositories +{ + public partial class UserRepositoryTests + { + [Test] + public async Task CreateAsync_ValidUser_InsertsUser() + { + var user = this.builder.Generate(); + + await this.repository.CreateAsync(user); + + DbContextAssert.AssertCreated(fixture.Context, user); + } + + [Test] + public async Task CreateAsync_ValidUser_ReturnsCreatedUser() + { + var user = this.builder.Generate(); + + var result = await this.repository.CreateAsync(user); + + Assert.That(result, Is.Not.Null); + EntityAssert.Equal(user, result); + } + + [Test] + public async Task CreateAsync_UserWithExistingId_InsertsIntoDatabaseWithDifferentId() + { + var user = this.builder.Generate(); + + await this.repository.CreateAsync(user); + await this.repository.CreateAsync(user); + + DbContextAssert.AssertCreated(fixture.Context, user); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Infra.Data.Tests/Repositories/Users/UserRepositoryTests.cs b/tests/auth/FinancialHub.Auth.Infra.Data.Tests/Repositories/Users/UserRepositoryTests.cs new file mode 100644 index 0000000..e4714a2 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Infra.Data.Tests/Repositories/Users/UserRepositoryTests.cs @@ -0,0 +1,29 @@ +namespace FinancialHub.Auth.Infra.Data.Tests.Repositories +{ + [TestFixtureSource(typeof(DatabaseFixture))] + public partial class UserRepositoryTests + { + private readonly DatabaseFixture fixture; + private IUserRepository repository; + private UserEntityBuilder builder; + + public UserRepositoryTests(DatabaseFixture fixture) + { + this.fixture = fixture; + } + + [SetUp] + public void SetUp() + { + this.fixture.SetUp(); + this.builder = new UserEntityBuilder(); + this.repository = new UserRepository(this.fixture.Context); + } + + [TearDown] + public void TearDown() + { + this.fixture.TearDown(); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Infra.Data.Tests/Repositories/Users/UserRepositoryTests.get.cs b/tests/auth/FinancialHub.Auth.Infra.Data.Tests/Repositories/Users/UserRepositoryTests.get.cs new file mode 100644 index 0000000..c2145bc --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Infra.Data.Tests/Repositories/Users/UserRepositoryTests.get.cs @@ -0,0 +1,26 @@ +namespace FinancialHub.Auth.Infra.Data.Tests.Repositories +{ + public partial class UserRepositoryTests + { + #region GetById + [Test] + public async Task GetAsync_ExistingUser_ReturnsUser() + { + var expectedUser = fixture.Context.Users.Add(builder.Generate()).Entity; + fixture.Context.SaveChanges(); + + var user = await repository.GetAsync(expectedUser.Id.GetValueOrDefault()); + + EntityAssert.Equal(expectedUser, user!); + } + + [Test] + public async Task GetAsync_NotExistingUser_ReturnsNull() + { + var user = await repository.GetAsync(Guid.NewGuid()); + + Assert.That(user, Is.Null); + } + #endregion + } +} diff --git a/tests/auth/FinancialHub.Auth.Infra.Data.Tests/Repositories/Users/UserRepositoryTests.update.cs b/tests/auth/FinancialHub.Auth.Infra.Data.Tests/Repositories/Users/UserRepositoryTests.update.cs new file mode 100644 index 0000000..b132199 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Infra.Data.Tests/Repositories/Users/UserRepositoryTests.update.cs @@ -0,0 +1,28 @@ +namespace FinancialHub.Auth.Infra.Data.Tests.Repositories +{ + public partial class UserRepositoryTests + { + [Test] + public async Task UpdateAsync_ExistingUser_ReturnsUpdatedUser() + { + var item = await this.fixture.InsertData(builder.Generate()); + + var newUser = builder.WithId(item.Id.GetValueOrDefault()).Generate(); + var updated = await repository.UpdateAsync(newUser); + + EntityAssert.Equal(newUser, updated); + } + + [Test] + public async Task UpdateAsync_ExistingUser_UpdatesUser() + { + var oldUser = await this.fixture.InsertData(builder.Generate()); + + var newUser = builder.WithId(oldUser.Id.GetValueOrDefault()).Generate(); + await repository.UpdateAsync(newUser); + + var data = fixture.Context.Users.FirstOrDefault(x => x.Id == oldUser.Id.GetValueOrDefault()); + EntityAssert.Equal(newUser, data!); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Infra.Data.Tests/Usings.cs b/tests/auth/FinancialHub.Auth.Infra.Data.Tests/Usings.cs new file mode 100644 index 0000000..0740575 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Infra.Data.Tests/Usings.cs @@ -0,0 +1,16 @@ +global using NUnit.Framework; +global using Microsoft.EntityFrameworkCore; + +global using FinancialHub.Common.Entities; + +global using FinancialHub.Auth.Common.Tests.Assertions; +global using FinancialHub.Auth.Common.Tests.Builders.Entities; + +global using FinancialHub.Auth.Domain.Interfaces.Repositories; + +global using FinancialHub.Auth.Infra.Data.Repositories; + +global using FinancialHub.Auth.Infra.Data.Tests.Fixtures; +global using FinancialHub.Auth.Infra.Data.Tests.Assertions; + +[assembly: Category("Unit")] \ No newline at end of file diff --git a/tests/auth/FinancialHub.Auth.Infra.Tests/FinancialHub.Auth.Infra.Tests.csproj b/tests/auth/FinancialHub.Auth.Infra.Tests/FinancialHub.Auth.Infra.Tests.csproj new file mode 100644 index 0000000..a638c99 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Infra.Tests/FinancialHub.Auth.Infra.Tests.csproj @@ -0,0 +1,25 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + + + + + diff --git a/tests/auth/FinancialHub.Auth.Infra.Tests/Providers/Credentials/CredentialProviderTests.create.cs b/tests/auth/FinancialHub.Auth.Infra.Tests/Providers/Credentials/CredentialProviderTests.create.cs new file mode 100644 index 0000000..cb7c6ea --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Infra.Tests/Providers/Credentials/CredentialProviderTests.create.cs @@ -0,0 +1,21 @@ +namespace FinancialHub.Auth.Infra.Tests.Providers +{ + public partial class CredentialProviderTests + { + [Test] + public async Task CreateAsync_ValidUser_ReturnsCreatedUser() + { + var credential = this.builder.Generate(); + + var credentialEntity = this.mapper.Map(credential); + mockRepository + .Setup(x => x.CreateAsync(It.IsAny())) + .ReturnsAsync(credentialEntity); + + var createdUser = await this.provider.CreateAsync(credential); + + Assert.That(createdUser, Is.Not.Null); + ModelAssert.Equal(credential, createdUser); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Infra.Tests/Providers/Credentials/CredentialProviderTests.cs b/tests/auth/FinancialHub.Auth.Infra.Tests/Providers/Credentials/CredentialProviderTests.cs new file mode 100644 index 0000000..716b163 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Infra.Tests/Providers/Credentials/CredentialProviderTests.cs @@ -0,0 +1,29 @@ +using FinancialHub.Auth.Domain.Interfaces.Helpers; + +namespace FinancialHub.Auth.Infra.Tests.Providers +{ + public partial class CredentialProviderTests + { + private IMapper mapper; + private Mock mockRepository; + private Mock passwordHelperMock; + private ICredentialProvider provider; + + private UserCredentialModelBuilder builder; + + [SetUp] + public void SetUp() + { + this.builder = new UserCredentialModelBuilder(); + + this.passwordHelperMock = new Mock(); + this.mapper = new MapperConfiguration(mc => + { + mc.AddProfile(new FinancialHubAuthCredentialProfile(this.passwordHelperMock.Object)); + } + ).CreateMapper(); + this.mockRepository = new Mock(); + this.provider = new CredentialProvider(this.mockRepository.Object, this.mapper); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Infra.Tests/Providers/Credentials/CredentialProviderTests.get.cs b/tests/auth/FinancialHub.Auth.Infra.Tests/Providers/Credentials/CredentialProviderTests.get.cs new file mode 100644 index 0000000..0268e57 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Infra.Tests/Providers/Credentials/CredentialProviderTests.get.cs @@ -0,0 +1,71 @@ +namespace FinancialHub.Auth.Infra.Tests.Providers +{ + public partial class CredentialProviderTests + { + [Test] + public async Task GetAsyncByLogin_ExistingCredential_ReturnsCredential() + { + var credentialModel = this.builder.Generate(); + var login = credentialModel.Login; + var credentialEntity = this.mapper.Map(credentialModel); + + mockRepository + .Setup(x => x.GetAsync(login)) + .ReturnsAsync(credentialEntity) + .Verifiable(); + + var resultUser = await this.provider.GetAsync(login); + + Assert.That(resultUser, Is.Not.Null); + ModelAssert.Equal(credentialModel, credentialEntity); + mockRepository.Verify(x => x.GetAsync(login), Times.Once); + } + + [Test] + public async Task GetAsyncByLogin_NotExistingCredential_ReturnsNull() + { + var credentialModel = this.builder.Generate(); + var login = credentialModel.Login; + + mockRepository + .Setup(x => x.GetAsync(login)) + .ReturnsAsync((CredentialEntity?)default); + + var credential = await this.provider.GetAsync(login); + + Assert.That(credential, Is.Null); + mockRepository.Verify(x => x.GetAsync(login), Times.Once); + } + + [Test] + public async Task GetByCredentialAsync_ValidEmailAndPassword_ReturnsFullCredential() + { + var credentialModel = this.builder.Generate(); + var credentialEntity = this.mapper.Map(credentialModel); + + mockRepository + .Setup(x => x.GetAsync(credentialModel.Login, credentialModel.Password)) + .ReturnsAsync(credentialEntity) + .Verifiable(); + var resultUser = await this.provider.GetAsync(credentialModel); + + Assert.That(resultUser, Is.Not.Null); + ModelAssert.Equal(credentialModel, resultUser); + mockRepository.Verify(x => x.GetAsync(credentialModel.Login, credentialModel.Password), Times.Once); + } + + [Test] + public async Task GetByCredentialAsync_NotExistingCredential_ReturnsFullCredential() + { + var credentialModel = this.builder.Generate(); + + mockRepository + .Setup(x => x.GetAsync(credentialModel.Login, credentialModel.Password)) + .ReturnsAsync((CredentialEntity?)default); + + var credential = await this.provider.GetAsync(credentialModel); + + Assert.That(credential, Is.Null); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Infra.Tests/Providers/Signin/SigninProviderTests.cs b/tests/auth/FinancialHub.Auth.Infra.Tests/Providers/Signin/SigninProviderTests.cs new file mode 100644 index 0000000..bfc2ad1 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Infra.Tests/Providers/Signin/SigninProviderTests.cs @@ -0,0 +1,41 @@ +using FinancialHub.Auth.Domain.Interfaces.Helpers; +using FinancialHub.Auth.Domain.Models; + +namespace FinancialHub.Auth.Infra.Tests.Providers +{ + public partial class SigninProviderTests + { + private IMapper mapper; + + private Mock credentialProvider; + private Mock userProvider; + private Mock passwordHelper; + private ISigninProvider provider; + + private SigninModelBuilder builder; + private UserCredentialModelBuilder credentialBuilder; + private UserModelBuilder userBuilder; + + [SetUp] + public void SetUp() + { + this.builder = new SigninModelBuilder(); + this.credentialBuilder = new UserCredentialModelBuilder(); + this.userBuilder = new UserModelBuilder(); + + this.passwordHelper = new Mock(); + this.passwordHelper + .Setup(x => x.Encrypt(It.IsAny())) + .Returns(x => x); + + this.mapper = new MapperConfiguration(mc => + { + mc.AddProfile(new FinancialHubAuthCredentialProfile(this.passwordHelper.Object)); + }).CreateMapper(); + + this.userProvider = new Mock(); + this.credentialProvider = new Mock(); + this.provider = new SigninProvider(this.credentialProvider.Object, this.userProvider.Object, this.mapper); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Infra.Tests/Providers/Signin/SigninProviderTests.get_accounts.cs b/tests/auth/FinancialHub.Auth.Infra.Tests/Providers/Signin/SigninProviderTests.get_accounts.cs new file mode 100644 index 0000000..67b6b55 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Infra.Tests/Providers/Signin/SigninProviderTests.get_accounts.cs @@ -0,0 +1,45 @@ +namespace FinancialHub.Auth.Infra.Tests.Providers +{ + public partial class SigninProviderTests + { + [Test] + public async Task GetAccountAsync_ExistingCredential_ReturnsExistingUser() + { + var signin = builder.Generate(); + var existingUser = userBuilder + .WithEmail(signin.Email) + .Generate(); + var credential = credentialBuilder + .WithLogin(signin.Email) + .WithPassword(signin.Password) + .WithUserId(existingUser.Id) + .Generate(); + + credentialProvider + .Setup(x => x.GetAsync(It.Is(x => x.Login == signin.Email))) + .ReturnsAsync(credential); + userProvider + .Setup(x => x.GetAsync(existingUser.Id.GetValueOrDefault())) + .ReturnsAsync(existingUser); + + var result = await provider.GetAccountAsync(signin); + + Assert.That(result, Is.Not.Null); + ModelAssert.Equal(existingUser, result); + } + + [Test] + public async Task GetAccountAsync_NotExistingCredential_ReturnsNull() + { + var signin = builder.Generate(); + credentialProvider + .Setup(x => x.GetAsync(It.Is(x => x.Login == signin.Email))) + .ReturnsAsync(default(CredentialModel?)) + .Verifiable(); + var result = await provider.GetAccountAsync(signin); + + Assert.That(result, Is.Null); + credentialProvider.Verify(x => x.GetAsync(It.Is(x => x.Login == signin.Email)), Times.Once); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Infra.Tests/Providers/Signup/SignupProviderTests.create_account.cs b/tests/auth/FinancialHub.Auth.Infra.Tests/Providers/Signup/SignupProviderTests.create_account.cs new file mode 100644 index 0000000..76fceb4 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Infra.Tests/Providers/Signup/SignupProviderTests.create_account.cs @@ -0,0 +1,53 @@ +namespace FinancialHub.Auth.Infra.Tests.Providers +{ + public partial class SignupProviderTests + { + [Test] + public async Task CreateAccountAsync_ValidUser_ReturnsCreatedUser() + { + var signupModel = builder.Generate(); + + var user = mapper.Map(signupModel); + userProvider + .Setup(x => x.CreateAsync(It.IsAny())) + .ReturnsAsync(user); + + var credential = mapper.Map(signupModel); + credentialProvider + .Setup(x => x.CreateAsync(It.IsAny())) + .ReturnsAsync(credential); + + var createdAccount = await provider.CreateAccountAsync(signupModel); + + Assert.That(createdAccount, Is.Not.Null); + ModelAssert.Equal(user, createdAccount); + } + + [Test] + public async Task CreateAccountAsync_ValidUser_CallsCreateCredentialWithUserId() + { + var signupModel = builder.Generate(); + var guid = Guid.NewGuid(); + + var user = mapper.Map(signupModel); + user.Id = guid; + userProvider + .Setup(x => x.CreateAsync(It.IsAny())) + .ReturnsAsync(user); + + passwordHelper + .Setup(x => x.Encrypt(signupModel.Password)) + .Returns(signupModel.Password); + + var credential = mapper.Map(signupModel); + credentialProvider + .Setup(x => x.CreateAsync(It.IsAny())) + .ReturnsAsync(credential) + .Verifiable(); + + await provider.CreateAccountAsync(signupModel); + + credentialProvider.Verify(x => x.CreateAsync(It.Is(c => c.UserId == guid)), Times.Once); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Infra.Tests/Providers/Signup/SignupProviderTests.cs b/tests/auth/FinancialHub.Auth.Infra.Tests/Providers/Signup/SignupProviderTests.cs new file mode 100644 index 0000000..5e3ae5a --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Infra.Tests/Providers/Signup/SignupProviderTests.cs @@ -0,0 +1,32 @@ +using FinancialHub.Auth.Domain.Interfaces.Helpers; + +namespace FinancialHub.Auth.Infra.Tests.Providers +{ + public partial class SignupProviderTests + { + private IMapper mapper; + + private Mock credentialProvider; + private Mock userProvider; + private Mock passwordHelper; + private ISignupProvider provider; + + private SignupModelBuilder builder; + + [SetUp] + public void SetUp() + { + this.builder = new SignupModelBuilder(); + + this.passwordHelper = new Mock(); + this.mapper = new MapperConfiguration(mc => + { + mc.AddProfile(new FinancialHubAuthCredentialProfile(this.passwordHelper.Object)); + }).CreateMapper(); + + this.userProvider = new Mock(); + this.credentialProvider = new Mock(); + this.provider = new SignupProvider(this.credentialProvider.Object, this.userProvider.Object, this.mapper); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Infra.Tests/Providers/Users/UserProviderTests.create.cs b/tests/auth/FinancialHub.Auth.Infra.Tests/Providers/Users/UserProviderTests.create.cs new file mode 100644 index 0000000..a3f343f --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Infra.Tests/Providers/Users/UserProviderTests.create.cs @@ -0,0 +1,21 @@ +namespace FinancialHub.Auth.Infra.Tests.Providers +{ + public partial class UserProviderTests + { + [Test] + public async Task CreateAsync_ValidUser_ReturnsCreatedUser() + { + var user = this.builder.Generate(); + + var userEntity = this.mapper.Map(user); + mockRepository + .Setup(x => x.CreateAsync(It.IsAny())) + .ReturnsAsync(userEntity); + + var createdUser = await this.provider.CreateAsync(user); + + Assert.That(createdUser, Is.Not.Null); + ModelAssert.Equal(user, createdUser); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Infra.Tests/Providers/Users/UserProviderTests.cs b/tests/auth/FinancialHub.Auth.Infra.Tests/Providers/Users/UserProviderTests.cs new file mode 100644 index 0000000..58ead0c --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Infra.Tests/Providers/Users/UserProviderTests.cs @@ -0,0 +1,25 @@ +namespace FinancialHub.Auth.Infra.Tests.Providers +{ + public partial class UserProviderTests + { + private IMapper mapper; + private Mock mockRepository; + private IUserProvider provider; + + private UserModelBuilder builder; + + [SetUp] + public void SetUp() + { + this.builder = new UserModelBuilder(); + + this.mapper = new MapperConfiguration(mc => + { + mc.AddProfile(new FinancialHubAuthProviderProfile()); + } + ).CreateMapper(); + this.mockRepository = new Mock(); + this.provider = new UserProvider(this.mockRepository.Object, this.mapper); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Infra.Tests/Providers/Users/UserProviderTests.get.cs b/tests/auth/FinancialHub.Auth.Infra.Tests/Providers/Users/UserProviderTests.get.cs new file mode 100644 index 0000000..5c509bc --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Infra.Tests/Providers/Users/UserProviderTests.get.cs @@ -0,0 +1,35 @@ +namespace FinancialHub.Auth.Infra.Tests.Providers +{ + public partial class UserProviderTests + { + [Test] + public async Task GetByIdAsync_ExistingUser_ReturnsUser() + { + var id = Guid.NewGuid(); + var userModel = this.builder.WithId(id).Generate(); + + var userEntity = this.mapper.Map(userModel); + mockRepository + .Setup(x => x.GetAsync(id)) + .ReturnsAsync(userEntity); + var resultUser = await this.provider.GetAsync(id); + + Assert.That(resultUser, Is.Not.Null); + ModelAssert.Equal(userModel, resultUser); + } + + [Test] + public async Task GetByIdAsync_NotExistingUser_ReturnsNull() + { + var id = Guid.NewGuid(); + var userModel = this.builder.WithId(id).Generate(); + + mockRepository + .Setup(x => x.GetAsync(id)) + .ReturnsAsync((UserEntity?)default); + var user = await this.provider.GetAsync(id); + + Assert.That(user, Is.Null); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Infra.Tests/Providers/Users/UserProviderTests.update.cs b/tests/auth/FinancialHub.Auth.Infra.Tests/Providers/Users/UserProviderTests.update.cs new file mode 100644 index 0000000..bb8fc2f --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Infra.Tests/Providers/Users/UserProviderTests.update.cs @@ -0,0 +1,20 @@ +namespace FinancialHub.Auth.Infra.Tests.Providers +{ + public partial class UserProviderTests + { + [Test] + public async Task UpdateAsync_ValidUser_ReturnsUpdatedUser() + { + var user = this.builder.Generate(); + + var userEntity = this.mapper.Map(user); + mockRepository + .Setup(x => x.UpdateAsync(It.IsAny())) + .ReturnsAsync(userEntity); + + var updatedUser = await this.provider.UpdateAsync(user); + + ModelAssert.Equal(user, updatedUser); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Infra.Tests/Usings.cs b/tests/auth/FinancialHub.Auth.Infra.Tests/Usings.cs new file mode 100644 index 0000000..2d251d0 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Infra.Tests/Usings.cs @@ -0,0 +1,18 @@ +global using AutoMapper; + +global using Moq; +global using NUnit.Framework; + +global using FinancialHub.Auth.Common.Tests.Assertions; +global using FinancialHub.Auth.Common.Tests.Builders.Models; + +global using FinancialHub.Auth.Domain.Models; +global using FinancialHub.Auth.Domain.Entities; + +global using FinancialHub.Auth.Domain.Interfaces.Providers; +global using FinancialHub.Auth.Domain.Interfaces.Repositories; + +global using FinancialHub.Auth.Infra.Mappers; +global using FinancialHub.Auth.Infra.Providers; + +[assembly: Category("Unit")] \ No newline at end of file diff --git a/tests/auth/FinancialHub.Auth.IntegrationTests/Base/BaseControllerTests.cs b/tests/auth/FinancialHub.Auth.IntegrationTests/Base/BaseControllerTests.cs new file mode 100644 index 0000000..8bb07ac --- /dev/null +++ b/tests/auth/FinancialHub.Auth.IntegrationTests/Base/BaseControllerTests.cs @@ -0,0 +1,30 @@ +namespace FinancialHub.Auth.IntegrationTests.Base +{ + [TestFixtureSource(typeof(FinancialHubAuthApiFixture))] + public abstract class BaseControllerTests + { + protected readonly FinancialHubAuthApiFixture fixture; + protected HttpClient Client => fixture.Client; + + protected readonly string baseEndpoint; + protected readonly Random random; + + protected BaseControllerTests(FinancialHubAuthApiFixture fixture, string endpoint) + { + this.fixture = fixture; + this.baseEndpoint = endpoint; + } + + [SetUp] + public virtual void SetUp() + { + this.fixture.CreateDatabase(); + } + + [TearDown] + public virtual void TearDown() + { + this.fixture.ClearData(); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.IntegrationTests/Controllers/SigninControllerTests.cs b/tests/auth/FinancialHub.Auth.IntegrationTests/Controllers/SigninControllerTests.cs new file mode 100644 index 0000000..a959850 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.IntegrationTests/Controllers/SigninControllerTests.cs @@ -0,0 +1,162 @@ +using FinancialHub.Auth.Domain.Interfaces.Helpers; +using Microsoft.Extensions.DependencyInjection; +using System.IdentityModel.Tokens.Jwt; + +namespace FinancialHub.Auth.IntegrationTests.Controllers +{ + public class SigninControllerTests : BaseControllerTests + { + protected UserCredentialEntityBuilder credentialEntityBuilder; + protected UserEntityBuilder userEntityBuilder; + + protected SigninModelBuilder signinModelBuilder; + + public SigninControllerTests(FinancialHubAuthApiFixture fixture,string endpoint) + : base(fixture, "/signin" + endpoint) + { + } + + [SetUp] + public override void SetUp() + { + credentialEntityBuilder = new UserCredentialEntityBuilder(); + userEntityBuilder = new UserEntityBuilder(); + + signinModelBuilder = new SigninModelBuilder(); + + base.SetUp(); + } + public class Signin : SigninControllerTests + { + public Signin(FinancialHubAuthApiFixture fixture) : base(fixture, string.Empty){ } + + [Test] + public async Task Signin_ValidData_ReturnsValidToken() + { + var now = DateTime.UtcNow; + using var scope = this.fixture.Api.Services.CreateScope(); + + var id = Guid.NewGuid(); + var user = userEntityBuilder.WithId(id).Generate(); + fixture.AddData(user); + + var passwordHelper = scope.ServiceProvider.GetRequiredService(); + + var password = Guid.NewGuid().ToString(); + var encryptedPassword = passwordHelper.Encrypt(password); + var credential = credentialEntityBuilder + .WithUserId(id) + .WithLogin(user.Email) + .WithPassword(encryptedPassword) + .Generate(); + fixture.AddData(credential); + + var signin = signinModelBuilder + .WithEmail(user.Email) + .WithPassword(password) + .Generate(); + + var response = await Client.PostAsync(baseEndpoint, signin.ToHttpContent()); + + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + + var jsonResponse = await response.ReadContentAsync>(); + Assert.Multiple(() => + { + Assert.That(jsonResponse?.Data.ExpiresIn, Is.InRange(now.AddMinutes(55), now.AddMinutes(65))); + + var jwt = new JwtSecurityTokenHandler().ReadJwtToken(jsonResponse?.Data.Token); + Assert.That(jwt.Payload.Jti, Is.EqualTo(id.ToString())); + Assert.That(jwt.Payload.FirstOrDefault(x => x.Key == "email").Value , Is.EqualTo(user.Email)); + }); + } + + [Test] + public async Task Signin_ValidationError_ReturnsError() + { + var signin = signinModelBuilder + .WithEmail(string.Empty) + .WithPassword(string.Empty) + .Generate(); + + var response = await Client.PostAsync(baseEndpoint, signin.ToHttpContent()); + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); + + var jsonResponse = await response.ReadContentAsync(); + Assert.Multiple(() => + { + Assert.That(jsonResponse, Is.Not.Null); + Assert.That(jsonResponse?.Errors!.Count, Is.EqualTo(2)); + }); + } + + [Test] + public async Task Signin_NotExitingCredential_DoesNotReturnToken() + { + using(var scope = this.fixture.Api.Services.CreateScope()) + { + var id = Guid.NewGuid(); + var user = userEntityBuilder.WithId(id).Generate(); + fixture.AddData(user); + + var passwordHelper = scope.ServiceProvider.GetRequiredService(); + + var password = Guid.NewGuid().ToString(); + var encryptedPassword = passwordHelper.Encrypt(password); + var credential = credentialEntityBuilder + .WithUserId(id) + .WithLogin(user.Email) + .WithPassword(encryptedPassword) + .Generate(); + fixture.AddData(credential); + } + + var signin = signinModelBuilder.Generate(); + + var response = await Client.PostAsync(baseEndpoint, signin.ToHttpContent()); + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized)); + + var jsonResponse = await response.ReadContentAsync(); + Assert.Multiple(() => + { + Assert.That(jsonResponse, Is.Not.Null); + Assert.That(jsonResponse?.Message, Is.EqualTo("Wrong e-mail or password")); + }); + } + + [Test] + public async Task Signin_InvalidCredential_DoesNotReturnToken() + { + using var scope = this.fixture.Api.Services.CreateScope(); + var id = Guid.NewGuid(); + var user = userEntityBuilder.WithId(id).Generate(); + fixture.AddData(user); + + var passwordHelper = scope.ServiceProvider.GetRequiredService(); + + var password = Guid.NewGuid().ToString(); + var encryptedPassword = passwordHelper.Encrypt(password); + var credential = credentialEntityBuilder + .WithUserId(id) + .WithLogin(user.Email) + .WithPassword(encryptedPassword) + .Generate(); + fixture.AddData(credential); + + var signin = signinModelBuilder + .WithEmail(user.Email) + .Generate(); + + var response = await Client.PostAsync(baseEndpoint, signin.ToHttpContent()); + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized)); + + var jsonResponse = await response.ReadContentAsync(); + Assert.Multiple(() => + { + Assert.That(jsonResponse, Is.Not.Null); + Assert.That(jsonResponse?.Message, Is.EqualTo("Wrong e-mail or password")); + }); + } + } + } +} diff --git a/tests/auth/FinancialHub.Auth.IntegrationTests/Controllers/SignupControllerTests.cs b/tests/auth/FinancialHub.Auth.IntegrationTests/Controllers/SignupControllerTests.cs new file mode 100644 index 0000000..f0b382b --- /dev/null +++ b/tests/auth/FinancialHub.Auth.IntegrationTests/Controllers/SignupControllerTests.cs @@ -0,0 +1,125 @@ +namespace FinancialHub.Auth.IntegrationTests.Controllers +{ + public class SignupControllerTests : BaseControllerTests + { + protected SignupModelBuilder signupModelBuilder; + + protected UserCredentialModelBuilder credentialModelBuilder; + protected UserCredentialEntityBuilder credentialEntityBuilder; + + protected UserModelBuilder userModelBuilder; + protected UserEntityBuilder userEntityBuilder; + + public SignupControllerTests(FinancialHubAuthApiFixture fixture, string endpoint) + : base(fixture, "/signup" + endpoint) { } + + [SetUp] + public override void SetUp() + { + signupModelBuilder = new SignupModelBuilder(); + + credentialModelBuilder = new UserCredentialModelBuilder(); + credentialEntityBuilder = new UserCredentialEntityBuilder(); + + userModelBuilder = new UserModelBuilder(); + userEntityBuilder = new UserEntityBuilder(); + + base.SetUp(); + } + + public class Signup : SignupControllerTests + { + public Signup(FinancialHubAuthApiFixture fixture) : base(fixture, string.Empty) { } + + [Test] + public async Task Signup_ValidData_ReturnsCreatedUser() + { + var signup = signupModelBuilder.Generate(); + var user = userModelBuilder + .WithName(signup.FirstName) + .WithLastName(signup.LastName) + .WithEmail(signup.Email) + .WithBirthDate(signup.BirthDate) + .Generate(); + + var response = await Client.PostAsync(baseEndpoint, signup.ToHttpContent()); + var jsonResponse = await response.ReadContentAsync>(); + + Assert.Multiple(() => + { + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + Assert.That(jsonResponse, Is.Not.Null); + user.Id = jsonResponse!.Data.Id; + ModelAssert.Equal(user, jsonResponse.Data); + }); + } + + [Test] + public async Task Signup_ValidData_AddsUser() + { + var signup = signupModelBuilder.Generate(); + var response = await Client.PostAsync(baseEndpoint, signup.ToHttpContent()); + var jsonResponse = await response.ReadContentAsync>(); + + Assert.Multiple(() => + { + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + Assert.That(jsonResponse, Is.Not.Null); + + var user = fixture.GetData().FirstOrDefault(x => x.Id == jsonResponse!.Data.Id); + Assert.That(user, Is.Not.Null); + }); + } + + [Test] + public async Task Signup_ValidationError_ReturnsError() + { + var signup = signupModelBuilder + .WithEmail(string.Empty) + .WithPassword(string.Empty) + .WithConfirmPassword("A") + .WithFirstName(string.Empty) + .WithLastName(string.Empty) + .Generate(); + + var response = await Client.PostAsync(baseEndpoint, signup.ToHttpContent()); + var jsonResponse = await response.ReadContentAsync(); + + Assert.Multiple(() => + { + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); + Assert.That(jsonResponse, Is.Not.Null); + Assert.That(jsonResponse!.Errors!, Has.Count.EqualTo(5)); + }); + } + + [Test] + public async Task Signup_ExistingCredential_ReturnsError() + { + var signup = signupModelBuilder.Generate(); + + var userEntity = userEntityBuilder + .WithEmail(signup.Email) + .Generate(); + fixture.AddData(userEntity); + + var credentialEntity = credentialEntityBuilder + .WithLogin(signup.Email) + .WithPassword(signup.Password) + .WithUserId(userEntity.Id) + .Generate(); + fixture.AddData(credentialEntity); + + var response = await Client.PostAsync(baseEndpoint, signup.ToHttpContent()); + var jsonResponse = await response.ReadContentAsync(); + + Assert.Multiple(() => + { + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); + Assert.That(jsonResponse, Is.Not.Null); + Assert.That(jsonResponse!.Message, Is.EqualTo("Credential already exists")); + }); + } + } + } +} diff --git a/tests/auth/FinancialHub.Auth.IntegrationTests/Controllers/UsersControllerTests.cs b/tests/auth/FinancialHub.Auth.IntegrationTests/Controllers/UsersControllerTests.cs new file mode 100644 index 0000000..abe63f1 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.IntegrationTests/Controllers/UsersControllerTests.cs @@ -0,0 +1,299 @@ +namespace FinancialHub.Auth.IntegrationTests.Controllers +{ + public class UsersControllerTests : BaseControllerTests + { + protected UserModelBuilder modelBuilder; + protected UserEntityBuilder entityBuilder; + + public UsersControllerTests(FinancialHubAuthApiFixture fixture, string? endpoint = null) : + base(fixture, "/users" + endpoint) + { + } + + [SetUp] + public override void SetUp() + { + modelBuilder = new UserModelBuilder(); + entityBuilder = new UserEntityBuilder(); + base.SetUp(); + } + + public class CreateUser : UsersControllerTests + { + public CreateUser(FinancialHubAuthApiFixture fixture) : + base(fixture) + { + } + + [Test] + public async Task CreateUser_InvalidToken_Returns401Unauthorized() + { + var data = modelBuilder.Generate(); + + var response = await Client.PostAsync(baseEndpoint, data, "token"); + + Assert.Multiple(() => + { + Assert.That(response.IsSuccessStatusCode, Is.False); + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized)); + }); + } + + [Test] + public async Task CreateUser_ValidUser_ReturnsCreatedUser() + { + var data = modelBuilder.Generate(); + var token = this.fixture.GetAuthToken(data); + + var response = await Client.PostAsync(baseEndpoint, data, token); + var jsonResponse = await response.ReadContentAsync>(); + + Assert.Multiple(() => + { + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + Assert.That(jsonResponse, Is.Not.Null); + data.Id = jsonResponse!.Data.Id; + ModelAssert.Equal(data, jsonResponse.Data); + }); + } + + [Test] + public async Task CreateUser_ValidUser_CreatesUser() + { + var data = modelBuilder.Generate(); + var token = this.fixture.GetAuthToken(data); + + var response = await Client.PostAsync(baseEndpoint, data, token); + var jsonResponse = await response.ReadContentAsync>(); + + var id = jsonResponse!.Data.Id; + var user = fixture.GetData().FirstOrDefault(x => x.Id == id); + Assert.That(user, Is.Not.Null); + } + + [Test] + public async Task CreateUser_InvalidUser_ReturnsValidationError() + { + var data = modelBuilder + .WithName(string.Empty) + .WithLastName(string.Empty) + .WithEmail(string.Empty) + .WithBirthDate(default) + .Generate(); + var token = this.fixture.GetAuthToken(data); + + var response = await Client.PostAsync(baseEndpoint, data, token); + var jsonResponse = await response.ReadContentAsync(); + + Assert.That(jsonResponse!.Errors, Has.Count.EqualTo(4)); + } + + [Test] + public async Task CreateUser_InvalidUser_DoestNotCreateUser() + { + var data = modelBuilder + .WithName(string.Empty) + .WithLastName(string.Empty) + .WithEmail(string.Empty) + .WithBirthDate(default) + .Generate(); + var token = this.fixture.GetAuthToken(data); + + await Client.PostAsync(baseEndpoint, data, token); + + var user = fixture.GetData().FirstOrDefault(); + Assert.That(user, Is.Null); + } + } + + public class GetUser : UsersControllerTests + { + public GetUser(FinancialHubAuthApiFixture fixture) : base(fixture) + { + } + + [Test] + public async Task CreateUser_InvalidToken_Returns401Unauthorized() + { + var response = await Client.GetAsync(baseEndpoint + $"/{Guid.NewGuid()}", "token"); + + Assert.Multiple(() => + { + Assert.That(response.IsSuccessStatusCode, Is.False); + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized)); + }); + } + + [Test] + public async Task GetUser_NotExistingUser_ReturnsNotFound() + { + var token = this.fixture.GetAuthToken(modelBuilder.Generate()); + + var id = Guid.NewGuid().ToString(); + var response = await Client.GetAsync(baseEndpoint + $"/{id}", token); + + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.NotFound)); + } + + [Test] + public async Task GetUser_ExistingUser_ReturnsUser() + { + var id = Guid.NewGuid(); + var entity = entityBuilder.WithId(id).Generate(); + fixture.AddData(entity); + + var token = this.fixture.GetAuthToken(modelBuilder.Generate()); + + var response = await Client.GetAsync(baseEndpoint + $"/{id}", token); + var jsonResponse = await response.ReadContentAsync>(); + + Assert.Multiple(() => + { + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + Assert.That(jsonResponse, Is.Not.Null); + Assert.That(jsonResponse!.Data.Id, Is.EqualTo(id)); + }); + } + } + + public class UpdateUser : UsersControllerTests + { + public UpdateUser(FinancialHubAuthApiFixture fixture) : base(fixture) + { + } + + [Test] + public async Task CreateUser_InvalidToken_Returns401Unauthorized() + { + var id = Guid.NewGuid(); + var entity = entityBuilder.WithId(id).Generate(); + fixture.AddData(entity); + + var data = modelBuilder.WithId(id).Generate(); + + var response = await Client.PatchAsync(baseEndpoint + $"/{id}", data, "token"); + + Assert.Multiple(() => + { + Assert.That(response.IsSuccessStatusCode, Is.False); + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Unauthorized)); + }); + } + + [Test] + public async Task UpdateUser_ExistingUser_UpdatesUser() + { + var id = Guid.NewGuid(); + var entity = entityBuilder.WithId(id).Generate(); + fixture.AddData(entity); + + var data = modelBuilder.WithId(id).Generate(); + var token = this.fixture.GetAuthToken(data); + + await Client.PatchAsync(baseEndpoint + $"/{id}", data, token); + + var databaseUsers = fixture.GetData(); + var databaseUser = databaseUsers.FirstOrDefault(x => x.Id == id); + Assert.Multiple(() => + { + Assert.That(databaseUsers.Count(), Is.EqualTo(1)); + Assert.That(databaseUser, Is.Not.Null); + EntityAssert.Equal(databaseUser!, data); + }); + } + + [Test] + public async Task UpdateUser_ExistingUser_ReturnsUpdatedValues() + { + var id = Guid.NewGuid(); + var entity = entityBuilder.WithId(id).Generate(); + fixture.AddData(entity); + + var data = modelBuilder.WithId(id).Generate(); + var token = this.fixture.GetAuthToken(data); + + var response = await Client.PatchAsync(baseEndpoint + $"/{id}", data, token); + var jsonResponse = await response.ReadContentAsync>(); + + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK)); + ModelAssert.Equal(data, jsonResponse!.Data); + } + + [Test] + public async Task UpdateUser_InvalidUser_ReturnsValidationError() + { + var id = Guid.NewGuid(); + var entity = entityBuilder.WithId(id).Generate(); + fixture.AddData(entity); + + var data = modelBuilder + .WithName(string.Empty) + .WithLastName(string.Empty) + .WithEmail(string.Empty) + .WithBirthDate(default) + .WithId(id) + .Generate(); + var token = this.fixture.GetAuthToken(data); + + var response = await Client.PatchAsync(baseEndpoint + $"/{id}", data, token); + var jsonResponse = await response.ReadContentAsync(); + + Assert.That(jsonResponse!.Errors, Has.Count.EqualTo(4)); + } + + [Test] + public async Task UpdateUser_InvalidUser_DoesNotUpdate() + { + var id = Guid.NewGuid(); + var data = modelBuilder + .WithName(string.Empty) + .WithLastName(string.Empty) + .WithEmail(string.Empty) + .WithBirthDate(default) + .WithId(id) + .Generate(); + var token = this.fixture.GetAuthToken(data); + + await Client.PatchAsync(baseEndpoint + $"/{id}", data, token); + + var databaseUsers = fixture.GetData(); + var databaseUser = databaseUsers.FirstOrDefault(x => x.Id == id); + Assert.Multiple(() => + { + Assert.That(databaseUsers.Count(), Is.EqualTo(0)); + Assert.That(databaseUser, Is.Null); + }); + } + + [Test] + public async Task UpdateUser_NotExistingUser_ReturnsNotFound() + { + var id = Guid.NewGuid(); + var data = modelBuilder.WithId(id).Generate(); + var token = this.fixture.GetAuthToken(data); + + var response = await Client.PatchAsync(baseEndpoint + $"/{id}", data, token); + + Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.NotFound)); + } + + [Test] + public async Task UpdateUser_NotExistingUser_DoesNotUpdate() + { + var id = Guid.NewGuid(); + var data = modelBuilder.WithId(id).Generate(); + var token = this.fixture.GetAuthToken(data); + + await Client.PatchAsync(baseEndpoint + $"/{id}", data, token); + + var databaseUsers = fixture.GetData(); + var databaseUser = databaseUsers.FirstOrDefault(x => x.Id == id); + Assert.Multiple(() => + { + Assert.That(databaseUsers.Count(), Is.EqualTo(0)); + Assert.That(databaseUser, Is.Null); + }); + } + } + } +} diff --git a/tests/auth/FinancialHub.Auth.IntegrationTests/Extensions/HttpClientExtensions.cs b/tests/auth/FinancialHub.Auth.IntegrationTests/Extensions/HttpClientExtensions.cs new file mode 100644 index 0000000..0b80553 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.IntegrationTests/Extensions/HttpClientExtensions.cs @@ -0,0 +1,90 @@ +using FinancialHub.Auth.IntegrationTests.Extensions.Utils; +using Microsoft.VisualStudio.TestPlatform.CommunicationUtilities; +using System.Net.Http.Headers; + +namespace FinancialHub.Auth.IntegrationTests.Extensions +{ + public static class HttpClientExtensions + { + private static void AddHeaders(this HttpRequestHeaders headers, Dictionary headerData) + { + foreach (var header in headerData) + { + if (!headers.TryGetValues(header.Key, out _)) + { + headers.Add(header.Key, header.Value); + } + } + } + + private static async Task SendAsync(this HttpClient httpClient, HttpClientExtensionsParameters parameters) + { + var message = new HttpRequestMessage(parameters.Method, parameters.Url); + + message.Headers.AddHeaders(parameters.Headers); + + return await httpClient.SendAsync(message); + } + + private static async Task SendAsync(this HttpClient httpClient, HttpClientExtensionsParameters parameters) + { + var message = new HttpRequestMessage(parameters.Method, parameters.Url); + + if(parameters.Body != null) + { + message.Content = parameters.Body.ToHttpContent(); + } + + message.Headers.AddHeaders(parameters.Headers); + + return await httpClient.SendAsync(message); + } + + public static async Task GetAsync(this HttpClient httpClient, string url, string token) + { + var message = new HttpClientExtensionsParameters() + { + Url = url, + Method = HttpMethod.Get, + Headers = new Dictionary() + { + { "Authorization", $"Bearer {token}" } + } + }; + + return await httpClient.SendAsync(message); + } + + public static async Task PostAsync(this HttpClient httpClient, string url, T body, string token) + { + var message = new HttpClientExtensionsParameters() + { + Url = url, + Method = HttpMethod.Post, + Headers = new Dictionary() + { + { "Authorization", $"Bearer {token}" } + }, + Body = body, + }; + + return await httpClient.SendAsync(message); + } + + public static async Task PatchAsync(this HttpClient httpClient, string url, T body, string token) + { + var message = new HttpClientExtensionsParameters() + { + Url = url, + Method = HttpMethod.Patch, + Headers = new Dictionary() + { + { "Authorization", $"Bearer {token}" } + }, + Body = body, + }; + + return await httpClient.SendAsync(message); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.IntegrationTests/Extensions/HttpContentExtensions.cs b/tests/auth/FinancialHub.Auth.IntegrationTests/Extensions/HttpContentExtensions.cs new file mode 100644 index 0000000..e2a524c --- /dev/null +++ b/tests/auth/FinancialHub.Auth.IntegrationTests/Extensions/HttpContentExtensions.cs @@ -0,0 +1,18 @@ +using System.Net.Mime; +using System.Text; +using System.Text.Json; + +namespace FinancialHub.Auth.IntegrationTests.Extensions +{ + public static class HttpContentExtensions + { + public static HttpContent ToHttpContent(this T content) + { + return new StringContent( + content: JsonSerializer.Serialize(content), + encoding: Encoding.UTF8, + mediaType: MediaTypeNames.Application.Json + ); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.IntegrationTests/Extensions/HttpResponseMessageExtensions.cs b/tests/auth/FinancialHub.Auth.IntegrationTests/Extensions/HttpResponseMessageExtensions.cs new file mode 100644 index 0000000..7ea9971 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.IntegrationTests/Extensions/HttpResponseMessageExtensions.cs @@ -0,0 +1,27 @@ +using System.Text.Json; + +namespace FinancialHub.Auth.IntegrationTests.Extensions +{ + public static class HttpResponseMessageExtensions + { + public static async Task ReadContentAsync(this HttpResponseMessage response) + { + try + { + var stream = await response.Content.ReadAsStreamAsync(); + + return await JsonSerializer.DeserializeAsync(stream, + new JsonSerializerOptions() + { + PropertyNameCaseInsensitive = true + } + ); + } + catch (Exception e) + { + var json = await response.Content.ReadAsStringAsync(); + throw new Exception($"Not able to Read the content:\n{json}", e); + } + } + } +} diff --git a/tests/auth/FinancialHub.Auth.IntegrationTests/Extensions/Utils/HttpClientExtensionsParameters.cs b/tests/auth/FinancialHub.Auth.IntegrationTests/Extensions/Utils/HttpClientExtensionsParameters.cs new file mode 100644 index 0000000..4b77737 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.IntegrationTests/Extensions/Utils/HttpClientExtensionsParameters.cs @@ -0,0 +1,16 @@ +using System.Net.Http.Headers; + +namespace FinancialHub.Auth.IntegrationTests.Extensions.Utils +{ + public class HttpClientExtensionsParameters : HttpClientExtensionsParameters + { + public T Body { init; get; } + } + + public class HttpClientExtensionsParameters + { + public string Url { init; get; } + public HttpMethod Method { init; get; } + public Dictionary Headers { init; get; } + } +} diff --git a/tests/auth/FinancialHub.Auth.IntegrationTests/FinancialHub.Auth.IntegrationTests.csproj b/tests/auth/FinancialHub.Auth.IntegrationTests/FinancialHub.Auth.IntegrationTests.csproj new file mode 100644 index 0000000..6460faf --- /dev/null +++ b/tests/auth/FinancialHub.Auth.IntegrationTests/FinancialHub.Auth.IntegrationTests.csproj @@ -0,0 +1,25 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + + + + + diff --git a/tests/auth/FinancialHub.Auth.IntegrationTests/Setup/FinancialHubAuthApiFixture.auth.cs b/tests/auth/FinancialHub.Auth.IntegrationTests/Setup/FinancialHubAuthApiFixture.auth.cs new file mode 100644 index 0000000..0f18afc --- /dev/null +++ b/tests/auth/FinancialHub.Auth.IntegrationTests/Setup/FinancialHubAuthApiFixture.auth.cs @@ -0,0 +1,15 @@ +using FinancialHub.Auth.Domain.Interfaces.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace FinancialHub.Auth.IntegrationTests.Setup +{ + public partial class FinancialHubAuthApiFixture + { + public string GetAuthToken(UserModel userModel) + { + using var scope = this.Api.Server.Services.CreateScope(); + var tokenService = scope.ServiceProvider.GetRequiredService(); + return tokenService.GenerateToken(userModel).Token; + } + } +} diff --git a/tests/auth/FinancialHub.Auth.IntegrationTests/Setup/FinancialHubAuthApiFixture.cs b/tests/auth/FinancialHub.Auth.IntegrationTests/Setup/FinancialHubAuthApiFixture.cs new file mode 100644 index 0000000..c87e3fd --- /dev/null +++ b/tests/auth/FinancialHub.Auth.IntegrationTests/Setup/FinancialHubAuthApiFixture.cs @@ -0,0 +1,38 @@ +using System.Collections; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using FinancialHub.Auth.WebApi; + +namespace FinancialHub.Auth.IntegrationTests.Setup +{ + public partial class FinancialHubAuthApiFixture : IEnumerable, IDisposable + { + public HttpClient Client { get; protected set; } + public WebApplicationFactory Api { get; protected set; } + + public FinancialHubAuthApiFixture() + { + this.Api = new WebApplicationFactory() + .WithWebHostBuilder( + builder => + { + builder.UseEnvironment("Testing"); + } + ); + + this.Client = this.Api.CreateClient(); + } + + public void Dispose() + { + this.Api.Dispose(); + this.Client.Dispose(); + GC.SuppressFinalize(this); + } + + public IEnumerator GetEnumerator() + { + yield return this; + } + } +} diff --git a/tests/auth/FinancialHub.Auth.IntegrationTests/Setup/FinancialHubAuthApiFixture.database.cs b/tests/auth/FinancialHub.Auth.IntegrationTests/Setup/FinancialHubAuthApiFixture.database.cs new file mode 100644 index 0000000..cc71fff --- /dev/null +++ b/tests/auth/FinancialHub.Auth.IntegrationTests/Setup/FinancialHubAuthApiFixture.database.cs @@ -0,0 +1,45 @@ +using FinancialHub.Auth.Infra.Data.Contexts; +using FinancialHub.Common.Entities; +using Microsoft.Extensions.DependencyInjection; + +namespace FinancialHub.Auth.IntegrationTests.Setup +{ + public partial class FinancialHubAuthApiFixture + { + public T[] AddData(params T[] data) + where T : BaseEntity + { + using var scope = this.Api.Server.Services.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + context.Set().AddRange(data); + context.SaveChanges(); + + context.ChangeTracker.Clear(); + + var res = context.Set().ToArray(); + return res.Where(x => data.Any(y => y.Id == x.Id)).ToArray(); + } + + public IEnumerable GetData() + where T : BaseEntity + { + using var scope = this.Api.Server.Services.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + return context.Set().ToArray(); + } + + public void CreateDatabase() + { + using var scope = this.Api.Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + db.Database.EnsureCreated(); + } + + public void ClearData() + { + using var scope = this.Api.Server.Services.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + context.Database.EnsureDeleted(); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.IntegrationTests/Usings.cs b/tests/auth/FinancialHub.Auth.IntegrationTests/Usings.cs new file mode 100644 index 0000000..a4aed74 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.IntegrationTests/Usings.cs @@ -0,0 +1,19 @@ +global using System.Net; + +global using NUnit.Framework; + +global using FinancialHub.Auth.IntegrationTests.Base; +global using FinancialHub.Auth.IntegrationTests.Setup; +global using FinancialHub.Auth.IntegrationTests.Extensions; + +global using FinancialHub.Common.Responses.Errors; +global using FinancialHub.Common.Responses.Success; + +global using FinancialHub.Auth.Domain.Entities; +global using FinancialHub.Auth.Domain.Models; + +global using FinancialHub.Auth.Common.Tests.Assertions; +global using FinancialHub.Auth.Common.Tests.Builders.Entities; +global using FinancialHub.Auth.Common.Tests.Builders.Models; + +[assembly: Category("Integration")] \ No newline at end of file diff --git a/tests/auth/FinancialHub.Auth.Presentation.Tests/Asserts/ControllerResponseAssert.cs b/tests/auth/FinancialHub.Auth.Presentation.Tests/Asserts/ControllerResponseAssert.cs new file mode 100644 index 0000000..a97653e --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Presentation.Tests/Asserts/ControllerResponseAssert.cs @@ -0,0 +1,31 @@ +using FinancialHub.Common.Tests.Assertions.Responses; + +namespace FinancialHub.Auth.Presentation.Tests.Asserts +{ + public static class ControllerResponseAssert + { + public static void IsValid(BaseResponse expectedResponse, ObjectResult result) + { + Assert.Multiple(() => + { + Assert.That(result.StatusCode, Is.EqualTo(200)); + Assert.That(result.Value, Is.TypeOf(expectedResponse.GetType())); + + var response = result.Value as BaseResponse; + BaseResponseAssert.IsValid(expectedResponse, response!); + }); + } + + public static void HasError(BaseErrorResponse expectedResponse, ObjectResult result, int expectedStatusCode = 400) + { + Assert.Multiple(() => + { + Assert.That(result.StatusCode, Is.EqualTo(expectedStatusCode)); + Assert.That(result.Value, Is.TypeOf(expectedResponse.GetType())); + + var response = result.Value as ValidationErrorResponse; + BaseResponseAssert.HasError(expectedResponse, response!); + }); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Presentation.Tests/Controllers/Signin/SigninControllerTests.cs b/tests/auth/FinancialHub.Auth.Presentation.Tests/Controllers/Signin/SigninControllerTests.cs new file mode 100644 index 0000000..84a7333 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Presentation.Tests/Controllers/Signin/SigninControllerTests.cs @@ -0,0 +1,19 @@ +namespace FinancialHub.Auth.Presentation.Tests.Controllers +{ + public partial class SigninControllerTests + { + private SigninController controller; + private Mock serviceMock; + private SigninModelBuilder builder; + private TokenModelBuilder tokenBuilder; + + [SetUp] + public void SetUp() + { + builder = new SigninModelBuilder(); + tokenBuilder = new TokenModelBuilder(); + serviceMock = new Mock(); + controller = new SigninController(serviceMock.Object); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Presentation.Tests/Controllers/Signin/SigninControllerTests.get.cs b/tests/auth/FinancialHub.Auth.Presentation.Tests/Controllers/Signin/SigninControllerTests.get.cs new file mode 100644 index 0000000..389cec3 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Presentation.Tests/Controllers/Signin/SigninControllerTests.get.cs @@ -0,0 +1,45 @@ +using FinancialHub.Auth.Presentation.Tests.Asserts; + +namespace FinancialHub.Auth.Presentation.Tests.Controllers +{ + public partial class SigninControllerTests + { + [Test] + public async Task GetAsync_SuccessfulValidation_Returns200Ok() + { + var signin = this.builder.Generate(); + var token = this.tokenBuilder.Generate(); + var expectedResponse = new ItemResponse(token); + + serviceMock + .Setup(x => x.AuthenticateAsync(signin)) + .ReturnsAsync(token); + + var response = await this.controller.SigninAsync(signin); + + var result = response as ObjectResult; + + Assert.That(result, Is.Not.Null); + ControllerResponseAssert.IsValid(expectedResponse, result); + } + + [Test] + public async Task CreateAsync_ValidationFailed_Returns401Unauthorized() + { + var signin = this.builder.Generate(); + var error = new ServiceError(401, "Wrong e-mail or passsword"); + var expectedResponse = new ValidationErrorResponse(error.Message); + + serviceMock + .Setup(x => x.AuthenticateAsync(signin)) + .ReturnsAsync(error); + + var response = await this.controller.SigninAsync(signin); + + var result = response as ObjectResult; + + Assert.That(result, Is.Not.Null); + ControllerResponseAssert.HasError(expectedResponse, result, 401); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Presentation.Tests/Controllers/Signup/SignupControllerTests.create.cs b/tests/auth/FinancialHub.Auth.Presentation.Tests/Controllers/Signup/SignupControllerTests.create.cs new file mode 100644 index 0000000..b208ed3 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Presentation.Tests/Controllers/Signup/SignupControllerTests.create.cs @@ -0,0 +1,50 @@ +using FinancialHub.Auth.Presentation.Tests.Asserts; + +namespace FinancialHub.Auth.Presentation.Tests.Controllers +{ + public partial class SignupControllerTests + { + [Test] + public async Task CreateAsync_SuccessfulCreation_Returns200Ok() + { + var signup = this.builder.Generate(); + var user = this.userBuilder + .WithName(signup.FirstName) + .WithLastName(signup.FirstName) + .WithEmail(signup.Email) + .WithBirthDate(signup.BirthDate) + .Generate(); + var expectedResponse = new ItemResponse(user); + + serviceMock + .Setup(x => x.CreateAccountAsync(signup)) + .ReturnsAsync(user); + + var response = await this.controller.SignupAsync(signup); + + var result = response as ObjectResult; + + Assert.That(result, Is.Not.Null); + ControllerResponseAssert.IsValid(expectedResponse, result); + } + + [Test] + public async Task CreateAsync_FailedCreation_Returns400BadRequest() + { + var signup = this.builder.Generate(); + var error = new ServiceError(400, "User data invalid"); + var expectedResponse = new ValidationErrorResponse(error.Message); + + serviceMock + .Setup(x => x.CreateAccountAsync(signup)) + .ReturnsAsync(error); + + var response = await this.controller.SignupAsync(signup); + + var result = response as ObjectResult; + + Assert.That(result, Is.Not.Null); + ControllerResponseAssert.HasError(expectedResponse, result); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Presentation.Tests/Controllers/Signup/SignupControllerTests.cs b/tests/auth/FinancialHub.Auth.Presentation.Tests/Controllers/Signup/SignupControllerTests.cs new file mode 100644 index 0000000..a05176e --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Presentation.Tests/Controllers/Signup/SignupControllerTests.cs @@ -0,0 +1,19 @@ +namespace FinancialHub.Auth.Presentation.Tests.Controllers +{ + public partial class SignupControllerTests + { + private SignupController controller; + private Mock serviceMock; + private SignupModelBuilder builder; + private UserModelBuilder userBuilder; + + [SetUp] + public void SetUp() + { + builder = new SignupModelBuilder(); + userBuilder = new UserModelBuilder(); + serviceMock = new Mock(); + controller = new SignupController(serviceMock.Object); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Presentation.Tests/Controllers/Users/UsersControllerTests.create.cs b/tests/auth/FinancialHub.Auth.Presentation.Tests/Controllers/Users/UsersControllerTests.create.cs new file mode 100644 index 0000000..fcc573a --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Presentation.Tests/Controllers/Users/UsersControllerTests.create.cs @@ -0,0 +1,60 @@ +using System.Diagnostics.CodeAnalysis; + +namespace FinancialHub.Auth.Presentation.Tests.Controllers +{ + public partial class UsersControllerTests + { + [Test] + [Obsolete("Obsolete while CreateUserAsync is Obsolete")] + [SuppressMessage("Info Code Smell", "S1133:Deprecated code should be removed", Justification = "Remove with CreateUserAsync")] + public async Task CreateAsync_SuccessfulCreation_Returns200Ok() + { + var user = this.builder.Generate(); + var expectedResponse = new SaveResponse(user); + + serviceMock + .Setup(x => x.CreateAsync(user)) + .ReturnsAsync(user); + + var response = await this.controller.CreateUserAsync(user); + + var result = response as ObjectResult; + Assert.Multiple(() => + { + Assert.That(result!.StatusCode, Is.EqualTo(200)); + Assert.That(result!.Value, Is.TypeOf(expectedResponse.GetType())); + + var response = result.Value as SaveResponse; + AssertValidResponse(expectedResponse, response!); + }); + } + + [Test] + [Obsolete("Obsolete while CreateUserAsync is Obsolete")] + [SuppressMessage("Info Code Smell", "S1133:Deprecated code should be removed", Justification = "Remove with CreateUserAsync")] + public async Task CreateAsync_FailedCreation_Returns400BadRequest() + { + var user = this.builder.Generate(); + var error = new ServiceError(400, "User data invalid"); + var serviceResult = new ServiceResult(error: error); + + serviceMock + .Setup(x => x.CreateAsync(user)) + .ReturnsAsync(serviceResult); + + var expectedResponse = new ValidationErrorResponse(error.Message); + + var response = await this.controller.CreateUserAsync(user); + + var result = response as ObjectResult; + Assert.Multiple(() => + { + Assert.That(result!.StatusCode, Is.EqualTo(400)); + Assert.That(result!.Value, Is.TypeOf(expectedResponse.GetType())); + + var response = result.Value as ValidationErrorResponse; + AssertErrorResponse(expectedResponse, response!); + }); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Presentation.Tests/Controllers/Users/UsersControllerTests.cs b/tests/auth/FinancialHub.Auth.Presentation.Tests/Controllers/Users/UsersControllerTests.cs new file mode 100644 index 0000000..eaae3af --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Presentation.Tests/Controllers/Users/UsersControllerTests.cs @@ -0,0 +1,31 @@ +namespace FinancialHub.Auth.Presentation.Tests.Controllers +{ + public partial class UsersControllerTests + { + private UsersController controller; + private Mock serviceMock; + private UserModelBuilder builder; + + [SetUp] + public void SetUp() + { + serviceMock = new Mock(); + controller = new UsersController(serviceMock.Object); + builder = new UserModelBuilder(); + } + + protected static void AssertValidResponse(BaseResponse expected, BaseResponse response) + { + Assert.That(response.Data, Is.EqualTo(expected.Data)); + } + + protected static void AssertErrorResponse(BaseErrorResponse expected, BaseErrorResponse response) + { + Assert.Multiple(() => + { + Assert.That(response.Message, Is.EqualTo(expected.Message)); + Assert.That(response.Code, Is.EqualTo(expected.Code)); + }); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Presentation.Tests/Controllers/Users/UsersControllerTests.get.cs b/tests/auth/FinancialHub.Auth.Presentation.Tests/Controllers/Users/UsersControllerTests.get.cs new file mode 100644 index 0000000..1412600 --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Presentation.Tests/Controllers/Users/UsersControllerTests.get.cs @@ -0,0 +1,57 @@ +namespace FinancialHub.Auth.Presentation.Tests.Controllers +{ + public partial class UsersControllerTests + { + [Test] + public async Task GetUserAsync_FoundUser_Returns200Ok() + { + var id = Guid.NewGuid(); + var user = builder + .WithId(id) + .Generate(); + var serviceResult = new ServiceResult(user); + + serviceMock + .Setup(x => x.GetAsync(id)) + .ReturnsAsync(serviceResult); + + var expectedResponse = new ItemResponse(user); + + var response = await controller.GetUserAsync(id); + var result = response as ObjectResult; + Assert.Multiple(() => + { + Assert.That(result!.StatusCode, Is.EqualTo(200)); + Assert.That(result!.Value, Is.TypeOf(expectedResponse.GetType())); + + var response = result.Value as ItemResponse; + AssertValidResponse(expectedResponse, response!); + }); + } + + [Test] + public async Task GetUserAsync_NotFoundUser_Returns404NotFound() + { + var id = Guid.NewGuid(); + var error = new ServiceError(404, "User not found"); + var serviceResult = new ServiceResult(error: error); + + serviceMock + .Setup(x => x.GetAsync(id)) + .ReturnsAsync(serviceResult); + + var expectedResponse = new NotFoundErrorResponse(error.Message); + + var response = await controller.GetUserAsync(id); + var result = response as ObjectResult; + Assert.Multiple(() => + { + Assert.That(result!.StatusCode, Is.EqualTo(404)); + Assert.That(result!.Value, Is.TypeOf(expectedResponse.GetType())); + + var response = result.Value as NotFoundErrorResponse; + AssertErrorResponse(expectedResponse, response!); + }); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Presentation.Tests/Controllers/Users/UsersControllerTests.update.cs b/tests/auth/FinancialHub.Auth.Presentation.Tests/Controllers/Users/UsersControllerTests.update.cs new file mode 100644 index 0000000..ed4779f --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Presentation.Tests/Controllers/Users/UsersControllerTests.update.cs @@ -0,0 +1,84 @@ +namespace FinancialHub.Auth.Presentation.Tests.Controllers +{ + public partial class UsersControllerTests + { + [Test] + public async Task UpdateAsync_ExistingUser_Returns200Ok() + { + var id = Guid.NewGuid(); + var user = this.builder.WithId(id).Generate(); + var serviceResult = new ServiceResult(user); + var expectedResponse = new SaveResponse(user); + + serviceMock + .Setup(x => x.UpdateAsync(id, user)) + .ReturnsAsync(serviceResult); + + var response = await this.controller.UpdateUserAsync(id, user); + var result = response as ObjectResult; + + Assert.Multiple(() => + { + Assert.That(result!.StatusCode, Is.EqualTo(200)); + Assert.That(result!.Value, Is.TypeOf(expectedResponse.GetType())); + + var response = result.Value as SaveResponse; + AssertValidResponse(expectedResponse, response!); + }); + } + + [Test] + public async Task UpdateAsync_NonExistingUser_Returns404NotFound() + { + var id = Guid.NewGuid(); + var user = this.builder.WithId(id).Generate(); + + var error = new ServiceError(404, "User data invalid"); + var serviceResult = new ServiceResult(error: error); + var expectedResponse = new NotFoundErrorResponse(error.Message); + + serviceMock + .Setup(x => x.UpdateAsync(id, user)) + .ReturnsAsync(serviceResult); + + var response = await this.controller.UpdateUserAsync(id, user); + var result = response as ObjectResult; + + Assert.Multiple(() => + { + Assert.That(result!.StatusCode, Is.EqualTo(404)); + Assert.That(result!.Value, Is.TypeOf(expectedResponse.GetType())); + + var response = result.Value as NotFoundErrorResponse; + AssertErrorResponse(expectedResponse, response!); + }); + } + + [Test] + public async Task UpdateAsync_UpdateError_Returns400BadRequest() + { + var id = Guid.NewGuid(); + var user = this.builder.WithId(id).Generate(); + + var error = new ServiceError(400, "User data invalid"); + var serviceResult = new ServiceResult(error: error); + var expectedResponse = new ValidationErrorResponse(error.Message); + + serviceMock + .Setup(x => x.UpdateAsync(id, user)) + .ReturnsAsync(serviceResult); + + var response = await this.controller.UpdateUserAsync(id, user); + var result = response as ObjectResult; + + Assert.Multiple(() => + { + Assert.That(result!.StatusCode, Is.EqualTo(400)); + Assert.That(result!.Value, Is.TypeOf(expectedResponse.GetType())); + + var response = result.Value as ValidationErrorResponse; + AssertErrorResponse(expectedResponse, response!); + }); + } + } +} diff --git a/tests/auth/FinancialHub.Auth.Presentation.Tests/FinancialHub.Auth.Presentation.Tests.csproj b/tests/auth/FinancialHub.Auth.Presentation.Tests/FinancialHub.Auth.Presentation.Tests.csproj new file mode 100644 index 0000000..eda2eac --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Presentation.Tests/FinancialHub.Auth.Presentation.Tests.csproj @@ -0,0 +1,25 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + + + + + diff --git a/tests/auth/FinancialHub.Auth.Presentation.Tests/Usings.cs b/tests/auth/FinancialHub.Auth.Presentation.Tests/Usings.cs new file mode 100644 index 0000000..01e062c --- /dev/null +++ b/tests/auth/FinancialHub.Auth.Presentation.Tests/Usings.cs @@ -0,0 +1,21 @@ +global using Microsoft.AspNetCore.Mvc; + +global using Moq; +global using NUnit.Framework; + +global using FinancialHub.Auth.Common.Tests.Assertions; +global using FinancialHub.Auth.Common.Tests.Builders.Models; + +global using FinancialHub.Common.Responses.Success; +global using FinancialHub.Common.Responses.Errors; + +global using FinancialHub.Common.Results; +global using FinancialHub.Common.Results.Errors; + +global using FinancialHub.Auth.Domain.Models; + +global using FinancialHub.Auth.Domain.Interfaces.Services; + +global using FinancialHub.Auth.Presentation.Controllers; + +[assembly: Category("Unit")] \ No newline at end of file diff --git a/tests/common/FinancialHub.Common.Tests/Assertions/Responses/BaseResponseAssert.cs b/tests/common/FinancialHub.Common.Tests/Assertions/Responses/BaseResponseAssert.cs new file mode 100644 index 0000000..5a142ec --- /dev/null +++ b/tests/common/FinancialHub.Common.Tests/Assertions/Responses/BaseResponseAssert.cs @@ -0,0 +1,28 @@ +using FinancialHub.Common.Responses.Errors; +using FinancialHub.Common.Responses.Success; +using NUnit.Framework; + +namespace FinancialHub.Common.Tests.Assertions.Responses +{ + public static class BaseResponseAssert + { + public static void IsValid(T expected, BaseResponse response) + { + Assert.That(expected, Is.EqualTo(response.Data)); + } + + public static void IsValid(BaseResponse expected, BaseResponse response) + { + Assert.That(response.Data, Is.EqualTo(expected.Data)); + } + + public static void HasError(BaseErrorResponse expected, BaseErrorResponse response) + { + Assert.Multiple(() => + { + Assert.That(response.Message, Is.EqualTo(expected.Message)); + Assert.That(response.Code, Is.EqualTo(expected.Code)); + }); + } + } +} diff --git a/tests/common/FinancialHub.Common.Tests/Builders/Entities/BaseEntityBuilder.cs b/tests/common/FinancialHub.Common.Tests/Builders/Entities/BaseEntityBuilder.cs new file mode 100644 index 0000000..78e40ae --- /dev/null +++ b/tests/common/FinancialHub.Common.Tests/Builders/Entities/BaseEntityBuilder.cs @@ -0,0 +1,34 @@ +using Bogus; +using FinancialHub.Common.Entities; + +namespace FinancialHub.Common.Tests.Builders.Entities +{ + public abstract class BaseEntityBuilder : Faker + where T : BaseEntity + { + protected BaseEntityBuilder() + { + this.RuleFor(x => x.Id, fake => fake.Random.Guid()); + this.RuleFor(x => x.CreationTime, fake => DateTimeOffset.Now); + this.RuleFor(x => x.UpdateTime, fake => DateTimeOffset.Now); + } + + public BaseEntityBuilder WithId(Guid id) + { + this.RuleFor(c => c.Id, id); + return this; + } + + public BaseEntityBuilder WithCreationTime(DateTimeOffset creationTime) + { + this.RuleFor(c => c.CreationTime, creationTime); + return this; + } + + public BaseEntityBuilder WithUpdateTime(DateTimeOffset updateTime) + { + this.RuleFor(c => c.UpdateTime, updateTime); + return this; + } + } +} diff --git a/tests/common/FinancialHub.Common.Tests/Builders/Models/BaseModelBuilder.cs b/tests/common/FinancialHub.Common.Tests/Builders/Models/BaseModelBuilder.cs new file mode 100644 index 0000000..7860072 --- /dev/null +++ b/tests/common/FinancialHub.Common.Tests/Builders/Models/BaseModelBuilder.cs @@ -0,0 +1,20 @@ +using Bogus; +using FinancialHub.Common.Models; + +namespace FinancialHub.Common.Tests.Builders.Models +{ + public abstract class BaseModelBuilder : Faker + where T : BaseModel + { + protected BaseModelBuilder() + { + this.RuleFor(x => x.Id, y => y.System.Random.Guid()); + } + + public BaseModelBuilder WithId(Guid guid) + { + this.RuleFor(x => x.Id, guid); + return this; + } + } +} diff --git a/tests/common/FinancialHub.Common.Tests/FinancialHub.Common.Tests.csproj b/tests/common/FinancialHub.Common.Tests/FinancialHub.Common.Tests.csproj new file mode 100644 index 0000000..ada9f94 --- /dev/null +++ b/tests/common/FinancialHub.Common.Tests/FinancialHub.Common.Tests.csproj @@ -0,0 +1,18 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + +