diff --git a/.editorconfig b/.editorconfig index d9f8964..e516371 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,261 @@ -[*.cs] +root = true -# IDE0055: Fix formatting +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +indent_style = space +tab_width = 4 +max_line_length = 170 + +# New line preferences +end_of_line = crlf +insert_final_newline = false + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Expression-level preferences +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = false # Override due to partial classes across folders +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_collection_expression = when_types_loosely_match +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Field preferences +dotnet_style_readonly_field = true + +# Parameter preferences +dotnet_code_quality_unused_parameters = all:silent + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +# New line preferences +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = false + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true +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 + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_switch_expression = true + +# Null-checking preferences +csharp_style_conditional_delegate_call = true + +# Modifier preferences +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async +csharp_style_prefer_readonly_struct = true +csharp_style_prefer_readonly_struct_member = true + +# Code-block preferences +csharp_prefer_braces = false:suggestion +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_namespace_declarations = file_scoped:warning +csharp_style_prefer_method_group_conversion = true:suggestion +csharp_style_prefer_primary_constructors = true:warning +csharp_style_prefer_top_level_statements = true:silent + +# Expression-level preferences +csharp_prefer_simple_default_expression = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_local_over_anonymous_function = true +csharp_style_prefer_null_check_over_type_check = true +csharp_style_prefer_range_operator = true +csharp_style_prefer_tuple_swap = true +csharp_style_prefer_utf8_string_literals = true +csharp_style_throw_expression = true +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace + +# New line preferences +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +# ========================================================================== +# Diagnostic overrides +# ========================================================================== + +# IDE0055: Fix formatting — disabled (handled by dotnet format on demand) dotnet_diagnostic.IDE0055.severity = none + +# IDE0005: Using directive is unnecessary +dotnet_diagnostic.IDE0005.severity = warning + +# IDE0130: Namespace does not match folder structure — this codebase uses +# partial classes across folders, so namespace != folder is expected +dotnet_diagnostic.IDE0130.severity = none + +# IDE* suggestions that can cause noisy or potentially breaking refactors when +# running `dotnet format style --severity info` (e.g., renames). Keep formatting +# verification focused on whitespace + explicit style preferences. +dotnet_diagnostic.IDE1006.severity = none +dotnet_diagnostic.IDE0028.severity = none +dotnet_diagnostic.IDE0300.severity = none +dotnet_diagnostic.IDE0305.severity = none + +# Meziantou/CA rules that have no auto code-fix — suppress so dotnet format +# does not emit "Unable to fix" warnings +dotnet_diagnostic.MA0011.severity = none +dotnet_diagnostic.MA0015.severity = none +dotnet_diagnostic.MA0016.severity = none +dotnet_diagnostic.MA0048.severity = none +dotnet_diagnostic.CA2024.severity = none diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..fde4c26 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Normalize all text files to CRLF on checkout (matches .editorconfig end_of_line = crlf) +* text=auto eol=crlf + +# Shell scripts must keep LF to run on Linux/macOS +*.sh text eol=lf diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100644 index 0000000..6f7daf3 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Git pre-commit hook. +# +# To enable for this repo: +# bash scripts/setup-githooks.sh +# (or) ./scripts/setup-githooks.ps1 +# +# To bypass once: +# git commit --no-verify + +if [[ "${SKIP_DOTNET_FORMAT:-}" == "1" ]]; then + exit 0 +fi + +ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || true)" +if [[ -z "$ROOT_DIR" ]]; then + echo "pre-commit: not inside a git repository" >&2 + exit 1 +fi + +bash "$ROOT_DIR/scripts/verify-format.sh" --verify || { + echo "" >&2 + echo "Commit aborted: formatting verification failed." >&2 + echo "Fix by running: bash scripts/verify-format.sh --fix" >&2 + exit 1 +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1d9dc4c..451b222 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,8 +31,21 @@ jobs: - name: Restore run: dotnet restore + - name: Verify formatting + run: bash scripts/verify-format.sh --verify + - name: Build run: dotnet build --no-restore -c Release - name: Test - run: dotnet test --no-build -c Release --verbosity normal + run: > + dotnet test --no-build -c Release --verbosity normal + --collect:"XPlat Code Coverage" + --results-directory ./coverage + + - name: Upload coverage + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: codecov/codecov-action@v4 + with: + directory: ./coverage + fail_ci_if_error: false diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f898bca..82abe25 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -33,13 +33,15 @@ jobs: - name: Restore run: dotnet restore + - name: Verify formatting + run: bash scripts/verify-format.sh --verify + - name: Build run: dotnet build --no-restore -c Release - name: Test run: dotnet test --no-build -c Release --verbosity normal - # Extract version from tag (v0.1.0-beta.1 -> 0.1.0-beta.1) - name: Extract version from tag id: version run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" @@ -48,7 +50,11 @@ jobs: run: dotnet pack src/Bitbucket.Net/Bitbucket.Net.csproj -c Release --no-build -o ./artifacts -p:PackageVersion=${{ steps.version.outputs.VERSION }} - name: Push to NuGet.org - run: dotnet nuget push ./artifacts/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate + run: | + dotnet nuget push ./artifacts/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate + dotnet nuget push ./artifacts/*.snupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate - name: Push to GitHub Packages - run: dotnet nuget push ./artifacts/*.nupkg --api-key ${{ secrets.GITHUB_TOKEN }} --source https://nuget.pkg.github.com/diomonogatari/index.json --skip-duplicate + run: | + dotnet nuget push ./artifacts/*.nupkg --api-key ${{ secrets.GITHUB_TOKEN }} --source https://nuget.pkg.github.com/diomonogatari/index.json --skip-duplicate + dotnet nuget push ./artifacts/*.snupkg --api-key ${{ secrets.GITHUB_TOKEN }} --source https://nuget.pkg.github.com/diomonogatari/index.json --skip-duplicate diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..02f5b73 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,12 @@ +{ + "editor.formatOnSave": true, + "editor.formatOnSaveMode": "modificationsIfAvailable", + "editor.formatOnType": false, + "editor.rulers": [170], + "editor.wordWrapColumn": 170, + "editor.codeActionsOnSave": { + "source.organizeImports": "always", + "source.removeUnusedImports": "always", + "source.fixAll": "always" + } +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 53fc0fb..a054b60 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -7,7 +7,7 @@ "type": "process", "args": [ "build", - "${workspaceFolder}/Bitbucket.Net.sln", + "${workspaceFolder}/Bitbucket.Net.slnx", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary;ForceNoAlign" ], @@ -23,7 +23,7 @@ "type": "process", "args": [ "build", - "${workspaceFolder}/Bitbucket.Net.sln", + "${workspaceFolder}/Bitbucket.Net.slnx", "-c", "Release", "/property:GenerateFullPaths=true", @@ -131,7 +131,7 @@ "type": "process", "args": [ "clean", - "${workspaceFolder}/Bitbucket.Net.sln" + "${workspaceFolder}/Bitbucket.Net.slnx" ], "problemMatcher": "$msCompile" }, diff --git a/Bitbucket.Net.sln b/Bitbucket.Net.sln deleted file mode 100644 index 28ea8dc..0000000 --- a/Bitbucket.Net.sln +++ /dev/null @@ -1,62 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.28803.156 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bitbucket.Net", "src\Bitbucket.Net\Bitbucket.Net.csproj", "{51EBF9F3-7DFA-4C72-B38D-D07B1ED7FCEE}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{CBBC7722-A73C-4504-81D7-E1FB82B851A5}" - ProjectSection(SolutionItems) = preProject - build\build.ps1 = build\build.ps1 - build\test.ps1 = build\test.ps1 - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{778D1009-F274-4E3A-AC8C-D3B357F2DBE1}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - .gitignore = .gitignore - appveyor.yml = appveyor.yml - Bitbucket.Net.snk = Bitbucket.Net.snk - global.json = global.json - LICENSE = LICENSE - README.md = README.md - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{B7E00533-033F-48D3-A01C-40BD264F245A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bitbucket.Net.Tests", "test\Bitbucket.Net.Tests\Bitbucket.Net.Tests.csproj", "{7775DD13-F980-4838-8FE4-6E8B96221298}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Benchmarks", "Benchmarks", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bitbucket.Net.Benchmarks", "benchmarks\Bitbucket.Net.Benchmarks\Bitbucket.Net.Benchmarks.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {51EBF9F3-7DFA-4C72-B38D-D07B1ED7FCEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {51EBF9F3-7DFA-4C72-B38D-D07B1ED7FCEE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {51EBF9F3-7DFA-4C72-B38D-D07B1ED7FCEE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {51EBF9F3-7DFA-4C72-B38D-D07B1ED7FCEE}.Release|Any CPU.Build.0 = Release|Any CPU - {7775DD13-F980-4838-8FE4-6E8B96221298}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7775DD13-F980-4838-8FE4-6E8B96221298}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7775DD13-F980-4838-8FE4-6E8B96221298}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7775DD13-F980-4838-8FE4-6E8B96221298}.Release|Any CPU.Build.0 = Release|Any CPU - {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Release|Any CPU - {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Release|Any CPU - {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {7775DD13-F980-4838-8FE4-6E8B96221298} = {B7E00533-033F-48D3-A01C-40BD264F245A} - {B2C3D4E5-F6A7-8901-BCDE-F12345678901} = {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {06DABD0E-1A16-4B84-9DF3-A1B8E73D18AF} - EndGlobalSection -EndGlobal diff --git a/Bitbucket.Net.slnx b/Bitbucket.Net.slnx new file mode 100644 index 0000000..f01e175 --- /dev/null +++ b/Bitbucket.Net.slnx @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Bitbucket.Net.snk b/Bitbucket.Net.snk deleted file mode 100644 index a5e5104..0000000 Binary files a/Bitbucket.Net.snk and /dev/null differ diff --git a/CHANGELOG.md b/CHANGELOG.md index ee69eff..665cd78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,19 +7,64 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [0.1.0-beta.1] - 2026-02-06 +## [0.2.0] - 2026-02-08 -### Notes +### Breaking Changes + +- **Exception handling**: The library now throws `BitbucketApiException` (and its typed subtypes) instead of `FlurlHttpException`. Consumers catching `FlurlHttpException` must update their catch blocks. +- **`Comment` model**: No longer inherits from `PullRequestInfo`. Properties such as `Title`, `Description`, `FromRef`, `ToRef`, `Locked`, and `Reviewers` are removed from `Comment`. These were always null/default on comments and should not have been exposed. +- **`Comment.State`**: Changed from `new string?` (hiding a `PullRequestStates` enum) to a plain `string?` property. +- **Global Flurl configuration removed**: The library no longer calls `FlurlHttp.Clients.WithDefaults()`. Other Flurl consumers in the same process are no longer affected. + +### Added + +- **SourceLink**: Consumers can step into library source during debugging. +- **Symbol packages**: `.snupkg` published alongside `.nupkg`. +- **XML documentation**: IntelliSense documentation included in the NuGet package. Model classes now have comprehensive `` and `` XML docs. +- **New streaming methods**: + - `GetPullRequestActivitiesStreamAsync` + - `GetPullRequestChangesStreamAsync` + - `GetPullRequestCommentsStreamAsync` + - `GetPullRequestParticipantsStreamAsync` + - `GetPullRequestTasksStreamAsync` + - `GetPullRequestBlockerCommentsStreamAsync` + - `GetDashboardPullRequestsStreamAsync` + - `GetInboxPullRequestsStreamAsync` + - `GetProjectRepositoryTagsStreamAsync` + - `GetChangesStreamAsync` + - `GetCommitChangesStreamAsync` +- **`global.json`**: SDK version pinned for reproducible builds. +- **`Directory.Build.props`**: Centralized build configuration (TFM, language version, nullable, warnings-as-errors). +- **Code coverage**: CI collects and reports test coverage. +- **File splitting**: Monolithic `Core/Projects/BitbucketClient.cs` (4 491 lines) split into 10 focused partial-class files by domain (projects, repositories, branches, commits, compare, pull requests, PR comments, PR details, tasks, repository settings). + +### Fixed -This is the first public release of the modernized fork by -[diomonogatari](https://github.com/diomonogatari). -The version number intentionally starts at `0.x` to signal that the -library is **not yet production-ready** — it is being dog-fooded -in an MCP Server for on-prem Bitbucket Server but not every endpoint -has been exhaustively tested. +- **Typed exceptions now fire correctly** for all HTTP error responses. Previously, Flurl intercepted errors before the custom handling could run. +- **`CancellationToken` propagation**: Helper methods now pass the token to underlying HTTP calls. +- **`PullRequest.ToString()`**: No longer throws `NullReferenceException` when `Author` or `Author.User` is null. +- **`Participant.ToString()`**: Same null-safety fix. -The original [lvermeulen/Bitbucket.Net](https://github.com/lvermeulen/Bitbucket.Net) -shipped up to 0.5.0 on NuGet; this fork is versioned independently. +### Changed + +- Removed commented-out `Avatar` property from `ProjectDefinition`. +- Fixed duplicate `` XML doc tag on `GetRepositoriesStreamAsync`. + +### Testing + +- Added streaming endpoint mock tests covering all 20 streaming methods (single-page, multi-page, empty result scenarios) +- Added diff streaming tests for commit, repository, compare, and PR diffs (single, multiple, empty) +- Added MCP extension method tests for `StreamDiffsWithLimitsAsync` and `TakeDiffsWithLimitsAsync` +- Added cancellation token propagation tests (pre-cancelled tokens for buffered, streaming, and diff methods; mid-stream cancellation) +- Added DI constructor integration tests for `HttpClient` and `IFlurlClient` injection paths (CRUD, error handling, streaming, auth header verification) +- Introduced paginated fixture data (`projects-page1.json`, `projects-page2.json`, etc.) and `SetupPagedEndpoint` helper for multi-page mock tests +- Total test count increased from 633 to 696 (+63 new tests) + +## [0.1.0-beta.1] - 2026-02-06 (pre-release) + +### Notes + +First public pre-release of the modernized fork. Superseded by 0.2.0. ## [2.0.0] - 2025-11-28 (internal) @@ -35,11 +80,13 @@ shipped up to 0.5.0 on NuGet; this fork is versioned independently. ### Added #### CancellationToken Support + - All async methods now accept an optional `CancellationToken` parameter - Enables graceful cancellation of long-running operations - Fully propagated to underlying HTTP calls #### IAsyncEnumerable Streaming + - New streaming variants for paginated endpoints that yield items as they arrive: - `GetProjectsStreamAsync()` - `GetProjectRepositoriesStreamAsync()` @@ -54,6 +101,7 @@ shipped up to 0.5.0 on NuGet; this fork is versioned independently. - Native `await foreach` support #### Diff and File Content Streaming + - New streaming methods for large diff responses: - `GetCommitDiffStreamAsync()` - Stream diffs for a specific commit - `GetRepositoryDiffStreamAsync()` - Stream repository diffs between refs @@ -68,6 +116,7 @@ shipped up to 0.5.0 on NuGet; this fork is versioned independently. - Reduced memory pressure for large file downloads #### Dependency Injection Support + - New constructor: `BitbucketClient(HttpClient httpClient, string baseUrl, Func getToken = null)` - Enables use with `IHttpClientFactory` - Supports Polly resilience policies (retry, circuit breaker, etc.) @@ -77,6 +126,7 @@ shipped up to 0.5.0 on NuGet; this fork is versioned independently. - Supports `IFlurlClientCache` for named client management #### Typed Exception Hierarchy + - New `BitbucketApiException` base class with rich error information: - `StatusCode`: HTTP status code as `HttpStatusCode` enum - `Context`: The field or resource that caused the error @@ -93,12 +143,14 @@ shipped up to 0.5.0 on NuGet; this fork is versioned independently. - `BitbucketServerException` (HTTP 5xx) #### Code Quality Enforcement + - Added `Meziantou.Analyzer` to enforce library best practices - ConfigureAwait(false) requirement enforced via MA0004 (warning level) - Nullable reference types enabled project-wide - EditorConfig configured with library-appropriate analyzer rules #### Performance Benchmarks + - New benchmark project (`benchmarks/Bitbucket.Net.Benchmarks`) using BenchmarkDotNet - Benchmark categories: - **JSON Serialization**: Measure System.Text.Json performance for serialization/deserialization @@ -126,6 +178,7 @@ shipped up to 0.5.0 on NuGet; this fork is versioned independently. #### Updating from 1.x to 2.0.0 1. **Update Target Framework** + ```xml netstandard1.4 @@ -137,6 +190,7 @@ shipped up to 0.5.0 on NuGet; this fork is versioned independently. 2. **No Code Changes Required** for basic usage - the API remains backward compatible 3. **Optional: Use CancellationToken** + ```csharp // Before var projects = await client.GetProjectsAsync(); @@ -147,6 +201,7 @@ shipped up to 0.5.0 on NuGet; this fork is versioned independently. ``` 4. **Optional: Use Streaming for Large Results** + ```csharp // Before - buffers all results in memory var allPRs = await client.GetPullRequestsAsync("PROJ", "repo"); @@ -159,6 +214,7 @@ shipped up to 0.5.0 on NuGet; this fork is versioned independently. ``` 5. **Optional: Use Dependency Injection** + ```csharp // Configure with IHttpClientFactory + Polly services.AddHttpClient() @@ -173,6 +229,7 @@ shipped up to 0.5.0 on NuGet; this fork is versioned independently. ``` 6. **Update Exception Handling** (Breaking Change) + ```csharp // Before - catching generic InvalidOperationException try diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ee9797b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,80 @@ +## Contributing + +Thanks for your interest in contributing! This repository enforces a consistent +C# code style via `.editorconfig`, a pre-commit hook (opt-in), and CI checks. + +## Prerequisites + +- Git +- .NET SDK 10.x +- Bash (macOS/Linux: built-in; Windows: Git for Windows ships Git Bash) + +## Enable the pre-commit hook (recommended) + +Git hooks are not transferred by default when you clone a repository. This repo +keeps hooks in `.githooks/` and uses `core.hooksPath` so they can be versioned. + +Run one of the setup scripts from the repo root: + +```powershell +./scripts/setup-githooks.ps1 +``` + +```bash +bash scripts/setup-githooks.sh +``` + +This sets: + +- `git config core.hooksPath .githooks` + +After that, every `git commit` will run the formatting verification. + +### Bypass (when you really need to) + +- One-off bypass: + +```bash +git commit --no-verify +``` + +- Disable the formatting hook for a command/session: + +```bash +SKIP_DOTNET_FORMAT=1 git commit +``` + +## Formatting: verify vs fix + +The hook (and CI) run formatting in **verify** mode: + +- `dotnet format whitespace ... --verify-no-changes` +- `dotnet format style ... --severity warn --verify-no-changes` + +To run the same checks manually: + +```powershell +./scripts/verify-format.ps1 +``` + +```bash +bash scripts/verify-format.sh --verify +``` + +To apply fixes locally: + +```powershell +./scripts/verify-format.ps1 -Fix +``` + +```bash +bash scripts/verify-format.sh --fix +``` + +## Tests + +Please ensure tests pass before submitting a PR. + +```bash +dotnet test ./test/Bitbucket.Net.Tests/Bitbucket.Net.Tests.csproj +``` diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..572bf01 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,15 @@ + + + net10.0 + latest + enable + enable + true + true + true + true + snupkg + true + true + + diff --git a/README.md b/README.md index 3c5ebc2..1af2ed3 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,27 @@ -![Icon](https://i.imgur.com/OsDAzyV.png) # Bitbucket.Net [![NuGet](https://img.shields.io/nuget/v/BitbucketServer.Net.svg)](https://www.nuget.org/packages/BitbucketServer.Net) [![NuGet Downloads](https://img.shields.io/nuget/dt/BitbucketServer.Net.svg)](https://www.nuget.org/packages/BitbucketServer.Net) [![CI](https://github.com/diomonogatari/Bitbucket.Net/actions/workflows/ci.yml/badge.svg)](https://github.com/diomonogatari/Bitbucket.Net/actions/workflows/ci.yml) +[![codecov](https://codecov.io/gh/diomonogatari/Bitbucket.Net/branch/main/graph/badge.svg)](https://codecov.io/gh/diomonogatari/Bitbucket.Net) [![license](https://img.shields.io/github/license/diomonogatari/Bitbucket.Net.svg?maxAge=2592000)](https://github.com/diomonogatari/Bitbucket.Net/blob/main/LICENSE) ![](https://img.shields.io/badge/.net-10.0-yellowgreen.svg) -![](https://img.shields.io/badge/status-beta-orange.svg) +![](https://img.shields.io/badge/status-0.x_(pre--stable)-orange.svg) Modernized C# client for **Bitbucket Server** (Stash) REST API. +## Contributing + +Development setup (including the pre-commit formatting hook) is documented in +[CONTRIBUTING.md](CONTRIBUTING.md). + > **Fork notice** — This is an actively maintained fork of > [lvermeulen/Bitbucket.Net](https://github.com/lvermeulen/Bitbucket.Net), > which appears to be abandoned (last release 2020). -> The fork is **not production-ready** yet — it works well for the -> author's own use case (an MCP Server for on-prem Bitbucket Server) but -> not every endpoint has been fully tested. +> The library is at **0.x** — the API surface may still change +> between minor versions. It is used in production by the author (as the +> backend for an MCP Server talking to on-prem Bitbucket Server), but +> not every endpoint has been verified against a live instance. > Contributions, bug reports, and feedback are very welcome. ### What changed from the original @@ -35,7 +41,7 @@ If you're looking for Bitbucket Cloud API, try [this repository](https://github. ## Installation ```bash -dotnet add package BitbucketServer.Net --prerelease +dotnet add package BitbucketServer.Net ``` ## Usage @@ -112,6 +118,19 @@ await foreach (var pr in client.GetPullRequestsStreamAsync("PROJ", "repo", cance { await ProcessPullRequestAsync(pr); } + +// Stream PR activities +await foreach (var activity in client.GetPullRequestActivitiesStreamAsync( + "PROJ", "repo", pullRequestId: 42)) +{ + ProcessActivity(activity); +} + +// Stream dashboard PRs +await foreach (var pr in client.GetDashboardPullRequestsStreamAsync()) +{ + Console.WriteLine($"#{pr.Id}: {pr.Title}"); +} ``` ### Exception Handling diff --git a/benchmarks/Bitbucket.Net.Benchmarks/Bitbucket.Net.Benchmarks.csproj b/benchmarks/Bitbucket.Net.Benchmarks/Bitbucket.Net.Benchmarks.csproj index aae63d2..267cf7f 100644 --- a/benchmarks/Bitbucket.Net.Benchmarks/Bitbucket.Net.Benchmarks.csproj +++ b/benchmarks/Bitbucket.Net.Benchmarks/Bitbucket.Net.Benchmarks.csproj @@ -2,10 +2,6 @@ Exe - net10.0 - enable - latest - enable Release diff --git a/benchmarks/Bitbucket.Net.Benchmarks/Config/BenchmarkConfig.cs b/benchmarks/Bitbucket.Net.Benchmarks/Config/BenchmarkConfig.cs index ed0699c..cac839f 100644 --- a/benchmarks/Bitbucket.Net.Benchmarks/Config/BenchmarkConfig.cs +++ b/benchmarks/Bitbucket.Net.Benchmarks/Config/BenchmarkConfig.cs @@ -72,20 +72,20 @@ public FullBenchmarkConfig() { AddJob(Job.Default); AddDiagnoser(MemoryDiagnoser.Default); - + AddColumn(StatisticColumn.Mean); AddColumn(StatisticColumn.StdErr); AddColumn(StatisticColumn.StdDev); AddColumn(StatisticColumn.Median); AddColumn(StatisticColumn.P95); AddColumn(StatisticColumn.OperationsPerSecond); - + AddExporter(MarkdownExporter.GitHub); AddExporter(HtmlExporter.Default); AddExporter(CsvExporter.Default); - + AddLogger(ConsoleLogger.Default); - + WithSummaryStyle(SummaryStyle.Default.WithRatioStyle(RatioStyle.Trend)); } -} +} \ No newline at end of file diff --git a/benchmarks/Bitbucket.Net.Benchmarks/Program.cs b/benchmarks/Bitbucket.Net.Benchmarks/Program.cs index 2483486..483cf4e 100644 --- a/benchmarks/Bitbucket.Net.Benchmarks/Program.cs +++ b/benchmarks/Bitbucket.Net.Benchmarks/Program.cs @@ -66,4 +66,4 @@ private static void RunAllBenchmarks() Console.WriteLine("All benchmarks completed!"); Console.WriteLine("Results are available in the BenchmarkDotNet.Artifacts folder."); } -} +} \ No newline at end of file diff --git a/benchmarks/Bitbucket.Net.Benchmarks/Response/ResponseHandlingBenchmarks.cs b/benchmarks/Bitbucket.Net.Benchmarks/Response/ResponseHandlingBenchmarks.cs index 9d0359e..e12f6d4 100644 --- a/benchmarks/Bitbucket.Net.Benchmarks/Response/ResponseHandlingBenchmarks.cs +++ b/benchmarks/Bitbucket.Net.Benchmarks/Response/ResponseHandlingBenchmarks.cs @@ -1,6 +1,6 @@ -using System.Text; using BenchmarkDotNet.Attributes; using Bitbucket.Net.Benchmarks.Config; +using System.Text; namespace Bitbucket.Net.Benchmarks.Response; @@ -57,7 +57,7 @@ public int ProcessLargeDiff() [Benchmark(Description = "Buffered diff processing (1000 lines)")] public List BufferedDiffProcessing() { - return _largeDiff.Split('\n').ToList(); + return [.. _largeDiff.Split('\n')]; } /// @@ -205,14 +205,14 @@ private static IEnumerable EnumerateLines(string content) { if (content[i] == '\n') { - yield return content.Substring(start, i - start); + yield return content[start..i]; start = i + 1; } } if (start < content.Length) { - yield return content.Substring(start); + yield return content[start..]; } } @@ -282,4 +282,4 @@ private static string GenerateFileContent(int lines) } #endregion -} +} \ No newline at end of file diff --git a/benchmarks/Bitbucket.Net.Benchmarks/Serialization/ColdStartBenchmarks.cs b/benchmarks/Bitbucket.Net.Benchmarks/Serialization/ColdStartBenchmarks.cs index b68fe68..18a64af 100644 --- a/benchmarks/Bitbucket.Net.Benchmarks/Serialization/ColdStartBenchmarks.cs +++ b/benchmarks/Bitbucket.Net.Benchmarks/Serialization/ColdStartBenchmarks.cs @@ -1,11 +1,11 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Engines; using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Core.Projects; using Bitbucket.Net.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; namespace Bitbucket.Net.Benchmarks.Serialization; @@ -164,4 +164,4 @@ private static PagedResults CreatePagedProjectsObject() Start = 0 }; } -} +} \ No newline at end of file diff --git a/benchmarks/Bitbucket.Net.Benchmarks/Serialization/JsonSerializationBenchmarks.cs b/benchmarks/Bitbucket.Net.Benchmarks/Serialization/JsonSerializationBenchmarks.cs index 66e37af..6259330 100644 --- a/benchmarks/Bitbucket.Net.Benchmarks/Serialization/JsonSerializationBenchmarks.cs +++ b/benchmarks/Bitbucket.Net.Benchmarks/Serialization/JsonSerializationBenchmarks.cs @@ -1,9 +1,9 @@ -using System.Text.Json; -using System.Text.Json.Serialization; using BenchmarkDotNet.Attributes; using Bitbucket.Net.Benchmarks.Config; using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Core.Projects; +using System.Text.Json; +using System.Text.Json.Serialization; namespace Bitbucket.Net.Benchmarks.Serialization; @@ -307,4 +307,4 @@ private static string CreatePagedCommitsJson(int count) } #endregion -} +} \ No newline at end of file diff --git a/benchmarks/Bitbucket.Net.Benchmarks/Serialization/SourceGenBenchmarks.cs b/benchmarks/Bitbucket.Net.Benchmarks/Serialization/SourceGenBenchmarks.cs index bee70c1..7dd09ad 100644 --- a/benchmarks/Bitbucket.Net.Benchmarks/Serialization/SourceGenBenchmarks.cs +++ b/benchmarks/Bitbucket.Net.Benchmarks/Serialization/SourceGenBenchmarks.cs @@ -1,12 +1,12 @@ -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; using BenchmarkDotNet.Attributes; using Bitbucket.Net.Benchmarks.Config; using Bitbucket.Net.Common.Converters; using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Core.Projects; using Bitbucket.Net.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; namespace Bitbucket.Net.Benchmarks.Serialization; @@ -605,4 +605,4 @@ private static string CreatePagedCommitsJson(int count) } #endregion -} +} \ No newline at end of file diff --git a/benchmarks/Bitbucket.Net.Benchmarks/Streaming/StreamingBenchmarks.cs b/benchmarks/Bitbucket.Net.Benchmarks/Streaming/StreamingBenchmarks.cs index dde68df..a0c86c3 100644 --- a/benchmarks/Bitbucket.Net.Benchmarks/Streaming/StreamingBenchmarks.cs +++ b/benchmarks/Bitbucket.Net.Benchmarks/Streaming/StreamingBenchmarks.cs @@ -1,10 +1,10 @@ -using System.Runtime.CompilerServices; -using System.Text.Json; -using System.Text.Json.Serialization; using BenchmarkDotNet.Attributes; using Bitbucket.Net.Benchmarks.Config; using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Core.Projects; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.Json.Serialization; namespace Bitbucket.Net.Benchmarks.Streaming; @@ -33,9 +33,7 @@ public class StreamingBenchmarks [GlobalSetup] public void Setup() { - _pagedResponses = Enumerable.Range(0, PageCount) - .Select(pageIndex => CreatePagedRepositoriesJson(ItemsPerPage, pageIndex, pageIndex < PageCount - 1)) - .ToList(); + _pagedResponses = [.. Enumerable.Range(0, PageCount).Select(pageIndex => CreatePagedRepositoriesJson(ItemsPerPage, pageIndex, pageIndex < PageCount - 1))]; } /// @@ -129,7 +127,7 @@ public async Task> StreamingEarlyTermination() public async Task> BufferedEarlyTermination() { var allResults = await BufferedApproach().ConfigureAwait(false); - return allResults.Take(10).ToList(); + return [.. allResults.Take(10)]; } private async IAsyncEnumerable StreamItemsAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) @@ -194,4 +192,4 @@ private static string CreatePagedRepositoriesJson(int count, int pageIndex, bool } """; } -} +} \ No newline at end of file diff --git a/benchmarks/Bitbucket.Net.Benchmarks/ZeroCopy/ZeroCopyBenchmarks.cs b/benchmarks/Bitbucket.Net.Benchmarks/ZeroCopy/ZeroCopyBenchmarks.cs index 667a704..81f3809 100644 --- a/benchmarks/Bitbucket.Net.Benchmarks/ZeroCopy/ZeroCopyBenchmarks.cs +++ b/benchmarks/Bitbucket.Net.Benchmarks/ZeroCopy/ZeroCopyBenchmarks.cs @@ -1,9 +1,9 @@ +using BenchmarkDotNet.Attributes; +using Bitbucket.Net.Benchmarks.Config; using System.Buffers; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; -using BenchmarkDotNet.Attributes; -using Bitbucket.Net.Benchmarks.Config; namespace Bitbucket.Net.Benchmarks.ZeroCopy; @@ -375,7 +375,7 @@ public sealed class BenchmarkPath public string? Parent { get; set; } public string? Name { get; set; } public string? Extension { get; set; } - + [JsonPropertyName("toString")] public string? PathString { get; set; } } @@ -388,4 +388,4 @@ public sealed class BenchmarkDiffHunk public int DestinationSpan { get; set; } public List? Segments { get; set; } public bool Truncated { get; set; } -} +} \ No newline at end of file diff --git a/global.json b/global.json new file mode 100644 index 0000000..6ce09c7 --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "10.0.100", + "rollForward": "latestFeature" + } +} \ No newline at end of file diff --git a/scripts/setup-githooks.ps1 b/scripts/setup-githooks.ps1 new file mode 100644 index 0000000..34bb30c --- /dev/null +++ b/scripts/setup-githooks.ps1 @@ -0,0 +1,23 @@ +[CmdletBinding()] +param() + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +if (-not (Get-Command git -ErrorAction SilentlyContinue)) { + throw 'git is required but was not found in PATH.' +} + +$rootDir = (git rev-parse --show-toplevel 2>$null) +if (-not $rootDir) { + throw 'Not inside a git repository.' +} + +Push-Location -LiteralPath $rootDir +try { + git config core.hooksPath .githooks + Write-Output 'Git hooks enabled (core.hooksPath=.githooks).' +} +finally { + Pop-Location +} diff --git a/scripts/setup-githooks.sh b/scripts/setup-githooks.sh new file mode 100644 index 0000000..5a5f520 --- /dev/null +++ b/scripts/setup-githooks.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(git rev-parse --show-toplevel 2>/dev/null || true)" + +if [[ -z "$ROOT_DIR" ]]; then + echo "Not inside a git repository." >&2 + exit 1 +fi + +cd "$ROOT_DIR" + +git config core.hooksPath .githooks + +# Make sure hook script is executable on macOS/Linux. +if command -v chmod >/dev/null 2>&1; then + chmod +x .githooks/pre-commit 2>/dev/null || true +fi + +echo "Git hooks enabled (core.hooksPath=.githooks)." >&2 diff --git a/scripts/verify-format.ps1 b/scripts/verify-format.ps1 new file mode 100644 index 0000000..5a919dd --- /dev/null +++ b/scripts/verify-format.ps1 @@ -0,0 +1,35 @@ +[CmdletBinding()] +param( + [Parameter()] + [switch]$Fix +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$rootDir = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path +$solutionPath = Join-Path $rootDir 'Bitbucket.Net.slnx' + +if (-not (Test-Path -LiteralPath $solutionPath)) { + throw "Solution not found at: $solutionPath" +} + +$verifyArgs = @() +if (-not $Fix) { + $verifyArgs += '--verify-no-changes' +} + +if (-not (Get-Command dotnet -ErrorAction SilentlyContinue)) { + throw 'dotnet is required but was not found in PATH.' +} + +if ($env:SKIP_DOTNET_FORMAT_WHITESPACE -ne '1') { + Write-Verbose "Running: dotnet format whitespace $solutionPath" + dotnet format whitespace $solutionPath @verifyArgs +} +else { + Write-Verbose 'Skipping whitespace formatting (SKIP_DOTNET_FORMAT_WHITESPACE=1)' +} + +Write-Verbose "Running: dotnet format style $solutionPath" +dotnet format style $solutionPath --severity warn @verifyArgs diff --git a/scripts/verify-format.sh b/scripts/verify-format.sh new file mode 100644 index 0000000..c8424d2 --- /dev/null +++ b/scripts/verify-format.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Verifies (default) or fixes formatting for the repository using dotnet format. +# +# Usage: +# bash scripts/verify-format.sh # verify +# bash scripts/verify-format.sh --verify # verify +# bash scripts/verify-format.sh --fix # apply fixes + +case "${1:-}" in + --fix) + MODE="fix" + ;; + --verify|"") + MODE="verify" + ;; + -h|--help) + cat <<'EOF' +verify-format.sh + +Verifies (default) or fixes formatting using dotnet format. + +Usage: + bash scripts/verify-format.sh # verify + bash scripts/verify-format.sh --verify # verify + bash scripts/verify-format.sh --fix # apply fixes + +Environment variables: + SKIP_DOTNET_FORMAT_WHITESPACE=1 Skip whitespace formatting checks. +EOF + exit 0 + ;; + *) + echo "Unknown argument: $1" >&2 + exit 2 + ;; +esac + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +SOLUTION_PATH="$ROOT_DIR/Bitbucket.Net.slnx" + +if [[ ! -f "$SOLUTION_PATH" ]]; then + echo "Solution not found at: $SOLUTION_PATH" >&2 + exit 1 +fi + +if ! command -v dotnet >/dev/null 2>&1; then + echo "dotnet is required but was not found in PATH." >&2 + exit 1 +fi + +VERIFY_ARGS=() +if [[ "$MODE" == "verify" ]]; then + VERIFY_ARGS+=(--verify-no-changes) +fi + +if [[ "${SKIP_DOTNET_FORMAT_WHITESPACE:-}" != "1" ]]; then + echo "Running: dotnet format whitespace ($MODE)" >&2 + dotnet format whitespace "$SOLUTION_PATH" "${VERIFY_ARGS[@]}" +else + echo "Skipping: dotnet format whitespace (SKIP_DOTNET_FORMAT_WHITESPACE=1)" >&2 +fi + +echo "Running: dotnet format style ($MODE)" >&2 +dotnet format style "$SOLUTION_PATH" --severity warn "${VERIFY_ARGS[@]}" diff --git a/src/Bitbucket.Net/Audit/BitbucketClient.cs b/src/Bitbucket.Net/Audit/BitbucketClient.cs index 830f27a..c2969f8 100644 --- a/src/Bitbucket.Net/Audit/BitbucketClient.cs +++ b/src/Bitbucket.Net/Audit/BitbucketClient.cs @@ -1,63 +1,96 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Bitbucket.Net.Common.Models; -using Bitbucket.Net.Models.Audit; -using Flurl.Http; - -namespace Bitbucket.Net -{ - public partial class BitbucketClient - { - private IFlurlRequest GetAuditUrl() => GetBaseUrl("/audit"); - - private IFlurlRequest GetAuditUrl(string path) => GetAuditUrl() - .AppendPathSegment(path); - - [System.Diagnostics.CodeAnalysis.SuppressMessage("AsyncUsage", "AsyncFixer01:Unnecessary async/await usage", Justification = "")] - public async Task> GetProjectAuditEventsAsync(string projectKey, - int? maxPages = null, - int? limit = null, - int? start = null, - int? avatarSize = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["avatarSize"] = avatarSize - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetAuditUrl($"/projects/{projectKey}/events") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("AsyncUsage", "AsyncFixer01:Unnecessary async/await usage", Justification = "")] - public async Task> GetProjectRepoAuditEventsAsync(string projectKey, string repositorySlug, - int? maxPages = null, - int? limit = null, - int? start = null, - int? avatarSize = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["avatarSize"] = avatarSize - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetAuditUrl($"/projects/{projectKey}/repos/{repositorySlug}/events") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - } -} +using Bitbucket.Net.Common; +using Bitbucket.Net.Common.Models; +using Bitbucket.Net.Models.Audit; +using Flurl.Http; + +namespace Bitbucket.Net; + +public partial class BitbucketClient +{ + /// + /// Gets the base audit URL. + /// + /// An representing the audit endpoint. + private IFlurlRequest GetAuditUrl() => GetBaseUrl("/audit"); + + /// + /// Gets the audit URL with the specified path appended. + /// + /// The path to append to the audit URL. + /// An representing the audit endpoint with the specified path. + private IFlurlRequest GetAuditUrl(string path) => GetAuditUrl() + .AppendPathSegment(path); + + /// + /// Retrieves audit events for a specific project. + /// + /// The key of the project. + /// The maximum number of pages to retrieve. If null, all pages are retrieved. + /// The maximum number of results per page. + /// The starting index for pagination. + /// The size of user avatars to include in the response. + /// A cancellation token that can be used to cancel the operation. + /// A task that represents the asynchronous operation. The task result contains a collection of objects. + public async Task> GetProjectAuditEventsAsync(string projectKey, + int? maxPages = null, + int? limit = null, + int? start = null, + int? avatarSize = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["avatarSize"] = avatarSize, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetAuditUrl($"/projects/{projectKey}/events") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Retrieves audit events for a specific repository within a project. + /// + /// The key of the project. + /// The slug (identifier) of the repository. + /// The maximum number of pages to retrieve. If , all pages are retrieved. + /// The maximum number of results per page. + /// The starting index for pagination. + /// The size of user avatars to include in the response. + /// A cancellation token that can be used to cancel the operation. + /// A task that represents the asynchronous operation. The task result contains a collection of objects. + public async Task> GetProjectRepoAuditEventsAsync(string projectKey, string repositorySlug, + int? maxPages = null, + int? limit = null, + int? start = null, + int? avatarSize = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["avatarSize"] = avatarSize, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetAuditUrl($"/projects/{projectKey}/repos/{repositorySlug}/events") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Bitbucket.Net.csproj b/src/Bitbucket.Net/Bitbucket.Net.csproj index e15653e..d40f0a9 100644 --- a/src/Bitbucket.Net/Bitbucket.Net.csproj +++ b/src/Bitbucket.Net/Bitbucket.Net.csproj @@ -1,11 +1,8 @@  - net10.0 - enable - latest BitbucketServer.Net - 0.1.0-beta.1 + 0.2.0 Diogo Carvalho Modernized fork of Bitbucket.Net — C# client for Bitbucket Server (Stash) REST API. Adds streaming, CancellationToken support, System.Text.Json, typed exceptions, and Bitbucket Server 9.0+ compatibility. bitbucket;bitbucket-server;stash;api;client;rest @@ -14,6 +11,10 @@ git MIT README.md + See https://github.com/diomonogatari/Bitbucket.Net/blob/main/CHANGELOG.md + Copyright (c) Diogo Carvalho + true + $(NoWarn);CS1591 true @@ -25,6 +26,10 @@ all runtime; build; native; contentfiles; analyzers + + all + runtime; build; native; contentfiles; analyzers + diff --git a/src/Bitbucket.Net/BitbucketClient.cs b/src/Bitbucket.Net/BitbucketClient.cs index f9a1328..0a72cf0 100644 --- a/src/Bitbucket.Net/BitbucketClient.cs +++ b/src/Bitbucket.Net/BitbucketClient.cs @@ -1,13 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Runtime.CompilerServices; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; -using System.Threading; -using System.Threading.Tasks; using Bitbucket.Net.Common; using Bitbucket.Net.Common.Converters; using Bitbucket.Net.Common.Exceptions; @@ -16,294 +6,396 @@ using Flurl; using Flurl.Http; using Flurl.Http.Configuration; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; + +namespace Bitbucket.Net; -namespace Bitbucket.Net +/// +/// Client for interacting with Bitbucket Server REST APIs. +/// +public partial class BitbucketClient { - public partial class BitbucketClient + private static readonly JsonSerializerOptions s_jsonOptions = new() { - private static readonly JsonSerializerOptions s_jsonOptions = new() + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + // Source-generated context with reflection fallback for edge cases + TypeInfoResolver = JsonTypeInfoResolver.Combine( + BitbucketJsonContext.Default, // Source-generated (fast path) + new DefaultJsonTypeInfoResolver() // Reflection fallback for unregistered types + ), + Converters = { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - // Source-generated context with reflection fallback for edge cases - TypeInfoResolver = JsonTypeInfoResolver.Combine( - BitbucketJsonContext.Default, // Source-generated (fast path) - new DefaultJsonTypeInfoResolver() // Reflection fallback for unregistered types - ), - Converters = - { - new UnixDateTimeOffsetConverter(), - new NullableUnixDateTimeOffsetConverter(), - new PermissionsConverter(), - new RolesConverter(), - new FileTypesConverter(), - new LineTypesConverter(), - new ParticipantStatusConverter(), - new PullRequestStatesConverter(), - new HookTypesConverter(), - new ScopeTypesConverter(), - new WebHookOutcomesConverter(), - new RefRestrictionTypesConverter(), - new SynchronizeActionsConverter(), - new BlockerCommentStateConverter(), - new CommentSeverityConverter() - } - }; + new UnixDateTimeOffsetConverter(), + new NullableUnixDateTimeOffsetConverter(), + new PermissionsConverter(), + new RolesConverter(), + new FileTypesConverter(), + new LineTypesConverter(), + new ParticipantStatusConverter(), + new PullRequestStatesConverter(), + new HookTypesConverter(), + new ScopeTypesConverter(), + new WebHookOutcomesConverter(), + new RefRestrictionTypesConverter(), + new SynchronizeActionsConverter(), + new BlockerCommentStateConverter(), + new CommentSeverityConverter() + }, + }; + + private static readonly ISerializer s_serializer = new DefaultJsonSerializer(s_jsonOptions); + + private readonly Url _url; + private readonly Func? _getToken; + private readonly string? _userName; + private readonly string? _password; + private readonly IFlurlClient? _injectedClient; + + /// + /// Initializes a new instance of the class with the specified base URL. + /// + /// The base Bitbucket Server URL. + private BitbucketClient(string url) + { + _url = url; + } - private static readonly ISerializer s_serializer = new DefaultJsonSerializer(s_jsonOptions); + /// + /// Creates a BitbucketClient with basic authentication. + /// + /// The base URL of the Bitbucket Server instance. + /// The username for basic authentication. + /// The password for basic authentication. + public BitbucketClient(string url, string userName, string password) + : this(url) + { + _userName = userName; + _password = password; + } - static BitbucketClient() - { - // Configure Flurl to use System.Text.Json globally - FlurlHttp.Clients.WithDefaults(builder => - builder.WithSettings(settings => - settings.JsonSerializer = s_serializer)); - } + /// + /// Creates a BitbucketClient with token-based authentication. + /// + /// The base URL of the Bitbucket Server instance. + /// A function that returns the bearer token. + public BitbucketClient(string url, Func getToken) + : this(url) + { + _getToken = getToken; + } - private readonly Url _url; - private readonly Func? _getToken; - private readonly string? _userName; - private readonly string? _password; - private readonly IFlurlClient? _injectedClient; + /// + /// Creates a BitbucketClient using an externally managed HttpClient. + /// This constructor is designed for dependency injection scenarios where consumers + /// want to configure the HttpClient with IHttpClientFactory, Polly resilience policies, + /// custom timeouts, or other middleware. + /// + /// The externally managed HttpClient instance. The client should be configured with any desired resilience policies, timeouts, etc. + /// The base URL of the Bitbucket Server instance. + /// Optional: A function that returns the bearer token for authentication. + /// + /// + /// When using this constructor, authentication should typically be handled by configuring + /// the HttpClient with appropriate headers via IHttpClientFactory or DelegatingHandlers. + /// If getToken is provided, it will add the Authorization header to each request. + /// + /// + /// Example DI registration: + /// + /// services.AddHttpClient<BitbucketClient>() + /// .AddTransientHttpErrorPolicy(p => p.WaitAndRetryAsync(3, _ => TimeSpan.FromSeconds(1))) + /// .ConfigureHttpClient(c => c.Timeout = TimeSpan.FromMinutes(2)); + /// + /// services.AddSingleton<BitbucketClient>(sp => { + /// var httpClient = sp.GetRequiredService<IHttpClientFactory>().CreateClient(nameof(BitbucketClient)); + /// return new BitbucketClient(httpClient, "https://bitbucket.example.com", () => GetToken()); + /// }); + /// + /// + /// + public BitbucketClient(HttpClient httpClient, string baseUrl, Func? getToken = null) + { + ArgumentNullException.ThrowIfNull(httpClient); + if (string.IsNullOrWhiteSpace(baseUrl)) throw new ArgumentNullException(nameof(baseUrl)); + + _url = baseUrl; + _getToken = getToken; + _injectedClient = new FlurlClient(httpClient, baseUrl) + .WithSettings(settings => settings.JsonSerializer = s_serializer); + } + + /// + /// Creates a BitbucketClient using an externally managed IFlurlClient. + /// This constructor provides maximum control over the Flurl client configuration. + /// + /// The pre-configured IFlurlClient instance. + /// Optional: A function that returns the bearer token for authentication. + /// + /// Use this constructor when you need fine-grained control over Flurl's configuration, + /// such as custom event handlers, advanced settings, or when using IFlurlClientCache. + /// + public BitbucketClient(IFlurlClient flurlClient, Func? getToken = null) + { + _injectedClient = flurlClient ?? throw new ArgumentNullException(nameof(flurlClient)); + _url = flurlClient.BaseUrl ?? throw new ArgumentException("FlurlClient must have a BaseUrl configured.", nameof(flurlClient)); + _getToken = getToken; + } - private BitbucketClient(string url) + /// + /// Builds a Flurl request rooted at the Bitbucket REST API. + /// + /// The API root segment (default is /api). + /// The API version segment (default is 1.0). + /// An configured with authentication and serialization. + private IFlurlRequest GetBaseUrl(string root = "/api", string version = "1.0") + { + IFlurlRequest request; + + // If using injected client, use it directly + if (_injectedClient != null) { - _url = url; - } + request = _injectedClient + .Request() + .AppendPathSegment($"/rest{root}/{version}"); - /// - /// Creates a BitbucketClient with basic authentication. - /// - /// The base URL of the Bitbucket Server instance. - /// The username for basic authentication. - /// The password for basic authentication. - public BitbucketClient(string url, string userName, string password) - : this(url) + // Apply token authentication if provided + if (_getToken != null) + { + request = request.WithOAuthBearerToken(_getToken()); + } + } + else { - _userName = userName; - _password = password; + // Original behavior for non-DI scenarios + var fullUrl = new Url(_url) + .AppendPathSegment($"/rest{root}/{version}"); + request = new FlurlRequest(fullUrl) + .WithAuthentication(_getToken, _userName, _password); } - /// - /// Creates a BitbucketClient with token-based authentication. - /// - /// The base URL of the Bitbucket Server instance. - /// A function that returns the bearer token. - public BitbucketClient(string url, Func getToken) - : this(url) + return request + .AllowAnyHttpStatus() + .WithSettings(settings => settings.JsonSerializer = s_serializer); + } + + private static async Task ReadResponseStringAsync(IFlurlResponse response, CancellationToken cancellationToken) + { + if (response.ResponseMessage?.Content is null) { - _getToken = getToken; + return string.Empty; } - /// - /// Creates a BitbucketClient using an externally managed HttpClient. - /// This constructor is designed for dependency injection scenarios where consumers - /// want to configure the HttpClient with IHttpClientFactory, Polly resilience policies, - /// custom timeouts, or other middleware. - /// - /// The externally managed HttpClient instance. The client should be configured with any desired resilience policies, timeouts, etc. - /// The base URL of the Bitbucket Server instance. - /// Optional: A function that returns the bearer token for authentication. - /// - /// - /// When using this constructor, authentication should typically be handled by configuring - /// the HttpClient with appropriate headers via IHttpClientFactory or DelegatingHandlers. - /// If getToken is provided, it will add the Authorization header to each request. - /// - /// - /// Example DI registration: - /// - /// services.AddHttpClient<BitbucketClient>() - /// .AddTransientHttpErrorPolicy(p => p.WaitAndRetryAsync(3, _ => TimeSpan.FromSeconds(1))) - /// .ConfigureHttpClient(c => c.Timeout = TimeSpan.FromMinutes(2)); - /// - /// services.AddSingleton<BitbucketClient>(sp => { - /// var httpClient = sp.GetRequiredService<IHttpClientFactory>().CreateClient(nameof(BitbucketClient)); - /// return new BitbucketClient(httpClient, "https://bitbucket.example.com", () => GetToken()); - /// }); - /// - /// - /// - public BitbucketClient(HttpClient httpClient, string baseUrl, Func? getToken = null) - { - if (httpClient == null) throw new ArgumentNullException(nameof(httpClient)); - if (string.IsNullOrWhiteSpace(baseUrl)) throw new ArgumentNullException(nameof(baseUrl)); + return await response.ResponseMessage.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + } - _url = baseUrl; - _getToken = getToken; - _injectedClient = new FlurlClient(httpClient, baseUrl) - .WithSettings(settings => settings.JsonSerializer = s_serializer); - } + private static StringContent CreateJsonContent(TValue value) + { + var json = JsonSerializer.Serialize(value, s_jsonOptions); + return new StringContent(json, Encoding.UTF8, "application/json"); + } - /// - /// Creates a BitbucketClient using an externally managed IFlurlClient. - /// This constructor provides maximum control over the Flurl client configuration. - /// - /// The pre-configured IFlurlClient instance. - /// Optional: A function that returns the bearer token for authentication. - /// - /// Use this constructor when you need fine-grained control over Flurl's configuration, - /// such as custom event handlers, advanced settings, or when using IFlurlClientCache. - /// - public BitbucketClient(IFlurlClient flurlClient, Func? getToken = null) + private static async Task ReadResponseBytesAsync(IFlurlResponse response, CancellationToken cancellationToken) + { + if (response.ResponseMessage?.Content is null) { - _injectedClient = flurlClient ?? throw new ArgumentNullException(nameof(flurlClient)); - _url = flurlClient.BaseUrl ?? throw new ArgumentException("FlurlClient must have a BaseUrl configured.", nameof(flurlClient)); - _getToken = getToken; + return Array.Empty(); } - private IFlurlRequest GetBaseUrl(string root = "/api", string version = "1.0") - { - // If using injected client, use it directly - if (_injectedClient != null) - { - var request = _injectedClient - .Request() - .AppendPathSegment($"/rest{root}/{version}") - .WithSettings(settings => settings.JsonSerializer = s_serializer); + return await response.ResponseMessage.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); + } - // Apply token authentication if provided - if (_getToken != null) - { - request = request.WithOAuthBearerToken(_getToken()); - } + private static async Task ReadResponseStreamAsync(IFlurlResponse response, CancellationToken cancellationToken) + { + if (response.ResponseMessage?.Content is null) + { + return Stream.Null; + } - return request; - } + return await response.ResponseMessage.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + } - // Original behavior for non-DI scenarios - return new Url(_url) - .AppendPathSegment($"/rest{root}/{version}") - .WithSettings(settings => settings.JsonSerializer = s_serializer) - .WithAuthentication(_getToken, _userName, _password); - } + /// + /// Reads the response content and deserializes it. + /// + /// The type of the result. + /// The HTTP response. + /// Optional custom handler to parse the response content. + /// Token to cancel the operation. + /// The deserialized response content. + private static async Task ReadResponseContentAsync(IFlurlResponse response, Func? contentHandler = null, CancellationToken cancellationToken = default) + { + string content = await ReadResponseStringAsync(response, cancellationToken).ConfigureAwait(false); - private async Task ReadResponseContentAsync(IFlurlResponse response, Func? contentHandler = null, CancellationToken cancellationToken = default) + if (contentHandler is not null) { - string content = await response.GetStringAsync().ConfigureAwait(false); - return contentHandler != null - ? contentHandler(content) - : JsonSerializer.Deserialize(content, s_jsonOptions)!; + return contentHandler(content); } - private async Task ReadResponseContentAsync(IFlurlResponse response, CancellationToken cancellationToken = default) + if (string.IsNullOrWhiteSpace(content)) { - string content = await response.GetStringAsync().ConfigureAwait(false); - return content == ""; + return default!; } - private async Task HandleErrorsAsync(IFlurlResponse response, CancellationToken cancellationToken = default) + return JsonSerializer.Deserialize(content, s_jsonOptions)!; + } + + /// + /// Reads the response content and returns success based on empty body. + /// + /// The HTTP response. + /// Token to cancel the operation. + /// true if the response body is empty; otherwise, false. + private static async Task ReadResponseContentAsync(IFlurlResponse response, CancellationToken cancellationToken = default) + { + string content = await ReadResponseStringAsync(response, cancellationToken).ConfigureAwait(false); + return string.IsNullOrWhiteSpace(content); + } + + /// + /// Throws an exception if the response indicates an error. + /// + /// The HTTP response. + /// Token to cancel the operation. + /// A task that completes when error handling is finished. + private static async Task HandleErrorsAsync(IFlurlResponse response, CancellationToken cancellationToken = default) + { + if (response.StatusCode >= 400) { - if (response.StatusCode >= 400) + var errors = Array.Empty(); + string? requestUrl = response.ResponseMessage?.RequestMessage?.RequestUri?.ToString(); + string? rawResponseBody = null; + + try { - var errors = Array.Empty(); - string? requestUrl = response.ResponseMessage?.RequestMessage?.RequestUri?.ToString(); - string? rawResponseBody = null; + // Read the response body first so we can include it in the error if parsing fails + rawResponseBody = await ReadResponseStringAsync(response, cancellationToken).ConfigureAwait(false); - try + if (!string.IsNullOrWhiteSpace(rawResponseBody)) { - // Read the response body first so we can include it in the error if parsing fails - rawResponseBody = await response.GetStringAsync().ConfigureAwait(false); - - if (!string.IsNullOrWhiteSpace(rawResponseBody)) + var errorResponse = JsonSerializer.Deserialize(rawResponseBody, s_jsonOptions); + if (errorResponse?.Errors != null && errorResponse.Errors.Any()) { - var errorResponse = JsonSerializer.Deserialize(rawResponseBody, s_jsonOptions); - if (errorResponse?.Errors != null && errorResponse.Errors.Any()) - { - errors = errorResponse.Errors.ToArray(); - } + errors = [.. errorResponse.Errors]; } } - catch + } + catch + { + // If we can't parse the error response as JSON, create a synthetic error with the raw body + if (!string.IsNullOrWhiteSpace(rawResponseBody)) { - // If we can't parse the error response as JSON, create a synthetic error with the raw body - if (!string.IsNullOrWhiteSpace(rawResponseBody)) - { - // Truncate very long responses - var truncatedBody = rawResponseBody.Length > 500 - ? rawResponseBody.Substring(0, 500) + "..." - : rawResponseBody; - errors = new[] { new Error { Message = truncatedBody } }; - } + // Truncate very long responses + var truncatedBody = rawResponseBody.Length > 500 + ? rawResponseBody[..500] + "..." + : rawResponseBody; + errors = [new Error { Message = truncatedBody }]; } - - throw BitbucketApiException.Create(response.StatusCode, errors, requestUrl); } - } - private async Task HandleResponseAsync(IFlurlResponse response, Func? contentHandler = null, CancellationToken cancellationToken = default) - { - await HandleErrorsAsync(response, cancellationToken).ConfigureAwait(false); - return await ReadResponseContentAsync(response, contentHandler, cancellationToken).ConfigureAwait(false); + throw BitbucketApiException.Create(response.StatusCode, errors, requestUrl); } + } - private async Task HandleResponseAsync(IFlurlResponse response, CancellationToken cancellationToken = default) - { - await HandleErrorsAsync(response, cancellationToken).ConfigureAwait(false); - return await ReadResponseContentAsync(response, cancellationToken).ConfigureAwait(false); - } + /// + /// Handles an HTTP response, throwing on errors and deserializing the content. + /// + /// The result type. + /// The HTTP response. + /// Optional custom content handler. + /// Token to cancel the operation. + /// The deserialized response content. + private static async Task HandleResponseAsync(IFlurlResponse response, Func? contentHandler = null, CancellationToken cancellationToken = default) + { + await HandleErrorsAsync(response, cancellationToken).ConfigureAwait(false); + return await ReadResponseContentAsync(response, contentHandler, cancellationToken).ConfigureAwait(false); + } - private async Task> GetPagedResultsAsync(int? maxPages, IDictionary queryParamValues, Func, CancellationToken, Task>> selector, CancellationToken cancellationToken = default) - { - var results = new List(); - bool isLastPage = false; - int numPages = 0; + /// + /// Handles an HTTP response, throwing on errors and returning a boolean success indicator. + /// + /// The HTTP response. + /// Token to cancel the operation. + /// true if the response body is empty; otherwise, false. + private static async Task HandleResponseAsync(IFlurlResponse response, CancellationToken cancellationToken = default) + { + await HandleErrorsAsync(response, cancellationToken).ConfigureAwait(false); + return await ReadResponseContentAsync(response, cancellationToken).ConfigureAwait(false); + } - while (!isLastPage && (maxPages == null || numPages < maxPages)) - { - cancellationToken.ThrowIfCancellationRequested(); - var selectorResults = await selector(queryParamValues, cancellationToken).ConfigureAwait(false); - results.AddRange(selectorResults.Values); + /// + /// Retrieves paged results from a paginated endpoint. + /// + /// The item type in the paged results. + /// Optional maximum number of pages to retrieve. + /// The query parameter values for requests. + /// A delegate that retrieves a page of results. + /// Token to cancel the operation. + /// All retrieved items. + private static async Task> GetPagedResultsAsync(int? maxPages, IDictionary queryParamValues, Func, CancellationToken, Task>> selector, CancellationToken cancellationToken = default) + { + var results = new List(); + bool isLastPage = false; + int numPages = 0; - isLastPage = selectorResults.IsLastPage; - if (!isLastPage && selectorResults.NextPageStart.HasValue) - { - queryParamValues["start"] = selectorResults.NextPageStart.Value; - } + while (!isLastPage && (maxPages == null || numPages < maxPages)) + { + cancellationToken.ThrowIfCancellationRequested(); + var selectorResults = await selector(queryParamValues, cancellationToken).ConfigureAwait(false); + results.AddRange(selectorResults.Values); - numPages++; + isLastPage = selectorResults.IsLastPage; + if (!isLastPage && selectorResults.NextPageStart.HasValue) + { + queryParamValues["start"] = selectorResults.NextPageStart.Value; } - return results; + numPages++; } - /// - /// Streams paged results as an IAsyncEnumerable, yielding items as they are retrieved. - /// This is more memory-efficient for large result sets and provides faster time-to-first-result. - /// - /// The type of items in the paged results. - /// Optional maximum number of pages to retrieve. - /// Query parameters for the API request. - /// Function to retrieve a page of results. - /// Cancellation token. - /// An async enumerable that yields items as they are retrieved. - private async IAsyncEnumerable GetPagedResultsStreamAsync( - int? maxPages, - IDictionary queryParamValues, - Func, CancellationToken, Task>> selector, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - bool isLastPage = false; - int numPages = 0; + return results; + } - while (!isLastPage && (maxPages == null || numPages < maxPages)) - { - cancellationToken.ThrowIfCancellationRequested(); - var selectorResults = await selector(queryParamValues, cancellationToken).ConfigureAwait(false); + /// + /// Streams paged results as an IAsyncEnumerable, yielding items as they are retrieved. + /// This is more memory-efficient for large result sets and provides faster time-to-first-result. + /// + /// The type of items in the paged results. + /// Optional maximum number of pages to retrieve. + /// Query parameters for the API request. + /// Function to retrieve a page of results. + /// Cancellation token. + /// An async enumerable that yields items as they are retrieved. + private static async IAsyncEnumerable GetPagedResultsStreamAsync( + int? maxPages, + IDictionary queryParamValues, + Func, CancellationToken, Task>> selector, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + bool isLastPage = false; + int numPages = 0; - foreach (var item in selectorResults.Values) - { - yield return item; - } + while (!isLastPage && (maxPages == null || numPages < maxPages)) + { + cancellationToken.ThrowIfCancellationRequested(); + var selectorResults = await selector(queryParamValues, cancellationToken).ConfigureAwait(false); - isLastPage = selectorResults.IsLastPage; - if (!isLastPage && selectorResults.NextPageStart.HasValue) - { - queryParamValues["start"] = selectorResults.NextPageStart.Value; - } + foreach (var item in selectorResults.Values) + { + yield return item; + } - numPages++; + isLastPage = selectorResults.IsLastPage; + if (!isLastPage && selectorResults.NextPageStart.HasValue) + { + queryParamValues["start"] = selectorResults.NextPageStart.Value; } + + numPages++; } } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Branches/BitbucketClient.cs b/src/Bitbucket.Net/Branches/BitbucketClient.cs index 4275f62..e7fdb11 100644 --- a/src/Bitbucket.Net/Branches/BitbucketClient.cs +++ b/src/Bitbucket.Net/Branches/BitbucketClient.cs @@ -1,82 +1,132 @@ -using System.Collections.Generic; -using System.Net.Http; -using System.Text; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; using Bitbucket.Net.Common; using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Branches; using Bitbucket.Net.Models.Core.Projects; using Flurl.Http; +using System.Text; +using System.Text.Json; + +namespace Bitbucket.Net; -namespace Bitbucket.Net +/// +/// Provides branch-related Bitbucket API operations. +/// +public partial class BitbucketClient { - public partial class BitbucketClient - { - private IFlurlRequest GetBranchUrl() => GetBaseUrl("/branch-utils"); + /// + /// Gets the base URL for branch utilities. + /// + /// An configured for branch utilities. + private IFlurlRequest GetBranchUrl() => GetBaseUrl("/branch-utils"); - private IFlurlRequest GetBranchUrl(string path) => GetBranchUrl() - .AppendPathSegment(path); + /// + /// Gets the branch utilities URL for the specified path. + /// + /// The path to append to the branch utilities root. + /// An pointing to the requested branch utilities path. + private IFlurlRequest GetBranchUrl(string path) => GetBranchUrl() + .AppendPathSegment(path); - public async Task> GetCommitBranchInfoAsync(string projectKey, string repositorySlug, string fullSha, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) + /// + /// Retrieves branch information for a specific commit. + /// + /// The project key. + /// The repository slug. + /// The full commit SHA to query. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Token to cancel the operation. + /// A collection of branch information entries for the commit. + public async Task> GetCommitBranchInfoAsync(string projectKey, string repositorySlug, string fullSha, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) { - var queryParamValues = new Dictionary + ["limit"] = limit, + ["start"] = start, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => { - ["limit"] = limit, - ["start"] = start - }; + var response = await GetBranchUrl($"/projects/{projectKey}/repos/{repositorySlug}/branches/info/{fullSha}") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetBranchUrl($"/projects/{projectKey}/repos/{repositorySlug}/branches/info/{fullSha}") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } + /// + /// Retrieves the branch model configuration for a repository. + /// + /// The project key. + /// The repository slug. + /// Token to cancel the operation. + /// The branch model configuration. + public async Task GetRepoBranchModelAsync(string projectKey, string repositorySlug, CancellationToken cancellationToken = default) + { + var response = await GetBranchUrl($"/projects/{projectKey}/repos/{repositorySlug}/branchmodel") + .GetAsync(cancellationToken) + .ConfigureAwait(false); - public async Task GetRepoBranchModelAsync(string projectKey, string repositorySlug, CancellationToken cancellationToken = default) - { - return await GetBranchUrl($"/projects/{projectKey}/repos/{repositorySlug}/branchmodel") - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } - public async Task CreateRepoBranchAsync(string projectKey, string repositorySlug, string branchName, string startPoint, CancellationToken cancellationToken = default) + /// + /// Creates a new branch in a repository. + /// + /// The project key. + /// The repository slug. + /// The name of the new branch. + /// The commit or ref from which to branch. + /// Token to cancel the operation. + /// The created branch. + public async Task CreateRepoBranchAsync(string projectKey, string repositorySlug, string branchName, string startPoint, CancellationToken cancellationToken = default) + { + var data = new { - var data = new - { - name = branchName, - startPoint - }; + name = branchName, + startPoint, + }; - var response = await GetBranchUrl($"/projects/{projectKey}/repos/{repositorySlug}/branches") - .PostJsonAsync(data, cancellationToken: cancellationToken) - .ConfigureAwait(false); + var response = await GetBranchUrl($"/projects/{projectKey}/repos/{repositorySlug}/branches") + .SendAsync(HttpMethod.Post, CreateJsonContent(data), cancellationToken: cancellationToken) + .ConfigureAwait(false); - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } - public async Task DeleteRepoBranchAsync(string projectKey, string repositorySlug, string branchName, bool dryRun, string? endPoint = null, CancellationToken cancellationToken = default) + /// + /// Deletes a branch from a repository. + /// + /// The project key. + /// The repository slug. + /// The name of the branch to delete. + /// If true, performs validation without deleting. + /// Optional endpoint ref to compare for merge checks. + /// Token to cancel the operation. + /// true if the branch was deleted; otherwise, false. + public async Task DeleteRepoBranchAsync(string projectKey, string repositorySlug, string branchName, bool dryRun, string? endPoint = null, CancellationToken cancellationToken = default) + { + var data = new { - var data = new - { - name = branchName, - dryRun = BitbucketHelpers.BoolToString(dryRun), - endPoint - }; + name = branchName, + dryRun = BitbucketHelpers.BoolToString(dryRun), + endPoint, + }; - var json = JsonSerializer.Serialize(data, s_jsonOptions); - var response = await GetBranchUrl($"/projects/{projectKey}/repos/{repositorySlug}/branches") - .WithHeader("Content-Type", "application/json") - .SendAsync(HttpMethod.Delete, new StringContent(json, Encoding.UTF8, "application/json"), cancellationToken: cancellationToken) - .ConfigureAwait(false); + var json = JsonSerializer.Serialize(data, s_jsonOptions); + var response = await GetBranchUrl($"/projects/{projectKey}/repos/{repositorySlug}/branches") + .WithHeader("Content-Type", "application/json") + .SendAsync(HttpMethod.Delete, new StringContent(json, Encoding.UTF8, "application/json"), cancellationToken: cancellationToken) + .ConfigureAwait(false); - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Builds/BitbucketClient.cs b/src/Bitbucket.Net/Builds/BitbucketClient.cs index 09bd59c..591fabc 100644 --- a/src/Bitbucket.Net/Builds/BitbucketClient.cs +++ b/src/Bitbucket.Net/Builds/BitbucketClient.cs @@ -1,68 +1,116 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; using Bitbucket.Net.Common; using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Builds; using Flurl.Http; -namespace Bitbucket.Net +namespace Bitbucket.Net; + +/// +/// Provides build status related Bitbucket API operations. +/// +public partial class BitbucketClient { - public partial class BitbucketClient + /// + /// Gets the base build status URL. + /// + /// An targeting the build-status root. + private IFlurlRequest GetBuildsUrl() => GetBaseUrl("/build-status"); + + /// + /// Gets the build status URL for the specified path. + /// + /// The path to append to the build-status root. + /// An pointing to the build-status path. + private IFlurlRequest GetBuildsUrl(string path) => GetBuildsUrl() + .AppendPathSegment(path); + + /// + /// Retrieves build statistics for a specific commit. + /// + /// The commit identifier. + /// Whether to include unique build statistics. + /// Token to cancel the operation. + /// Build statistics for the commit. + public async Task GetBuildStatsForCommitAsync(string commitId, bool includeUnique = false, CancellationToken cancellationToken = default) { - private IFlurlRequest GetBuildsUrl() => GetBaseUrl("/build-status"); + var response = await GetBuildsUrl($"/commits/stats/{commitId}") + .SetQueryParam("includeUnique", BitbucketHelpers.BoolToString(includeUnique)) + .GetAsync(cancellationToken) + .ConfigureAwait(false); - private IFlurlRequest GetBuildsUrl(string path) => GetBuildsUrl() - .AppendPathSegment(path); + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } - public async Task GetBuildStatsForCommitAsync(string commitId, bool includeUnique = false, CancellationToken cancellationToken = default) - { - return await GetBuildsUrl($"/commits/stats/{commitId}") - .SetQueryParam("includeUnique", BitbucketHelpers.BoolToString(includeUnique)) - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } + /// + /// Retrieves build statistics for multiple commits. + /// + /// Token to cancel the operation. + /// The commit identifiers. + /// A dictionary mapping commit IDs to build statistics. + public async Task> GetBuildStatsForCommitsAsync(CancellationToken cancellationToken, params string[] commitIds) + { + var response = await GetBuildsUrl("/commits/stats") + .SendAsync(HttpMethod.Post, CreateJsonContent(commitIds), cancellationToken: cancellationToken) + .ConfigureAwait(false); - public async Task> GetBuildStatsForCommitsAsync(CancellationToken cancellationToken, params string[] commitIds) - { - var response = await GetBuildsUrl("/commits/stats") - .PostJsonAsync(commitIds, cancellationToken: cancellationToken) - .ConfigureAwait(false); + return await HandleResponseAsync>(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } - return await HandleResponseAsync>(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } + /// + /// Retrieves build statistics for multiple commits using the default cancellation token. + /// + /// The commit identifiers. + /// A dictionary mapping commit IDs to build statistics. + public async Task> GetBuildStatsForCommitsAsync(params string[] commitIds) + { + return await GetBuildStatsForCommitsAsync(default, commitIds).ConfigureAwait(false); + } - public async Task> GetBuildStatsForCommitsAsync(params string[] commitIds) + /// + /// Retrieves build status entries for a specific commit. + /// + /// The commit identifier. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Token to cancel the operation. + /// A collection of build status entries. + public async Task> GetBuildStatusForCommitAsync(string commitId, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) { - return await GetBuildStatsForCommitsAsync(default, commitIds).ConfigureAwait(false); - } + ["limit"] = limit, + ["start"] = start, + }; - public async Task> GetBuildStatusForCommitAsync(string commitId, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => { - ["limit"] = limit, - ["start"] = start - }; + var response = await GetBuildsUrl($"/commits/{commitId}") + .GetAsync(ct) + .ConfigureAwait(false); - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetBuildsUrl($"/commits/{commitId}") - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } - public async Task AssociateBuildStatusWithCommitAsync(string commitId, BuildStatus buildStatus, CancellationToken cancellationToken = default) - { - var response = await GetBuildsUrl($"/commits/{commitId}") - .PostJsonAsync(buildStatus, cancellationToken: cancellationToken) - .ConfigureAwait(false); + /// + /// Associates a build status with a commit. + /// + /// The commit identifier. + /// The build status to associate. + /// Token to cancel the operation. + /// true if the association was successful; otherwise, false. + public async Task AssociateBuildStatusWithCommitAsync(string commitId, BuildStatus buildStatus, CancellationToken cancellationToken = default) + { + var response = await GetBuildsUrl($"/commits/{commitId}") + .SendAsync(HttpMethod.Post, CreateJsonContent(buildStatus), cancellationToken: cancellationToken) + .ConfigureAwait(false); - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/CommentLikes/BitbucketClient.cs b/src/Bitbucket.Net/CommentLikes/BitbucketClient.cs index aaf9778..e86fee7 100644 --- a/src/Bitbucket.Net/CommentLikes/BitbucketClient.cs +++ b/src/Bitbucket.Net/CommentLikes/BitbucketClient.cs @@ -1,96 +1,173 @@ -using System.Collections.Generic; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; +using Bitbucket.Net.Common; using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Core.Users; using Flurl.Http; -namespace Bitbucket.Net +namespace Bitbucket.Net; + +/// +/// Provides comment-like related Bitbucket API operations. +/// +public partial class BitbucketClient { - public partial class BitbucketClient - { - private IFlurlRequest GetCommentLikesUrl() => GetBaseUrl("/comment-likes"); + /// + /// Gets the base URL for comment likes. + /// + /// An targeting the comment likes root. + private IFlurlRequest GetCommentLikesUrl() => GetBaseUrl("/comment-likes"); - private IFlurlRequest GetCommentLikesUrl(string path) => GetCommentLikesUrl() - .AppendPathSegment(path); + /// + /// Gets the comment likes URL for the specified path. + /// + /// The path to append to the comment likes root. + /// An pointing to the comment likes path. + private IFlurlRequest GetCommentLikesUrl(string path) => GetCommentLikesUrl() + .AppendPathSegment(path); - public async Task> GetCommitCommentLikesAsync(string projectKey, string repositorySlug, string commitId, string commentId, - int? maxPages = null, - int? limit = null, - int? start = null, - int? avatarSize = null, - CancellationToken cancellationToken = default) + /// + /// Retrieves users who liked a commit comment. + /// + /// The project key. + /// The repository slug. + /// The commit identifier. + /// The comment identifier. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Optional avatar size for returned users. + /// Token to cancel the operation. + /// A collection of users who liked the comment. + public async Task> GetCommitCommentLikesAsync(string projectKey, string repositorySlug, string commitId, string commentId, + int? maxPages = null, + int? limit = null, + int? start = null, + int? avatarSize = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) { - var queryParamValues = new Dictionary + ["limit"] = limit, + ["start"] = start, + ["avatarSize"] = avatarSize, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => { - ["limit"] = limit, - ["start"] = start, - ["avatarSize"] = avatarSize - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetCommentLikesUrl($"/projects/{projectKey}/repos/{repositorySlug}/commits/{commitId}/comments/{commentId}/likes") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task LikeCommitCommentAsync(string projectKey, string repositorySlug, string commitId, string commentId, CancellationToken cancellationToken = default) - { - var response = await GetCommentLikesUrl($"/projects/{projectKey}/repos/{repositorySlug}/commits/{commitId}/comments/{commentId}/likes") - .PostJsonAsync(new StringContent(""), cancellationToken: cancellationToken) - .ConfigureAwait(false); + var response = await GetCommentLikesUrl($"/projects/{projectKey}/repos/{repositorySlug}/commits/{commitId}/comments/{commentId}/likes") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } - public async Task UnlikeCommitCommentAsync(string projectKey, string repositorySlug, string commitId, string commentId, CancellationToken cancellationToken = default) - { - var response = await GetCommentLikesUrl($"/projects/{projectKey}/repos/{repositorySlug}/commits/{commitId}/comments/{commentId}/likes") - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - public async Task> GetPullRequestCommentLikesAsync(string projectKey, string repositorySlug, string pullRequestId, string commentId, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) + /// + /// Adds a like to a commit comment. + /// + /// The project key. + /// The repository slug. + /// The commit identifier. + /// The comment identifier. + /// Token to cancel the operation. + /// true if the like was added; otherwise, false. + public async Task LikeCommitCommentAsync(string projectKey, string repositorySlug, string commitId, string commentId, CancellationToken cancellationToken = default) + { + var response = await GetCommentLikesUrl($"/projects/{projectKey}/repos/{repositorySlug}/commits/{commitId}/comments/{commentId}/likes") + .SendAsync(HttpMethod.Post, new StringContent(string.Empty), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Removes a like from a commit comment. + /// + /// The project key. + /// The repository slug. + /// The commit identifier. + /// The comment identifier. + /// Token to cancel the operation. + /// true if the like was removed; otherwise, false. + public async Task UnlikeCommitCommentAsync(string projectKey, string repositorySlug, string commitId, string commentId, CancellationToken cancellationToken = default) + { + var response = await GetCommentLikesUrl($"/projects/{projectKey}/repos/{repositorySlug}/commits/{commitId}/comments/{commentId}/likes") + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves users who liked a pull request comment. + /// + /// The project key. + /// The repository slug. + /// The pull request identifier. + /// The comment identifier. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Token to cancel the operation. + /// A collection of users who liked the pull request comment. + public async Task> GetPullRequestCommentLikesAsync(string projectKey, string repositorySlug, string pullRequestId, string commentId, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) { - var queryParamValues = new Dictionary + ["limit"] = limit, + ["start"] = start, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => { - ["limit"] = limit, - ["start"] = start - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetCommentLikesUrl($"/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}/comments/{commentId}/likes") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task LikePullRequestCommentAsync(string projectKey, string repositorySlug, string pullRequestId, string commentId, CancellationToken cancellationToken = default) - { - var response = await GetCommentLikesUrl($"/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}/comments/{commentId}/likes") - .PostJsonAsync(new StringContent(""), cancellationToken: cancellationToken) - .ConfigureAwait(false); + var response = await GetCommentLikesUrl($"/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}/comments/{commentId}/likes") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } + /// + /// Adds a like to a pull request comment. + /// + /// The project key. + /// The repository slug. + /// The pull request identifier. + /// The comment identifier. + /// Token to cancel the operation. + /// true if the like was added; otherwise, false. + public async Task LikePullRequestCommentAsync(string projectKey, string repositorySlug, string pullRequestId, string commentId, CancellationToken cancellationToken = default) + { + var response = await GetCommentLikesUrl($"/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}/comments/{commentId}/likes") + .SendAsync(HttpMethod.Post, new StringContent(string.Empty), cancellationToken: cancellationToken) + .ConfigureAwait(false); - public async Task UnlikePullRequestCommentAsync(string projectKey, string repositorySlug, string pullRequestId, string commentId, CancellationToken cancellationToken = default) - { - var response = await GetCommentLikesUrl($"/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}/comments/{commentId}/likes") - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Removes a like from a pull request comment. + /// + /// The project key. + /// The repository slug. + /// The pull request identifier. + /// The comment identifier. + /// Token to cancel the operation. + /// true if the like was removed; otherwise, false. + public async Task UnlikePullRequestCommentAsync(string projectKey, string repositorySlug, string pullRequestId, string commentId, CancellationToken cancellationToken = default) + { + var response = await GetCommentLikesUrl($"/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}/comments/{commentId}/likes") + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/BitbucketHelpers.cs b/src/Bitbucket.Net/Common/BitbucketHelpers.cs index 599ad1a..e6e3e9b 100644 --- a/src/Bitbucket.Net/Common/BitbucketHelpers.cs +++ b/src/Bitbucket.Net/Common/BitbucketHelpers.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Bitbucket.Net.Models.Core.Admin; using Bitbucket.Net.Models.Core.Logs; using Bitbucket.Net.Models.Core.Projects; @@ -8,786 +5,1090 @@ using Bitbucket.Net.Models.RefRestrictions; using Bitbucket.Net.Models.RefSync; -namespace Bitbucket.Net.Common +namespace Bitbucket.Net.Common; + +/// +/// Helper methods for converting between Bitbucket enum values and their wire-format string representations. +/// +public static class BitbucketHelpers { - public static class BitbucketHelpers + #region Bool + + /// + /// Converts a boolean value to the lowercase string expected by Bitbucket query parameters. + /// + /// The boolean value to convert. + /// "true" or "false". + public static string BoolToString(bool value) => value + ? "true" + : "false"; + + /// + /// Converts an optional boolean value to the lowercase string expected by Bitbucket query parameters. + /// + /// The optional boolean value to convert. + /// "true", "false", or when no value is supplied. + public static string? BoolToString(bool? value) => value.HasValue + ? BoolToString(value.Value) + : null; + + /// + /// Parses a case-insensitive boolean string returned by the Bitbucket API. + /// + /// The string to parse. + /// when the value is "true"; otherwise . + public static bool StringToBool(string value) => value.Equals("true", StringComparison.OrdinalIgnoreCase); + + #endregion + + #region BranchOrderBy + + private static readonly Dictionary s_stringByBranchOrderBy = new() { - #region Bool - - public static string BoolToString(bool value) => value - ? "true" - : "false"; - - public static string? BoolToString(bool? value) => value.HasValue - ? BoolToString(value.Value) - : null; - - public static bool StringToBool(string value) => value.Equals("true", StringComparison.OrdinalIgnoreCase); - - #endregion - - #region BranchOrderBy - - private static readonly Dictionary s_stringByBranchOrderBy = new Dictionary - { - [BranchOrderBy.Alphabetical] = "ALPHABETICAL", - [BranchOrderBy.Modification] = "MODIFICATION" - }; - - public static string BranchOrderByToString(BranchOrderBy orderBy) + [BranchOrderBy.Alphabetical] = "ALPHABETICAL", + [BranchOrderBy.Modification] = "MODIFICATION", + }; + + /// + /// Converts a value to the Bitbucket API string. + /// + /// The ordering to convert. + /// The API string representation. + /// Thrown when the value is not recognized. + public static string BranchOrderByToString(BranchOrderBy orderBy) + { + if (!s_stringByBranchOrderBy.TryGetValue(orderBy, out string? result)) { - if (!s_stringByBranchOrderBy.TryGetValue(orderBy, out string? result)) - { - throw new ArgumentException($"Unknown branch order by: {orderBy}"); - } - - return result; + throw new ArgumentException($"Unknown branch order by: {orderBy}"); } - #endregion + return result; + } - #region PullRequestDirections + #endregion - private static readonly Dictionary s_stringByPullRequestDirection = new Dictionary - { - [PullRequestDirections.Incoming] = "INCOMING", - [PullRequestDirections.Outgoing] = "OUTGOING" - }; + #region PullRequestDirections - public static string PullRequestDirectionToString(PullRequestDirections direction) + private static readonly Dictionary s_stringByPullRequestDirection = new() + { + [PullRequestDirections.Incoming] = "INCOMING", + [PullRequestDirections.Outgoing] = "OUTGOING", + }; + + /// + /// Converts a value to the Bitbucket API string. + /// + /// The direction to convert. + /// The API string representation. + /// Thrown when the value is not recognized. + public static string PullRequestDirectionToString(PullRequestDirections direction) + { + if (!s_stringByPullRequestDirection.TryGetValue(direction, out string? result)) { - if (!s_stringByPullRequestDirection.TryGetValue(direction, out string? result)) - { - throw new ArgumentException($"Unknown pull request direction: {direction}"); - } - - return result; + throw new ArgumentException($"Unknown pull request direction: {direction}"); } - #endregion + return result; + } - #region PullRequestStates + #endregion - private static readonly Dictionary s_stringByPullRequestState = new Dictionary - { - [PullRequestStates.Open] = "OPEN", - [PullRequestStates.Declined] = "DECLINED", - [PullRequestStates.Merged] = "MERGED", - [PullRequestStates.All] = "ALL" - }; + #region PullRequestStates - public static string PullRequestStateToString(PullRequestStates state) + private static readonly Dictionary s_stringByPullRequestState = new() + { + [PullRequestStates.Open] = "OPEN", + [PullRequestStates.Declined] = "DECLINED", + [PullRequestStates.Merged] = "MERGED", + [PullRequestStates.All] = "ALL", + }; + + /// + /// Converts a value to the Bitbucket API string. + /// + /// The state to convert. + /// The API string representation. + /// Thrown when the value is not recognized. + public static string PullRequestStateToString(PullRequestStates state) + { + if (!s_stringByPullRequestState.TryGetValue(state, out string? result)) { - if (!s_stringByPullRequestState.TryGetValue(state, out string? result)) - { - throw new ArgumentException($"Unknown pull request state: {state}"); - } - - return result; + throw new ArgumentException($"Unknown pull request state: {state}"); } - public static string? PullRequestStateToString(PullRequestStates? state) => state.HasValue - ? PullRequestStateToString(state.Value) - : null; + return result; + } - public static PullRequestStates StringToPullRequestState(string s) + /// + /// Converts an optional value to the Bitbucket API string. + /// + /// The state to convert. + /// The API string representation or when no state is provided. + public static string? PullRequestStateToString(PullRequestStates? state) => state.HasValue + ? PullRequestStateToString(state.Value) + : null; + + /// + /// Parses a Bitbucket pull request state string into a value. + /// + /// The string returned by the API. + /// The parsed state. + /// Thrown when the value is not recognized. + public static PullRequestStates StringToPullRequestState(string s) + { + var pair = s_stringByPullRequestState.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); + // ReSharper disable once SuspiciousTypeConversion.Global + if (EqualityComparer>.Default.Equals(pair)) { - var pair = s_stringByPullRequestState.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); - // ReSharper disable once SuspiciousTypeConversion.Global - if (EqualityComparer>.Default.Equals(pair)) - { - throw new ArgumentException($"Unknown pull request state: {s}"); - } - - return pair.Key; + throw new ArgumentException($"Unknown pull request state: {s}"); } - #endregion + return pair.Key; + } - #region PullRequestOrders + #endregion - private static readonly Dictionary s_stringByPullRequestOrder = new Dictionary - { - [PullRequestOrders.Newest] = "NEWEST", - [PullRequestOrders.Oldest] = "OLDEST" - }; + #region PullRequestOrders - public static string PullRequestOrderToString(PullRequestOrders order) + private static readonly Dictionary s_stringByPullRequestOrder = new() + { + [PullRequestOrders.Newest] = "NEWEST", + [PullRequestOrders.Oldest] = "OLDEST", + }; + + /// + /// Converts a value to the Bitbucket API string. + /// + /// The order to convert. + /// The API string representation. + /// Thrown when the value is not recognized. + public static string PullRequestOrderToString(PullRequestOrders order) + { + if (!s_stringByPullRequestOrder.TryGetValue(order, out string? result)) { - if (!s_stringByPullRequestOrder.TryGetValue(order, out string? result)) - { - throw new ArgumentException($"Unknown pull request order: {order}"); - } - - return result; + throw new ArgumentException($"Unknown pull request order: {order}"); } - public static string? PullRequestOrderToString(PullRequestOrders? order) => order.HasValue - ? PullRequestOrderToString(order.Value) - : null; + return result; + } - #endregion + /// + /// Converts an optional value to the Bitbucket API string. + /// + /// The order to convert. + /// The API string representation or when no order is provided. + public static string? PullRequestOrderToString(PullRequestOrders? order) => order.HasValue + ? PullRequestOrderToString(order.Value) + : null; - #region PullRequestFromTypes + #endregion - private static readonly Dictionary s_stringByPullRequestFromType = new Dictionary - { - [PullRequestFromTypes.Comment] = "COMMENT", - [PullRequestFromTypes.Activity] = "ACTIVITY" - }; + #region PullRequestFromTypes - private static string PullRequestFromTypeToString(PullRequestFromTypes fromType) - { - if (!s_stringByPullRequestFromType.TryGetValue(fromType, out string? result)) - { - throw new ArgumentException($"Unknown pull request from type: {fromType}"); - } + private static readonly Dictionary s_stringByPullRequestFromType = new() + { + [PullRequestFromTypes.Comment] = "COMMENT", + [PullRequestFromTypes.Activity] = "ACTIVITY", + }; - return result; + private static string PullRequestFromTypeToString(PullRequestFromTypes fromType) + { + if (!s_stringByPullRequestFromType.TryGetValue(fromType, out string? result)) + { + throw new ArgumentException($"Unknown pull request from type: {fromType}"); } - public static string? PullRequestFromTypeToString(PullRequestFromTypes? fromType) => fromType.HasValue - ? PullRequestFromTypeToString(fromType.Value) - : null; + return result; + } - #endregion + /// + /// Converts an optional value to the Bitbucket API string. + /// + /// The source type to convert. + /// The API string representation or when no source is provided. + public static string? PullRequestFromTypeToString(PullRequestFromTypes? fromType) => fromType.HasValue + ? PullRequestFromTypeToString(fromType.Value) + : null; - #region Permissions + #endregion - private static readonly Dictionary s_stringByPermissions = new Dictionary - { - [Permissions.Admin] = "ADMIN", - [Permissions.LicensedUser] = "LICENSED_USER", - [Permissions.ProjectAdmin] = "PROJECT_ADMIN", - [Permissions.ProjectCreate] = "PROJECT_CREATE", - [Permissions.ProjectRead] = "PROJECT_READ", - [Permissions.ProjectView] = "PROJECT_VIEW", - [Permissions.ProjectWrite] = "PROJECT_WRITE", - [Permissions.RepoAdmin] = "REPO_ADMIN", - [Permissions.RepoRead] = "REPO_READ", - [Permissions.RepoWrite] = "REPO_WRITE", - [Permissions.SysAdmin] = "SYS_ADMIN" - }; + #region Permissions - public static string PermissionToString(Permissions permission) + private static readonly Dictionary s_stringByPermissions = new() + { + [Permissions.Admin] = "ADMIN", + [Permissions.LicensedUser] = "LICENSED_USER", + [Permissions.ProjectAdmin] = "PROJECT_ADMIN", + [Permissions.ProjectCreate] = "PROJECT_CREATE", + [Permissions.ProjectRead] = "PROJECT_READ", + [Permissions.ProjectView] = "PROJECT_VIEW", + [Permissions.ProjectWrite] = "PROJECT_WRITE", + [Permissions.RepoAdmin] = "REPO_ADMIN", + [Permissions.RepoRead] = "REPO_READ", + [Permissions.RepoWrite] = "REPO_WRITE", + [Permissions.SysAdmin] = "SYS_ADMIN", + }; + + /// + /// Converts a value to the Bitbucket API string. + /// + /// The permission to convert. + /// The API string representation. + /// Thrown when the value is not recognized. + public static string PermissionToString(Permissions permission) + { + if (!s_stringByPermissions.TryGetValue(permission, out string? result)) { - if (!s_stringByPermissions.TryGetValue(permission, out string? result)) - { - throw new ArgumentException($"Unknown permission: {permission}"); - } - - return result; + throw new ArgumentException($"Unknown permission: {permission}"); } - public static string? PermissionToString(Permissions? permission) => permission.HasValue - ? PermissionToString(permission.Value) - : null; + return result; + } - public static Permissions StringToPermission(string s) + /// + /// Converts an optional value to the Bitbucket API string. + /// + /// The permission to convert. + /// The API string representation or when not supplied. + public static string? PermissionToString(Permissions? permission) => permission.HasValue + ? PermissionToString(permission.Value) + : null; + + /// + /// Parses a Bitbucket permission string into a value. + /// + /// The string returned by the API. + /// The parsed permission. + /// Thrown when the value is not recognized. + public static Permissions StringToPermission(string s) + { + var pair = s_stringByPermissions.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); + // ReSharper disable once SuspiciousTypeConversion.Global + if (EqualityComparer>.Default.Equals(pair)) { - var pair = s_stringByPermissions.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); - // ReSharper disable once SuspiciousTypeConversion.Global - if (EqualityComparer>.Default.Equals(pair)) - { - throw new ArgumentException($"Unknown permission: {s}"); - } - - return pair.Key; + throw new ArgumentException($"Unknown permission: {s}"); } - #endregion + return pair.Key; + } - #region MergeCommits + #endregion - private static readonly Dictionary s_stringByMergeCommits = new Dictionary - { - [MergeCommits.Exclude] = "exclude", - [MergeCommits.Include] = "include", - [MergeCommits.Only] = "only" - }; + #region MergeCommits - public static string MergeCommitsToString(MergeCommits mergeCommits) + private static readonly Dictionary s_stringByMergeCommits = new() + { + [MergeCommits.Exclude] = "exclude", + [MergeCommits.Include] = "include", + [MergeCommits.Only] = "only", + }; + + /// + /// Converts a preference to the Bitbucket API string. + /// + /// The merge commit option. + /// The API string representation. + /// Thrown when the value is not recognized. + public static string MergeCommitsToString(MergeCommits mergeCommits) + { + if (!s_stringByMergeCommits.TryGetValue(mergeCommits, out string? result)) { - if (!s_stringByMergeCommits.TryGetValue(mergeCommits, out string? result)) - { - throw new ArgumentException($"Unknown merge commit: {mergeCommits}"); - } - - return result; + throw new ArgumentException($"Unknown merge commit: {mergeCommits}"); } - #endregion + return result; + } - #region Roles + #endregion - private static readonly Dictionary s_stringByRoles = new Dictionary - { - [Roles.Author] = "AUTHOR", - [Roles.Reviewer] = "REVIEWER", - [Roles.Participant] = "PARTICIPANT" - }; + #region Roles - public static string RoleToString(Roles role) + private static readonly Dictionary s_stringByRoles = new() + { + [Roles.Author] = "AUTHOR", + [Roles.Reviewer] = "REVIEWER", + [Roles.Participant] = "PARTICIPANT", + }; + + /// + /// Converts a pull request value to the Bitbucket API string. + /// + /// The role to convert. + /// The API string representation. + /// Thrown when the value is not recognized. + public static string RoleToString(Roles role) + { + if (!s_stringByRoles.TryGetValue(role, out string? result)) { - if (!s_stringByRoles.TryGetValue(role, out string? result)) - { - throw new ArgumentException($"Unknown role: {role}"); - } - - return result; + throw new ArgumentException($"Unknown role: {role}"); } - public static string? RoleToString(Roles? role) => role.HasValue - ? RoleToString(role.Value) - : null; + return result; + } - public static Roles StringToRole(string s) + /// + /// Converts an optional pull request value to the Bitbucket API string. + /// + /// The role to convert. + /// The API string representation or when not supplied. + public static string? RoleToString(Roles? role) => role.HasValue + ? RoleToString(role.Value) + : null; + + /// + /// Parses a pull request role string into a value. + /// + /// The string returned by the API. + /// The parsed role. + /// Thrown when the value is not recognized. + public static Roles StringToRole(string s) + { + var pair = s_stringByRoles.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); + // ReSharper disable once SuspiciousTypeConversion.Global + if (EqualityComparer>.Default.Equals(pair)) { - var pair = s_stringByRoles.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); - // ReSharper disable once SuspiciousTypeConversion.Global - if (EqualityComparer>.Default.Equals(pair)) - { - throw new ArgumentException($"Unknown role: {s}"); - } - - return pair.Key; + throw new ArgumentException($"Unknown role: {s}"); } - #endregion + return pair.Key; + } - #region LineTypes + #endregion - private static readonly Dictionary s_stringByLineTypes = new Dictionary - { - [LineTypes.Added] = "ADDED", - [LineTypes.Removed] = "REMOVED", - [LineTypes.Context] = "CONTEXT" - }; + #region LineTypes - public static string LineTypeToString(LineTypes lineType) + private static readonly Dictionary s_stringByLineTypes = new() + { + [LineTypes.Added] = "ADDED", + [LineTypes.Removed] = "REMOVED", + [LineTypes.Context] = "CONTEXT", + }; + + /// + /// Converts a value to the Bitbucket API string. + /// + /// The line type to convert. + /// The API string representation. + /// Thrown when the value is not recognized. + public static string LineTypeToString(LineTypes lineType) + { + if (!s_stringByLineTypes.TryGetValue(lineType, out string? result)) { - if (!s_stringByLineTypes.TryGetValue(lineType, out string? result)) - { - throw new ArgumentException($"Unknown line type: {lineType}"); - } - - return result; + throw new ArgumentException($"Unknown line type: {lineType}"); } - public static string? LineTypeToString(LineTypes? lineType) - { - return lineType.HasValue - ? LineTypeToString(lineType.Value) - : null; - } + return result; + } - public static LineTypes StringToLineType(string s) - { - var pair = s_stringByLineTypes.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); - // ReSharper disable once SuspiciousTypeConversion.Global - if (EqualityComparer>.Default.Equals(pair)) - { - throw new ArgumentException($"Unknown line type: {s}"); - } + /// + /// Converts an optional value to the Bitbucket API string. + /// + /// The line type to convert. + /// The API string representation or when not supplied. + public static string? LineTypeToString(LineTypes? lineType) + { + return lineType.HasValue + ? LineTypeToString(lineType.Value) + : null; + } - return pair.Key; + /// + /// Parses a line type string into a value. + /// + /// The string returned by the API. + /// The parsed line type. + /// Thrown when the value is not recognized. + public static LineTypes StringToLineType(string s) + { + var pair = s_stringByLineTypes.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); + // ReSharper disable once SuspiciousTypeConversion.Global + if (EqualityComparer>.Default.Equals(pair)) + { + throw new ArgumentException($"Unknown line type: {s}"); } - #endregion + return pair.Key; + } - #region FileTypes + #endregion - private static readonly Dictionary s_stringByFileTypes = new Dictionary - { - [FileTypes.From] = "FROM", - [FileTypes.To] = "TO" - }; + #region FileTypes - public static string FileTypeToString(FileTypes fileType) + private static readonly Dictionary s_stringByFileTypes = new() + { + [FileTypes.From] = "FROM", + [FileTypes.To] = "TO", + }; + + /// + /// Converts a value to the Bitbucket API string. + /// + /// The file type to convert. + /// The API string representation. + /// Thrown when the value is not recognized. + public static string FileTypeToString(FileTypes fileType) + { + if (!s_stringByFileTypes.TryGetValue(fileType, out string? result)) { - if (!s_stringByFileTypes.TryGetValue(fileType, out string? result)) - { - throw new ArgumentException($"Unknown file type: {fileType}"); - } - - return result; + throw new ArgumentException($"Unknown file type: {fileType}"); } - public static string? FileTypeToString(FileTypes? fileType) - { - return fileType.HasValue - ? FileTypeToString(fileType.Value) - : null; - } + return result; + } - public static FileTypes StringToFileType(string s) - { - var pair = s_stringByFileTypes.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); - // ReSharper disable once SuspiciousTypeConversion.Global - if (EqualityComparer>.Default.Equals(pair)) - { - throw new ArgumentException($"Unknown file type: {s}"); - } + /// + /// Converts an optional value to the Bitbucket API string. + /// + /// The file type to convert. + /// The API string representation or when not supplied. + public static string? FileTypeToString(FileTypes? fileType) + { + return fileType.HasValue + ? FileTypeToString(fileType.Value) + : null; + } - return pair.Key; + /// + /// Parses a file type string into a value. + /// + /// The string returned by the API. + /// The parsed file type. + /// Thrown when the value is not recognized. + public static FileTypes StringToFileType(string s) + { + var pair = s_stringByFileTypes.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); + // ReSharper disable once SuspiciousTypeConversion.Global + if (EqualityComparer>.Default.Equals(pair)) + { + throw new ArgumentException($"Unknown file type: {s}"); } - #endregion + return pair.Key; + } - #region ChangeScopes + #endregion - private static readonly Dictionary s_stringByChangeScopes = new Dictionary - { - [ChangeScopes.All] = "ALL", - [ChangeScopes.Unreviewed] = "UNREVIEWED", - [ChangeScopes.Range] = "RANGE" - }; + #region ChangeScopes - public static string ChangeScopeToString(ChangeScopes changeScope) + private static readonly Dictionary s_stringByChangeScopes = new() + { + [ChangeScopes.All] = "ALL", + [ChangeScopes.Unreviewed] = "UNREVIEWED", + [ChangeScopes.Range] = "RANGE", + }; + + /// + /// Converts a value to the Bitbucket API string. + /// + /// The change scope to convert. + /// The API string representation. + /// Thrown when the value is not recognized. + public static string ChangeScopeToString(ChangeScopes changeScope) + { + if (!s_stringByChangeScopes.TryGetValue(changeScope, out string? result)) { - if (!s_stringByChangeScopes.TryGetValue(changeScope, out string? result)) - { - throw new ArgumentException($"Unknown change scope: {changeScope}"); - } - - return result; + throw new ArgumentException($"Unknown change scope: {changeScope}"); } - #endregion + return result; + } - #region LogLevels + #endregion - private static readonly Dictionary s_stringByLogLevels = new Dictionary - { - [LogLevels.Trace] = "TRACE", - [LogLevels.Debug] = "DEBUG", - [LogLevels.Info] = "INFO", - [LogLevels.Warn] = "WARN", - [LogLevels.Error] = "ERROR" - }; + #region LogLevels - public static string LogLevelToString(LogLevels logLevel) + private static readonly Dictionary s_stringByLogLevels = new() + { + [LogLevels.Trace] = "TRACE", + [LogLevels.Debug] = "DEBUG", + [LogLevels.Info] = "INFO", + [LogLevels.Warn] = "WARN", + [LogLevels.Error] = "ERROR", + }; + + /// + /// Converts a value to the Bitbucket API string. + /// + /// The log level to convert. + /// The API string representation. + /// Thrown when the value is not recognized. + public static string LogLevelToString(LogLevels logLevel) + { + if (!s_stringByLogLevels.TryGetValue(logLevel, out string? result)) { - if (!s_stringByLogLevels.TryGetValue(logLevel, out string? result)) - { - throw new ArgumentException($"Unknown log level: {logLevel}"); - } - - return result; + throw new ArgumentException($"Unknown log level: {logLevel}"); } - public static LogLevels StringToLogLevel(string s) - { - var pair = s_stringByLogLevels.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); - // ReSharper disable once SuspiciousTypeConversion.Global - if (EqualityComparer>.Default.Equals(pair)) - { - throw new ArgumentException($"Unknown log level: {s}"); - } + return result; + } - return pair.Key; + /// + /// Parses a log level string into a value. + /// + /// The string returned by the API. + /// The parsed log level. + /// Thrown when the value is not recognized. + public static LogLevels StringToLogLevel(string s) + { + var pair = s_stringByLogLevels.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); + // ReSharper disable once SuspiciousTypeConversion.Global + if (EqualityComparer>.Default.Equals(pair)) + { + throw new ArgumentException($"Unknown log level: {s}"); } - #endregion + return pair.Key; + } - #region ParticipantStatus + #endregion - private static readonly Dictionary s_stringByParticipantStatus = new Dictionary - { - [ParticipantStatus.Approved] = "APPROVED", - [ParticipantStatus.NeedsWork] = "NEEDS_WORK", - [ParticipantStatus.Unapproved] = "UNAPPROVED" - }; + #region ParticipantStatus - public static string ParticipantStatusToString(ParticipantStatus participantStatus) + private static readonly Dictionary s_stringByParticipantStatus = new() + { + [ParticipantStatus.Approved] = "APPROVED", + [ParticipantStatus.NeedsWork] = "NEEDS_WORK", + [ParticipantStatus.Unapproved] = "UNAPPROVED", + }; + + /// + /// Converts a value to the Bitbucket API string. + /// + /// The status to convert. + /// The API string representation. + /// Thrown when the value is not recognized. + public static string ParticipantStatusToString(ParticipantStatus participantStatus) + { + if (!s_stringByParticipantStatus.TryGetValue(participantStatus, out string? result)) { - if (!s_stringByParticipantStatus.TryGetValue(participantStatus, out string? result)) - { - throw new ArgumentException($"Unknown participant status: {participantStatus}"); - } - - return result; + throw new ArgumentException($"Unknown participant status: {participantStatus}"); } - public static ParticipantStatus StringToParticipantStatus(string s) - { - var pair = s_stringByParticipantStatus.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); - // ReSharper disable once SuspiciousTypeConversion.Global - if (EqualityComparer>.Default.Equals(pair)) - { - throw new ArgumentException($"Unknown participant status: {s}"); - } + return result; + } - return pair.Key; + /// + /// Parses a participant status string into a value. + /// + /// The string returned by the API. + /// The parsed status. + /// Thrown when the value is not recognized. + public static ParticipantStatus StringToParticipantStatus(string s) + { + var pair = s_stringByParticipantStatus.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); + // ReSharper disable once SuspiciousTypeConversion.Global + if (EqualityComparer>.Default.Equals(pair)) + { + throw new ArgumentException($"Unknown participant status: {s}"); } - #endregion + return pair.Key; + } - #region HookTypes + #endregion - private static readonly Dictionary s_stringByHookTypes = new Dictionary - { - [HookTypes.PreReceive] = "PRE_RECEIVE", - [HookTypes.PostReceive] = "POST_RECEIVE", - [HookTypes.PrePullRequestMerge] = "PRE_PULL_REQUEST_MERGE" - }; + #region HookTypes - public static string HookTypeToString(HookTypes hookType) + private static readonly Dictionary s_stringByHookTypes = new() + { + [HookTypes.PreReceive] = "PRE_RECEIVE", + [HookTypes.PostReceive] = "POST_RECEIVE", + [HookTypes.PrePullRequestMerge] = "PRE_PULL_REQUEST_MERGE", + }; + + /// + /// Converts a hook value to the Bitbucket API string. + /// + /// The hook type to convert. + /// The API string representation. + /// Thrown when the value is not recognized. + public static string HookTypeToString(HookTypes hookType) + { + if (!s_stringByHookTypes.TryGetValue(hookType, out string? result)) { - if (!s_stringByHookTypes.TryGetValue(hookType, out string? result)) - { - throw new ArgumentException($"Unknown hook type: {hookType}"); - } - - return result; + throw new ArgumentException($"Unknown hook type: {hookType}"); } - public static HookTypes StringToHookType(string s) - { - var pair = s_stringByHookTypes.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); - // ReSharper disable once SuspiciousTypeConversion.Global - if (EqualityComparer>.Default.Equals(pair)) - { - throw new ArgumentException($"Unknown hook type: {s}"); - } + return result; + } - return pair.Key; + /// + /// Parses a hook type string into a value. + /// + /// The string returned by the API. + /// The parsed hook type. + /// Thrown when the value is not recognized. + public static HookTypes StringToHookType(string s) + { + var pair = s_stringByHookTypes.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); + // ReSharper disable once SuspiciousTypeConversion.Global + if (EqualityComparer>.Default.Equals(pair)) + { + throw new ArgumentException($"Unknown hook type: {s}"); } - #endregion + return pair.Key; + } - #region ScopeTypes + #endregion - private static readonly Dictionary s_stringByScopeTypes = new Dictionary - { - [ScopeTypes.Project] = "PROJECT", - [ScopeTypes.Repository] = "REPOSITORY" - }; + #region ScopeTypes - public static string ScopeTypeToString(ScopeTypes scopeType) + private static readonly Dictionary s_stringByScopeTypes = new() + { + [ScopeTypes.Project] = "PROJECT", + [ScopeTypes.Repository] = "REPOSITORY", + }; + + /// + /// Converts a value to the Bitbucket API string. + /// + /// The scope type to convert. + /// The API string representation. + /// Thrown when the value is not recognized. + public static string ScopeTypeToString(ScopeTypes scopeType) + { + if (!s_stringByScopeTypes.TryGetValue(scopeType, out string? result)) { - if (!s_stringByScopeTypes.TryGetValue(scopeType, out string? result)) - { - throw new ArgumentException($"Unknown scope type: {scopeType}"); - } - - return result; + throw new ArgumentException($"Unknown scope type: {scopeType}"); } - public static ScopeTypes StringToScopeType(string s) - { - var pair = s_stringByScopeTypes.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); - // ReSharper disable once SuspiciousTypeConversion.Global - if (EqualityComparer>.Default.Equals(pair)) - { - throw new ArgumentException($"Unknown scope type: {s}"); - } + return result; + } - return pair.Key; + /// + /// Parses a scope type string into a value. + /// + /// The string returned by the API. + /// The parsed scope type. + /// Thrown when the value is not recognized. + public static ScopeTypes StringToScopeType(string s) + { + var pair = s_stringByScopeTypes.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); + // ReSharper disable once SuspiciousTypeConversion.Global + if (EqualityComparer>.Default.Equals(pair)) + { + throw new ArgumentException($"Unknown scope type: {s}"); } - #endregion + return pair.Key; + } - #region ArchiveFormats + #endregion - private static readonly Dictionary s_stringByArchiveFormats = new Dictionary - { - [ArchiveFormats.Zip] = "zip", - [ArchiveFormats.Tar] = "tar", - [ArchiveFormats.TarGz] = "tar.gz", - [ArchiveFormats.Tgz] = "tgz" - }; + #region ArchiveFormats - public static string ArchiveFormatToString(ArchiveFormats archiveFormat) + private static readonly Dictionary s_stringByArchiveFormats = new() + { + [ArchiveFormats.Zip] = "zip", + [ArchiveFormats.Tar] = "tar", + [ArchiveFormats.TarGz] = "tar.gz", + [ArchiveFormats.Tgz] = "tgz", + }; + + /// + /// Converts an value to the Bitbucket API string. + /// + /// The archive format to convert. + /// The API string representation. + /// Thrown when the value is not recognized. + public static string ArchiveFormatToString(ArchiveFormats archiveFormat) + { + if (!s_stringByArchiveFormats.TryGetValue(archiveFormat, out string? result)) { - if (!s_stringByArchiveFormats.TryGetValue(archiveFormat, out string? result)) - { - throw new ArgumentException($"Unknown archive format: {archiveFormat}"); - } - - return result; + throw new ArgumentException($"Unknown archive format: {archiveFormat}"); } - #endregion + return result; + } - #region WebHookOutcomes + #endregion - private static readonly Dictionary s_stringByWebHookOutcomes = new Dictionary - { - [WebHookOutcomes.Success] = "SUCCESS", - [WebHookOutcomes.Failure] = "FAILURE", - [WebHookOutcomes.Error] = "ERROR" - }; + #region WebHookOutcomes - public static string WebHookOutcomeToString(WebHookOutcomes webHookOutcome) + private static readonly Dictionary s_stringByWebHookOutcomes = new() + { + [WebHookOutcomes.Success] = "SUCCESS", + [WebHookOutcomes.Failure] = "FAILURE", + [WebHookOutcomes.Error] = "ERROR", + }; + + /// + /// Converts a value to the Bitbucket API string. + /// + /// The outcome to convert. + /// The API string representation. + /// Thrown when the value is not recognized. + public static string WebHookOutcomeToString(WebHookOutcomes webHookOutcome) + { + if (!s_stringByWebHookOutcomes.TryGetValue(webHookOutcome, out string? result)) { - if (!s_stringByWebHookOutcomes.TryGetValue(webHookOutcome, out string? result)) - { - throw new ArgumentException($"Unknown web hook outcome: {webHookOutcome}"); - } - - return result; + throw new ArgumentException($"Unknown web hook outcome: {webHookOutcome}"); } - public static string? WebHookOutcomeToString(WebHookOutcomes? webHookOutcome) => webHookOutcome.HasValue - ? WebHookOutcomeToString(webHookOutcome.Value) - : null; + return result; + } - public static WebHookOutcomes StringToWebHookOutcome(string s) + /// + /// Converts an optional value to the Bitbucket API string. + /// + /// The outcome to convert. + /// The API string representation or when not supplied. + public static string? WebHookOutcomeToString(WebHookOutcomes? webHookOutcome) => webHookOutcome.HasValue + ? WebHookOutcomeToString(webHookOutcome.Value) + : null; + + /// + /// Parses a webhook outcome string into a value. + /// + /// The string returned by the API. + /// The parsed outcome. + /// Thrown when the value is not recognized. + public static WebHookOutcomes StringToWebHookOutcome(string s) + { + var pair = s_stringByWebHookOutcomes.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); + // ReSharper disable once SuspiciousTypeConversion.Global + if (EqualityComparer>.Default.Equals(pair)) { - var pair = s_stringByWebHookOutcomes.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); - // ReSharper disable once SuspiciousTypeConversion.Global - if (EqualityComparer>.Default.Equals(pair)) - { - throw new ArgumentException($"Unknown web hook outcome: {s}"); - } - - return pair.Key; + throw new ArgumentException($"Unknown web hook outcome: {s}"); } - #endregion + return pair.Key; + } - #region AnchorStates + #endregion - private static readonly Dictionary s_stringByAnchorStates = new Dictionary - { - [AnchorStates.Active] = "ACTIVE", - [AnchorStates.Orphaned] = "ORPHANED", - [AnchorStates.All] = "ALL" - }; + #region AnchorStates - public static string AnchorStateToString(AnchorStates anchorState) + private static readonly Dictionary s_stringByAnchorStates = new() + { + [AnchorStates.Active] = "ACTIVE", + [AnchorStates.Orphaned] = "ORPHANED", + [AnchorStates.All] = "ALL", + }; + + /// + /// Converts an value to the Bitbucket API string. + /// + /// The anchor state to convert. + /// The API string representation. + /// Thrown when the value is not recognized. + public static string AnchorStateToString(AnchorStates anchorState) + { + if (!s_stringByAnchorStates.TryGetValue(anchorState, out string? result)) { - if (!s_stringByAnchorStates.TryGetValue(anchorState, out string? result)) - { - throw new ArgumentException($"Unknown anchor state: {anchorState}"); - } - - return result; + throw new ArgumentException($"Unknown anchor state: {anchorState}"); } - #endregion + return result; + } - #region DiffTypes + #endregion - private static readonly Dictionary s_stringByDiffTypes = new Dictionary - { - [DiffTypes.Effective] = "EFFECTIVE", - [DiffTypes.Range] = "RANGE", - [DiffTypes.Commit] = "COMMIT" - }; + #region DiffTypes - public static string DiffTypeToString(DiffTypes diffType) + private static readonly Dictionary s_stringByDiffTypes = new() + { + [DiffTypes.Effective] = "EFFECTIVE", + [DiffTypes.Range] = "RANGE", + [DiffTypes.Commit] = "COMMIT", + }; + + /// + /// Converts a value to the Bitbucket API string. + /// + /// The diff type to convert. + /// The API string representation. + /// Thrown when the value is not recognized. + public static string DiffTypeToString(DiffTypes diffType) + { + if (!s_stringByDiffTypes.TryGetValue(diffType, out string? result)) { - if (!s_stringByDiffTypes.TryGetValue(diffType, out string? result)) - { - throw new ArgumentException($"Unknown diff type: {diffType}"); - } - - return result; + throw new ArgumentException($"Unknown diff type: {diffType}"); } - public static string? DiffTypeToString(DiffTypes? diffType) - { - return diffType.HasValue - ? DiffTypeToString(diffType.Value) - : null; - } + return result; + } - #endregion + /// + /// Converts an optional value to the Bitbucket API string. + /// + /// The diff type to convert. + /// The API string representation or when not supplied. + public static string? DiffTypeToString(DiffTypes? diffType) + { + return diffType.HasValue + ? DiffTypeToString(diffType.Value) + : null; + } - #region TagTypes + #endregion - private static readonly Dictionary s_stringByTagTypes = new Dictionary - { - [TagTypes.LightWeight] = "LIGHTWEIGHT", - [TagTypes.Annotated] = "ANNOTATED" - }; + #region TagTypes - public static string TagTypeToString(TagTypes tagType) + private static readonly Dictionary s_stringByTagTypes = new() + { + [TagTypes.LightWeight] = "LIGHTWEIGHT", + [TagTypes.Annotated] = "ANNOTATED", + }; + + /// + /// Converts a value to the Bitbucket API string. + /// + /// The tag type to convert. + /// The API string representation. + /// Thrown when the value is not recognized. + public static string TagTypeToString(TagTypes tagType) + { + if (!s_stringByTagTypes.TryGetValue(tagType, out string? result)) { - if (!s_stringByTagTypes.TryGetValue(tagType, out string? result)) - { - throw new ArgumentException($"Unknown tag type: {tagType}"); - } - - return result; + throw new ArgumentException($"Unknown tag type: {tagType}"); } - #endregion + return result; + } - #region RefRestrictionTypes + #endregion - private static readonly Dictionary s_stringByRefRestrictionTypes = new Dictionary - { - [RefRestrictionTypes.AllChanges] = "read-only", - [RefRestrictionTypes.RewritingHistory] = "fast-forward-only", - [RefRestrictionTypes.Deletion] = "no-deletes", - [RefRestrictionTypes.ChangesWithoutPullRequest] = "pull-request-only" - }; + #region RefRestrictionTypes - public static string RefRestrictionTypeToString(RefRestrictionTypes refRestrictionType) + private static readonly Dictionary s_stringByRefRestrictionTypes = new() + { + [RefRestrictionTypes.AllChanges] = "read-only", + [RefRestrictionTypes.RewritingHistory] = "fast-forward-only", + [RefRestrictionTypes.Deletion] = "no-deletes", + [RefRestrictionTypes.ChangesWithoutPullRequest] = "pull-request-only", + }; + + /// + /// Converts a value to the Bitbucket API string. + /// + /// The restriction to convert. + /// The API string representation. + /// Thrown when the value is not recognized. + public static string RefRestrictionTypeToString(RefRestrictionTypes refRestrictionType) + { + if (!s_stringByRefRestrictionTypes.TryGetValue(refRestrictionType, out string? result)) { - if (!s_stringByRefRestrictionTypes.TryGetValue(refRestrictionType, out string? result)) - { - throw new ArgumentException($"Unknown ref restriction type: {refRestrictionType}"); - } - - return result; + throw new ArgumentException($"Unknown ref restriction type: {refRestrictionType}"); } - public static string? RefRestrictionTypeToString(RefRestrictionTypes? refRestrictionType) - { - return refRestrictionType.HasValue - ? RefRestrictionTypeToString(refRestrictionType.Value) - : null; - } + return result; + } - public static RefRestrictionTypes StringToRefRestrictionType(string s) - { - var pair = s_stringByRefRestrictionTypes.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); - // ReSharper disable once SuspiciousTypeConversion.Global - if (EqualityComparer>.Default.Equals(pair)) - { - throw new ArgumentException($"Unknown ref restriction type: {s}"); - } + /// + /// Converts an optional value to the Bitbucket API string. + /// + /// The restriction to convert. + /// The API string representation or when not supplied. + public static string? RefRestrictionTypeToString(RefRestrictionTypes? refRestrictionType) + { + return refRestrictionType.HasValue + ? RefRestrictionTypeToString(refRestrictionType.Value) + : null; + } - return pair.Key; + /// + /// Parses a ref restriction string into a value. + /// + /// The string returned by the API. + /// The parsed restriction. + /// Thrown when the value is not recognized. + public static RefRestrictionTypes StringToRefRestrictionType(string s) + { + var pair = s_stringByRefRestrictionTypes.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); + // ReSharper disable once SuspiciousTypeConversion.Global + if (EqualityComparer>.Default.Equals(pair)) + { + throw new ArgumentException($"Unknown ref restriction type: {s}"); } - #endregion + return pair.Key; + } - #region RefMatcherTypes + #endregion - private static readonly Dictionary s_stringByRefMatcherTypes = new Dictionary - { - [RefMatcherTypes.Branch] = "BRANCH", - [RefMatcherTypes.Pattern] = "PATTERN", - [RefMatcherTypes.ModelCategory] = "MODEL_CATEGORY", - [RefMatcherTypes.ModelBranch] = "MODEL_BRANCH" - }; + #region RefMatcherTypes - private static string RefMatcherTypeToString(RefMatcherTypes refMatcherType) - { - if (!s_stringByRefMatcherTypes.TryGetValue(refMatcherType, out string? result)) - { - throw new ArgumentException($"Unknown ref matcher type: {refMatcherType}"); - } - - return result; - } + private static readonly Dictionary s_stringByRefMatcherTypes = new() + { + [RefMatcherTypes.Branch] = "BRANCH", + [RefMatcherTypes.Pattern] = "PATTERN", + [RefMatcherTypes.ModelCategory] = "MODEL_CATEGORY", + [RefMatcherTypes.ModelBranch] = "MODEL_BRANCH", + }; - public static string? RefMatcherTypeToString(RefMatcherTypes? refMatcherType) + private static string RefMatcherTypeToString(RefMatcherTypes refMatcherType) + { + if (!s_stringByRefMatcherTypes.TryGetValue(refMatcherType, out string? result)) { - return refMatcherType.HasValue - ? RefMatcherTypeToString(refMatcherType.Value) - : null; + throw new ArgumentException($"Unknown ref matcher type: {refMatcherType}"); } - #endregion + return result; + } - #region SynchronizeActions + /// + /// Converts an optional value to the Bitbucket API string. + /// + /// The matcher type to convert. + /// The API string representation or when not supplied. + public static string? RefMatcherTypeToString(RefMatcherTypes? refMatcherType) + { + return refMatcherType.HasValue + ? RefMatcherTypeToString(refMatcherType.Value) + : null; + } - private static readonly Dictionary s_stringBySynchronizeActions = new Dictionary - { - [SynchronizeActions.Merge] = "MERGE", - [SynchronizeActions.Discard] = "DISCARD" - }; + #endregion - public static string SynchronizeActionToString(SynchronizeActions synchronizeAction) - { - if (!s_stringBySynchronizeActions.TryGetValue(synchronizeAction, out string? result)) - { - throw new ArgumentException($"Unknown synchronize action: {synchronizeAction}"); - } + #region SynchronizeActions - return result; + private static readonly Dictionary s_stringBySynchronizeActions = new() + { + [SynchronizeActions.Merge] = "MERGE", + [SynchronizeActions.Discard] = "DISCARD", + }; + + /// + /// Converts a value to the Bitbucket API string. + /// + /// The synchronization action to convert. + /// The API string representation. + /// Thrown when the value is not recognized. + public static string SynchronizeActionToString(SynchronizeActions synchronizeAction) + { + if (!s_stringBySynchronizeActions.TryGetValue(synchronizeAction, out string? result)) + { + throw new ArgumentException($"Unknown synchronize action: {synchronizeAction}"); } - public static SynchronizeActions StringToSynchronizeAction(string s) - { - var pair = s_stringBySynchronizeActions.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); - // ReSharper disable once SuspiciousTypeConversion.Global - if (EqualityComparer>.Default.Equals(pair)) - { - throw new ArgumentException($"Unknown synchronize action: {s}"); - } + return result; + } - return pair.Key; + /// + /// Parses a synchronization action string into a value. + /// + /// The string returned by the API. + /// The parsed action. + /// Thrown when the value is not recognized. + public static SynchronizeActions StringToSynchronizeAction(string s) + { + var pair = s_stringBySynchronizeActions.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); + // ReSharper disable once SuspiciousTypeConversion.Global + if (EqualityComparer>.Default.Equals(pair)) + { + throw new ArgumentException($"Unknown synchronize action: {s}"); } - #endregion + return pair.Key; + } - #region BlockerCommentState + #endregion - private static readonly Dictionary s_stringByBlockerCommentState = new Dictionary - { - [BlockerCommentState.Open] = "OPEN", - [BlockerCommentState.Resolved] = "RESOLVED" - }; + #region BlockerCommentState - public static string BlockerCommentStateToString(BlockerCommentState state) + private static readonly Dictionary s_stringByBlockerCommentState = new() + { + [BlockerCommentState.Open] = "OPEN", + [BlockerCommentState.Resolved] = "RESOLVED", + }; + + /// + /// Converts a value to the Bitbucket API string. + /// + /// The blocker comment state to convert. + /// The API string representation. + /// Thrown when the value is not recognized. + public static string BlockerCommentStateToString(BlockerCommentState state) + { + if (!s_stringByBlockerCommentState.TryGetValue(state, out string? result)) { - if (!s_stringByBlockerCommentState.TryGetValue(state, out string? result)) - { - throw new ArgumentException($"Unknown blocker comment state: {state}"); - } - - return result; + throw new ArgumentException($"Unknown blocker comment state: {state}"); } - public static string? BlockerCommentStateToString(BlockerCommentState? state) => state.HasValue - ? BlockerCommentStateToString(state.Value) - : null; + return result; + } - public static BlockerCommentState StringToBlockerCommentState(string s) + /// + /// Converts an optional value to the Bitbucket API string. + /// + /// The blocker comment state to convert. + /// The API string representation or when not supplied. + public static string? BlockerCommentStateToString(BlockerCommentState? state) => state.HasValue + ? BlockerCommentStateToString(state.Value) + : null; + + /// + /// Parses a blocker comment state string into a value. + /// + /// The string returned by the API. + /// The parsed state. + /// Thrown when the value is not recognized. + public static BlockerCommentState StringToBlockerCommentState(string s) + { + var pair = s_stringByBlockerCommentState.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); + // ReSharper disable once SuspiciousTypeConversion.Global + if (EqualityComparer>.Default.Equals(pair)) { - var pair = s_stringByBlockerCommentState.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); - // ReSharper disable once SuspiciousTypeConversion.Global - if (EqualityComparer>.Default.Equals(pair)) - { - throw new ArgumentException($"Unknown blocker comment state: {s}"); - } - - return pair.Key; + throw new ArgumentException($"Unknown blocker comment state: {s}"); } - #endregion + return pair.Key; + } - #region CommentSeverity + #endregion - private static readonly Dictionary s_stringByCommentSeverity = new Dictionary - { - [CommentSeverity.Normal] = "NORMAL", - [CommentSeverity.Blocker] = "BLOCKER" - }; + #region CommentSeverity - public static string CommentSeverityToString(CommentSeverity severity) + private static readonly Dictionary s_stringByCommentSeverity = new() + { + [CommentSeverity.Normal] = "NORMAL", + [CommentSeverity.Blocker] = "BLOCKER", + }; + + /// + /// Converts a value to the Bitbucket API string. + /// + /// The comment severity to convert. + /// The API string representation. + /// Thrown when the value is not recognized. + public static string CommentSeverityToString(CommentSeverity severity) + { + if (!s_stringByCommentSeverity.TryGetValue(severity, out string? result)) { - if (!s_stringByCommentSeverity.TryGetValue(severity, out string? result)) - { - throw new ArgumentException($"Unknown comment severity: {severity}"); - } - - return result; + throw new ArgumentException($"Unknown comment severity: {severity}"); } - public static string? CommentSeverityToString(CommentSeverity? severity) => severity.HasValue - ? CommentSeverityToString(severity.Value) - : null; + return result; + } - public static CommentSeverity StringToCommentSeverity(string s) + /// + /// Converts an optional value to the Bitbucket API string. + /// + /// The comment severity to convert. + /// The API string representation or when not supplied. + public static string? CommentSeverityToString(CommentSeverity? severity) => severity.HasValue + ? CommentSeverityToString(severity.Value) + : null; + + /// + /// Parses a comment severity string into a value. + /// + /// The string returned by the API. + /// The parsed severity. + /// Thrown when the value is not recognized. + public static CommentSeverity StringToCommentSeverity(string s) + { + var pair = s_stringByCommentSeverity.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); + // ReSharper disable once SuspiciousTypeConversion.Global + if (EqualityComparer>.Default.Equals(pair)) { - var pair = s_stringByCommentSeverity.FirstOrDefault(kvp => kvp.Value.Equals(s, StringComparison.OrdinalIgnoreCase)); - // ReSharper disable once SuspiciousTypeConversion.Global - if (EqualityComparer>.Default.Equals(pair)) - { - throw new ArgumentException($"Unknown comment severity: {s}"); - } - - return pair.Key; + throw new ArgumentException($"Unknown comment severity: {s}"); } - #endregion + return pair.Key; } -} + + #endregion +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Converters/BlockerCommentStateConverter.cs b/src/Bitbucket.Net/Common/Converters/BlockerCommentStateConverter.cs index 51a090b..caf0338 100644 --- a/src/Bitbucket.Net/Common/Converters/BlockerCommentStateConverter.cs +++ b/src/Bitbucket.Net/Common/Converters/BlockerCommentStateConverter.cs @@ -1,20 +1,19 @@ using Bitbucket.Net.Models.Core.Projects; -namespace Bitbucket.Net.Common.Converters +namespace Bitbucket.Net.Common.Converters; + +/// +/// JSON converter for enum values. +/// +public class BlockerCommentStateConverter : JsonEnumConverter { - /// - /// JSON converter for enum values. - /// - public class BlockerCommentStateConverter : JsonEnumConverter + protected override string ConvertToString(BlockerCommentState value) { - protected override string ConvertToString(BlockerCommentState value) - { - return BitbucketHelpers.BlockerCommentStateToString(value); - } + return BitbucketHelpers.BlockerCommentStateToString(value); + } - protected override BlockerCommentState ConvertFromString(string s) - { - return BitbucketHelpers.StringToBlockerCommentState(s); - } + protected override BlockerCommentState ConvertFromString(string s) + { + return BitbucketHelpers.StringToBlockerCommentState(s); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Converters/CommentSeverityConverter.cs b/src/Bitbucket.Net/Common/Converters/CommentSeverityConverter.cs index 57e59f6..b189903 100644 --- a/src/Bitbucket.Net/Common/Converters/CommentSeverityConverter.cs +++ b/src/Bitbucket.Net/Common/Converters/CommentSeverityConverter.cs @@ -1,20 +1,19 @@ using Bitbucket.Net.Models.Core.Projects; -namespace Bitbucket.Net.Common.Converters +namespace Bitbucket.Net.Common.Converters; + +/// +/// JSON converter for enum values. +/// +public class CommentSeverityConverter : JsonEnumConverter { - /// - /// JSON converter for enum values. - /// - public class CommentSeverityConverter : JsonEnumConverter + protected override string ConvertToString(CommentSeverity value) { - protected override string ConvertToString(CommentSeverity value) - { - return BitbucketHelpers.CommentSeverityToString(value); - } + return BitbucketHelpers.CommentSeverityToString(value); + } - protected override CommentSeverity ConvertFromString(string s) - { - return BitbucketHelpers.StringToCommentSeverity(s); - } + protected override CommentSeverity ConvertFromString(string s) + { + return BitbucketHelpers.StringToCommentSeverity(s); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Converters/FileTypesConverter.cs b/src/Bitbucket.Net/Common/Converters/FileTypesConverter.cs index dfd3d12..0eaec6b 100644 --- a/src/Bitbucket.Net/Common/Converters/FileTypesConverter.cs +++ b/src/Bitbucket.Net/Common/Converters/FileTypesConverter.cs @@ -1,17 +1,21 @@ using Bitbucket.Net.Models.Core.Projects; -namespace Bitbucket.Net.Common.Converters +namespace Bitbucket.Net.Common.Converters; + +/// +/// JSON converter for Bitbucket values. +/// +public class FileTypesConverter : JsonEnumConverter { - public class FileTypesConverter : JsonEnumConverter + /// + protected override string ConvertToString(FileTypes value) { - protected override string ConvertToString(FileTypes value) - { - return BitbucketHelpers.FileTypeToString(value); - } + return BitbucketHelpers.FileTypeToString(value); + } - protected override FileTypes ConvertFromString(string s) - { - return BitbucketHelpers.StringToFileType(s); - } + /// + protected override FileTypes ConvertFromString(string s) + { + return BitbucketHelpers.StringToFileType(s); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Converters/HookTypesConverter.cs b/src/Bitbucket.Net/Common/Converters/HookTypesConverter.cs index 9935739..44d41f0 100644 --- a/src/Bitbucket.Net/Common/Converters/HookTypesConverter.cs +++ b/src/Bitbucket.Net/Common/Converters/HookTypesConverter.cs @@ -1,17 +1,21 @@ using Bitbucket.Net.Models.Core.Projects; -namespace Bitbucket.Net.Common.Converters +namespace Bitbucket.Net.Common.Converters; + +/// +/// JSON converter for Bitbucket hook type values. +/// +public class HookTypesConverter : JsonEnumConverter { - public class HookTypesConverter : JsonEnumConverter + /// + protected override string ConvertToString(HookTypes value) { - protected override string ConvertToString(HookTypes value) - { - return BitbucketHelpers.HookTypeToString(value); - } + return BitbucketHelpers.HookTypeToString(value); + } - protected override HookTypes ConvertFromString(string s) - { - return BitbucketHelpers.StringToHookType(s); - } + /// + protected override HookTypes ConvertFromString(string s) + { + return BitbucketHelpers.StringToHookType(s); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Converters/JsonEnumConverter.cs b/src/Bitbucket.Net/Common/Converters/JsonEnumConverter.cs index c4306d4..cc8ec6b 100644 --- a/src/Bitbucket.Net/Common/Converters/JsonEnumConverter.cs +++ b/src/Bitbucket.Net/Common/Converters/JsonEnumConverter.cs @@ -1,109 +1,110 @@ -using System; -using System.Collections.Generic; -using System.Text.Json; +using System.Text.Json; using System.Text.Json.Serialization; -namespace Bitbucket.Net.Common.Converters +namespace Bitbucket.Net.Common.Converters; + +/// +/// Abstract base class for custom enum converters that convert between enum values and their string representations. +/// +/// The enum type to convert. +public abstract class JsonEnumConverter : JsonConverter + where TEnum : struct, Enum { /// - /// Abstract base class for custom enum converters that convert between enum values and their string representations. + /// Converts an enum value to its string representation. /// - /// The enum type to convert. - public abstract class JsonEnumConverter : JsonConverter - where TEnum : struct, Enum - { - /// - /// Converts an enum value to its string representation. - /// - protected abstract string ConvertToString(TEnum value); + protected abstract string ConvertToString(TEnum value); - /// - /// Converts a string representation to its enum value. - /// - protected abstract TEnum ConvertFromString(string s); + /// + /// Converts a string representation to its enum value. + /// + protected abstract TEnum ConvertFromString(string s); - public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + /// + public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) { - if (reader.TokenType == JsonTokenType.Null) - { - return default; - } - - if (reader.TokenType == JsonTokenType.String) - { - string value = reader.GetString()!; - return ConvertFromString(value); - } - - throw new JsonException($"Unexpected token {reader.TokenType} when parsing enum {typeof(TEnum).Name}."); + return default; } - public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options) + if (reader.TokenType == JsonTokenType.String) { - writer.WriteStringValue(ConvertToString(value)); + string value = reader.GetString()!; + return ConvertFromString(value); } + + throw new JsonException($"Unexpected token {reader.TokenType} when parsing enum {typeof(TEnum).Name}."); + } + + /// + public override void Write(Utf8JsonWriter writer, TEnum value, JsonSerializerOptions options) + { + writer.WriteStringValue(ConvertToString(value)); } +} +/// +/// Abstract base class for custom enum list converters that convert between lists of enum values and their JSON array representations. +/// +/// The enum type to convert. +public abstract class JsonEnumListConverter : JsonConverter?> + where TEnum : struct, Enum +{ /// - /// Abstract base class for custom enum list converters that convert between lists of enum values and their JSON array representations. + /// Converts an enum value to its string representation. /// - /// The enum type to convert. - public abstract class JsonEnumListConverter : JsonConverter?> - where TEnum : struct, Enum + protected abstract string ConvertToString(TEnum value); + + /// + /// Converts a string representation to its enum value. + /// + protected abstract TEnum ConvertFromString(string s); + + /// + public override List? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - /// - /// Converts an enum value to its string representation. - /// - protected abstract string ConvertToString(TEnum value); + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } - /// - /// Converts a string representation to its enum value. - /// - protected abstract TEnum ConvertFromString(string s); + if (reader.TokenType != JsonTokenType.StartArray) + { + throw new JsonException($"Expected StartArray token, got {reader.TokenType}."); + } - public override List? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + var items = new List(); + while (reader.Read()) { - if (reader.TokenType == JsonTokenType.Null) + if (reader.TokenType == JsonTokenType.EndArray) { - return null; + return items; } - if (reader.TokenType != JsonTokenType.StartArray) + if (reader.TokenType == JsonTokenType.String) { - throw new JsonException($"Expected StartArray token, got {reader.TokenType}."); + items.Add(ConvertFromString(reader.GetString()!)); } + } - var items = new List(); - while (reader.Read()) - { - if (reader.TokenType == JsonTokenType.EndArray) - { - return items; - } - - if (reader.TokenType == JsonTokenType.String) - { - items.Add(ConvertFromString(reader.GetString()!)); - } - } + throw new JsonException("Unexpected end of JSON while reading array."); + } - throw new JsonException("Unexpected end of JSON while reading array."); + /// + public override void Write(Utf8JsonWriter writer, List? value, JsonSerializerOptions options) + { + if (value is null) + { + writer.WriteNullValue(); + return; } - public override void Write(Utf8JsonWriter writer, List? value, JsonSerializerOptions options) + writer.WriteStartArray(); + foreach (var item in value) { - if (value is null) - { - writer.WriteNullValue(); - return; - } - - writer.WriteStartArray(); - foreach (var item in value) - { - writer.WriteStringValue(ConvertToString(item)); - } - writer.WriteEndArray(); + writer.WriteStringValue(ConvertToString(item)); } + writer.WriteEndArray(); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Converters/LineTypesConverter.cs b/src/Bitbucket.Net/Common/Converters/LineTypesConverter.cs index e6809e4..f1bcf51 100644 --- a/src/Bitbucket.Net/Common/Converters/LineTypesConverter.cs +++ b/src/Bitbucket.Net/Common/Converters/LineTypesConverter.cs @@ -1,17 +1,21 @@ using Bitbucket.Net.Models.Core.Projects; -namespace Bitbucket.Net.Common.Converters +namespace Bitbucket.Net.Common.Converters; + +/// +/// JSON converter for Bitbucket line classification values. +/// +public class LineTypesConverter : JsonEnumConverter { - public class LineTypesConverter : JsonEnumConverter + /// + protected override string ConvertToString(LineTypes value) { - protected override string ConvertToString(LineTypes value) - { - return BitbucketHelpers.LineTypeToString(value); - } + return BitbucketHelpers.LineTypeToString(value); + } - protected override LineTypes ConvertFromString(string s) - { - return BitbucketHelpers.StringToLineType(s); - } + /// + protected override LineTypes ConvertFromString(string s) + { + return BitbucketHelpers.StringToLineType(s); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Converters/ParticipantStatusConverter.cs b/src/Bitbucket.Net/Common/Converters/ParticipantStatusConverter.cs index ea94e57..f2d1b20 100644 --- a/src/Bitbucket.Net/Common/Converters/ParticipantStatusConverter.cs +++ b/src/Bitbucket.Net/Common/Converters/ParticipantStatusConverter.cs @@ -1,17 +1,21 @@ using Bitbucket.Net.Models.Core.Projects; -namespace Bitbucket.Net.Common.Converters +namespace Bitbucket.Net.Common.Converters; + +/// +/// JSON converter for Bitbucket participant status values. +/// +public class ParticipantStatusConverter : JsonEnumConverter { - public class ParticipantStatusConverter : JsonEnumConverter + /// + protected override string ConvertToString(ParticipantStatus value) { - protected override string ConvertToString(ParticipantStatus value) - { - return BitbucketHelpers.ParticipantStatusToString(value); - } + return BitbucketHelpers.ParticipantStatusToString(value); + } - protected override ParticipantStatus ConvertFromString(string s) - { - return BitbucketHelpers.StringToParticipantStatus(s); - } + /// + protected override ParticipantStatus ConvertFromString(string s) + { + return BitbucketHelpers.StringToParticipantStatus(s); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Converters/PermissionsConverter.cs b/src/Bitbucket.Net/Common/Converters/PermissionsConverter.cs index e74e09a..decbb44 100644 --- a/src/Bitbucket.Net/Common/Converters/PermissionsConverter.cs +++ b/src/Bitbucket.Net/Common/Converters/PermissionsConverter.cs @@ -1,17 +1,39 @@ using Bitbucket.Net.Models.Core.Admin; -namespace Bitbucket.Net.Common.Converters +namespace Bitbucket.Net.Common.Converters; + +/// +/// JSON converter for Bitbucket permission values. +/// +public class PermissionsConverter : JsonEnumConverter { - public class PermissionsConverter : JsonEnumConverter + /// + protected override string ConvertToString(Permissions value) { - protected override string ConvertToString(Permissions value) - { - return BitbucketHelpers.PermissionToString(value); - } + return BitbucketHelpers.PermissionToString(value); + } - protected override Permissions ConvertFromString(string s) - { - return BitbucketHelpers.StringToPermission(s); - } + /// + protected override Permissions ConvertFromString(string s) + { + return BitbucketHelpers.StringToPermission(s); } } + +/// +/// JSON converter for lists of Bitbucket permission values. +/// +public class PermissionsListConverter : JsonEnumListConverter +{ + /// + protected override string ConvertToString(Permissions value) + { + return BitbucketHelpers.PermissionToString(value); + } + + /// + protected override Permissions ConvertFromString(string s) + { + return BitbucketHelpers.StringToPermission(s); + } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Converters/PullRequestStatesConverter.cs b/src/Bitbucket.Net/Common/Converters/PullRequestStatesConverter.cs index 59ca6de..f1ee423 100644 --- a/src/Bitbucket.Net/Common/Converters/PullRequestStatesConverter.cs +++ b/src/Bitbucket.Net/Common/Converters/PullRequestStatesConverter.cs @@ -1,17 +1,21 @@ using Bitbucket.Net.Models.Core.Projects; -namespace Bitbucket.Net.Common.Converters +namespace Bitbucket.Net.Common.Converters; + +/// +/// JSON converter for Bitbucket pull request states. +/// +public class PullRequestStatesConverter : JsonEnumConverter { - public class PullRequestStatesConverter : JsonEnumConverter + /// + protected override string ConvertToString(PullRequestStates value) { - protected override string ConvertToString(PullRequestStates value) - { - return BitbucketHelpers.PullRequestStateToString(value); - } + return BitbucketHelpers.PullRequestStateToString(value); + } - protected override PullRequestStates ConvertFromString(string s) - { - return BitbucketHelpers.StringToPullRequestState(s); - } + /// + protected override PullRequestStates ConvertFromString(string s) + { + return BitbucketHelpers.StringToPullRequestState(s); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Converters/RefRestrictionTypesConverter.cs b/src/Bitbucket.Net/Common/Converters/RefRestrictionTypesConverter.cs index e8bda96..e0641f5 100644 --- a/src/Bitbucket.Net/Common/Converters/RefRestrictionTypesConverter.cs +++ b/src/Bitbucket.Net/Common/Converters/RefRestrictionTypesConverter.cs @@ -1,17 +1,21 @@ using Bitbucket.Net.Models.RefRestrictions; -namespace Bitbucket.Net.Common.Converters +namespace Bitbucket.Net.Common.Converters; + +/// +/// JSON converter for repository ref restriction types. +/// +public class RefRestrictionTypesConverter : JsonEnumConverter { - public class RefRestrictionTypesConverter : JsonEnumConverter + /// + protected override string ConvertToString(RefRestrictionTypes value) { - protected override string ConvertToString(RefRestrictionTypes value) - { - return BitbucketHelpers.RefRestrictionTypeToString(value); - } + return BitbucketHelpers.RefRestrictionTypeToString(value); + } - protected override RefRestrictionTypes ConvertFromString(string s) - { - return BitbucketHelpers.StringToRefRestrictionType(s); - } + /// + protected override RefRestrictionTypes ConvertFromString(string s) + { + return BitbucketHelpers.StringToRefRestrictionType(s); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Converters/RolesConverter.cs b/src/Bitbucket.Net/Common/Converters/RolesConverter.cs index 4eaeb61..b9f2431 100644 --- a/src/Bitbucket.Net/Common/Converters/RolesConverter.cs +++ b/src/Bitbucket.Net/Common/Converters/RolesConverter.cs @@ -1,17 +1,21 @@ using Bitbucket.Net.Models.Core.Projects; -namespace Bitbucket.Net.Common.Converters +namespace Bitbucket.Net.Common.Converters; + +/// +/// JSON converter for pull request role values. +/// +public class RolesConverter : JsonEnumConverter { - public class RolesConverter : JsonEnumConverter + /// + protected override string ConvertToString(Roles value) { - protected override string ConvertToString(Roles value) - { - return BitbucketHelpers.RoleToString(value); - } + return BitbucketHelpers.RoleToString(value); + } - protected override Roles ConvertFromString(string s) - { - return BitbucketHelpers.StringToRole(s); - } + /// + protected override Roles ConvertFromString(string s) + { + return BitbucketHelpers.StringToRole(s); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Converters/ScopeTypesConverter.cs b/src/Bitbucket.Net/Common/Converters/ScopeTypesConverter.cs index f71e90a..7a3e859 100644 --- a/src/Bitbucket.Net/Common/Converters/ScopeTypesConverter.cs +++ b/src/Bitbucket.Net/Common/Converters/ScopeTypesConverter.cs @@ -1,17 +1,21 @@ using Bitbucket.Net.Models.Core.Projects; -namespace Bitbucket.Net.Common.Converters +namespace Bitbucket.Net.Common.Converters; + +/// +/// JSON converter for Bitbucket permission scope types. +/// +public class ScopeTypesConverter : JsonEnumConverter { - public class ScopeTypesConverter : JsonEnumConverter + /// + protected override string ConvertToString(ScopeTypes value) { - protected override string ConvertToString(ScopeTypes value) - { - return BitbucketHelpers.ScopeTypeToString(value); - } + return BitbucketHelpers.ScopeTypeToString(value); + } - protected override ScopeTypes ConvertFromString(string s) - { - return BitbucketHelpers.StringToScopeType(s); - } + /// + protected override ScopeTypes ConvertFromString(string s) + { + return BitbucketHelpers.StringToScopeType(s); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Converters/SynchronizeActionsConverter.cs b/src/Bitbucket.Net/Common/Converters/SynchronizeActionsConverter.cs index b1cdca3..9eb61bd 100644 --- a/src/Bitbucket.Net/Common/Converters/SynchronizeActionsConverter.cs +++ b/src/Bitbucket.Net/Common/Converters/SynchronizeActionsConverter.cs @@ -1,17 +1,21 @@ using Bitbucket.Net.Models.RefSync; -namespace Bitbucket.Net.Common.Converters +namespace Bitbucket.Net.Common.Converters; + +/// +/// JSON converter for repository synchronization actions. +/// +public class SynchronizeActionsConverter : JsonEnumConverter { - public class SynchronizeActionsConverter : JsonEnumConverter + /// + protected override string ConvertToString(SynchronizeActions value) { - protected override string ConvertToString(SynchronizeActions value) - { - return BitbucketHelpers.SynchronizeActionToString(value); - } + return BitbucketHelpers.SynchronizeActionToString(value); + } - protected override SynchronizeActions ConvertFromString(string s) - { - return BitbucketHelpers.StringToSynchronizeAction(s); - } + /// + protected override SynchronizeActions ConvertFromString(string s) + { + return BitbucketHelpers.StringToSynchronizeAction(s); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Converters/UnixDateTimeOffsetConverter.cs b/src/Bitbucket.Net/Common/Converters/UnixDateTimeOffsetConverter.cs index 5cb074e..91e4a87 100644 --- a/src/Bitbucket.Net/Common/Converters/UnixDateTimeOffsetConverter.cs +++ b/src/Bitbucket.Net/Common/Converters/UnixDateTimeOffsetConverter.cs @@ -1,58 +1,62 @@ -using System; -using System.Text.Json; +using System.Text.Json; using System.Text.Json.Serialization; -namespace Bitbucket.Net.Common.Converters +namespace Bitbucket.Net.Common.Converters; + +/// +/// Converts Unix timestamps (milliseconds since epoch) to/from DateTimeOffset. +/// Bitbucket Server returns all timestamps as epoch milliseconds. +/// +public sealed class UnixDateTimeOffsetConverter : JsonConverter { - /// - /// Converts Unix timestamps (seconds since epoch) to/from DateTimeOffset. - /// - public sealed class UnixDateTimeOffsetConverter : JsonConverter + /// + public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - public override DateTimeOffset Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + return reader.TokenType switch { - return reader.TokenType switch - { - JsonTokenType.Null => default, - JsonTokenType.Number when reader.TryGetInt64(out long unixTime) => unixTime.FromUnixTimeSeconds(), - JsonTokenType.String when long.TryParse(reader.GetString(), out long unixTime) => unixTime.FromUnixTimeSeconds(), - _ => throw new JsonException($"Cannot convert {reader.TokenType} to {nameof(DateTimeOffset)}.") - }; - } + JsonTokenType.Null => default, + JsonTokenType.Number when reader.TryGetInt64(out long unixTime) => unixTime.FromUnixTimeMilliseconds(), + JsonTokenType.String when long.TryParse(reader.GetString(), out long unixTime) => unixTime.FromUnixTimeMilliseconds(), + _ => throw new JsonException($"Cannot convert {reader.TokenType} to {nameof(DateTimeOffset)}."), + }; + } + + /// + public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) + { + writer.WriteNumberValue(value.ToUnixTimeMilliseconds()); + } +} - public override void Write(Utf8JsonWriter writer, DateTimeOffset value, JsonSerializerOptions options) +/// +/// Converts Unix timestamps (milliseconds since epoch) to/from nullable DateTimeOffset. +/// Bitbucket Server returns all timestamps as epoch milliseconds. +/// +public sealed class NullableUnixDateTimeOffsetConverter : JsonConverter +{ + /// + public override DateTimeOffset? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.TokenType switch { - writer.WriteNumberValue(value.ToUnixTimeSeconds()); - } + JsonTokenType.Null => null, + JsonTokenType.Number when reader.TryGetInt64(out long unixTime) => unixTime.FromUnixTimeMilliseconds(), + JsonTokenType.String when string.IsNullOrEmpty(reader.GetString()) => null, + JsonTokenType.String when long.TryParse(reader.GetString(), out long unixTime) => unixTime.FromUnixTimeMilliseconds(), + _ => throw new JsonException($"Cannot convert {reader.TokenType} to nullable {nameof(DateTimeOffset)}."), + }; } - /// - /// Converts Unix timestamps (seconds since epoch) to/from nullable DateTimeOffset. - /// - public sealed class NullableUnixDateTimeOffsetConverter : JsonConverter + /// + public override void Write(Utf8JsonWriter writer, DateTimeOffset? value, JsonSerializerOptions options) { - public override DateTimeOffset? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + if (value.HasValue) { - return reader.TokenType switch - { - JsonTokenType.Null => null, - JsonTokenType.Number when reader.TryGetInt64(out long unixTime) => unixTime.FromUnixTimeSeconds(), - JsonTokenType.String when string.IsNullOrEmpty(reader.GetString()) => null, - JsonTokenType.String when long.TryParse(reader.GetString(), out long unixTime) => unixTime.FromUnixTimeSeconds(), - _ => throw new JsonException($"Cannot convert {reader.TokenType} to nullable {nameof(DateTimeOffset)}.") - }; + writer.WriteNumberValue(value.Value.ToUnixTimeMilliseconds()); } - - public override void Write(Utf8JsonWriter writer, DateTimeOffset? value, JsonSerializerOptions options) + else { - if (value.HasValue) - { - writer.WriteNumberValue(value.Value.ToUnixTimeSeconds()); - } - else - { - writer.WriteNullValue(); - } + writer.WriteNullValue(); } } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Converters/WebHookOutcomesConverter.cs b/src/Bitbucket.Net/Common/Converters/WebHookOutcomesConverter.cs index a7496bc..322a512 100644 --- a/src/Bitbucket.Net/Common/Converters/WebHookOutcomesConverter.cs +++ b/src/Bitbucket.Net/Common/Converters/WebHookOutcomesConverter.cs @@ -1,17 +1,21 @@ using Bitbucket.Net.Models.Core.Projects; -namespace Bitbucket.Net.Common.Converters +namespace Bitbucket.Net.Common.Converters; + +/// +/// JSON converter for webhook outcome values. +/// +public class WebHookOutcomesConverter : JsonEnumConverter { - public class WebHookOutcomesConverter : JsonEnumConverter + /// + protected override string ConvertToString(WebHookOutcomes value) { - protected override string ConvertToString(WebHookOutcomes value) - { - return BitbucketHelpers.WebHookOutcomeToString(value); - } + return BitbucketHelpers.WebHookOutcomeToString(value); + } - protected override WebHookOutcomes ConvertFromString(string s) - { - return BitbucketHelpers.StringToWebHookOutcome(s); - } + /// + protected override WebHookOutcomes ConvertFromString(string s) + { + return BitbucketHelpers.StringToWebHookOutcome(s); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/DynamicMultipartFormDataContent.cs b/src/Bitbucket.Net/Common/DynamicMultipartFormDataContent.cs index 40c47de..dfe9cda 100644 --- a/src/Bitbucket.Net/Common/DynamicMultipartFormDataContent.cs +++ b/src/Bitbucket.Net/Common/DynamicMultipartFormDataContent.cs @@ -1,36 +1,54 @@ using System.Collections; -using System.Collections.Generic; -using System.Net.Http; -namespace Bitbucket.Net.Common +namespace Bitbucket.Net.Common; + +/// +/// A helper for building multipart/form-data payloads where parts are added conditionally. +/// +public class DynamicMultipartFormDataContent : IEnumerable { - public class DynamicMultipartFormDataContent : IEnumerable + private readonly MultipartFormDataContent _multipartFormDataContent = new MultipartFormDataContent(); + + /// + /// Adds a required multipart section. + /// + /// The HTTP content to add. + /// The form field name. + public void Add(HttpContent value, string key) { - private readonly MultipartFormDataContent _multipartFormDataContent = new MultipartFormDataContent(); + _multipartFormDataContent.Add(value, key); + } - public void Add(HttpContent value, string key) + /// + /// Adds a multipart section when a value is provided and not equal to the default for . + /// + /// The type of the guard value. + /// The guard value to test. + /// The HTTP content to add when is present. + /// The form field name. + public void Add(T t, HttpContent? value, string key) + { + if (!EqualityComparer.Default.Equals(t, default) && value is not null) { _multipartFormDataContent.Add(value, key); } + } - public void Add(T t, HttpContent? value, string key) - { - if (!EqualityComparer.Default.Equals(t, default(T)) && value is not null) - { - _multipartFormDataContent.Add(value, key); - } - } - - public MultipartFormDataContent ToMultipartFormDataContent() => _multipartFormDataContent; + /// + /// Finalizes the builder and returns the underlying instance. + /// + /// The built multipart form data content. + public MultipartFormDataContent ToMultipartFormDataContent() => _multipartFormDataContent; - public IEnumerator GetEnumerator() - { - return _multipartFormDataContent.GetEnumerator(); - } + /// + public IEnumerator GetEnumerator() + { + return _multipartFormDataContent.GetEnumerator(); + } - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Exceptions/BitbucketApiException.cs b/src/Bitbucket.Net/Common/Exceptions/BitbucketApiException.cs index 87ba60c..2406a2d 100644 --- a/src/Bitbucket.Net/Common/Exceptions/BitbucketApiException.cs +++ b/src/Bitbucket.Net/Common/Exceptions/BitbucketApiException.cs @@ -1,117 +1,114 @@ -using System; -using System.Collections.Generic; -using System.Net; using Bitbucket.Net.Common.Models; +using System.Net; + +namespace Bitbucket.Net.Common.Exceptions; -namespace Bitbucket.Net.Common.Exceptions +/// +/// Base exception for all Bitbucket API errors. Contains detailed error information +/// from the Bitbucket Server response. +/// +public class BitbucketApiException : Exception { /// - /// Base exception for all Bitbucket API errors. Contains detailed error information - /// from the Bitbucket Server response. + /// Gets the HTTP status code returned by the Bitbucket Server. /// - public class BitbucketApiException : Exception - { - /// - /// Gets the HTTP status code returned by the Bitbucket Server. - /// - public HttpStatusCode StatusCode { get; } + public HttpStatusCode StatusCode { get; } - /// - /// Gets the context information from the first error, if available. - /// This typically contains the field or resource that caused the error. - /// - public string? Context { get; } + /// + /// Gets the context information from the first error, if available. + /// This typically contains the field or resource that caused the error. + /// + public string? Context { get; } - /// - /// Gets the collection of errors returned by the Bitbucket Server. - /// - public IReadOnlyList Errors { get; } + /// + /// Gets the collection of errors returned by the Bitbucket Server. + /// + public IReadOnlyList Errors { get; } - /// - /// Gets the request URL that caused the error, if available. - /// - public string? RequestUrl { get; } + /// + /// Gets the request URL that caused the error, if available. + /// + public string? RequestUrl { get; } - /// - /// Initializes a new instance of the class. - /// - /// The error message. - /// The HTTP status code. - /// The collection of errors from the Bitbucket response. - /// The request URL that caused the error. - public BitbucketApiException(string message, HttpStatusCode statusCode, IReadOnlyList errors, string? requestUrl = null) - : base(message) - { - StatusCode = statusCode; - Errors = errors ?? Array.Empty(); - Context = errors?.Count > 0 ? errors[0].Context : null; - RequestUrl = requestUrl; - } + /// + /// Initializes a new instance of the class. + /// + /// The error message. + /// The HTTP status code. + /// The collection of errors from the Bitbucket response. + /// The request URL that caused the error. + public BitbucketApiException(string message, HttpStatusCode statusCode, IReadOnlyList errors, string? requestUrl = null) + : base(message) + { + StatusCode = statusCode; + Errors = errors ?? []; + Context = errors?.Count > 0 ? errors[0].Context : null; + RequestUrl = requestUrl; + } - /// - /// Initializes a new instance of the class with an inner exception. - /// - /// The error message. - /// The HTTP status code. - /// The collection of errors from the Bitbucket response. - /// The inner exception. - /// The request URL that caused the error. - public BitbucketApiException(string message, HttpStatusCode statusCode, IReadOnlyList errors, Exception innerException, string? requestUrl = null) - : base(message, innerException) - { - StatusCode = statusCode; - Errors = errors ?? Array.Empty(); - Context = errors?.Count > 0 ? errors[0].Context : null; - RequestUrl = requestUrl; - } + /// + /// Initializes a new instance of the class with an inner exception. + /// + /// The error message. + /// The HTTP status code. + /// The collection of errors from the Bitbucket response. + /// The inner exception. + /// The request URL that caused the error. + public BitbucketApiException(string message, HttpStatusCode statusCode, IReadOnlyList errors, Exception innerException, string? requestUrl = null) + : base(message, innerException) + { + StatusCode = statusCode; + Errors = errors ?? []; + Context = errors?.Count > 0 ? errors[0].Context : null; + RequestUrl = requestUrl; + } + + /// + /// Creates the appropriate exception type based on the HTTP status code. + /// + /// The HTTP status code. + /// The collection of errors from the Bitbucket response. + /// The request URL that caused the error. + /// A typed exception matching the HTTP status code. + public static BitbucketApiException Create(int statusCode, IReadOnlyList errors, string? requestUrl = null) + { + var httpStatusCode = (HttpStatusCode)statusCode; + string message = BuildErrorMessage(httpStatusCode, errors); - /// - /// Creates the appropriate exception type based on the HTTP status code. - /// - /// The HTTP status code. - /// The collection of errors from the Bitbucket response. - /// The request URL that caused the error. - /// A typed exception matching the HTTP status code. - public static BitbucketApiException Create(int statusCode, IReadOnlyList errors, string? requestUrl = null) + return statusCode switch { - var httpStatusCode = (HttpStatusCode)statusCode; - string message = BuildErrorMessage(httpStatusCode, errors); + 400 => new BitbucketBadRequestException(message, errors, requestUrl), + 401 => new BitbucketAuthenticationException(message, errors, requestUrl), + 403 => new BitbucketForbiddenException(message, errors, requestUrl), + 404 => new BitbucketNotFoundException(message, errors, requestUrl), + 409 => new BitbucketConflictException(message, errors, requestUrl), + 422 => new BitbucketValidationException(message, errors, requestUrl), + 429 => new BitbucketRateLimitException(message, errors, requestUrl), + >= 500 and < 600 => new BitbucketServerException(message, httpStatusCode, errors, requestUrl), + _ => new BitbucketApiException(message, httpStatusCode, errors, requestUrl), + }; + } - return statusCode switch - { - 400 => new BitbucketBadRequestException(message, errors, requestUrl), - 401 => new BitbucketAuthenticationException(message, errors, requestUrl), - 403 => new BitbucketForbiddenException(message, errors, requestUrl), - 404 => new BitbucketNotFoundException(message, errors, requestUrl), - 409 => new BitbucketConflictException(message, errors, requestUrl), - 422 => new BitbucketValidationException(message, errors, requestUrl), - 429 => new BitbucketRateLimitException(message, errors, requestUrl), - >= 500 and < 600 => new BitbucketServerException(message, httpStatusCode, errors, requestUrl), - _ => new BitbucketApiException(message, httpStatusCode, errors, requestUrl) - }; + private static string BuildErrorMessage(HttpStatusCode statusCode, IReadOnlyList errors) + { + if (errors == null || errors.Count == 0) + { + return $"Bitbucket API request failed with status {(int)statusCode} ({statusCode})"; } - private static string BuildErrorMessage(HttpStatusCode statusCode, IReadOnlyList errors) + var messages = new List(errors.Count); + foreach (var error in errors) { - if (errors == null || errors.Count == 0) + if (!string.IsNullOrEmpty(error.Context)) { - return $"Bitbucket API request failed with status {(int)statusCode} ({statusCode})"; + messages.Add($"[{error.Context}] {error.Message}"); } - - var messages = new List(errors.Count); - foreach (var error in errors) + else { - if (!string.IsNullOrEmpty(error.Context)) - { - messages.Add($"[{error.Context}] {error.Message}"); - } - else - { - messages.Add(error.Message); - } + messages.Add(error.Message); } - - return $"Bitbucket API request failed with status {(int)statusCode} ({statusCode}): {string.Join("; ", messages)}"; } + + return $"Bitbucket API request failed with status {(int)statusCode} ({statusCode}): {string.Join("; ", messages)}"; } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Exceptions/BitbucketAuthenticationException.cs b/src/Bitbucket.Net/Common/Exceptions/BitbucketAuthenticationException.cs index e87adc7..f9cdaab 100644 --- a/src/Bitbucket.Net/Common/Exceptions/BitbucketAuthenticationException.cs +++ b/src/Bitbucket.Net/Common/Exceptions/BitbucketAuthenticationException.cs @@ -1,37 +1,34 @@ -using System; -using System.Collections.Generic; -using System.Net; using Bitbucket.Net.Common.Models; +using System.Net; -namespace Bitbucket.Net.Common.Exceptions +namespace Bitbucket.Net.Common.Exceptions; + +/// +/// Exception thrown when authentication fails (HTTP 401 Unauthorized). +/// This typically indicates invalid or missing credentials. +/// +public class BitbucketAuthenticationException : BitbucketApiException { /// - /// Exception thrown when authentication fails (HTTP 401 Unauthorized). - /// This typically indicates invalid or missing credentials. + /// Initializes a new instance of the class. /// - public class BitbucketAuthenticationException : BitbucketApiException + /// The error message. + /// The collection of errors from the Bitbucket response. + /// The request URL that caused the error. + public BitbucketAuthenticationException(string message, IReadOnlyList errors, string? requestUrl = null) + : base(message, HttpStatusCode.Unauthorized, errors, requestUrl) { - /// - /// Initializes a new instance of the class. - /// - /// The error message. - /// The collection of errors from the Bitbucket response. - /// The request URL that caused the error. - public BitbucketAuthenticationException(string message, IReadOnlyList errors, string? requestUrl = null) - : base(message, HttpStatusCode.Unauthorized, errors, requestUrl) - { - } + } - /// - /// Initializes a new instance of the class with an inner exception. - /// - /// The error message. - /// The collection of errors from the Bitbucket response. - /// The inner exception. - /// The request URL that caused the error. - public BitbucketAuthenticationException(string message, IReadOnlyList errors, Exception innerException, string? requestUrl = null) - : base(message, HttpStatusCode.Unauthorized, errors, innerException, requestUrl) - { - } + /// + /// Initializes a new instance of the class with an inner exception. + /// + /// The error message. + /// The collection of errors from the Bitbucket response. + /// The inner exception. + /// The request URL that caused the error. + public BitbucketAuthenticationException(string message, IReadOnlyList errors, Exception innerException, string? requestUrl = null) + : base(message, HttpStatusCode.Unauthorized, errors, innerException, requestUrl) + { } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Exceptions/BitbucketBadRequestException.cs b/src/Bitbucket.Net/Common/Exceptions/BitbucketBadRequestException.cs index 631df4d..b586898 100644 --- a/src/Bitbucket.Net/Common/Exceptions/BitbucketBadRequestException.cs +++ b/src/Bitbucket.Net/Common/Exceptions/BitbucketBadRequestException.cs @@ -1,37 +1,34 @@ -using System; -using System.Collections.Generic; -using System.Net; using Bitbucket.Net.Common.Models; +using System.Net; -namespace Bitbucket.Net.Common.Exceptions +namespace Bitbucket.Net.Common.Exceptions; + +/// +/// Exception thrown when the request is malformed (HTTP 400 Bad Request). +/// This indicates invalid parameters, malformed JSON, or other request-level issues. +/// +public class BitbucketBadRequestException : BitbucketApiException { /// - /// Exception thrown when the request is malformed (HTTP 400 Bad Request). - /// This indicates invalid parameters, malformed JSON, or other request-level issues. + /// Initializes a new instance of the class. /// - public class BitbucketBadRequestException : BitbucketApiException + /// The error message. + /// The collection of errors from the Bitbucket response. + /// The request URL that caused the error. + public BitbucketBadRequestException(string message, IReadOnlyList errors, string? requestUrl = null) + : base(message, HttpStatusCode.BadRequest, errors, requestUrl) { - /// - /// Initializes a new instance of the class. - /// - /// The error message. - /// The collection of errors from the Bitbucket response. - /// The request URL that caused the error. - public BitbucketBadRequestException(string message, IReadOnlyList errors, string? requestUrl = null) - : base(message, HttpStatusCode.BadRequest, errors, requestUrl) - { - } + } - /// - /// Initializes a new instance of the class with an inner exception. - /// - /// The error message. - /// The collection of errors from the Bitbucket response. - /// The inner exception. - /// The request URL that caused the error. - public BitbucketBadRequestException(string message, IReadOnlyList errors, Exception innerException, string? requestUrl = null) - : base(message, HttpStatusCode.BadRequest, errors, innerException, requestUrl) - { - } + /// + /// Initializes a new instance of the class with an inner exception. + /// + /// The error message. + /// The collection of errors from the Bitbucket response. + /// The inner exception. + /// The request URL that caused the error. + public BitbucketBadRequestException(string message, IReadOnlyList errors, Exception innerException, string? requestUrl = null) + : base(message, HttpStatusCode.BadRequest, errors, innerException, requestUrl) + { } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Exceptions/BitbucketConflictException.cs b/src/Bitbucket.Net/Common/Exceptions/BitbucketConflictException.cs index f80d3e1..e9c97ff 100644 --- a/src/Bitbucket.Net/Common/Exceptions/BitbucketConflictException.cs +++ b/src/Bitbucket.Net/Common/Exceptions/BitbucketConflictException.cs @@ -1,37 +1,34 @@ -using System; -using System.Collections.Generic; -using System.Net; using Bitbucket.Net.Common.Models; +using System.Net; -namespace Bitbucket.Net.Common.Exceptions +namespace Bitbucket.Net.Common.Exceptions; + +/// +/// Exception thrown when there is a resource conflict (HTTP 409 Conflict). +/// This typically indicates a merge conflict, duplicate resource, or state conflict. +/// +public class BitbucketConflictException : BitbucketApiException { /// - /// Exception thrown when there is a resource conflict (HTTP 409 Conflict). - /// This typically indicates a merge conflict, duplicate resource, or state conflict. + /// Initializes a new instance of the class. /// - public class BitbucketConflictException : BitbucketApiException + /// The error message. + /// The collection of errors from the Bitbucket response. + /// The request URL that caused the error. + public BitbucketConflictException(string message, IReadOnlyList errors, string? requestUrl = null) + : base(message, HttpStatusCode.Conflict, errors, requestUrl) { - /// - /// Initializes a new instance of the class. - /// - /// The error message. - /// The collection of errors from the Bitbucket response. - /// The request URL that caused the error. - public BitbucketConflictException(string message, IReadOnlyList errors, string? requestUrl = null) - : base(message, HttpStatusCode.Conflict, errors, requestUrl) - { - } + } - /// - /// Initializes a new instance of the class with an inner exception. - /// - /// The error message. - /// The collection of errors from the Bitbucket response. - /// The inner exception. - /// The request URL that caused the error. - public BitbucketConflictException(string message, IReadOnlyList errors, Exception innerException, string? requestUrl = null) - : base(message, HttpStatusCode.Conflict, errors, innerException, requestUrl) - { - } + /// + /// Initializes a new instance of the class with an inner exception. + /// + /// The error message. + /// The collection of errors from the Bitbucket response. + /// The inner exception. + /// The request URL that caused the error. + public BitbucketConflictException(string message, IReadOnlyList errors, Exception innerException, string? requestUrl = null) + : base(message, HttpStatusCode.Conflict, errors, innerException, requestUrl) + { } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Exceptions/BitbucketForbiddenException.cs b/src/Bitbucket.Net/Common/Exceptions/BitbucketForbiddenException.cs index e63fc6e..87fe27f 100644 --- a/src/Bitbucket.Net/Common/Exceptions/BitbucketForbiddenException.cs +++ b/src/Bitbucket.Net/Common/Exceptions/BitbucketForbiddenException.cs @@ -1,37 +1,34 @@ -using System; -using System.Collections.Generic; -using System.Net; using Bitbucket.Net.Common.Models; +using System.Net; -namespace Bitbucket.Net.Common.Exceptions +namespace Bitbucket.Net.Common.Exceptions; + +/// +/// Exception thrown when access is forbidden (HTTP 403 Forbidden). +/// This indicates the user is authenticated but lacks permission for the requested operation. +/// +public class BitbucketForbiddenException : BitbucketApiException { /// - /// Exception thrown when access is forbidden (HTTP 403 Forbidden). - /// This indicates the user is authenticated but lacks permission for the requested operation. + /// Initializes a new instance of the class. /// - public class BitbucketForbiddenException : BitbucketApiException + /// The error message. + /// The collection of errors from the Bitbucket response. + /// The request URL that caused the error. + public BitbucketForbiddenException(string message, IReadOnlyList errors, string? requestUrl = null) + : base(message, HttpStatusCode.Forbidden, errors, requestUrl) { - /// - /// Initializes a new instance of the class. - /// - /// The error message. - /// The collection of errors from the Bitbucket response. - /// The request URL that caused the error. - public BitbucketForbiddenException(string message, IReadOnlyList errors, string? requestUrl = null) - : base(message, HttpStatusCode.Forbidden, errors, requestUrl) - { - } + } - /// - /// Initializes a new instance of the class with an inner exception. - /// - /// The error message. - /// The collection of errors from the Bitbucket response. - /// The inner exception. - /// The request URL that caused the error. - public BitbucketForbiddenException(string message, IReadOnlyList errors, Exception innerException, string? requestUrl = null) - : base(message, HttpStatusCode.Forbidden, errors, innerException, requestUrl) - { - } + /// + /// Initializes a new instance of the class with an inner exception. + /// + /// The error message. + /// The collection of errors from the Bitbucket response. + /// The inner exception. + /// The request URL that caused the error. + public BitbucketForbiddenException(string message, IReadOnlyList errors, Exception innerException, string? requestUrl = null) + : base(message, HttpStatusCode.Forbidden, errors, innerException, requestUrl) + { } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Exceptions/BitbucketNotFoundException.cs b/src/Bitbucket.Net/Common/Exceptions/BitbucketNotFoundException.cs index 1d1c609..726246d 100644 --- a/src/Bitbucket.Net/Common/Exceptions/BitbucketNotFoundException.cs +++ b/src/Bitbucket.Net/Common/Exceptions/BitbucketNotFoundException.cs @@ -1,37 +1,34 @@ -using System; -using System.Collections.Generic; -using System.Net; using Bitbucket.Net.Common.Models; +using System.Net; -namespace Bitbucket.Net.Common.Exceptions +namespace Bitbucket.Net.Common.Exceptions; + +/// +/// Exception thrown when a requested resource is not found (HTTP 404 Not Found). +/// This typically indicates the project, repository, branch, or other resource does not exist. +/// +public class BitbucketNotFoundException : BitbucketApiException { /// - /// Exception thrown when a requested resource is not found (HTTP 404 Not Found). - /// This typically indicates the project, repository, branch, or other resource does not exist. + /// Initializes a new instance of the class. /// - public class BitbucketNotFoundException : BitbucketApiException + /// The error message. + /// The collection of errors from the Bitbucket response. + /// The request URL that caused the error. + public BitbucketNotFoundException(string message, IReadOnlyList errors, string? requestUrl = null) + : base(message, HttpStatusCode.NotFound, errors, requestUrl) { - /// - /// Initializes a new instance of the class. - /// - /// The error message. - /// The collection of errors from the Bitbucket response. - /// The request URL that caused the error. - public BitbucketNotFoundException(string message, IReadOnlyList errors, string? requestUrl = null) - : base(message, HttpStatusCode.NotFound, errors, requestUrl) - { - } + } - /// - /// Initializes a new instance of the class with an inner exception. - /// - /// The error message. - /// The collection of errors from the Bitbucket response. - /// The inner exception. - /// The request URL that caused the error. - public BitbucketNotFoundException(string message, IReadOnlyList errors, Exception innerException, string? requestUrl = null) - : base(message, HttpStatusCode.NotFound, errors, innerException, requestUrl) - { - } + /// + /// Initializes a new instance of the class with an inner exception. + /// + /// The error message. + /// The collection of errors from the Bitbucket response. + /// The inner exception. + /// The request URL that caused the error. + public BitbucketNotFoundException(string message, IReadOnlyList errors, Exception innerException, string? requestUrl = null) + : base(message, HttpStatusCode.NotFound, errors, innerException, requestUrl) + { } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Exceptions/BitbucketRateLimitException.cs b/src/Bitbucket.Net/Common/Exceptions/BitbucketRateLimitException.cs index d09d959..5e72612 100644 --- a/src/Bitbucket.Net/Common/Exceptions/BitbucketRateLimitException.cs +++ b/src/Bitbucket.Net/Common/Exceptions/BitbucketRateLimitException.cs @@ -1,37 +1,34 @@ -using System; -using System.Collections.Generic; -using System.Net; using Bitbucket.Net.Common.Models; +using System.Net; -namespace Bitbucket.Net.Common.Exceptions +namespace Bitbucket.Net.Common.Exceptions; + +/// +/// Exception thrown when rate limiting is applied (HTTP 429 Too Many Requests). +/// This indicates too many requests have been made in a given time period. +/// +public class BitbucketRateLimitException : BitbucketApiException { /// - /// Exception thrown when rate limiting is applied (HTTP 429 Too Many Requests). - /// This indicates too many requests have been made in a given time period. + /// Initializes a new instance of the class. /// - public class BitbucketRateLimitException : BitbucketApiException + /// The error message. + /// The collection of errors from the Bitbucket response. + /// The request URL that caused the error. + public BitbucketRateLimitException(string message, IReadOnlyList errors, string? requestUrl = null) + : base(message, HttpStatusCode.TooManyRequests, errors, requestUrl) { - /// - /// Initializes a new instance of the class. - /// - /// The error message. - /// The collection of errors from the Bitbucket response. - /// The request URL that caused the error. - public BitbucketRateLimitException(string message, IReadOnlyList errors, string? requestUrl = null) - : base(message, HttpStatusCode.TooManyRequests, errors, requestUrl) - { - } + } - /// - /// Initializes a new instance of the class with an inner exception. - /// - /// The error message. - /// The collection of errors from the Bitbucket response. - /// The inner exception. - /// The request URL that caused the error. - public BitbucketRateLimitException(string message, IReadOnlyList errors, Exception innerException, string? requestUrl = null) - : base(message, HttpStatusCode.TooManyRequests, errors, innerException, requestUrl) - { - } + /// + /// Initializes a new instance of the class with an inner exception. + /// + /// The error message. + /// The collection of errors from the Bitbucket response. + /// The inner exception. + /// The request URL that caused the error. + public BitbucketRateLimitException(string message, IReadOnlyList errors, Exception innerException, string? requestUrl = null) + : base(message, HttpStatusCode.TooManyRequests, errors, innerException, requestUrl) + { } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Exceptions/BitbucketServerException.cs b/src/Bitbucket.Net/Common/Exceptions/BitbucketServerException.cs index 318c15d..ab9a05d 100644 --- a/src/Bitbucket.Net/Common/Exceptions/BitbucketServerException.cs +++ b/src/Bitbucket.Net/Common/Exceptions/BitbucketServerException.cs @@ -1,39 +1,36 @@ -using System; -using System.Collections.Generic; -using System.Net; using Bitbucket.Net.Common.Models; +using System.Net; -namespace Bitbucket.Net.Common.Exceptions +namespace Bitbucket.Net.Common.Exceptions; + +/// +/// Exception thrown when a server error occurs (HTTP 5xx). +/// This indicates an internal server error on the Bitbucket Server side. +/// +public class BitbucketServerException : BitbucketApiException { /// - /// Exception thrown when a server error occurs (HTTP 5xx). - /// This indicates an internal server error on the Bitbucket Server side. + /// Initializes a new instance of the class. /// - public class BitbucketServerException : BitbucketApiException + /// The error message. + /// The HTTP status code (must be 5xx). + /// The collection of errors from the Bitbucket response. + /// The request URL that caused the error. + public BitbucketServerException(string message, HttpStatusCode statusCode, IReadOnlyList errors, string? requestUrl = null) + : base(message, statusCode, errors, requestUrl) { - /// - /// Initializes a new instance of the class. - /// - /// The error message. - /// The HTTP status code (must be 5xx). - /// The collection of errors from the Bitbucket response. - /// The request URL that caused the error. - public BitbucketServerException(string message, HttpStatusCode statusCode, IReadOnlyList errors, string? requestUrl = null) - : base(message, statusCode, errors, requestUrl) - { - } + } - /// - /// Initializes a new instance of the class with an inner exception. - /// - /// The error message. - /// The HTTP status code (must be 5xx). - /// The collection of errors from the Bitbucket response. - /// The inner exception. - /// The request URL that caused the error. - public BitbucketServerException(string message, HttpStatusCode statusCode, IReadOnlyList errors, Exception innerException, string? requestUrl = null) - : base(message, statusCode, errors, innerException, requestUrl) - { - } + /// + /// Initializes a new instance of the class with an inner exception. + /// + /// The error message. + /// The HTTP status code (must be 5xx). + /// The collection of errors from the Bitbucket response. + /// The inner exception. + /// The request URL that caused the error. + public BitbucketServerException(string message, HttpStatusCode statusCode, IReadOnlyList errors, Exception innerException, string? requestUrl = null) + : base(message, statusCode, errors, innerException, requestUrl) + { } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Exceptions/BitbucketValidationException.cs b/src/Bitbucket.Net/Common/Exceptions/BitbucketValidationException.cs index 9338584..e61481a 100644 --- a/src/Bitbucket.Net/Common/Exceptions/BitbucketValidationException.cs +++ b/src/Bitbucket.Net/Common/Exceptions/BitbucketValidationException.cs @@ -1,37 +1,34 @@ -using System; -using System.Collections.Generic; -using System.Net; using Bitbucket.Net.Common.Models; +using System.Net; -namespace Bitbucket.Net.Common.Exceptions +namespace Bitbucket.Net.Common.Exceptions; + +/// +/// Exception thrown when validation fails (HTTP 422 Unprocessable Entity). +/// This indicates the request was well-formed but contained semantic errors. +/// +public class BitbucketValidationException : BitbucketApiException { /// - /// Exception thrown when validation fails (HTTP 422 Unprocessable Entity). - /// This indicates the request was well-formed but contained semantic errors. + /// Initializes a new instance of the class. /// - public class BitbucketValidationException : BitbucketApiException + /// The error message. + /// The collection of errors from the Bitbucket response. + /// The request URL that caused the error. + public BitbucketValidationException(string message, IReadOnlyList errors, string? requestUrl = null) + : base(message, HttpStatusCode.UnprocessableEntity, errors, requestUrl) { - /// - /// Initializes a new instance of the class. - /// - /// The error message. - /// The collection of errors from the Bitbucket response. - /// The request URL that caused the error. - public BitbucketValidationException(string message, IReadOnlyList errors, string? requestUrl = null) - : base(message, HttpStatusCode.UnprocessableEntity, errors, requestUrl) - { - } + } - /// - /// Initializes a new instance of the class with an inner exception. - /// - /// The error message. - /// The collection of errors from the Bitbucket response. - /// The inner exception. - /// The request URL that caused the error. - public BitbucketValidationException(string message, IReadOnlyList errors, Exception innerException, string? requestUrl = null) - : base(message, HttpStatusCode.UnprocessableEntity, errors, innerException, requestUrl) - { - } + /// + /// Initializes a new instance of the class with an inner exception. + /// + /// The error message. + /// The collection of errors from the Bitbucket response. + /// The inner exception. + /// The request URL that caused the error. + public BitbucketValidationException(string message, IReadOnlyList errors, Exception innerException, string? requestUrl = null) + : base(message, HttpStatusCode.UnprocessableEntity, errors, innerException, requestUrl) + { } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/FlurlRequestExtensions.cs b/src/Bitbucket.Net/Common/FlurlRequestExtensions.cs index 8b89ceb..911c252 100644 --- a/src/Bitbucket.Net/Common/FlurlRequestExtensions.cs +++ b/src/Bitbucket.Net/Common/FlurlRequestExtensions.cs @@ -1,19 +1,39 @@ -using System; -using Flurl.Http; +using Flurl.Http; -namespace Bitbucket.Net.Common +namespace Bitbucket.Net.Common; + +/// +/// Extension methods for configuring and invoking Flurl requests in the Bitbucket client. +/// +public static class FlurlRequestExtensions { - public static class FlurlRequestExtensions + /// + /// Applies either bearer-token or basic authentication to the request. + /// + /// The request to decorate. + /// Delegate that supplies an OAuth bearer token. When provided, bearer auth is used. + /// The user name for basic authentication. + /// The password for basic authentication. + /// The authenticated . + public static IFlurlRequest WithAuthentication(this IFlurlRequest request, Func? getToken, string? userName, string? password) { - public static IFlurlRequest WithAuthentication(this IFlurlRequest request, Func? getToken, string? userName, string? password) + if (getToken != null) { - if (getToken != null) - { - string token = getToken(); - return request.WithOAuthBearerToken(token); - } - - return request.WithBasicAuth(userName, password); + string token = getToken(); + return request.WithOAuthBearerToken(token); } + + return request.WithBasicAuth(userName, password); + } + + /// + /// Sends a GET request, honoring the provided cancellation token. + /// + /// The request to execute. + /// The cancellation token. + /// The Flurl response. + public static Task GetAsync(this IFlurlRequest request, CancellationToken cancellationToken) + { + return request.GetAsync(HttpCompletionOption.ResponseContentRead, cancellationToken); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Mcp/DiffStreamingExtensions.cs b/src/Bitbucket.Net/Common/Mcp/DiffStreamingExtensions.cs index 7a44541..a87b214 100644 --- a/src/Bitbucket.Net/Common/Mcp/DiffStreamingExtensions.cs +++ b/src/Bitbucket.Net/Common/Mcp/DiffStreamingExtensions.cs @@ -1,359 +1,344 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; using Bitbucket.Net.Models.Core.Projects; +using System.Runtime.CompilerServices; -namespace Bitbucket.Net.Common.Mcp +namespace Bitbucket.Net.Common.Mcp; + +/// +/// MCP-optimized diff streaming extensions for context window management. +/// Diffs are typically the largest response payloads in MCP usage (100KB-10MB+). +/// These extensions provide line-count-aware streaming with early termination. +/// +public static class DiffStreamingExtensions { /// - /// MCP-optimized diff streaming extensions for context window management. - /// Diffs are typically the largest response payloads in MCP usage (100KB-10MB+). - /// These extensions provide line-count-aware streaming with early termination. + /// Streams diff hunks from an async enumerable of diffs with line and file limits. + /// Enables early termination when MCP context window limits are reached. /// - public static class DiffStreamingExtensions + /// The async enumerable of diffs to process. + /// Maximum total lines to yield across all diffs. Null for unlimited. + /// Maximum number of files to process. Null for unlimited. + /// Cancellation token. + /// An async enumerable of diff results with truncation metadata. + public static async IAsyncEnumerable StreamDiffsWithLimitsAsync( + this IAsyncEnumerable diffs, + int? maxLines = null, + int? maxFiles = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) { - /// - /// Streams diff hunks from an async enumerable of diffs with line and file limits. - /// Enables early termination when MCP context window limits are reached. - /// - /// The async enumerable of diffs to process. - /// Maximum total lines to yield across all diffs. Null for unlimited. - /// Maximum number of files to process. Null for unlimited. - /// Cancellation token. - /// An async enumerable of diff results with truncation metadata. - public static async IAsyncEnumerable StreamDiffsWithLimitsAsync( - this IAsyncEnumerable diffs, - int? maxLines = null, - int? maxFiles = null, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - int totalLines = 0; - int totalFiles = 0; + int totalLines = 0; + int totalFiles = 0; - await foreach (var diff in diffs.WithCancellation(cancellationToken).ConfigureAwait(false)) + await foreach (var diff in diffs.WithCancellation(cancellationToken).ConfigureAwait(false)) + { + // Check file limit + if (maxFiles.HasValue && totalFiles >= maxFiles.Value) { - // Check file limit - if (maxFiles.HasValue && totalFiles >= maxFiles.Value) - { - yield return DiffStreamResult.CreateTruncated(totalLines, totalFiles, "max_files_reached"); - yield break; - } - - int diffLineCount = CountDiffLines(diff); - - // Check if this diff would exceed line limit - if (maxLines.HasValue && totalLines + diffLineCount > maxLines.Value) - { - // Calculate how many lines we can still include - int remainingLines = maxLines.Value - totalLines; - - if (remainingLines > 0) - { - // Truncate this diff and yield partial result - var truncatedDiff = TruncateDiff(diff, remainingLines); - yield return DiffStreamResult.CreatePartial(truncatedDiff, totalLines + remainingLines, totalFiles + 1); - } - - yield return DiffStreamResult.CreateTruncated(totalLines + remainingLines, totalFiles + 1, "max_lines_reached"); - yield break; - } - - totalLines += diffLineCount; - totalFiles++; - - yield return DiffStreamResult.Create(diff, totalLines, totalFiles); + yield return DiffStreamResult.CreateTruncated(totalLines, totalFiles, "max_files_reached"); + yield break; } - } - /// - /// Takes diffs up to specified line and file limits, returning pagination metadata. - /// - /// The async enumerable of diffs to process. - /// Maximum total lines. Null for unlimited. - /// Maximum number of files. Null for unlimited. - /// Cancellation token. - /// A result containing the diffs and truncation metadata. - public static async Task TakeDiffsWithLimitsAsync( - this IAsyncEnumerable diffs, - int? maxLines = null, - int? maxFiles = null, - CancellationToken cancellationToken = default) - { - var collectedDiffs = new List(); - int totalLines = 0; - int totalFiles = 0; - bool wasTruncated = false; - string? truncationReason = null; + int diffLineCount = CountDiffLines(diff); - await foreach (var diff in diffs.WithCancellation(cancellationToken).ConfigureAwait(false)) + // Check if this diff would exceed line limit + if (maxLines.HasValue && totalLines + diffLineCount > maxLines.Value) { - // Check file limit - if (maxFiles.HasValue && totalFiles >= maxFiles.Value) - { - wasTruncated = true; - truncationReason = "max_files_reached"; - break; - } + // Calculate how many lines we can still include + int remainingLines = maxLines.Value - totalLines; - int diffLineCount = CountDiffLines(diff); - - // Check if this diff would exceed line limit - if (maxLines.HasValue && totalLines + diffLineCount > maxLines.Value) + if (remainingLines > 0) { - int remainingLines = maxLines.Value - totalLines; - - if (remainingLines > 0) - { - var truncatedDiff = TruncateDiff(diff, remainingLines); - collectedDiffs.Add(truncatedDiff); - totalLines += remainingLines; - totalFiles++; - } - - wasTruncated = true; - truncationReason = "max_lines_reached"; - break; + // Truncate this diff and yield partial result + var truncatedDiff = TruncateDiff(diff, remainingLines); + yield return DiffStreamResult.CreatePartial(truncatedDiff, totalLines + remainingLines, totalFiles + 1); } - collectedDiffs.Add(diff); - totalLines += diffLineCount; - totalFiles++; + yield return DiffStreamResult.CreateTruncated(totalLines + remainingLines, totalFiles + 1, "max_lines_reached"); + yield break; } - return new DiffPaginatedResult( - collectedDiffs, - totalLines, - totalFiles, - wasTruncated, - truncationReason); - } + totalLines += diffLineCount; + totalFiles++; - /// - /// Counts the total number of lines in a diff. - /// - public static int CountDiffLines(Diff diff) - { - if (diff.Hunks == null) - return 0; - - return diff.Hunks.Sum(hunk => - hunk.Segments?.Sum(segment => segment.Lines?.Count ?? 0) ?? 0); + yield return DiffStreamResult.Create(diff, totalLines, totalFiles); } + } - private static Diff TruncateDiff(Diff original, int maxLines) + /// + /// Takes diffs up to specified line and file limits, returning pagination metadata. + /// + /// The async enumerable of diffs to process. + /// Maximum total lines. Null for unlimited. + /// Maximum number of files. Null for unlimited. + /// Cancellation token. + /// A result containing the diffs and truncation metadata. + public static async Task TakeDiffsWithLimitsAsync( + this IAsyncEnumerable diffs, + int? maxLines = null, + int? maxFiles = null, + CancellationToken cancellationToken = default) + { + var collectedDiffs = new List(); + int totalLines = 0; + int totalFiles = 0; + bool wasTruncated = false; + string? truncationReason = null; + + await foreach (var diff in diffs.WithCancellation(cancellationToken).ConfigureAwait(false)) { - if (original.Hunks == null || maxLines <= 0) + // Check file limit + if (maxFiles.HasValue && totalFiles >= maxFiles.Value) { - return new Diff - { - Source = original.Source, - Destination = original.Destination, - Hunks = [] - }; + wasTruncated = true; + truncationReason = "max_files_reached"; + break; } - var truncatedHunks = new List(); - int linesRemaining = maxLines; + int diffLineCount = CountDiffLines(diff); - foreach (var hunk in original.Hunks) + // Check if this diff would exceed line limit + if (maxLines.HasValue && totalLines + diffLineCount > maxLines.Value) { - if (linesRemaining <= 0) - break; + int remainingLines = maxLines.Value - totalLines; - var truncatedHunk = TruncateHunk(hunk, linesRemaining); - truncatedHunks.Add(truncatedHunk); + if (remainingLines > 0) + { + var truncatedDiff = TruncateDiff(diff, remainingLines); + collectedDiffs.Add(truncatedDiff); + totalLines += remainingLines; + totalFiles++; + } - int hunkLines = truncatedHunk.Segments?.Sum(s => s.Lines?.Count ?? 0) ?? 0; - linesRemaining -= hunkLines; + wasTruncated = true; + truncationReason = "max_lines_reached"; + break; } + collectedDiffs.Add(diff); + totalLines += diffLineCount; + totalFiles++; + } + + return new DiffPaginatedResult( + collectedDiffs, + totalLines, + totalFiles, + wasTruncated, + truncationReason); + } + + /// + /// Counts the total number of lines in a diff. + /// + public static int CountDiffLines(Diff diff) + { + if (diff.Hunks == null) + return 0; + + return diff.Hunks.Sum(hunk => + hunk.Segments?.Sum(segment => segment.Lines?.Count ?? 0) ?? 0); + } + + private static Diff TruncateDiff(Diff original, int maxLines) + { + if (original.Hunks == null || maxLines <= 0) + { return new Diff { Source = original.Source, Destination = original.Destination, - Hunks = truncatedHunks + Hunks = [], }; } - private static DiffHunk TruncateHunk(DiffHunk original, int maxLines) - { - if (original.Segments == null || maxLines <= 0) - { - return new DiffHunk - { - SourceLine = original.SourceLine, - SourceSpan = original.SourceSpan, - DestinationLine = original.DestinationLine, - DestinationSpan = original.DestinationSpan, - Segments = [], - Truncated = true - }; - } + var truncatedHunks = new List(); + int linesRemaining = maxLines; - var truncatedSegments = new List(); - int linesRemaining = maxLines; + foreach (var hunk in original.Hunks) + { + if (linesRemaining <= 0) + break; - foreach (var segment in original.Segments) - { - if (linesRemaining <= 0) - break; + var truncatedHunk = TruncateHunk(hunk, linesRemaining); + truncatedHunks.Add(truncatedHunk); - var truncatedSegment = TruncateSegment(segment, linesRemaining); - truncatedSegments.Add(truncatedSegment); + int hunkLines = truncatedHunk.Segments?.Sum(s => s.Lines?.Count ?? 0) ?? 0; + linesRemaining -= hunkLines; + } - int segmentLines = truncatedSegment.Lines?.Count ?? 0; - linesRemaining -= segmentLines; - } + return new Diff + { + Source = original.Source, + Destination = original.Destination, + Hunks = truncatedHunks, + }; + } + private static DiffHunk TruncateHunk(DiffHunk original, int maxLines) + { + if (original.Segments == null || maxLines <= 0) + { return new DiffHunk { SourceLine = original.SourceLine, SourceSpan = original.SourceSpan, DestinationLine = original.DestinationLine, DestinationSpan = original.DestinationSpan, - Segments = truncatedSegments, - Truncated = linesRemaining <= 0 || original.Truncated + Segments = [], + Truncated = true, }; } - private static Segment TruncateSegment(Segment original, int maxLines) + var truncatedSegments = new List(); + int linesRemaining = maxLines; + + foreach (var segment in original.Segments) { - if (original.Lines == null || maxLines <= 0) - { - return new Segment - { - Type = original.Type, - Lines = [], - Truncated = true - }; - } + if (linesRemaining <= 0) + break; + + var truncatedSegment = TruncateSegment(segment, linesRemaining); + truncatedSegments.Add(truncatedSegment); - int linesToTake = Math.Min(original.Lines.Count, maxLines); - bool needsTruncation = linesToTake < original.Lines.Count; + int segmentLines = truncatedSegment.Lines?.Count ?? 0; + linesRemaining -= segmentLines; + } + + return new DiffHunk + { + SourceLine = original.SourceLine, + SourceSpan = original.SourceSpan, + DestinationLine = original.DestinationLine, + DestinationSpan = original.DestinationSpan, + Segments = truncatedSegments, + Truncated = linesRemaining <= 0 || original.Truncated, + }; + } + private static Segment TruncateSegment(Segment original, int maxLines) + { + if (original.Lines == null || maxLines <= 0) + { return new Segment { Type = original.Type, - Lines = original.Lines.Take(linesToTake).ToList(), - Truncated = needsTruncation || original.Truncated + Lines = [], + Truncated = true, }; } + + int linesToTake = Math.Min(original.Lines.Count, maxLines); + bool needsTruncation = linesToTake < original.Lines.Count; + + return new Segment + { + Type = original.Type, + Lines = [.. original.Lines.Take(linesToTake)], + Truncated = needsTruncation || original.Truncated, + }; } +} +/// +/// Result of streaming a single diff with metadata. +/// +public sealed class DiffStreamResult +{ /// - /// Result of streaming a single diff with metadata. + /// The diff content. Null if this is a truncation marker. /// - public sealed class DiffStreamResult - { - /// - /// The diff content. Null if this is a truncation marker. - /// - public Diff? Diff { get; } - - /// - /// Total lines yielded so far (including this diff). - /// - public int TotalLines { get; } - - /// - /// Total files yielded so far (including this diff). - /// - public int TotalFiles { get; } - - /// - /// True if this diff was partially truncated. - /// - public bool IsPartial { get; } - - /// - /// True if streaming was truncated after this result. - /// - public bool IsTruncated { get; } - - /// - /// Reason for truncation, if applicable. - /// - public string? TruncationReason { get; } - - private DiffStreamResult(Diff? diff, int totalLines, int totalFiles, bool isPartial, bool isTruncated, string? truncationReason) - { - Diff = diff; - TotalLines = totalLines; - TotalFiles = totalFiles; - IsPartial = isPartial; - IsTruncated = isTruncated; - TruncationReason = truncationReason; - } + public Diff? Diff { get; } - internal static DiffStreamResult Create(Diff diff, int totalLines, int totalFiles) - => new(diff, totalLines, totalFiles, isPartial: false, isTruncated: false, truncationReason: null); + /// + /// Total lines yielded so far (including this diff). + /// + public int TotalLines { get; } - internal static DiffStreamResult CreatePartial(Diff diff, int totalLines, int totalFiles) - => new(diff, totalLines, totalFiles, isPartial: true, isTruncated: false, truncationReason: null); + /// + /// Total files yielded so far (including this diff). + /// + public int TotalFiles { get; } - internal static DiffStreamResult CreateTruncated(int totalLines, int totalFiles, string reason) - => new(null, totalLines, totalFiles, isPartial: false, isTruncated: true, truncationReason: reason); - } + /// + /// True if this diff was partially truncated. + /// + public bool IsPartial { get; } /// - /// Result of taking diffs with limits, including truncation metadata. - /// This class is designed to be thread-safe for read operations. + /// True if streaming was truncated after this result. /// - public sealed class DiffPaginatedResult - { - private readonly List _diffs; - - /// - /// The collected diffs (may be truncated). Read-only view. - /// - public IReadOnlyList Diffs => _diffs; - - /// - /// Total lines in the result. - /// - public int TotalLines { get; } - - /// - /// Total files in the result. - /// - public int TotalFiles { get; } - - /// - /// True if the result was truncated due to limits. - /// - public bool WasTruncated { get; } - - /// - /// Reason for truncation, if applicable. Values: "max_lines_reached", "max_files_reached". - /// - public string? TruncationReason { get; } - - /// - /// Per MCP best practices, indicates if more results exist. - /// - public bool HasMore => WasTruncated; - - public DiffPaginatedResult(List diffs, int totalLines, int totalFiles, bool wasTruncated, string? truncationReason) - { - _diffs = diffs; - TotalLines = totalLines; - TotalFiles = totalFiles; - WasTruncated = wasTruncated; - TruncationReason = truncationReason; - } + public bool IsTruncated { get; } - /// - /// Deconstructs the result for tuple-style usage. - /// - public void Deconstruct(out IReadOnlyList diffs, out bool hasMore, out int totalLines, out int totalFiles) - { - diffs = Diffs; - hasMore = HasMore; - totalLines = TotalLines; - totalFiles = TotalFiles; - } + /// + /// Reason for truncation, if applicable. + /// + public string? TruncationReason { get; } + + private DiffStreamResult(Diff? diff, int totalLines, int totalFiles, bool isPartial, bool isTruncated, string? truncationReason) + { + Diff = diff; + TotalLines = totalLines; + TotalFiles = totalFiles; + IsPartial = isPartial; + IsTruncated = isTruncated; + TruncationReason = truncationReason; } + + internal static DiffStreamResult Create(Diff diff, int totalLines, int totalFiles) + => new(diff, totalLines, totalFiles, isPartial: false, isTruncated: false, truncationReason: null); + + internal static DiffStreamResult CreatePartial(Diff diff, int totalLines, int totalFiles) + => new(diff, totalLines, totalFiles, isPartial: true, isTruncated: false, truncationReason: null); + + internal static DiffStreamResult CreateTruncated(int totalLines, int totalFiles, string reason) + => new(diff: null, totalLines, totalFiles, isPartial: false, isTruncated: true, truncationReason: reason); } + +/// +/// Result of taking diffs with limits, including truncation metadata. +/// This class is designed to be thread-safe for read operations. +/// +public sealed class DiffPaginatedResult(List diffs, int totalLines, int totalFiles, bool wasTruncated, string? truncationReason) +{ + private readonly List _diffs = diffs; + + /// + /// The collected diffs (may be truncated). Read-only view. + /// + public IReadOnlyList Diffs => _diffs; + + /// + /// Total lines in the result. + /// + public int TotalLines { get; } = totalLines; + + /// + /// Total files in the result. + /// + public int TotalFiles { get; } = totalFiles; + + /// + /// True if the result was truncated due to limits. + /// + public bool WasTruncated { get; } = wasTruncated; + + /// + /// Reason for truncation, if applicable. Values: "max_lines_reached", "max_files_reached". + /// + public string? TruncationReason { get; } = truncationReason; + + /// + /// Per MCP best practices, indicates if more results exist. + /// + public bool HasMore => WasTruncated; + + /// + /// Deconstructs the result for tuple-style usage. + /// + public void Deconstruct(out IReadOnlyList diffs, out bool hasMore, out int totalLines, out int totalFiles) + { + diffs = Diffs; + hasMore = HasMore; + totalLines = TotalLines; + totalFiles = TotalFiles; + } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Mcp/McpExtensions.cs b/src/Bitbucket.Net/Common/Mcp/McpExtensions.cs index beec206..0aa9ad3 100644 --- a/src/Bitbucket.Net/Common/Mcp/McpExtensions.cs +++ b/src/Bitbucket.Net/Common/Mcp/McpExtensions.cs @@ -1,229 +1,218 @@ -using System.Collections.Generic; using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; -namespace Bitbucket.Net.Common.Mcp +namespace Bitbucket.Net.Common.Mcp; + +/// +/// MCP-optimized extension methods for common truncation and pagination patterns. +/// Designed for Model Context Protocol (MCP) server integration where context window +/// limits require intelligent truncation of large result sets. +/// +public static class McpExtensions { /// - /// MCP-optimized extension methods for common truncation and pagination patterns. - /// Designed for Model Context Protocol (MCP) server integration where context window - /// limits require intelligent truncation of large result sets. + /// Takes the first N items from an async enumerable with pagination metadata preserved. + /// This method is optimized for MCP servers that need to truncate large result sets + /// while maintaining pagination information for follow-up requests. /// - public static class McpExtensions + /// The type of items in the sequence. + /// The async enumerable source. + /// Maximum number of items to return. + /// Cancellation token. + /// + /// A PaginatedResult containing: + /// - Items: The first N items from the source + /// - HasMore: True if there are more items beyond the limit + /// - NextOffset: The offset for the next page (equal to limit if HasMore is true) + /// + /// + /// Per MCP best practices, pagination responses should include has_more, next_offset, and total_count. + /// This method fetches limit+1 items to determine if more exist without fetching the entire collection. + /// + /// Usage with streaming APIs: + /// + /// var result = await client.GetPullRequestsStreamAsync(projectKey, repoSlug) + /// .TakeWithPaginationAsync(limit: 25); + /// + /// // Return to MCP client + /// return new { + /// items = result.Items, + /// has_more = result.HasMore, + /// next_offset = result.NextOffset + /// }; + /// + /// + public static async Task> TakeWithPaginationAsync( + this IAsyncEnumerable source, + int limit, + CancellationToken cancellationToken = default) { - /// - /// Takes the first N items from an async enumerable with pagination metadata preserved. - /// This method is optimized for MCP servers that need to truncate large result sets - /// while maintaining pagination information for follow-up requests. - /// - /// The type of items in the sequence. - /// The async enumerable source. - /// Maximum number of items to return. - /// Cancellation token. - /// - /// A PaginatedResult containing: - /// - Items: The first N items from the source - /// - HasMore: True if there are more items beyond the limit - /// - NextOffset: The offset for the next page (equal to limit if HasMore is true) - /// - /// - /// Per MCP best practices, pagination responses should include has_more, next_offset, and total_count. - /// This method fetches limit+1 items to determine if more exist without fetching the entire collection. - /// - /// Usage with streaming APIs: - /// - /// var result = await client.GetPullRequestsStreamAsync(projectKey, repoSlug) - /// .TakeWithPaginationAsync(limit: 25); - /// - /// // Return to MCP client - /// return new { - /// items = result.Items, - /// has_more = result.HasMore, - /// next_offset = result.NextOffset - /// }; - /// - /// - public static async Task> TakeWithPaginationAsync( - this IAsyncEnumerable source, - int limit, - CancellationToken cancellationToken = default) - { - var items = new List(limit); - int count = 0; + var items = new List(limit); + int count = 0; - await foreach (var item in source.WithCancellation(cancellationToken).ConfigureAwait(false)) + await foreach (var item in source.WithCancellation(cancellationToken).ConfigureAwait(false)) + { + if (count < limit) { - if (count < limit) - { - items.Add(item); - } - - count++; - - // Found one more than requested - we know there are more items - if (count > limit) - { - return new PaginatedResult(items, hasMore: true, nextOffset: limit); - } + items.Add(item); } - return new PaginatedResult(items, hasMore: false, nextOffset: null); - } + count++; - /// - /// Streams items with a hard limit, stopping enumeration after the limit is reached. - /// More memory-efficient than TakeWithPaginationAsync when you don't need HasMore metadata. - /// - /// The type of items in the sequence. - /// The async enumerable source. - /// Maximum number of items to yield. - /// Cancellation token. - /// An async enumerable that yields at most limit items. - /// - /// This is the most efficient option when you only need to limit results without - /// knowing if more exist. The enumeration stops immediately after yielding - /// the limit-th item. - /// - /// Usage: - /// - /// await foreach (var pr in client.GetPullRequestsStreamAsync(projectKey, repoSlug) - /// .TakeAsync(10)) - /// { - /// // Process at most 10 PRs - /// } - /// - /// - public static async IAsyncEnumerable TakeAsync( - this IAsyncEnumerable source, - int limit, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - int count = 0; - await foreach (var item in source.WithCancellation(cancellationToken).ConfigureAwait(false)) + // Found one more than requested - we know there are more items + if (count > limit) { - if (count >= limit) - { - yield break; - } - - yield return item; - count++; + return new PaginatedResult(items, hasMore: true, nextOffset: limit); } } - /// - /// Skips the first N items and then yields the remaining items. - /// Useful for implementing offset-based pagination on top of streaming APIs. - /// - /// The type of items in the sequence. - /// The async enumerable source. - /// Number of items to skip. - /// Cancellation token. - /// An async enumerable that skips the first count items. - public static async IAsyncEnumerable SkipAsync( - this IAsyncEnumerable source, - int count, - [EnumeratorCancellation] CancellationToken cancellationToken = default) + return new PaginatedResult(items, hasMore: false, nextOffset: null); + } + + /// + /// Streams items with a hard limit, stopping enumeration after the limit is reached. + /// More memory-efficient than TakeWithPaginationAsync when you don't need HasMore metadata. + /// + /// The type of items in the sequence. + /// The async enumerable source. + /// Maximum number of items to yield. + /// Cancellation token. + /// An async enumerable that yields at most limit items. + /// + /// This is the most efficient option when you only need to limit results without + /// knowing if more exist. The enumeration stops immediately after yielding + /// the limit-th item. + /// + /// Usage: + /// + /// await foreach (var pr in client.GetPullRequestsStreamAsync(projectKey, repoSlug) + /// .TakeAsync(10)) + /// { + /// // Process at most 10 PRs + /// } + /// + /// + public static async IAsyncEnumerable TakeAsync( + this IAsyncEnumerable source, + int limit, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + int count = 0; + await foreach (var item in source.WithCancellation(cancellationToken).ConfigureAwait(false)) { - int skipped = 0; - await foreach (var item in source.WithCancellation(cancellationToken).ConfigureAwait(false)) + if (count >= limit) { - if (skipped < count) - { - skipped++; - continue; - } - - yield return item; + yield break; } - } - /// - /// Implements offset/limit pagination on top of a streaming source. - /// Combines Skip and Take for traditional pagination patterns. - /// - /// The type of items in the sequence. - /// The async enumerable source. - /// Number of items to skip (0-based offset). - /// Maximum number of items to return. - /// Cancellation token. - /// - /// A PaginatedResult with items from offset to offset+limit-1. - /// Note: NextOffset in the result is relative to the current window (equals limit when HasMore is true). - /// To calculate the absolute offset for the next page, use: offset + result.NextOffset. - /// - /// - /// This is useful when an MCP client requests a specific page: - /// - /// // Client requests page 3 with 25 items per page - /// var result = await client.GetCommitsStreamAsync(projectKey, repoSlug) - /// .PageAsync(offset: 50, limit: 25); - /// - /// // To get next page offset: - /// int nextOffset = result.HasMore ? 50 + result.NextOffset.Value : -1; - /// - /// - public static async Task> PageAsync( - this IAsyncEnumerable source, - int offset, - int limit, - CancellationToken cancellationToken = default) - { - return await source - .SkipAsync(offset, cancellationToken) - .TakeWithPaginationAsync(limit, cancellationToken) - .ConfigureAwait(false); + yield return item; + count++; } } /// - /// Result of a paginated query with MCP-friendly metadata. - /// This class is designed to be thread-safe for read operations. + /// Skips the first N items and then yields the remaining items. + /// Useful for implementing offset-based pagination on top of streaming APIs. /// - /// The type of items in the result. - public sealed class PaginatedResult + /// The type of items in the sequence. + /// The async enumerable source. + /// Number of items to skip. + /// Cancellation token. + /// An async enumerable that skips the first count items. + public static async IAsyncEnumerable SkipAsync( + this IAsyncEnumerable source, + int count, + [EnumeratorCancellation] CancellationToken cancellationToken = default) { - private readonly List _items; - - /// - /// The items in the current page (read-only view). - /// - public IReadOnlyList Items => _items; - - /// - /// Indicates if more results are available beyond this page. - /// Per MCP best practices: pagination responses should include has_more. - /// - public bool HasMore { get; } - - /// - /// The offset for retrieving the next page of results. - /// Null if there are no more results. - /// Per MCP best practices: pagination responses should include next_offset. - /// - public int? NextOffset { get; } - - /// - /// The number of items in the current result set. - /// - public int Count => _items.Count; - - public PaginatedResult(List items, bool hasMore, int? nextOffset) + int skipped = 0; + await foreach (var item in source.WithCancellation(cancellationToken).ConfigureAwait(false)) { - _items = items; - HasMore = hasMore; - NextOffset = nextOffset; - } + if (skipped < count) + { + skipped++; + continue; + } - /// - /// Deconstructs the result for tuple-style usage. - /// - public void Deconstruct(out IReadOnlyList items, out bool hasMore, out int? nextOffset) - { - items = Items; - hasMore = HasMore; - nextOffset = NextOffset; + yield return item; } } + + /// + /// Implements offset/limit pagination on top of a streaming source. + /// Combines Skip and Take for traditional pagination patterns. + /// + /// The type of items in the sequence. + /// The async enumerable source. + /// Number of items to skip (0-based offset). + /// Maximum number of items to return. + /// Cancellation token. + /// + /// A PaginatedResult with items from offset to offset+limit-1. + /// Note: NextOffset in the result is relative to the current window (equals limit when HasMore is true). + /// To calculate the absolute offset for the next page, use: offset + result.NextOffset. + /// + /// + /// This is useful when an MCP client requests a specific page: + /// + /// // Client requests page 3 with 25 items per page + /// var result = await client.GetCommitsStreamAsync(projectKey, repoSlug) + /// .PageAsync(offset: 50, limit: 25); + /// + /// // To get next page offset: + /// int nextOffset = result.HasMore ? 50 + result.NextOffset.Value : -1; + /// + /// + public static async Task> PageAsync( + this IAsyncEnumerable source, + int offset, + int limit, + CancellationToken cancellationToken = default) + { + return await source + .SkipAsync(offset, cancellationToken) + .TakeWithPaginationAsync(limit, cancellationToken) + .ConfigureAwait(false); + } } + +/// +/// Result of a paginated query with MCP-friendly metadata. +/// This class is designed to be thread-safe for read operations. +/// +/// The type of items in the result. +public sealed class PaginatedResult(List items, bool hasMore, int? nextOffset) +{ + private readonly List _items = items; + + /// + /// The items in the current page (read-only view). + /// + public IReadOnlyList Items => _items; + + /// + /// Indicates if more results are available beyond this page. + /// Per MCP best practices: pagination responses should include has_more. + /// + public bool HasMore { get; } = hasMore; + + /// + /// The offset for retrieving the next page of results. + /// Null if there are no more results. + /// Per MCP best practices: pagination responses should include next_offset. + /// + public int? NextOffset { get; } = nextOffset; + + /// + /// The number of items in the current result set. + /// + public int Count => _items.Count; + + /// + /// Deconstructs the result for tuple-style usage. + /// + public void Deconstruct(out IReadOnlyList items, out bool hasMore, out int? nextOffset) + { + items = Items; + hasMore = HasMore; + nextOffset = NextOffset; + } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Models/Error.cs b/src/Bitbucket.Net/Common/Models/Error.cs index b620ca7..f0c4621 100644 --- a/src/Bitbucket.Net/Common/Models/Error.cs +++ b/src/Bitbucket.Net/Common/Models/Error.cs @@ -1,23 +1,22 @@ -namespace Bitbucket.Net.Common.Models +namespace Bitbucket.Net.Common.Models; + +/// +/// Represents an error returned by the Bitbucket Server API. +/// +public class Error { /// - /// Represents an error returned by the Bitbucket Server API. + /// Gets or sets the context of the error (e.g., the field or resource that caused the error). /// - public class Error - { - /// - /// Gets or sets the context of the error (e.g., the field or resource that caused the error). - /// - public string? Context { get; set; } + public string? Context { get; set; } - /// - /// Gets or sets the error message. - /// - public string Message { get; set; } = string.Empty; + /// + /// Gets or sets the error message. + /// + public string Message { get; set; } = string.Empty; - /// - /// Gets or sets the name of the exception that occurred on the server, if available. - /// - public string? ExceptionName { get; set; } - } -} + /// + /// Gets or sets the name of the exception that occurred on the server, if available. + /// + public string? ExceptionName { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Models/ErrorResponse.cs b/src/Bitbucket.Net/Common/Models/ErrorResponse.cs index 059a706..19b4117 100644 --- a/src/Bitbucket.Net/Common/Models/ErrorResponse.cs +++ b/src/Bitbucket.Net/Common/Models/ErrorResponse.cs @@ -1,15 +1,12 @@ -using System.Collections.Generic; +namespace Bitbucket.Net.Common.Models; -namespace Bitbucket.Net.Common.Models +/// +/// Represents the error response returned by the Bitbucket Server API. +/// +public class ErrorResponse { /// - /// Represents the error response returned by the Bitbucket Server API. + /// Gets or sets the collection of errors returned by the server. /// - public class ErrorResponse - { - /// - /// Gets or sets the collection of errors returned by the server. - /// - public IEnumerable? Errors { get; set; } - } -} + public IEnumerable? Errors { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Models/PagedResults.cs b/src/Bitbucket.Net/Common/Models/PagedResults.cs index 0cc7955..75e00bf 100644 --- a/src/Bitbucket.Net/Common/Models/PagedResults.cs +++ b/src/Bitbucket.Net/Common/Models/PagedResults.cs @@ -1,23 +1,34 @@ -using System.Collections.Generic; +namespace Bitbucket.Net.Common.Models; -namespace Bitbucket.Net.Common.Models +/// +/// Represents a Bitbucket paged response containing a collection of items. +/// +public class PagedResults : PagedResultsBase { - public class PagedResults : PagedResultsBase - { - public int Limit { get; set; } - public List Values { get; set; } = []; - public int? NextPageStart { get; set; } + /// + /// Gets or sets the maximum number of items returned in this page. + /// + public int Limit { get; set; } - /// - /// MCP-friendly property indicating if more results are available. - /// Per MCP best practices, pagination responses should include has_more. - /// - public bool HasMore => !IsLastPage; + /// + /// Gets or sets the page of items returned by the request. + /// + public List Values { get; set; } = []; - /// - /// MCP-friendly property for the current offset position. - /// Per MCP best practices, pagination responses should include current offset. - /// - public int CurrentOffset => Start; - } -} + /// + /// Gets or sets the starting offset of the next page, when available. + /// + public int? NextPageStart { get; set; } + + /// + /// MCP-friendly property indicating if more results are available. + /// Per MCP best practices, pagination responses should include has_more. + /// + public bool HasMore => !IsLastPage; + + /// + /// MCP-friendly property for the current offset position. + /// Per MCP best practices, pagination responses should include current offset. + /// + public int CurrentOffset => Start; +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/Models/PagedResultsBase.cs b/src/Bitbucket.Net/Common/Models/PagedResultsBase.cs index a59118a..e66322a 100644 --- a/src/Bitbucket.Net/Common/Models/PagedResultsBase.cs +++ b/src/Bitbucket.Net/Common/Models/PagedResultsBase.cs @@ -1,9 +1,8 @@ -namespace Bitbucket.Net.Common.Models +namespace Bitbucket.Net.Common.Models; + +public abstract class PagedResultsBase { - public abstract class PagedResultsBase - { - public int Size { get; set; } - public bool IsLastPage { get; set; } - public int Start { get; set; } - } + public int Size { get; set; } + public bool IsLastPage { get; set; } + public int Start { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/TypeExtensions.cs b/src/Bitbucket.Net/Common/TypeExtensions.cs index 030b0dd..9dadc55 100644 --- a/src/Bitbucket.Net/Common/TypeExtensions.cs +++ b/src/Bitbucket.Net/Common/TypeExtensions.cs @@ -1,10 +1,16 @@ -using System; -using System.Reflection; +using System.Reflection; -namespace Bitbucket.Net.Common +namespace Bitbucket.Net.Common; + +/// +/// Provides reflection-based helpers for working with instances. +/// +public static class TypeExtensions { - public static class TypeExtensions - { - public static bool IsNullableType(Type type) => type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); - } -} + /// + /// Determines whether the specified is a . + /// + /// The type to inspect. + /// when the type is a nullable value type; otherwise . + public static bool IsNullableType(Type type) => type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Common/UnixDateTimeExtensions.cs b/src/Bitbucket.Net/Common/UnixDateTimeExtensions.cs index 392927d..b69a343 100644 --- a/src/Bitbucket.Net/Common/UnixDateTimeExtensions.cs +++ b/src/Bitbucket.Net/Common/UnixDateTimeExtensions.cs @@ -1,19 +1,28 @@ -using System; +namespace Bitbucket.Net.Common; -namespace Bitbucket.Net.Common +/// +/// Extension methods for converting between Unix epoch milliseconds and . +/// Bitbucket Server represents all timestamps as milliseconds since the Unix epoch (1970-01-01T00:00:00Z). +/// +public static class UnixDateTimeExtensions { - public static class UnixDateTimeExtensions + /// + /// Converts a Unix epoch millisecond timestamp to a UTC . + /// + /// The number of milliseconds since 1970-01-01T00:00:00Z. + /// A in UTC representing the given timestamp. + public static DateTimeOffset FromUnixTimeMilliseconds(this long value) { - public static DateTimeOffset FromUnixTimeSeconds(this long value) - { - return new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, TimeSpan.Zero) - .AddMilliseconds(value) - .ToLocalTime(); - } + return DateTimeOffset.FromUnixTimeMilliseconds(value); + } - public static long ToUnixTimeSeconds(this DateTimeOffset dateTimeOffset) - { - return dateTimeOffset.Subtract(new DateTimeOffset(1970, 1, 1, 0, 0, 0, 0, TimeSpan.Zero)).Ticks; - } + /// + /// Converts a to Unix epoch milliseconds. + /// + /// The date and time to convert. + /// The number of milliseconds since 1970-01-01T00:00:00Z. + public static long ToUnixTimeMilliseconds(this DateTimeOffset dateTimeOffset) + { + return dateTimeOffset.ToUnixTimeMilliseconds(); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Admin/BitbucketClient.cs b/src/Bitbucket.Net/Core/Admin/BitbucketClient.cs index 013d054..58a26c7 100644 --- a/src/Bitbucket.Net/Core/Admin/BitbucketClient.cs +++ b/src/Bitbucket.Net/Core/Admin/BitbucketClient.cs @@ -1,7 +1,3 @@ -using System.Collections.Generic; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; using Bitbucket.Net.Common; using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Core.Admin; @@ -9,510 +5,830 @@ using Flurl.Http; using PasswordChange = Bitbucket.Net.Models.Core.Admin.PasswordChange; -namespace Bitbucket.Net +namespace Bitbucket.Net; + +/// +/// Provides administrative operations for Bitbucket Server, including user, group, permissions, license, and mail server management. +/// +public partial class BitbucketClient { - public partial class BitbucketClient + /// + /// Gets the base admin URL for Bitbucket Server operations. + /// + /// An targeting the admin endpoint. + private IFlurlRequest GetAdminUrl() => GetBaseUrl() + .AppendPathSegment("/admin"); + + /// + /// Gets the admin URL for a specific path. + /// + /// The path to append. + /// An targeting the admin path. + private IFlurlRequest GetAdminUrl(string path) => GetAdminUrl() + .AppendPathSegment(path); + + /// + /// Retrieves groups with optional filtering. + /// + /// Optional filter string. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Cancellation token. + /// A collection of groups. + public async Task> GetAdminGroupsAsync(string? filter = null, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) { - private IFlurlRequest GetAdminUrl() => GetBaseUrl() - .AppendPathSegment("/admin"); - - private IFlurlRequest GetAdminUrl(string path) => GetAdminUrl() - .AppendPathSegment(path); - - public async Task> GetAdminGroupsAsync(string? filter = null, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) + var queryParamValues = new Dictionary(StringComparer.Ordinal) { - var queryParamValues = new Dictionary + ["limit"] = limit, + ["start"] = start, + ["filter"] = filter, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => { - ["limit"] = limit, - ["start"] = start, - ["filter"] = filter - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetAdminUrl("/groups") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task CreateAdminGroupAsync(string name, CancellationToken cancellationToken = default) - { - var response = await GetAdminUrl("/groups") - .SetQueryParam("name", name) - .PostJsonAsync(new StringContent(""), cancellationToken: cancellationToken) - .ConfigureAwait(false); + var response = await GetAdminUrl("/groups") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } + /// + /// Creates a group. + /// + /// The group name. + /// Cancellation token. + /// The created group. + public async Task CreateAdminGroupAsync(string name, CancellationToken cancellationToken = default) + { + var response = await GetAdminUrl("/groups") + .SetQueryParam("name", name) + .SendAsync(HttpMethod.Post, new StringContent(string.Empty), cancellationToken: cancellationToken) + .ConfigureAwait(false); - public async Task DeleteAdminGroupAsync(string name, CancellationToken cancellationToken = default) - { - var response = await GetAdminUrl("/groups") - .SetQueryParam("name", name) - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } + /// + /// Deletes a group. + /// + /// The group name. + /// Cancellation token. + /// The deleted group info. + public async Task DeleteAdminGroupAsync(string name, CancellationToken cancellationToken = default) + { + var response = await GetAdminUrl("/groups") + .SetQueryParam("name", name) + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); - public async Task AddAdminGroupUsersAsync(GroupUsers groupUsers, CancellationToken cancellationToken = default) - { - var response = await GetAdminUrl("/groups/add-users") - .PostJsonAsync(groupUsers, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - public async Task> GetAdminGroupMoreMembersAsync(string context, string? filter = null, - int? maxPages = null, - int? limit = null, - int? start = null, - int? avatarSize = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["context"] = context, - ["filter"] = filter, - ["avatarSize"] = avatarSize - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetAdminUrl("/groups/more-members") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task> GetAdminGroupMoreNonMembersAsync(string context, string? filter = null, - int? maxPages = null, - int? limit = null, - int? start = null, - int? avatarSize = null, - CancellationToken cancellationToken = default) + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Adds users to a group. + /// + /// The group and user payload. + /// Cancellation token. + /// true if users were added; otherwise, false. + public async Task AddAdminGroupUsersAsync(GroupUsers groupUsers, CancellationToken cancellationToken = default) + { + var response = await GetAdminUrl("/groups/add-users") + .SendAsync(HttpMethod.Post, CreateJsonContent(groupUsers), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves members of a group beyond the initial page. + /// + /// The group context. + /// Optional filter string. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Optional avatar size. + /// Cancellation token. + /// A collection of group members. + public async Task> GetAdminGroupMoreMembersAsync(string context, string? filter = null, + int? maxPages = null, + int? limit = null, + int? start = null, + int? avatarSize = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) { - var queryParamValues = new Dictionary + ["limit"] = limit, + ["start"] = start, + ["context"] = context, + ["filter"] = filter, + ["avatarSize"] = avatarSize, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => { - ["limit"] = limit, - ["start"] = start, - ["context"] = context, - ["filter"] = filter, - ["avatarSize"] = avatarSize - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetAdminUrl("/groups/more-non-members") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task> GetAdminUsersAsync(string? filter = null, - int? maxPages = null, - int? limit = null, - int? start = null, - int? avatarSize = null, - CancellationToken cancellationToken = default) + var response = await GetAdminUrl("/groups/more-members") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Retrieves non-members for a group beyond the initial page. + /// + /// The group context. + /// Optional filter string. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Optional avatar size. + /// Cancellation token. + /// A collection of non-members. + public async Task> GetAdminGroupMoreNonMembersAsync(string context, string? filter = null, + int? maxPages = null, + int? limit = null, + int? start = null, + int? avatarSize = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) { - var queryParamValues = new Dictionary + ["limit"] = limit, + ["start"] = start, + ["context"] = context, + ["filter"] = filter, + ["avatarSize"] = avatarSize, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => { - ["limit"] = limit, - ["start"] = start, - ["filter"] = filter, - ["avatarSize"] = avatarSize - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetAdminUrl("/users") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task CreateAdminUserAsync(string name, string password, string displayName, string emailAddress, - bool addToDefaultGroup = true, string notify = "false", CancellationToken cancellationToken = default) + var response = await GetAdminUrl("/groups/more-non-members") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Retrieves users with optional filtering. + /// + /// Optional filter string. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Optional avatar size. + /// Cancellation token. + /// A collection of users. + public async Task> GetAdminUsersAsync(string? filter = null, + int? maxPages = null, + int? limit = null, + int? start = null, + int? avatarSize = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) { - var queryParamValues = new Dictionary + ["limit"] = limit, + ["start"] = start, + ["filter"] = filter, + ["avatarSize"] = avatarSize, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => { - ["name"] = name, - ["password"] = password, - ["displayName"] = displayName, - ["emailAddress"] = emailAddress, - ["addToDefaultGroup"] = BitbucketHelpers.BoolToString(addToDefaultGroup), - ["notify"] = notify - }; - - var response = await GetAdminUrl("/users") - .SetQueryParams(queryParamValues) - .PostJsonAsync(new StringContent(""), cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - public async Task UpdateAdminUserAsync(string? name = null, string? displayName = null, string? emailAddress = null, CancellationToken cancellationToken = default) + var response = await GetAdminUrl("/users") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Creates a user. + /// + /// The username. + /// The password. + /// The display name. + /// The email address. + /// Whether to add to the default group. + /// Whether to notify the user. + /// Cancellation token. + /// true if creation succeeded; otherwise, false. + public async Task CreateAdminUserAsync(string name, string password, string displayName, string emailAddress, + bool addToDefaultGroup = true, string notify = "false", CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["name"] = name, + ["password"] = password, + ["displayName"] = displayName, + ["emailAddress"] = emailAddress, + ["addToDefaultGroup"] = BitbucketHelpers.BoolToString(addToDefaultGroup), + ["notify"] = notify, + }; + + var response = await GetAdminUrl("/users") + .SetQueryParams(queryParamValues) + .SendAsync(HttpMethod.Post, new StringContent(string.Empty), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Updates user details. + /// + /// Optional username to update. + /// Optional display name. + /// Optional email address. + /// Cancellation token. + /// The updated user. + public async Task UpdateAdminUserAsync(string? name = null, string? displayName = null, string? emailAddress = null, CancellationToken cancellationToken = default) + { + var data = new { - var data = new - { - name, - displayName, - email = emailAddress - }; + name, + displayName, + email = emailAddress, + }; - var response = await GetAdminUrl("/users") - .PutJsonAsync(data, cancellationToken: cancellationToken) - .ConfigureAwait(false); + var response = await GetAdminUrl("/users") + .SendAsync(HttpMethod.Put, CreateJsonContent(data), cancellationToken: cancellationToken) + .ConfigureAwait(false); - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } - public async Task DeleteAdminUserAsync(string name, CancellationToken cancellationToken = default) - { - var response = await GetAdminUrl("/users") - .SetQueryParam("name", name) - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); + /// + /// Deletes a user. + /// + /// The username. + /// Cancellation token. + /// The deleted user info. + public async Task DeleteAdminUserAsync(string name, CancellationToken cancellationToken = default) + { + var response = await GetAdminUrl("/users") + .SetQueryParam("name", name) + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } - public async Task AddAdminUserGroupsAsync(UserGroups userGroups, CancellationToken cancellationToken = default) - { - var response = await GetAdminUrl("/users/add-groups") - .PostJsonAsync(userGroups, cancellationToken: cancellationToken) - .ConfigureAwait(false); + /// + /// Adds groups to a user. + /// + /// The user groups payload. + /// Cancellation token. + /// true if groups were added; otherwise, false. + public async Task AddAdminUserGroupsAsync(UserGroups userGroups, CancellationToken cancellationToken = default) + { + var response = await GetAdminUrl("/users/add-groups") + .SendAsync(HttpMethod.Post, CreateJsonContent(userGroups), cancellationToken: cancellationToken) + .ConfigureAwait(false); - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } - public async Task DeleteAdminUserCaptcha(string name, CancellationToken cancellationToken = default) - { - var response = await GetAdminUrl("/users/captcha") - .SetQueryParam("name", name) - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); + /// + /// Deletes captcha for a user. + /// + /// The username. + /// Cancellation token. + /// true if deletion succeeded; otherwise, false. + public async Task DeleteAdminUserCaptcha(string name, CancellationToken cancellationToken = default) + { + var response = await GetAdminUrl("/users/captcha") + .SetQueryParam("name", name) + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } - public async Task UpdateAdminUserCredentialsAsync(PasswordChange passwordChange, CancellationToken cancellationToken = default) - { - var response = await GetAdminUrl("/users/credentials") - .PutJsonAsync(passwordChange, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - public async Task> GetAdminUserMoreMembersAsync(string context, string? filter = null, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) + /// + /// Updates user credentials. + /// + /// The password change payload. + /// Cancellation token. + /// true if update succeeded; otherwise, false. + public async Task UpdateAdminUserCredentialsAsync(PasswordChange passwordChange, CancellationToken cancellationToken = default) + { + var response = await GetAdminUrl("/users/credentials") + .SendAsync(HttpMethod.Put, CreateJsonContent(passwordChange), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves additional groups for a user (memberships) beyond the first page. + /// + /// The username context. + /// Optional filter string. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Cancellation token. + /// A collection of group memberships. + public async Task> GetAdminUserMoreMembersAsync(string context, string? filter = null, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) { - var queryParamValues = new Dictionary + ["limit"] = limit, + ["start"] = start, + ["context"] = context, + ["filter"] = filter, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => { - ["limit"] = limit, - ["start"] = start, - ["context"] = context, - ["filter"] = filter - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetAdminUrl("/users/more-members") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task> GetAdminUserMoreNonMembersAsync(string context, string? filter = null, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) + var response = await GetAdminUrl("/users/more-members") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Retrieves additional groups that a user is not a member of. + /// + /// The username context. + /// Optional filter string. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Cancellation token. + /// A collection of non-member groups. + public async Task> GetAdminUserMoreNonMembersAsync(string context, string? filter = null, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) { - var queryParamValues = new Dictionary + ["limit"] = limit, + ["start"] = start, + ["context"] = context, + ["filter"] = filter, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => { - ["limit"] = limit, - ["start"] = start, - ["context"] = context, - ["filter"] = filter - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetAdminUrl("/users/more-non-members") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task RemoveAdminUserFromGroupAsync(string userName, string groupName, CancellationToken cancellationToken = default) + var response = await GetAdminUrl("/users/more-non-members") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Removes a user from a group. + /// + /// The username. + /// The group name. + /// Cancellation token. + /// true if removal succeeded; otherwise, false. + public async Task RemoveAdminUserFromGroupAsync(string userName, string groupName, CancellationToken cancellationToken = default) + { + var data = new { - var data = new - { - context = userName, - itemName = groupName - }; + context = userName, + itemName = groupName, + }; - var response = await GetAdminUrl("/users/remove-group") - .PostJsonAsync(data, cancellationToken: cancellationToken) - .ConfigureAwait(false); + var response = await GetAdminUrl("/users/remove-group") + .SendAsync(HttpMethod.Post, CreateJsonContent(data), cancellationToken: cancellationToken) + .ConfigureAwait(false); - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } - public async Task RenameAdminUserAsync(UserRename userRename, int? avatarSize = null, CancellationToken cancellationToken = default) - { - var response = await GetAdminUrl("users/rename") - .SetQueryParam("avatarSize", avatarSize) - .PostJsonAsync(userRename, cancellationToken: cancellationToken) - .ConfigureAwait(false); + /// + /// Renames a user. + /// + /// The rename payload. + /// Optional avatar size. + /// Cancellation token. + /// The updated user info. + public async Task RenameAdminUserAsync(UserRename userRename, int? avatarSize = null, CancellationToken cancellationToken = default) + { + var response = await GetAdminUrl("users/rename") + .SetQueryParam("avatarSize", avatarSize) + .SendAsync(HttpMethod.Post, CreateJsonContent(userRename), cancellationToken: cancellationToken) + .ConfigureAwait(false); - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } - public async Task GetAdminClusterAsync(CancellationToken cancellationToken = default) - { - return await GetAdminUrl("/cluster") - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } + /// + /// Retrieves cluster information. + /// + /// Cancellation token. + /// The cluster details. + public async Task GetAdminClusterAsync(CancellationToken cancellationToken = default) + { + var response = await GetAdminUrl("/cluster") + .GetAsync(cancellationToken) + .ConfigureAwait(false); - public async Task GetAdminLicenseAsync(CancellationToken cancellationToken = default) - { - return await GetAdminUrl("/license") - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } - public async Task UpdateAdminLicenseAsync(LicenseInfo licenseInfo, CancellationToken cancellationToken = default) - { - var response = await GetAdminUrl("/license") - .PostJsonAsync(licenseInfo, cancellationToken: cancellationToken) - .ConfigureAwait(false); + /// + /// Retrieves license details. + /// + /// Cancellation token. + /// The license details. + public async Task GetAdminLicenseAsync(CancellationToken cancellationToken = default) + { + var response = await GetAdminUrl("/license") + .GetAsync(cancellationToken) + .ConfigureAwait(false); - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } - public async Task GetAdminMailServerAsync(CancellationToken cancellationToken = default) - { - return await GetAdminUrl("/mail-server") - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } + /// + /// Updates license information. + /// + /// The license payload. + /// Cancellation token. + /// The updated license details. + public async Task UpdateAdminLicenseAsync(LicenseInfo licenseInfo, CancellationToken cancellationToken = default) + { + var response = await GetAdminUrl("/license") + .SendAsync(HttpMethod.Post, CreateJsonContent(licenseInfo), cancellationToken: cancellationToken) + .ConfigureAwait(false); - public async Task UpdateAdminMailServerAsync(MailServerConfiguration mailServerConfiguration, CancellationToken cancellationToken = default) - { - var response = await GetAdminUrl("/mail-server") - .PutJsonAsync(mailServerConfiguration, cancellationToken: cancellationToken) - .ConfigureAwait(false); + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } + /// + /// Retrieves mail server configuration. + /// + /// Cancellation token. + /// The mail server configuration. + public async Task GetAdminMailServerAsync(CancellationToken cancellationToken = default) + { + var response = await GetAdminUrl("/mail-server") + .GetAsync(cancellationToken) + .ConfigureAwait(false); - public async Task DeleteAdminMailServerAsync(CancellationToken cancellationToken = default) - { - var response = await GetAdminUrl("/mail-server") - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } + /// + /// Updates mail server configuration. + /// + /// The configuration payload. + /// Cancellation token. + /// The updated configuration. + public async Task UpdateAdminMailServerAsync(MailServerConfiguration mailServerConfiguration, CancellationToken cancellationToken = default) + { + var response = await GetAdminUrl("/mail-server") + .SendAsync(HttpMethod.Put, CreateJsonContent(mailServerConfiguration), cancellationToken: cancellationToken) + .ConfigureAwait(false); - public async Task GetAdminMailServerSenderAddressAsync(CancellationToken cancellationToken = default) - { - var response = await GetAdminUrl("/mail-server/sender-address") - .GetAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } - return await HandleResponseAsync(response, s => s, cancellationToken).ConfigureAwait(false); - } + /// + /// Deletes mail server configuration. + /// + /// Cancellation token. + /// true if deletion succeeded; otherwise, false. + public async Task DeleteAdminMailServerAsync(CancellationToken cancellationToken = default) + { + var response = await GetAdminUrl("/mail-server") + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); - public async Task UpdateAdminMailServerSenderAddressAsync(string senderAddress, CancellationToken cancellationToken = default) - { - var response = await GetAdminUrl("/mail-server/sender-address") - .PutJsonAsync(senderAddress, cancellationToken: cancellationToken) - .ConfigureAwait(false); + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } - return await HandleResponseAsync(response, s => s, cancellationToken).ConfigureAwait(false); - } + /// + /// Retrieves the mail server sender address. + /// + /// Cancellation token. + /// The sender address. + public async Task GetAdminMailServerSenderAddressAsync(CancellationToken cancellationToken = default) + { + var response = await GetAdminUrl("/mail-server/sender-address") + .GetAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); - public async Task DeleteAdminMailServerSenderAddressAsync(CancellationToken cancellationToken = default) - { - var response = await GetAdminUrl("/mail-server/sender-address") - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - public async Task> GetAdminGroupPermissionsAsync(string? filter = null, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) + return await HandleResponseAsync(response, s => s, cancellationToken).ConfigureAwait(false); + } + + /// + /// Updates the mail server sender address. + /// + /// The sender address. + /// Cancellation token. + /// The updated sender address. + public async Task UpdateAdminMailServerSenderAddressAsync(string senderAddress, CancellationToken cancellationToken = default) + { + var response = await GetAdminUrl("/mail-server/sender-address") + .SendAsync(HttpMethod.Put, CreateJsonContent(senderAddress), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, s => s, cancellationToken).ConfigureAwait(false); + } + + /// + /// Deletes the mail server sender address. + /// + /// Cancellation token. + /// true if deletion succeeded; otherwise, false. + public async Task DeleteAdminMailServerSenderAddressAsync(CancellationToken cancellationToken = default) + { + var response = await GetAdminUrl("/mail-server/sender-address") + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves admin group permissions with optional filtering. + /// + /// Optional filter string. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Cancellation token. + /// A collection of group permissions. + public async Task> GetAdminGroupPermissionsAsync(string? filter = null, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) { - var queryParamValues = new Dictionary + ["limit"] = limit, + ["start"] = start, + ["filter"] = filter, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => { - ["limit"] = limit, - ["start"] = start, - ["filter"] = filter - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetAdminUrl("/permissions/groups") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task UpdateAdminGroupPermissionsAsync(Permissions permission, string name, CancellationToken cancellationToken = default) + var response = await GetAdminUrl("/permissions/groups") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Updates a group's permissions. + /// + /// The permission to grant. + /// The group name. + /// Cancellation token. + /// true if the update succeeded; otherwise, false. + public async Task UpdateAdminGroupPermissionsAsync(Permissions permission, string name, CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) { - var queryParamValues = new Dictionary - { - ["permission"] = permission, - ["name"] = name - }; + ["permission"] = permission, + ["name"] = name, + }; - var response = await GetAdminUrl("/permissions/groups") - .SetQueryParams(queryParamValues) - .PutJsonAsync(new StringContent(""), cancellationToken: cancellationToken) - .ConfigureAwait(false); + var response = await GetAdminUrl("/permissions/groups") + .SetQueryParams(queryParamValues) + .SendAsync(HttpMethod.Put, new StringContent(string.Empty), cancellationToken: cancellationToken) + .ConfigureAwait(false); - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } - public async Task DeleteAdminGroupPermissionsAsync(string name, CancellationToken cancellationToken = default) - { - var response = await GetAdminUrl("/permissions/groups") - .SetQueryParam("name", name) - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - public async Task> GetAdminGroupPermissionsNoneAsync(string? filter = null, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) + /// + /// Removes a group's permissions. + /// + /// The group name. + /// Cancellation token. + /// true if the permissions were removed; otherwise, false. + public async Task DeleteAdminGroupPermissionsAsync(string name, CancellationToken cancellationToken = default) + { + var response = await GetAdminUrl("/permissions/groups") + .SetQueryParam("name", name) + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves groups that currently have no admin permissions. + /// + /// Optional filter string. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Cancellation token. + /// A collection of groups without permissions. + public async Task> GetAdminGroupPermissionsNoneAsync(string? filter = null, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) { - var queryParamValues = new Dictionary + ["limit"] = limit, + ["start"] = start, + ["filter"] = filter, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => { - ["limit"] = limit, - ["start"] = start, - ["filter"] = filter - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetAdminUrl("/permissions/groups/none") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task> GetAdminUserPermissionsAsync(string? filter = null, - int? maxPages = null, - int? limit = null, - int? start = null, - int? avatarSize = null, - CancellationToken cancellationToken = default) + var response = await GetAdminUrl("/permissions/groups/none") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Retrieves admin user permissions with optional filtering. + /// + /// Optional filter string. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Optional avatar size. + /// Cancellation token. + /// A collection of user permissions. + public async Task> GetAdminUserPermissionsAsync(string? filter = null, + int? maxPages = null, + int? limit = null, + int? start = null, + int? avatarSize = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) { - var queryParamValues = new Dictionary + ["limit"] = limit, + ["start"] = start, + ["filter"] = filter, + ["avatarSize"] = avatarSize, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => { - ["limit"] = limit, - ["start"] = start, - ["filter"] = filter, - ["avatarSize"] = avatarSize - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetAdminUrl("/permissions/users") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task UpdateAdminUserPermissionsAsync(Permissions permission, string name, CancellationToken cancellationToken = default) + var response = await GetAdminUrl("/permissions/users") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Updates a user's permissions. + /// + /// The permission to grant. + /// The username. + /// Cancellation token. + /// true if the update succeeded; otherwise, false. + public async Task UpdateAdminUserPermissionsAsync(Permissions permission, string name, CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) { - var queryParamValues = new Dictionary - { - ["permission"] = permission, - ["name"] = name - }; + ["permission"] = permission, + ["name"] = name, + }; - var response = await GetAdminUrl("/permissions/users") - .SetQueryParams(queryParamValues) - .PutJsonAsync(new StringContent(""), cancellationToken: cancellationToken) - .ConfigureAwait(false); + var response = await GetAdminUrl("/permissions/users") + .SetQueryParams(queryParamValues) + .SendAsync(HttpMethod.Put, new StringContent(string.Empty), cancellationToken: cancellationToken) + .ConfigureAwait(false); - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } - public async Task DeleteAdminUserPermissionsAsync(string name, CancellationToken cancellationToken = default) - { - var response = await GetAdminUrl("/permissions/users") - .SetQueryParam("name", name) - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - public async Task> GetAdminUserPermissionsNoneAsync(string? filter = null, - int? maxPages = null, - int? limit = null, - int? start = null, - int? avatarSize = null, - CancellationToken cancellationToken = default) + /// + /// Removes a user's permissions. + /// + /// The username. + /// Cancellation token. + /// true if the permissions were removed; otherwise, false. + public async Task DeleteAdminUserPermissionsAsync(string name, CancellationToken cancellationToken = default) + { + var response = await GetAdminUrl("/permissions/users") + .SetQueryParam("name", name) + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves users that currently have no admin permissions. + /// + /// Optional filter string. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Optional avatar size. + /// Cancellation token. + /// A collection of users without permissions. + public async Task> GetAdminUserPermissionsNoneAsync(string? filter = null, + int? maxPages = null, + int? limit = null, + int? start = null, + int? avatarSize = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) { - var queryParamValues = new Dictionary + ["limit"] = limit, + ["start"] = start, + ["filter"] = filter, + ["avatarSize"] = avatarSize, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => { - ["limit"] = limit, - ["start"] = start, - ["filter"] = filter, - ["avatarSize"] = avatarSize - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetAdminUrl("/permissions/users/none") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task GetAdminPullRequestsMergeStrategiesAsync(string scmId, CancellationToken cancellationToken = default) - { - return await GetAdminUrl($"/pull-requests/{scmId}") - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } + var response = await GetAdminUrl("/permissions/users/none") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } - public async Task UpdateAdminPullRequestsMergeStrategiesAsync(string scmId, MergeStrategies mergeStrategies, CancellationToken cancellationToken = default) - { - var response = await GetAdminUrl($"/pull-requests/{scmId}") - .PostJsonAsync(mergeStrategies, cancellationToken: cancellationToken) - .ConfigureAwait(false); + /// + /// Retrieves merge strategies for pull requests for a specific SCM. + /// + /// The SCM identifier. + /// Cancellation token. + /// The merge strategies configuration. + public async Task GetAdminPullRequestsMergeStrategiesAsync(string scmId, CancellationToken cancellationToken = default) + { + var response = await GetAdminUrl($"/pull-requests/{scmId}") + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Updates merge strategies for pull requests for a specific SCM. + /// + /// The SCM identifier. + /// The merge strategies payload. + /// Cancellation token. + /// The updated merge strategies. + public async Task UpdateAdminPullRequestsMergeStrategiesAsync(string scmId, MergeStrategies mergeStrategies, CancellationToken cancellationToken = default) + { + var response = await GetAdminUrl($"/pull-requests/{scmId}") + .SendAsync(HttpMethod.Post, CreateJsonContent(mergeStrategies), cancellationToken: cancellationToken) + .ConfigureAwait(false); - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/ApplicationProperties/BitbucketClient.cs b/src/Bitbucket.Net/Core/ApplicationProperties/BitbucketClient.cs index b2642a0..555fa16 100644 --- a/src/Bitbucket.Net/Core/ApplicationProperties/BitbucketClient.cs +++ b/src/Bitbucket.Net/Core/ApplicationProperties/BitbucketClient.cs @@ -1,23 +1,31 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; +using Bitbucket.Net.Common; using Flurl.Http; -namespace Bitbucket.Net +namespace Bitbucket.Net; + +/// +/// Provides application properties Bitbucket API operations. +/// +public partial class BitbucketClient { - public partial class BitbucketClient - { - private IFlurlRequest GetApplicationPropertiesUrl() => GetBaseUrl() - .AppendPathSegment("/application-properties"); + /// + /// Gets the base application properties URL. + /// + /// An targeting the application-properties endpoint. + private IFlurlRequest GetApplicationPropertiesUrl() => GetBaseUrl() + .AppendPathSegment("/application-properties"); - public async Task> GetApplicationPropertiesAsync(CancellationToken cancellationToken = default) - { - var response = await GetApplicationPropertiesUrl() - .GetJsonAsync>(cancellationToken: cancellationToken) - .ConfigureAwait(false); + /// + /// Retrieves application properties. + /// + /// Token to cancel the operation. + /// A dictionary of application property values. + public async Task> GetApplicationPropertiesAsync(CancellationToken cancellationToken = default) + { + var response = await GetApplicationPropertiesUrl() + .GetAsync(cancellationToken) + .ConfigureAwait(false); - return response; - } + return await HandleResponseAsync>(response, cancellationToken: cancellationToken).ConfigureAwait(false); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Dashboard/BitbucketClient.cs b/src/Bitbucket.Net/Core/Dashboard/BitbucketClient.cs index 9686adc..3bf9725 100644 --- a/src/Bitbucket.Net/Core/Dashboard/BitbucketClient.cs +++ b/src/Bitbucket.Net/Core/Dashboard/BitbucketClient.cs @@ -1,70 +1,152 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; using Bitbucket.Net.Common; using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Core.Projects; using Flurl.Http; -namespace Bitbucket.Net +namespace Bitbucket.Net; + +/// +/// Provides dashboard-related Bitbucket API operations. +/// +public partial class BitbucketClient { - public partial class BitbucketClient + /// + /// Gets the base dashboard URL. + /// + /// An targeting the dashboard root. + private IFlurlRequest GetDashboardUrl() => GetBaseUrl() + .AppendPathSegment("/dashboard"); + + /// + /// Gets the dashboard URL for the specified path. + /// + /// The path to append to the dashboard root. + /// An pointing to the dashboard path. + private IFlurlRequest GetDashboardUrl(string path) => GetDashboardUrl() + .AppendPathSegment(path); + + /// + /// Retrieves pull requests for the current user's dashboard. + /// + /// Optional pull request state filter. + /// Optional participant role filter. + /// Optional participant status filters. + /// Optional sort order. + /// Optional filter for recently closed PRs (seconds). + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Token to cancel the operation. + /// A collection of pull requests. + public async Task> GetDashboardPullRequestsAsync(PullRequestStates? state = null, + Roles? role = null, + List? status = null, + PullRequestOrders? order = PullRequestOrders.Newest, + int? closedSinceSeconds = null, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) { - private IFlurlRequest GetDashboardUrl() => GetBaseUrl() - .AppendPathSegment("/dashboard"); + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["state"] = BitbucketHelpers.PullRequestStateToString(state), + ["role"] = BitbucketHelpers.RoleToString(role), + ["status"] = status != null ? string.Join(',', status.Select(BitbucketHelpers.ParticipantStatusToString)) : null, + ["order"] = BitbucketHelpers.PullRequestOrderToString(order), + ["closedSince"] = closedSinceSeconds, + ["limit"] = limit, + ["start"] = start, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetDashboardUrl("/pull-requests") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); - private IFlurlRequest GetDashboardUrl(string path) => GetDashboardUrl() - .AppendPathSegment(path); + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } - public async Task> GetDashboardPullRequestsAsync(PullRequestStates? state = null, - Roles? role = null, - List? status = null, - PullRequestOrders? order = PullRequestOrders.Newest, - int? closedSinceSeconds = null, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) + /// + /// Streams pull requests for the current user's dashboard, yielding items as they are retrieved. + /// + /// Optional pull request state filter. + /// Optional participant role filter. + /// Optional participant status filters. + /// Optional sort order. + /// Optional filter for recently closed PRs (seconds). + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Token to cancel the operation. + /// An async enumerable of pull requests. + public IAsyncEnumerable GetDashboardPullRequestsStreamAsync(PullRequestStates? state = null, + Roles? role = null, + List? status = null, + PullRequestOrders? order = PullRequestOrders.Newest, + int? closedSinceSeconds = null, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) { - var queryParamValues = new Dictionary + ["state"] = BitbucketHelpers.PullRequestStateToString(state), + ["role"] = BitbucketHelpers.RoleToString(role), + ["status"] = status is not null ? string.Join(',', status.Select(BitbucketHelpers.ParticipantStatusToString)) : null, + ["order"] = BitbucketHelpers.PullRequestOrderToString(order), + ["closedSince"] = closedSinceSeconds, + ["limit"] = limit, + ["start"] = start, + }; + + return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => { - ["state"] = BitbucketHelpers.PullRequestStateToString(state), - ["role"] = BitbucketHelpers.RoleToString(role), - ["status"] = status != null ? string.Join(",", status.Select(BitbucketHelpers.ParticipantStatusToString)) : null, - ["order"] = BitbucketHelpers.PullRequestOrderToString(order), - ["closedSince"] = closedSinceSeconds, - ["limit"] = limit, - ["start"] = start - }; + var response = await GetDashboardUrl("/pull-requests") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetDashboardUrl("/pull-requests") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken); + } - public async Task> GetDashboardPullRequestSuggestionsAsync(int changesSinceSeconds = 172800, - int? maxPages = null, - int? limit = 3, - int? start = null, - CancellationToken cancellationToken = default) + /// + /// Retrieves pull request suggestions for the current user. + /// + /// Time window in seconds to consider changes. + /// Optional maximum number of pages to retrieve. + /// Optional page size (default 3). + /// Optional starting index for pagination. + /// Token to cancel the operation. + /// A collection of pull request suggestions. + public async Task> GetDashboardPullRequestSuggestionsAsync(int changesSinceSeconds = 172800, + int? maxPages = null, + int? limit = 3, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) { - var queryParamValues = new Dictionary + ["changesSince"] = changesSinceSeconds, + ["limit"] = limit, + ["start"] = start, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => { - ["changesSince"] = changesSinceSeconds, - ["limit"] = limit, - ["start"] = start - }; + var response = await GetDashboardUrl("/pull-request-suggestions") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetDashboardUrl("/pull-request-suggestions") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Groups/BitbucketClient.cs b/src/Bitbucket.Net/Core/Groups/BitbucketClient.cs index d9508f3..a44e41c 100644 --- a/src/Bitbucket.Net/Core/Groups/BitbucketClient.cs +++ b/src/Bitbucket.Net/Core/Groups/BitbucketClient.cs @@ -1,35 +1,52 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; +using Bitbucket.Net.Common; using Bitbucket.Net.Common.Models; using Flurl.Http; -namespace Bitbucket.Net +namespace Bitbucket.Net; + +/// +/// Provides group-related Bitbucket API operations. +/// +public partial class BitbucketClient { - public partial class BitbucketClient - { - private IFlurlRequest GetGroupsUrl() => GetBaseUrl() - .AppendPathSegment("/groups"); + /// + /// Gets the base groups URL. + /// + /// An targeting the groups endpoint. + private IFlurlRequest GetGroupsUrl() => GetBaseUrl() + .AppendPathSegment("/groups"); - public async Task> GetGroupNamesAsync(string? filter = null, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) + /// + /// Retrieves group names with optional filtering. + /// + /// Optional filter string. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Token to cancel the operation. + /// A collection of group names. + public async Task> GetGroupNamesAsync(string? filter = null, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) { - var queryParamValues = new Dictionary + ["filter"] = filter, + ["limit"] = limit, + ["start"] = start, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => { - ["filter"] = filter, - ["limit"] = limit, - ["start"] = start - }; + var response = await GetGroupsUrl() + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetGroupsUrl() - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Hooks/BitbucketClient.cs b/src/Bitbucket.Net/Core/Hooks/BitbucketClient.cs index df77155..878ea2b 100644 --- a/src/Bitbucket.Net/Core/Hooks/BitbucketClient.cs +++ b/src/Bitbucket.Net/Core/Hooks/BitbucketClient.cs @@ -1,21 +1,36 @@ -using System.Threading; -using System.Threading.Tasks; +using Bitbucket.Net.Common; using Flurl.Http; -namespace Bitbucket.Net +namespace Bitbucket.Net; + +/// +/// Provides hook-related Bitbucket API operations. +/// +public partial class BitbucketClient { - public partial class BitbucketClient + /// + /// Gets the base hooks URL. + /// + /// An targeting the hooks endpoint. + private IFlurlRequest GetHooksUrl() => GetBaseUrl() + .AppendPathSegment("/hooks"); + + /// + /// Retrieves the avatar for a project hook. + /// + /// The hook key. + /// Optional avatar version. + /// Token to cancel the operation. + /// The avatar image bytes. + public async Task GetProjectHooksAvatarAsync(string hookKey, string? version = null, CancellationToken cancellationToken = default) { - private IFlurlRequest GetHooksUrl() => GetBaseUrl() - .AppendPathSegment("/hooks"); + var response = await GetHooksUrl() + .AppendPathSegment($"/{hookKey}/avatar") + .SetQueryParam("version", version) + .GetAsync(cancellationToken) + .ConfigureAwait(false); - public async Task GetProjectHooksAvatarAsync(string hookKey, string? version = null, CancellationToken cancellationToken = default) - { - return await GetHooksUrl() - .AppendPathSegment($"/{hookKey}/avatar") - .SetQueryParam("version", version) - .GetBytesAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } + await HandleErrorsAsync(response, cancellationToken).ConfigureAwait(false); + return await ReadResponseBytesAsync(response, cancellationToken).ConfigureAwait(false); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Inbox/BitbucketClient.cs b/src/Bitbucket.Net/Core/Inbox/BitbucketClient.cs index 3d68017..762e7bc 100644 --- a/src/Bitbucket.Net/Core/Inbox/BitbucketClient.cs +++ b/src/Bitbucket.Net/Core/Inbox/BitbucketClient.cs @@ -1,56 +1,116 @@ -using System.Collections.Generic; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; using Bitbucket.Net.Common; using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Core.Projects; using Flurl.Http; +using System.Text.Json; + +namespace Bitbucket.Net; -namespace Bitbucket.Net +/// +/// Provides inbox-related Bitbucket API operations. +/// +public partial class BitbucketClient { - public partial class BitbucketClient + /// + /// Gets the base inbox URL. + /// + /// An targeting the inbox endpoint. + private IFlurlRequest GetInboxUrl() => GetBaseUrl() + .AppendPathSegment("/inbox"); + + /// + /// Gets the inbox URL for the specified path. + /// + /// The path to append to the inbox endpoint. + /// An pointing to the inbox path. + private IFlurlRequest GetInboxUrl(string path) => GetInboxUrl() + .AppendPathSegment(path); + + /// + /// Retrieves pull requests in the user's inbox. + /// + /// Optional maximum number of pages to retrieve. + /// Optional page size (default 25). + /// Optional starting index for pagination. + /// The participant role filter (default reviewer). + /// Token to cancel the operation. + /// A collection of pull requests. + public async Task> GetInboxPullRequestsAsync( + int? maxPages = null, + int? limit = 25, + int? start = 0, + Roles role = Roles.Reviewer, + CancellationToken cancellationToken = default) { - private IFlurlRequest GetInboxUrl() => GetBaseUrl() - .AppendPathSegment("/inbox"); - - private IFlurlRequest GetInboxUrl(string path) => GetInboxUrl() - .AppendPathSegment(path); - - public async Task> GetInboxPullRequestsAsync( - int? maxPages = null, - int? limit = 25, - int? start = 0, - Roles role = Roles.Reviewer, - CancellationToken cancellationToken = default) + var queryParamValues = new Dictionary(StringComparer.Ordinal) { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["role"] = BitbucketHelpers.RoleToString(role) - }; + ["limit"] = limit, + ["start"] = start, + ["role"] = BitbucketHelpers.RoleToString(role), + }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetInboxUrl("/pull-requests") + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetInboxUrl("/pull-requests") .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } - public async Task GetInboxPullRequestsCountAsync(CancellationToken cancellationToken = default) + /// + /// Streams pull requests from the user's inbox, yielding items as they are retrieved. + /// + /// Optional maximum number of pages to retrieve. + /// Optional page size (default 25). + /// Optional starting index for pagination. + /// The participant role filter (default reviewer). + /// Token to cancel the operation. + /// An async enumerable of pull requests. + public IAsyncEnumerable GetInboxPullRequestsStreamAsync( + int? maxPages = null, + int? limit = 25, + int? start = 0, + Roles role = Roles.Reviewer, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) { - var response = await GetInboxUrl("/pull-requests/count") - .GetAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); + ["limit"] = limit, + ["start"] = start, + ["role"] = BitbucketHelpers.RoleToString(role), + }; - return await HandleResponseAsync(response, s => + return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => { - using var doc = JsonDocument.Parse(s); - return doc.RootElement.GetProperty("count").GetInt32(); - }, cancellationToken) - .ConfigureAwait(false); - } + var response = await GetInboxUrl("/pull-requests") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken); + } + + /// + /// Retrieves the count of pull requests in the user's inbox. + /// + /// Token to cancel the operation. + /// The number of inbox pull requests. + public async Task GetInboxPullRequestsCountAsync(CancellationToken cancellationToken = default) + { + var response = await GetInboxUrl("/pull-requests/count") + .GetAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, s => + { + using var doc = JsonDocument.Parse(s); + return doc.RootElement.GetProperty("count").GetInt32(); + }, cancellationToken) + .ConfigureAwait(false); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Logs/BitbucketClient.cs b/src/Bitbucket.Net/Core/Logs/BitbucketClient.cs index ea033d2..69e42fd 100644 --- a/src/Bitbucket.Net/Core/Logs/BitbucketClient.cs +++ b/src/Bitbucket.Net/Core/Logs/BitbucketClient.cs @@ -1,65 +1,97 @@ -using System.Net.Http; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; using Bitbucket.Net.Common; using Bitbucket.Net.Models.Core.Logs; using Flurl.Http; +using System.Text.Json; -namespace Bitbucket.Net -{ - public partial class BitbucketClient - { - private IFlurlRequest GetLogsUrl() => GetBaseUrl() - .AppendPathSegment("/logs"); +namespace Bitbucket.Net; - private IFlurlRequest GetLogsUrl(string path) => GetLogsUrl() - .AppendPathSegment(path); +/// +/// Provides log-level management Bitbucket API operations. +/// +public partial class BitbucketClient +{ + /// + /// Gets the base logs URL. + /// + /// An targeting the logs endpoint. + private IFlurlRequest GetLogsUrl() => GetBaseUrl() + .AppendPathSegment("/logs"); - public async Task GetLogLevelAsync(string loggerName, CancellationToken cancellationToken = default) - { - var response = await GetLogsUrl($"/logger/{loggerName}") - .GetAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); + /// + /// Gets the logs URL for the specified path. + /// + /// The path to append to the logs endpoint. + /// An pointing to the logs path. + private IFlurlRequest GetLogsUrl(string path) => GetLogsUrl() + .AppendPathSegment(path); - return await HandleResponseAsync(response, s => - { - using var doc = JsonDocument.Parse(s); - return BitbucketHelpers.StringToLogLevel(doc.RootElement.GetProperty("logLevel").GetString()!); - }, cancellationToken) - .ConfigureAwait(false); - } + /// + /// Retrieves the log level for a specific logger. + /// + /// The logger name. + /// Token to cancel the operation. + /// The configured log level. + public async Task GetLogLevelAsync(string loggerName, CancellationToken cancellationToken = default) + { + var response = await GetLogsUrl($"/logger/{loggerName}") + .GetAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); - public async Task SetLogLevelAsync(string loggerName, LogLevels logLevel, CancellationToken cancellationToken = default) + return await HandleResponseAsync(response, s => { - var response = await GetLogsUrl($"/logger/{loggerName}/{BitbucketHelpers.LogLevelToString(logLevel)}") - .PutAsync(new StringContent(""), cancellationToken: cancellationToken) - .ConfigureAwait(false); + using var doc = JsonDocument.Parse(s); + return BitbucketHelpers.StringToLogLevel(doc.RootElement.GetProperty("logLevel").GetString()!); + }, cancellationToken) + .ConfigureAwait(false); + } - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } + /// + /// Sets the log level for a specific logger. + /// + /// The logger name. + /// The log level to set. + /// Token to cancel the operation. + /// true if the update succeeded; otherwise, false. + public async Task SetLogLevelAsync(string loggerName, LogLevels logLevel, CancellationToken cancellationToken = default) + { + var response = await GetLogsUrl($"/logger/{loggerName}/{BitbucketHelpers.LogLevelToString(logLevel)}") + .PutAsync(new StringContent(""), cancellationToken: cancellationToken) + .ConfigureAwait(false); - public async Task GetRootLogLevelAsync(CancellationToken cancellationToken = default) - { - var response = await GetLogsUrl("/logger/rootLogger") - .GetAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } - return await HandleResponseAsync(response, s => - { - using var doc = JsonDocument.Parse(s); - return BitbucketHelpers.StringToLogLevel(doc.RootElement.GetProperty("logLevel").GetString()!); - }, cancellationToken) - .ConfigureAwait(false); - } + /// + /// Retrieves the root logger's log level. + /// + /// Token to cancel the operation. + /// The root log level. + public async Task GetRootLogLevelAsync(CancellationToken cancellationToken = default) + { + var response = await GetLogsUrl("/logger/rootLogger") + .GetAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); - public async Task SetRootLogLevelAsync(LogLevels logLevel, CancellationToken cancellationToken = default) + return await HandleResponseAsync(response, s => { - var response = await GetLogsUrl($"/logger/rootLogger/{BitbucketHelpers.LogLevelToString(logLevel)}") - .PutAsync(new StringContent(""), cancellationToken: cancellationToken) - .ConfigureAwait(false); + using var doc = JsonDocument.Parse(s); + return BitbucketHelpers.StringToLogLevel(doc.RootElement.GetProperty("logLevel").GetString()!); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Sets the root logger's log level. + /// + /// The log level to set. + /// Token to cancel the operation. + /// true if the update succeeded; otherwise, false. + public async Task SetRootLogLevelAsync(LogLevels logLevel, CancellationToken cancellationToken = default) + { + var response = await GetLogsUrl($"/logger/rootLogger/{BitbucketHelpers.LogLevelToString(logLevel)}") + .PutAsync(new StringContent(""), cancellationToken: cancellationToken) + .ConfigureAwait(false); - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Markup/BitbucketClient.cs b/src/Bitbucket.Net/Core/Markup/BitbucketClient.cs index 07e1574..9e76bb6 100644 --- a/src/Bitbucket.Net/Core/Markup/BitbucketClient.cs +++ b/src/Bitbucket.Net/Core/Markup/BitbucketClient.cs @@ -1,46 +1,62 @@ -using System.Collections.Generic; -using System.Net.Http; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; using Bitbucket.Net.Common; using Flurl.Http; +using System.Text.Json; + +namespace Bitbucket.Net; -namespace Bitbucket.Net +/// +/// Provides markup preview Bitbucket API operations. +/// +public partial class BitbucketClient { - public partial class BitbucketClient - { - private IFlurlRequest GetMarkupUrl() => GetBaseUrl() - .AppendPathSegment("/markup"); + /// + /// Gets the base markup URL. + /// + /// An targeting the markup endpoint. + private IFlurlRequest GetMarkupUrl() => GetBaseUrl() + .AppendPathSegment("/markup"); - private IFlurlRequest GetMarkupUrl(string path) => GetMarkupUrl() - .AppendPathSegment(path); + /// + /// Gets the markup URL for the specified path. + /// + /// The path to append to the markup endpoint. + /// An pointing to the markup path. + private IFlurlRequest GetMarkupUrl(string path) => GetMarkupUrl() + .AppendPathSegment(path); - public async Task PreviewMarkupAsync(string text, - string? urlMode = null, - bool? hardWrap = null, - bool? htmlEscape = null, - CancellationToken cancellationToken = default) + /// + /// Previews markup text as HTML. + /// + /// The markup text to preview. + /// Optional URL rendering mode. + /// Whether to hard wrap lines. + /// Whether to HTML-escape content. + /// Token to cancel the operation. + /// The rendered HTML. + public async Task PreviewMarkupAsync(string text, + string? urlMode = null, + bool? hardWrap = null, + bool? htmlEscape = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) { - var queryParamValues = new Dictionary - { - ["urlMode"] = urlMode, - ["hardWrap"] = BitbucketHelpers.BoolToString(hardWrap), - ["htmlEscape"] = BitbucketHelpers.BoolToString(htmlEscape) - }; + ["urlMode"] = urlMode, + ["hardWrap"] = BitbucketHelpers.BoolToString(hardWrap), + ["htmlEscape"] = BitbucketHelpers.BoolToString(htmlEscape), + }; - var response = await GetMarkupUrl("/preview") - .WithHeader("X-Atlassian-Token", "no-check") - .SetQueryParams(queryParamValues) - .PostJsonAsync(new StringContent(text), cancellationToken: cancellationToken) - .ConfigureAwait(false); + var response = await GetMarkupUrl("/preview") + .WithHeader("X-Atlassian-Token", "no-check") + .SetQueryParams(queryParamValues) + .SendAsync(HttpMethod.Post, new StringContent(text), cancellationToken: cancellationToken) + .ConfigureAwait(false); - return await HandleResponseAsync(response, s => - { - using var doc = JsonDocument.Parse(s); - return doc.RootElement.GetProperty("html").GetString()!; - }, cancellationToken) - .ConfigureAwait(false); - } + return await HandleResponseAsync(response, s => + { + using var doc = JsonDocument.Parse(s); + return doc.RootElement.GetProperty("html").GetString()!; + }, cancellationToken) + .ConfigureAwait(false); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Profile/BitbucketClient.cs b/src/Bitbucket.Net/Core/Profile/BitbucketClient.cs index 9f1dfd6..313445c 100644 --- a/src/Bitbucket.Net/Core/Profile/BitbucketClient.cs +++ b/src/Bitbucket.Net/Core/Profile/BitbucketClient.cs @@ -1,41 +1,62 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; using Bitbucket.Net.Common; using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Core.Admin; using Bitbucket.Net.Models.Core.Projects; using Flurl.Http; -namespace Bitbucket.Net +namespace Bitbucket.Net; + +/// +/// Provides profile-related Bitbucket API operations. +/// +public partial class BitbucketClient { - public partial class BitbucketClient - { - private IFlurlRequest GetProfileUrl() => GetBaseUrl() - .AppendPathSegment("/profile"); + /// + /// Gets the base profile URL. + /// + /// An targeting the profile endpoint. + private IFlurlRequest GetProfileUrl() => GetBaseUrl() + .AppendPathSegment("/profile"); - private IFlurlRequest GetProfileUrl(string path) => GetProfileUrl() - .AppendPathSegment(path); + /// + /// Gets the profile URL for the specified path. + /// + /// The path to append to the profile endpoint. + /// An pointing to the profile path. + private IFlurlRequest GetProfileUrl(string path) => GetProfileUrl() + .AppendPathSegment(path); - public async Task> GetRecentReposAsync(Permissions? permission = null, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) + /// + /// Retrieves recent repositories for the current user. + /// + /// Optional permission filter. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Token to cancel the operation. + /// A collection of recent repositories. + public async Task> GetRecentReposAsync(Permissions? permission = null, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["permission"] = BitbucketHelpers.PermissionToString(permission) - }; + ["limit"] = limit, + ["start"] = start, + ["permission"] = BitbucketHelpers.PermissionToString(permission), + }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProfileUrl("/recent/repos") + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProfileUrl("/recent/repos") .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Branches.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Branches.cs new file mode 100644 index 0000000..592ac82 --- /dev/null +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Branches.cs @@ -0,0 +1,332 @@ +using Bitbucket.Net.Common; +using Bitbucket.Net.Common.Models; +using Bitbucket.Net.Models.Core.Projects; +using Flurl.Http; +using System.Buffers; +using System.Runtime.CompilerServices; + +namespace Bitbucket.Net; + +public partial class BitbucketClient +{ + /// + /// Retrieves branches for a repository. + /// + /// The project key. + /// The repository slug. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Optional base branch or tag filter. + /// Whether to include additional details. + /// Optional branch name filter. + /// Optional branch ordering. + /// Cancellation token. + /// A collection of branches. + public async Task> GetBranchesAsync(string projectKey, string repositorySlug, + int? maxPages = null, + int? limit = null, + int? start = null, + string? baseBranchOrTag = null, + bool? details = null, + string? filterText = null, + BranchOrderBy? orderBy = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["base"] = baseBranchOrTag, + ["details"] = details.HasValue ? BitbucketHelpers.BoolToString(details.Value) : null, + ["filterText"] = filterText, + ["orderBy"] = orderBy.HasValue ? BitbucketHelpers.BranchOrderByToString(orderBy.Value) : null, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/branches") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Streams all branches for a repository as an IAsyncEnumerable. + /// + public IAsyncEnumerable GetBranchesStreamAsync(string projectKey, string repositorySlug, + int? maxPages = null, + int? limit = null, + int? start = null, + string? baseBranchOrTag = null, + bool? details = null, + string? filterText = null, + BranchOrderBy? orderBy = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["base"] = baseBranchOrTag, + ["details"] = details.HasValue ? BitbucketHelpers.BoolToString(details.Value) : null, + ["filterText"] = filterText, + ["orderBy"] = orderBy.HasValue ? BitbucketHelpers.BranchOrderByToString(orderBy.Value) : null, + }; + + return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/branches") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken); + } + + /// + /// Creates a branch in a repository. + /// + /// The project key. + /// The repository slug. + /// The branch information. + /// Cancellation token. + /// The created branch. + public async Task CreateBranchAsync(string projectKey, string repositorySlug, BranchInfo branchInfo, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/branches") + .SendAsync(HttpMethod.Post, CreateJsonContent(branchInfo), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves the default branch for a repository. + /// + /// The project key. + /// The repository slug. + /// Cancellation token. + /// The default branch. + public async Task GetDefaultBranchAsync(string projectKey, string repositorySlug, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/branches/default") + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Sets the default branch for a repository. + /// + /// The project key. + /// The repository slug. + /// The target branch reference. + /// Cancellation token. + /// true if the default branch was updated; otherwise, false. + public async Task SetDefaultBranchAsync(string projectKey, string repositorySlug, BranchRef branchRef, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/branches") + .SendAsync(HttpMethod.Put, CreateJsonContent(branchRef), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Browses repository content at a specific ref. + /// + /// The project key. + /// The repository slug. + /// The ref (branch, tag, or commit). + /// Whether to include type information. + /// Whether to include blame metadata. + /// If true and blame is requested, omit file content. + /// Cancellation token. + /// The browsed item metadata. + public async Task BrowseProjectRepositoryAsync(string projectKey, string repositorySlug, string at, bool type = false, + bool blame = false, + bool noContent = false, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["at"] = at, + ["type"] = BitbucketHelpers.BoolToString(type), + }; + if (blame) + { + queryParamValues.Add("blame", value: null); + } + if (blame && noContent) + { + queryParamValues.Add("noContent", value: null); + } + + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/browse") + .SetQueryParams(queryParamValues, Flurl.NullValueHandling.NameOnly) + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Browses a specific path within a repository at a given ref. + /// + /// The project key. + /// The repository slug. + /// The path to browse. + /// The ref (branch, tag, or commit). + /// Whether to include type information. + /// Whether to include blame metadata. + /// If true and blame is requested, omit file content. + /// Cancellation token. + /// The browsed path item metadata. + public async Task BrowseProjectRepositoryPathAsync(string projectKey, string repositorySlug, string path, string at, bool type = false, + bool blame = false, + bool noContent = false, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["at"] = at, + ["type"] = BitbucketHelpers.BoolToString(type), + }; + if (blame) + { + queryParamValues.Add("blame", value: null); + } + if (blame && noContent) + { + queryParamValues.Add("noContent", value: null); + } + + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/browse/{path}") + .SetQueryParams(queryParamValues, Flurl.NullValueHandling.NameOnly) + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Gets the raw content of a file as a stream. This is optimal for large files as it doesn't buffer the entire content in memory. + /// + /// The project key. + /// The repository slug. + /// The file path within the repository. + /// Optional ref (branch, tag, or commit) to get the file content at. Defaults to default branch. + /// Cancellation token. + /// A stream containing the raw file content. Caller is responsible for disposing. + public async Task GetRawFileContentStreamAsync(string projectKey, string repositorySlug, string path, + string? at = null, + CancellationToken cancellationToken = default) + { + var request = GetProjectsReposUrl(projectKey, repositorySlug, $"/raw/{path}"); + + if (!string.IsNullOrEmpty(at)) + { + request = request.SetQueryParam("at", at); + } + + var response = await request + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + await HandleErrorsAsync(response, cancellationToken).ConfigureAwait(false); + return await ReadResponseStreamAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Streams the raw content of a file line by line. This is optimal for large text files. + /// + /// The project key. + /// The repository slug. + /// The file path within the repository. + /// Optional ref (branch, tag, or commit) to get the file content at. Defaults to default branch. + /// Cancellation token. + /// An async enumerable of lines from the file. + public async IAsyncEnumerable GetRawFileContentLinesStreamAsync(string projectKey, string repositorySlug, string path, + string? at = null, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var stream = await GetRawFileContentStreamAsync(projectKey, repositorySlug, path, at, cancellationToken).ConfigureAwait(false); + + await using (stream.ConfigureAwait(false)) + { + using var reader = new StreamReader(stream); + while (!reader.EndOfStream) + { + cancellationToken.ThrowIfCancellationRequested(); + var line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false); + if (line is not null) + { + yield return line; + } + } + } + } + + /// + /// Updates a file at the specified path in the repository. + /// Uses ArrayPool<byte> for zero-copy buffer management to minimize heap allocations. + /// + public async Task UpdateProjectRepositoryPathAsync(string projectKey, string repositorySlug, string path, + string fileName, + string branch, + string? message = null, + string? sourceCommitId = null, + string? sourceBranch = null, + CancellationToken cancellationToken = default) + { + if (!File.Exists(fileName)) + { + throw new ArgumentException($"File doesn't exist: {fileName}", nameof(fileName)); + } + + var fileInfo = new FileInfo(fileName); + int fileSize = checked((int)fileInfo.Length); + + // Use ArrayPool to rent a buffer instead of allocating new array + byte[] buffer = ArrayPool.Shared.Rent(fileSize); + try + { + int bytesRead; + var stm = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 4096, useAsync: true); + await using (stm.ConfigureAwait(false)) + { + bytesRead = await stm.ReadAsync(buffer.AsMemory(0, fileSize), cancellationToken).ConfigureAwait(false); + } + + // Create MemoryStream over the exact bytes read (not the rented buffer size) + using var memoryStream = new MemoryStream(buffer, 0, bytesRead, writable: false); + + var data = new DynamicMultipartFormDataContent + { + { new StreamContent(memoryStream), "content" }, + { new StringContent(branch), "branch" }, + { message, message == null ? null : new StringContent(message), "message" }, + { sourceCommitId, sourceCommitId == null ? null : new StringContent(sourceCommitId), "sourceCommitId" }, + { sourceBranch, sourceBranch == null ? null : new StringContent(sourceBranch), "sourceBranch" }, + }; + + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/browse/{path}") + .PutAsync(data.ToMultipartFormDataContent(), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + finally + { + // Always return the buffer to the pool + ArrayPool.Shared.Return(buffer); + } + } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Commits.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Commits.cs new file mode 100644 index 0000000..ac8a753 --- /dev/null +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Commits.cs @@ -0,0 +1,547 @@ +using Bitbucket.Net.Common; +using Bitbucket.Net.Common.Models; +using Bitbucket.Net.Models.Core.Projects; +using Flurl.Http; +using System.Runtime.CompilerServices; + +namespace Bitbucket.Net; + +public partial class BitbucketClient +{ + /// + /// Retrieves changes for a repository between two refs. + /// + /// The project key. + /// The repository slug. + /// The target ref. + /// Optional starting ref. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Cancellation token. + /// A collection of changes. + public async Task> GetChangesAsync(string projectKey, string repositorySlug, string until, string? since = null, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["since"] = since, + ["until"] = until, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/changes") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Streams changes for a repository between refs, yielding items as they are retrieved. + /// + /// The project key. + /// The repository slug. + /// The target ref. + /// Optional starting ref. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Token to cancel the operation. + /// An async enumerable of changes. + public IAsyncEnumerable GetChangesStreamAsync(string projectKey, string repositorySlug, string until, string? since = null, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["since"] = since, + ["until"] = until, + }; + + return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/changes") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken); + } + + /// + /// Retrieves commits for a repository. + /// + /// The project key. + /// The repository slug. + /// The ref to retrieve commits until. + /// Whether to follow renames. + /// Whether to ignore missing commits. + /// Merge commit inclusion policy. + /// Optional path filter. + /// Optional starting ref. + /// Whether to include commit counts. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Cancellation token. + /// A collection of commits. + public async Task> GetCommitsAsync(string projectKey, string repositorySlug, + string until, + bool followRenames = false, + bool ignoreMissing = false, + MergeCommits merges = MergeCommits.Exclude, + string? path = null, + string? since = null, + bool withCounts = false, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["followRenames"] = BitbucketHelpers.BoolToString(followRenames), + ["ignoreMissing"] = BitbucketHelpers.BoolToString(ignoreMissing), + ["merges"] = BitbucketHelpers.MergeCommitsToString(merges), + ["path"] = path, + ["since"] = since, + ["until"] = until, + ["withCounts"] = BitbucketHelpers.BoolToString(withCounts), + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/commits") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Streams all commits for a repository as an IAsyncEnumerable. + /// + public IAsyncEnumerable GetCommitsStreamAsync(string projectKey, string repositorySlug, + string until, + bool followRenames = false, + bool ignoreMissing = false, + MergeCommits merges = MergeCommits.Exclude, + string? path = null, + string? since = null, + bool withCounts = false, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["followRenames"] = BitbucketHelpers.BoolToString(followRenames), + ["ignoreMissing"] = BitbucketHelpers.BoolToString(ignoreMissing), + ["merges"] = BitbucketHelpers.MergeCommitsToString(merges), + ["path"] = path, + ["since"] = since, + ["until"] = until, + ["withCounts"] = BitbucketHelpers.BoolToString(withCounts), + }; + + return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/commits") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken); + } + + /// + /// Retrieves a commit by ID. + /// + /// The project key. + /// The repository slug. + /// The commit ID. + /// Optional path filter. + /// Cancellation token. + /// The requested commit. + public async Task GetCommitAsync(string projectKey, string repositorySlug, string commitId, string? path = null, CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["path"] = path, + }; + + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/commits/{commitId}") + .SetQueryParams(queryParamValues) + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves the list of file changes for a commit. + /// + /// The project key. + /// The repository slug. + /// The commit ID. + /// Optional starting commit ID. + /// Whether to include comment counts. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Cancellation token. + /// A collection of changes. + public async Task> GetCommitChangesAsync(string projectKey, string repositorySlug, string commitId, + string? since = null, + bool withComments = true, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["since"] = since, + ["withComments"] = BitbucketHelpers.BoolToString(withComments), + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/commits/{commitId}/changes") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Streams changes for a specific commit, yielding items as they are retrieved. + /// + /// The project key. + /// The repository slug. + /// The commit ID. + /// Optional starting commit ID. + /// Whether to include comment counts. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Token to cancel the operation. + /// An async enumerable of changes. + public IAsyncEnumerable GetCommitChangesStreamAsync(string projectKey, string repositorySlug, string commitId, + string? since = null, + bool withComments = true, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["since"] = since, + ["withComments"] = BitbucketHelpers.BoolToString(withComments), + }; + + return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/commits/{commitId}/changes") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken); + } + + /// + /// Retrieves comments for a commit. + /// + /// The project key. + /// The repository slug. + /// The commit ID. + /// The file path within the commit. + /// Optional starting comment ID. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Cancellation token. + /// A collection of comments. + public async Task> GetCommitCommentsAsync(string projectKey, string repositorySlug, string commitId, + string path, + string? since = null, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["path"] = path, + ["since"] = since, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/commits/{commitId}/comments") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Creates a comment on a commit. + /// + /// The project key. + /// The repository slug. + /// The commit ID. + /// The comment payload. + /// Optional starting comment ID for context. + /// Cancellation token. + /// The created comment reference. + public async Task CreateCommitCommentAsync(string projectKey, string repositorySlug, string commitId, + CommentInfo commentInfo, string? since = null, CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["since"] = since, + }; + + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/commits/{commitId}/comments") + .SetQueryParams(queryParamValues) + .SendAsync(HttpMethod.Post, CreateJsonContent(commentInfo), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves a specific commit comment by ID. + /// + /// The project key. + /// The repository slug. + /// The commit ID. + /// The comment ID. + /// Optional avatar size. + /// Cancellation token. + /// The requested comment reference. + public async Task GetCommitCommentAsync(string projectKey, string repositorySlug, string commitId, long commentId, + int? avatarSize = null, + CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/commits/{commitId}/comments/{commentId}") + .SetQueryParam("avatarSize", avatarSize) + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Updates the text of a commit comment. + /// + /// The project key. + /// The repository slug. + /// The commit ID. + /// The comment ID. + /// The updated comment text. + /// Cancellation token. + /// The updated comment reference. + public async Task UpdateCommitCommentAsync(string projectKey, string repositorySlug, string commitId, long commentId, + CommentText commentText, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/commits/{commitId}/comments/{commentId}") + .SendAsync(HttpMethod.Put, CreateJsonContent(commentText), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Deletes a commit comment. + /// + /// The project key. + /// The repository slug. + /// The commit ID. + /// The comment ID. + /// Optional comment version for concurrency. + /// Cancellation token. + /// true if deleted; otherwise, false. + public async Task DeleteCommitCommentAsync(string projectKey, string repositorySlug, string commitId, long commentId, + int version = -1, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["version"] = version, + }; + + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/commits/{commitId}/comments/{commentId}") + .SetQueryParams(queryParamValues) + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves a diff for a commit. + /// + /// The project key. + /// The repository slug. + /// The commit ID. + /// Whether to auto-detect source path. + /// Context lines to include. + /// Optional since commit. + /// Optional source path filter. + /// Whitespace handling strategy. + /// Whether to include comments. + /// Cancellation token. + /// The diff result. + public async Task GetCommitDiffAsync(string projectKey, string repositorySlug, string commitId, + bool autoSrcPath = false, + int contextLines = -1, + string? since = null, + string? srcPath = null, + string whitespace = "ignore-all", + bool withComments = true, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["autoSrcPath"] = BitbucketHelpers.BoolToString(autoSrcPath), + ["contextLines"] = contextLines, + ["since"] = since, + ["srcPath"] = srcPath, + ["whitespace"] = whitespace, + ["withComments"] = BitbucketHelpers.BoolToString(withComments), + }; + + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/commits/{commitId}/diff") + .SetQueryParams(queryParamValues) + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Streams the diff for a specific commit, yielding individual diff entries as they are parsed. + /// This is more memory-efficient for large diffs. + /// + /// The project key. + /// The repository slug. + /// The commit ID. + /// Auto source path. + /// Number of context lines. + /// Since commit. + /// Source path filter. + /// Whitespace handling. + /// Include comments. + /// Cancellation token. + /// An async enumerable of diffs. + public async IAsyncEnumerable GetCommitDiffStreamAsync(string projectKey, string repositorySlug, string commitId, + bool autoSrcPath = false, + int contextLines = -1, + string? since = null, + string? srcPath = null, + string whitespace = "ignore-all", + bool withComments = true, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["autoSrcPath"] = BitbucketHelpers.BoolToString(autoSrcPath), + ["contextLines"] = contextLines, + ["since"] = since, + ["srcPath"] = srcPath, + ["whitespace"] = whitespace, + ["withComments"] = BitbucketHelpers.BoolToString(withComments), + }; + + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/commits/{commitId}/diff") + .SetQueryParams(queryParamValues) + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + await HandleErrorsAsync(response, cancellationToken).ConfigureAwait(false); + var responseStream = await ReadResponseStreamAsync(response, cancellationToken).ConfigureAwait(false); + + await using (responseStream.ConfigureAwait(false)) + { + await foreach (var diff in DeserializeDiffsFromStreamAsync(responseStream, cancellationToken).ConfigureAwait(false)) + { + yield return diff; + } + } + } + + /// + /// Starts watching a commit for notifications. + /// + /// The project key. + /// The repository slug. + /// The commit ID. + /// Cancellation token. + /// true if watch was created; otherwise, false. + public async Task CreateCommitWatchAsync(string projectKey, string repositorySlug, string commitId, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/commits/{commitId}/watch") + .SendAsync(HttpMethod.Post, new StringContent(string.Empty), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Stops watching a commit. + /// + /// The project key. + /// The repository slug. + /// The commit ID. + /// Cancellation token. + /// true if the watch was removed; otherwise, false. + public async Task DeleteCommitWatchAsync(string projectKey, string repositorySlug, string commitId, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/commits/{commitId}/watch") + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Compare.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Compare.cs new file mode 100644 index 0000000..fcdd17d --- /dev/null +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Compare.cs @@ -0,0 +1,314 @@ +using Bitbucket.Net.Common; +using Bitbucket.Net.Common.Models; +using Bitbucket.Net.Models.Core.Projects; +using Flurl.Http; +using System.Runtime.CompilerServices; + +namespace Bitbucket.Net; + +public partial class BitbucketClient +{ + /// + /// Compares two refs and returns the list of changes. + /// + /// The project key. + /// The repository slug. + /// The source ref. + /// The target ref. + /// Optional source repository key for cross-repo compare. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Cancellation token. + /// A collection of changes between the refs. + public async Task> GetRepositoryCompareChangesAsync(string projectKey, string repositorySlug, string from, string to, + string? fromRepo = null, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["from"] = from, + ["to"] = to, + ["fromRepo"] = fromRepo, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/compare/changes") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Compares two refs and returns a diff. + /// + /// The project key. + /// The repository slug. + /// The source ref. + /// The target ref. + /// Optional source repository key for cross-repo compare. + /// Optional source path filter. + /// Number of context lines. + /// Whitespace handling strategy. + /// Cancellation token. + /// The diff between the refs. + public async Task GetRepositoryCompareDiffAsync(string projectKey, string repositorySlug, string from, string to, + string? fromRepo = null, + string? srcPath = null, + int contextLines = -1, + string whitespace = "ignore-all", + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["from"] = from, + ["to"] = to, + ["fromRepo"] = fromRepo, + ["srcPath"] = srcPath, + ["contextLines"] = contextLines, + ["whitespace"] = whitespace, + }; + + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/compare/diff") + .SetQueryParams(queryParamValues) + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Streams the compare diff between two refs, yielding individual diff entries as they are parsed. + /// This is more memory-efficient for large diffs. + /// + /// The project key. + /// The repository slug. + /// The source ref (branch, tag, or commit). + /// The target ref (branch, tag, or commit). + /// Optional source repository if comparing across forks. + /// Source path filter. + /// Number of context lines. + /// Whitespace handling. + /// Cancellation token. + /// An async enumerable of diffs. + public async IAsyncEnumerable GetRepositoryCompareDiffStreamAsync(string projectKey, string repositorySlug, string from, string to, + string? fromRepo = null, + string? srcPath = null, + int contextLines = -1, + string whitespace = "ignore-all", + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["from"] = from, + ["to"] = to, + ["fromRepo"] = fromRepo, + ["srcPath"] = srcPath, + ["contextLines"] = contextLines, + ["whitespace"] = whitespace, + }; + + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/compare/diff") + .SetQueryParams(queryParamValues) + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + await HandleErrorsAsync(response, cancellationToken).ConfigureAwait(false); + var responseStream = await ReadResponseStreamAsync(response, cancellationToken).ConfigureAwait(false); + + await using (responseStream.ConfigureAwait(false)) + { + await foreach (var diff in DeserializeDiffsFromStreamAsync(responseStream, cancellationToken).ConfigureAwait(false)) + { + yield return diff; + } + } + } + + /// + /// Compares two refs and returns the commits between them. + /// + /// The project key. + /// The repository slug. + /// The source ref. + /// The target ref. + /// Optional source repository key for cross-repo compare. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Cancellation token. + /// A collection of commits between the refs. + public async Task> GetRepositoryCompareCommitsAsync(string projectKey, string repositorySlug, string from, string to, + string? fromRepo = null, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["from"] = from, + ["to"] = to, + ["fromRepo"] = fromRepo, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/compare/commits") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Retrieves a repository diff between two commits. + /// + /// The project key. + /// The repository slug. + /// The target commit ID. + /// Number of context lines. + /// Optional starting commit ID. + /// Optional source path filter. + /// Whitespace handling strategy. + /// Cancellation token. + /// The diff result. + public async Task GetRepositoryDiffAsync(string projectKey, string repositorySlug, string until, + int contextLines = -1, + string? since = null, + string? srcPath = null, + string whitespace = "ignore-all", + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["contextLines"] = contextLines, + ["since"] = since, + ["srcPath"] = srcPath, + ["until"] = until, + ["whitespace"] = whitespace, + }; + + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/diff") + .SetQueryParams(queryParamValues) + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Streams the repository diff, yielding individual diff entries as they are parsed. + /// This is more memory-efficient for large diffs. + /// + /// The project key. + /// The repository slug. + /// The commit ID to diff until. + /// Number of context lines. + /// The commit ID to diff since. + /// Source path filter. + /// Whitespace handling. + /// Cancellation token. + /// An async enumerable of diffs. + public async IAsyncEnumerable GetRepositoryDiffStreamAsync(string projectKey, string repositorySlug, string until, + int contextLines = -1, + string? since = null, + string? srcPath = null, + string whitespace = "ignore-all", + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["contextLines"] = contextLines, + ["since"] = since, + ["srcPath"] = srcPath, + ["until"] = until, + ["whitespace"] = whitespace, + }; + + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/diff") + .SetQueryParams(queryParamValues) + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + await HandleErrorsAsync(response, cancellationToken).ConfigureAwait(false); + var responseStream = await ReadResponseStreamAsync(response, cancellationToken).ConfigureAwait(false); + + await using (responseStream.ConfigureAwait(false)) + { + await foreach (var diff in DeserializeDiffsFromStreamAsync(responseStream, cancellationToken).ConfigureAwait(false)) + { + yield return diff; + } + } + } + + /// + /// Retrieves file paths in a repository at the specified ref. + /// + /// The project key. + /// The repository slug. + /// Optional ref (branch, tag, commit). + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Cancellation token. + /// A collection of file paths. + public async Task> GetRepositoryFilesAsync(string projectKey, string repositorySlug, string? at = null, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["at"] = at, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/files") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Retrieves last-modified metadata for a repository at a ref. + /// + /// The project key. + /// The repository slug. + /// The ref (branch, tag, or commit). + /// Cancellation token. + /// Last modified information. + public async Task GetProjectRepositoryLastModifiedAsync(string projectKey, string repositorySlug, string at, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/last-modified") + .SetQueryParam("at", at) + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Projects.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Projects.cs new file mode 100644 index 0000000..58f43da --- /dev/null +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Projects.cs @@ -0,0 +1,483 @@ +using Bitbucket.Net.Common; +using Bitbucket.Net.Common.Models; +using Bitbucket.Net.Models.Core.Admin; +using Bitbucket.Net.Models.Core.Projects; +using Flurl.Http; +using System.Text.Json; + +namespace Bitbucket.Net; + +public partial class BitbucketClient +{ + /// + /// Gets the base projects URL. + /// + /// An targeting the projects endpoint. + private IFlurlRequest GetProjectsUrl() => GetBaseUrl() + .AppendPathSegment("/projects"); + + /// + /// Gets the projects URL for the specified path. + /// + /// The path to append. + /// An pointing to the projects path. + private IFlurlRequest GetProjectsUrl(string path) => GetProjectsUrl() + .AppendPathSegment(path); + + /// + /// Gets the URL for a specific project. + /// + /// The project key. + /// An pointing to the project. + private IFlurlRequest GetProjectUrl(string projectKey) => GetProjectsUrl() + .AppendPathSegment($"/{projectKey}"); + + /// + /// Gets the URL for a repository within a project. + /// + /// The project key. + /// The repository slug. + /// An pointing to the repository. + private IFlurlRequest GetProjectsReposUrl(string projectKey, string repositorySlug) => GetProjectsUrl($"/{projectKey}/repos/{repositorySlug}"); + + /// + /// Gets the URL for a specific path within a project repository. + /// + /// The project key. + /// The repository slug. + /// The additional path to append. + /// An pointing to the repository path. + private IFlurlRequest GetProjectsReposUrl(string projectKey, string repositorySlug, string path) => GetProjectsReposUrl(projectKey, repositorySlug) + .AppendPathSegment(path); + + /// + /// Retrieves projects accessible to the current user. + /// + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Optional project name filter. + /// Optional permission filter. + /// Token to cancel the operation. + /// A collection of projects. + public async Task> GetProjectsAsync( + int? maxPages = null, + int? limit = null, + int? start = null, + string? name = null, + Permissions? permission = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["name"] = name, + ["permission"] = BitbucketHelpers.PermissionToString(permission), + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsUrl() + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Streams projects accessible to the current user as they are retrieved, improving memory efficiency for large result sets. + /// + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Optional project name filter. + /// Optional permission filter. + /// Token to cancel the operation. + /// An async enumerable of projects. + public IAsyncEnumerable GetProjectsStreamAsync( + int? maxPages = null, + int? limit = null, + int? start = null, + string? name = null, + Permissions? permission = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["name"] = name, + ["permission"] = BitbucketHelpers.PermissionToString(permission), + }; + + return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsUrl() + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken); + } + + /// + /// Creates a project. + /// + /// The project definition. + /// Token to cancel the operation. + /// The created project. + public async Task CreateProjectAsync(ProjectDefinition projectDefinition, CancellationToken cancellationToken = default) + { + var response = await GetProjectsUrl() + .SendAsync(HttpMethod.Post, CreateJsonContent(projectDefinition), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Deletes a project. + /// + /// The project key. + /// Token to cancel the operation. + /// true if deletion succeeded; otherwise, false. + public async Task DeleteProjectAsync(string projectKey, CancellationToken cancellationToken = default) + { + var response = await GetProjectsUrl($"/{projectKey}") + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Updates a project. + /// + /// The project key. + /// The updated project definition. + /// Token to cancel the operation. + /// The updated project. + public async Task UpdateProjectAsync(string projectKey, ProjectDefinition projectDefinition, CancellationToken cancellationToken = default) + { + var response = await GetProjectsUrl($"/{projectKey}") + .SendAsync(HttpMethod.Put, CreateJsonContent(projectDefinition), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves a project by key. + /// + /// The project key. + /// Token to cancel the operation. + /// The requested project. + public async Task GetProjectAsync(string projectKey, CancellationToken cancellationToken = default) + { + var response = await GetProjectsUrl($"/{projectKey}") + .GetAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves user permissions for a project. + /// + /// The project key. + /// Optional filter string. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Optional avatar size. + /// Token to cancel the operation. + /// A collection of user permissions. + public async Task> GetProjectUserPermissionsAsync(string projectKey, string? filter = null, + int? maxPages = null, + int? limit = null, + int? start = null, + int? avatarSize = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["filter"] = filter, + ["avatarSize"] = avatarSize, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsUrl($"/{projectKey}/permissions/users") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Removes a user's permissions from a project. + /// + /// The project key. + /// The user name. + /// Token to cancel the operation. + /// true if removal succeeded; otherwise, false. + public async Task DeleteProjectUserPermissionsAsync(string projectKey, string userName, CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["name"] = userName, + }; + + var response = await GetProjectsUrl($"/{projectKey}/permissions/users") + .SetQueryParams(queryParamValues) + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Updates a user's permissions for a project. + /// + /// The project key. + /// The user name. + /// The permission to grant. + /// Token to cancel the operation. + /// true if the update succeeded; otherwise, false. + public async Task UpdateProjectUserPermissionsAsync(string projectKey, string userName, Permissions permission, CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["name"] = userName, + ["permission"] = BitbucketHelpers.PermissionToString(permission), + }; + + var response = await GetProjectsUrl($"/{projectKey}/permissions/users") + .SetQueryParams(queryParamValues) + .PutAsync(new StringContent(""), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves users with no permissions on a project. + /// + /// The project key. + /// Optional filter string. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Token to cancel the operation. + /// A collection of licensed users without project permissions. + public async Task> GetProjectUserPermissionsNoneAsync(string projectKey, string? filter = null, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["filter"] = filter, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsUrl($"/{projectKey}/permissions/users/none") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Retrieves group permissions for a project. + /// + /// The project key. + /// Optional filter string. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Token to cancel the operation. + /// A collection of group permissions. + public async Task> GetProjectGroupPermissionsAsync(string projectKey, string? filter = null, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["filter"] = filter, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsUrl($"/{projectKey}/permissions/groups") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Removes a group's permissions from a project. + /// + /// The project key. + /// The group name. + /// Token to cancel the operation. + /// true if the group permissions were removed; otherwise, false. + public async Task DeleteProjectGroupPermissionsAsync(string projectKey, string groupName, CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["name"] = groupName, + }; + + var response = await GetProjectsUrl($"/{projectKey}/permissions/groups") + .SetQueryParams(queryParamValues) + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Updates a group's permissions for a project. + /// + /// The project key. + /// The group name. + /// The permission to grant. + /// Token to cancel the operation. + /// true if the update succeeded; otherwise, false. + public async Task UpdateProjectGroupPermissionsAsync(string projectKey, string groupName, Permissions permission, CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["name"] = groupName, + ["permission"] = BitbucketHelpers.PermissionToString(permission), + }; + + var response = await GetProjectsUrl($"/{projectKey}/permissions/groups") + .SetQueryParams(queryParamValues) + .PutAsync(new StringContent(""), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves groups that currently have no permissions on a project. + /// + /// The project key. + /// Optional filter string. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Token to cancel the operation. + /// A collection of licensed users representing groups without permissions. + public async Task> GetProjectGroupPermissionsNoneAsync(string projectKey, string? filter = null, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["filter"] = filter, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsUrl($"/{projectKey}/permissions/groups/none") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Checks whether a permission is granted to all users for a project. + /// + /// The project key. + /// The permission to check. + /// Token to cancel the operation. + /// true if the permission is granted to all; otherwise, false. + public async Task IsProjectDefaultPermissionAsync(string projectKey, Permissions permission, CancellationToken cancellationToken = default) + { + var response = await GetProjectsUrl($"/{projectKey}/permissions/{BitbucketHelpers.PermissionToString(permission)}/all") + .GetAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, s => + { + using var doc = JsonDocument.Parse(s); + return doc.RootElement.GetProperty("permitted").GetBoolean(); + }, cancellationToken) + .ConfigureAwait(false); + } + + private async Task SetProjectDefaultPermissionAsync(string projectKey, Permissions permission, bool allow, CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["allow"] = BitbucketHelpers.BoolToString(allow), + }; + + var response = await GetProjectsUrl($"/{projectKey}/permissions/{BitbucketHelpers.PermissionToString(permission)}/all") + .SetQueryParams(queryParamValues) + .SendAsync(HttpMethod.Post, new StringContent(string.Empty), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Grants a permission to all users for a project. + /// + /// The project key. + /// The permission to grant. + /// Token to cancel the operation. + /// true if the permission was granted; otherwise, false. + public async Task GrantProjectPermissionToAllAsync(string projectKey, Permissions permission, CancellationToken cancellationToken = default) + { + return await SetProjectDefaultPermissionAsync(projectKey, permission, allow: true, cancellationToken).ConfigureAwait(false); + } + + /// + /// Revokes a permission from all users for a project. + /// + /// The project key. + /// The permission to revoke. + /// Token to cancel the operation. + /// true if the permission was revoked; otherwise, false. + public async Task RevokeProjectPermissionFromAllAsync(string projectKey, Permissions permission, CancellationToken cancellationToken = default) + { + return await SetProjectDefaultPermissionAsync(projectKey, permission, allow: false, cancellationToken).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequestComments.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequestComments.cs new file mode 100644 index 0000000..77a4dc9 --- /dev/null +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequestComments.cs @@ -0,0 +1,269 @@ +using Bitbucket.Net.Common; +using Bitbucket.Net.Common.Models; +using Bitbucket.Net.Models.Core.Projects; +using Flurl.Http; + +namespace Bitbucket.Net; + +public partial class BitbucketClient +{ + + /// + /// Creates a comment on a pull request. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// The comment text. + /// Optional parent comment ID to create a reply. + /// Optional diff type. + /// Optional from commit hash for anchoring. + /// Optional file path for anchoring. + /// Optional source path for move/rename anchors. + /// Optional to commit hash for anchoring. + /// Optional line number for anchoring. + /// Optional file type for anchoring. + /// Optional line type for anchoring. + /// Cancellation token. + /// The created comment reference. + public async Task CreatePullRequestCommentAsync(string projectKey, string repositorySlug, long pullRequestId, + string text, + string? parentId = null, + DiffTypes? diffType = null, + string? fromHash = null, + string? path = null, + string? srcPath = null, + string? toHash = null, + int? line = null, + FileTypes? fileType = null, + LineTypes? lineType = null, + CancellationToken cancellationToken = default) + { + // Build the comment payload dynamically to avoid sending empty anchor objects + // which Bitbucket Server 9.0 rejects with HTTP 500. + // See: BUG-003 - add_pull_request_comment returns 500 error + var data = new Dictionary(StringComparer.Ordinal) + { + ["text"] = text, + }; + + if (!string.IsNullOrEmpty(parentId)) + { + data["parent"] = new { id = parentId }; + } + + // Only include anchor if at least one anchor-related field is specified + // Empty anchor objects cause HTTP 500 on Bitbucket Server 9.0 + var hasAnchorData = diffType.HasValue + || !string.IsNullOrEmpty(fromHash) + || !string.IsNullOrEmpty(path) + || !string.IsNullOrEmpty(srcPath) + || !string.IsNullOrEmpty(toHash) + || line.HasValue + || fileType.HasValue + || lineType.HasValue; + + if (hasAnchorData) + { + data["anchor"] = new + { + diffType = BitbucketHelpers.DiffTypeToString(diffType), + fromHash, + path, + srcPath, + toHash, + line, + fileType = BitbucketHelpers.FileTypeToString(fileType), + lineType = BitbucketHelpers.LineTypeToString(lineType), + }; + } + + var response = await GetProjectsReposUrl(projectKey, repositorySlug) + .AppendPathSegment($"/pull-requests/{pullRequestId}/comments") + .SendAsync(HttpMethod.Post, CreateJsonContent(data), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves comments for a pull request path. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// The file path to filter comments. + /// Anchor state filter. + /// Diff type filter. + /// Optional from commit hash. + /// Optional to commit hash. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Optional avatar size. + /// Cancellation token. + /// A collection of pull request comments. + public async Task> GetPullRequestCommentsAsync(string projectKey, string repositorySlug, long pullRequestId, + string path, + AnchorStates anchorState = AnchorStates.Active, + DiffTypes diffType = DiffTypes.Effective, + string? fromHash = null, + string? toHash = null, + int? maxPages = null, + int? limit = null, + int? start = null, + int? avatarSize = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["avatarSize"] = avatarSize, + ["path"] = path, + ["anchorState"] = BitbucketHelpers.AnchorStateToString(anchorState), + ["diffType"] = BitbucketHelpers.DiffTypeToString(diffType), + ["fromHash"] = fromHash, + ["toHash"] = toHash, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/comments") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Streams comments for a pull request, yielding items as they are retrieved. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// The file path to filter comments for. + /// The anchor state filter. + /// The diff type filter. + /// Optional from commit hash. + /// Optional to commit hash. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Optional avatar size. + /// Token to cancel the operation. + /// An async enumerable of comment references. + public IAsyncEnumerable GetPullRequestCommentsStreamAsync(string projectKey, string repositorySlug, long pullRequestId, + string path, + AnchorStates anchorState = AnchorStates.Active, + DiffTypes diffType = DiffTypes.Effective, + string? fromHash = null, + string? toHash = null, + int? maxPages = null, + int? limit = null, + int? start = null, + int? avatarSize = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["avatarSize"] = avatarSize, + ["path"] = path, + ["anchorState"] = BitbucketHelpers.AnchorStateToString(anchorState), + ["diffType"] = BitbucketHelpers.DiffTypeToString(diffType), + ["fromHash"] = fromHash, + ["toHash"] = toHash, + }; + + return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/comments") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken); + } + + /// + /// Retrieves a single pull request comment by ID. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// The comment ID. + /// Optional avatar size. + /// Cancellation token. + /// The requested comment reference. + public async Task GetPullRequestCommentAsync(string projectKey, string repositorySlug, long pullRequestId, long commentId, + int? avatarSize = null, + CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug) + .AppendPathSegment($"/pull-requests/{pullRequestId}/comments/{commentId}") + .SetQueryParam("avatarSize", avatarSize) + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Updates a pull request comment. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// The comment ID. + /// The comment version for optimistic concurrency. + /// The updated comment text. + /// Cancellation token. + /// The updated comment reference. + public async Task UpdatePullRequestCommentAsync(string projectKey, string repositorySlug, long pullRequestId, long commentId, + int version, string text, CancellationToken cancellationToken = default) + { + var data = new + { + version, + text, + }; + + var response = await GetProjectsReposUrl(projectKey, repositorySlug) + .AppendPathSegment($"/pull-requests/{pullRequestId}/comments/{commentId}") + .SetQueryParam("version", version) + .SendAsync(HttpMethod.Put, CreateJsonContent(data), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Deletes a pull request comment. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// The comment ID. + /// Optional version for optimistic concurrency. + /// Cancellation token. + /// true if the comment was deleted; otherwise, false. + public async Task DeletePullRequestCommentAsync(string projectKey, string repositorySlug, long pullRequestId, long commentId, + int version = -1, + CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug) + .AppendPathSegment($"/pull-requests/{pullRequestId}/comments/{commentId}") + .SetQueryParam("version", version) + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequestDetails.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequestDetails.cs new file mode 100644 index 0000000..1f17f63 --- /dev/null +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequestDetails.cs @@ -0,0 +1,436 @@ +using Bitbucket.Net.Common; +using Bitbucket.Net.Common.Models; +using Bitbucket.Net.Models.Core.Projects; +using Flurl.Http; +using System.Runtime.CompilerServices; +using System.Text.Json; + +namespace Bitbucket.Net; + +public partial class BitbucketClient +{ + /// + /// Retrieves activities for a pull request. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// Optional starting activity ID. + /// Optional activity type filter. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Optional avatar size. + /// Cancellation token. + /// A collection of pull request activities. + public async Task> GetPullRequestActivitiesAsync(string projectKey, string repositorySlug, long pullRequestId, + long? fromId = null, + PullRequestFromTypes? fromType = null, + int? maxPages = null, + int? limit = null, + int? start = null, + int? avatarSize = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["fromId"] = fromId, + ["fromType"] = BitbucketHelpers.PullRequestFromTypeToString(fromType), + ["avatarSize"] = avatarSize, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/activities") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Streams activities for a pull request, yielding items as they are retrieved. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// Optional starting activity ID. + /// Optional from type filter. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Optional avatar size. + /// Token to cancel the operation. + /// An async enumerable of pull request activities. + public IAsyncEnumerable GetPullRequestActivitiesStreamAsync(string projectKey, string repositorySlug, long pullRequestId, + long? fromId = null, + PullRequestFromTypes? fromType = null, + int? maxPages = null, + int? limit = null, + int? start = null, + int? avatarSize = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["fromId"] = fromId, + ["fromType"] = BitbucketHelpers.PullRequestFromTypeToString(fromType), + ["avatarSize"] = avatarSize, + }; + + return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/activities") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken); + } + + + /// + /// Retrieves changes for a pull request. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// Scope of changes to include. + /// Optional since commit ID. + /// Optional until commit ID. + /// Whether to include comment counts. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Cancellation token. + /// A collection of changes. + public async Task> GetPullRequestChangesAsync(string projectKey, string repositorySlug, long pullRequestId, + ChangeScopes changeScope = ChangeScopes.All, + string? sinceId = null, + string? untilId = null, + bool withComments = true, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["changeScope"] = BitbucketHelpers.ChangeScopeToString(changeScope), + ["sinceId"] = sinceId, + ["untilId"] = untilId, + ["withComments"] = BitbucketHelpers.BoolToString(withComments), + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/changes") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Streams changes for a pull request, yielding items as they are retrieved. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// The change scope filter. + /// Optional since commit ID. + /// Optional until commit ID. + /// Whether to include comment counts. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Token to cancel the operation. + /// An async enumerable of changes. + public IAsyncEnumerable GetPullRequestChangesStreamAsync(string projectKey, string repositorySlug, long pullRequestId, + ChangeScopes changeScope = ChangeScopes.All, + string? sinceId = null, + string? untilId = null, + bool withComments = true, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["changeScope"] = BitbucketHelpers.ChangeScopeToString(changeScope), + ["sinceId"] = sinceId, + ["untilId"] = untilId, + ["withComments"] = BitbucketHelpers.BoolToString(withComments), + }; + + return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/changes") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken); + } + + + /// + /// Retrieves commits associated with a pull request. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// Whether to include change counts. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Cancellation token. + /// A collection of commits. + public async Task> GetPullRequestCommitsAsync(string projectKey, string repositorySlug, long pullRequestId, + bool withCounts = false, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["withCounts"] = BitbucketHelpers.BoolToString(withCounts), + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/commits") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Streams all commits for a pull request as an IAsyncEnumerable. + /// + public IAsyncEnumerable GetPullRequestCommitsStreamAsync(string projectKey, string repositorySlug, long pullRequestId, + bool withCounts = false, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["withCounts"] = BitbucketHelpers.BoolToString(withCounts), + }; + + return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/commits") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken); + } + + /// + /// Retrieves the diff for a pull request. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// Number of context lines to include. + /// Diff type. + /// Optional since commit ID. + /// Optional source path filter. + /// Optional until commit ID. + /// Whitespace handling option. + /// Whether to include comments. + /// Cancellation token. + /// Differences for the pull request. + public async Task GetPullRequestDiffAsync(string projectKey, string repositorySlug, long pullRequestId, + int contextLines = -1, + DiffTypes diffType = DiffTypes.Effective, + string? sinceId = null, + string? srcPath = null, + string? untilId = null, + string whitespace = "ignore-all", + bool withComments = true, + CancellationToken cancellationToken = default) + { + var queryParamValues = CreatePullRequestDiffQueryParams(contextLines, diffType, sinceId, srcPath, untilId, whitespace, withComments); + + var response = await GetProjectsReposUrl(projectKey, repositorySlug) + .AppendPathSegment($"/pull-requests/{pullRequestId}/diff") + .SetQueryParams(queryParamValues) + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Streams diff entries for a pull request as an . + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// Number of context lines to include. + /// Diff type. + /// Optional since commit ID. + /// Optional source path filter. + /// Optional until commit ID. + /// Whitespace handling option. + /// Whether to include comments. + /// Cancellation token. + /// A stream of diff entries. + public async IAsyncEnumerable GetPullRequestDiffStreamAsync(string projectKey, string repositorySlug, long pullRequestId, + int contextLines = -1, + DiffTypes diffType = DiffTypes.Effective, + string? sinceId = null, + string? srcPath = null, + string? untilId = null, + string whitespace = "ignore-all", + bool withComments = true, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + var queryParamValues = CreatePullRequestDiffQueryParams(contextLines, diffType, sinceId, srcPath, untilId, whitespace, withComments); + var response = await GetProjectsReposUrl(projectKey, repositorySlug) + .AppendPathSegment($"/pull-requests/{pullRequestId}/diff") + .SetQueryParams(queryParamValues) + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + await HandleErrorsAsync(response, cancellationToken).ConfigureAwait(false); + var responseStream = await ReadResponseStreamAsync(response, cancellationToken).ConfigureAwait(false); + + try + { + await foreach (var diff in DeserializePullRequestDiffsAsync(responseStream, cancellationToken).ConfigureAwait(false)) + { + yield return diff; + } + } + finally + { + await responseStream.DisposeAsync().ConfigureAwait(false); + } + } + + /// + /// Retrieves the diff for a specific path within a pull request. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// The file path to filter by. + /// Number of context lines to include. + /// Diff type. + /// Optional since commit ID. + /// Optional source path filter. + /// Optional until commit ID. + /// Whitespace handling option. + /// Whether to include comments. + /// Cancellation token. + /// Differences for the specified path. + public async Task GetPullRequestDiffPathAsync(string projectKey, string repositorySlug, long pullRequestId, + string path, + int contextLines = -1, + DiffTypes diffType = DiffTypes.Effective, + string? sinceId = null, + string? srcPath = null, + string? untilId = null, + string whitespace = "ignore-all", + bool withComments = true, + CancellationToken cancellationToken = default) + { + var queryParamValues = CreatePullRequestDiffQueryParams(contextLines, diffType, sinceId, srcPath, untilId, whitespace, withComments); + + var response = await GetProjectsReposUrl(projectKey, repositorySlug) + .AppendPathSegment($"/pull-requests/{pullRequestId}/diff/{path}") + .SetQueryParams(queryParamValues) + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + private static Dictionary CreatePullRequestDiffQueryParams(int contextLines, DiffTypes diffType, string? sinceId, + string? srcPath, string? untilId, string whitespace, bool withComments) + { + return new Dictionary(StringComparer.Ordinal) + { + ["contextLines"] = contextLines, + ["diffType"] = BitbucketHelpers.DiffTypeToString(diffType), + ["sinceId"] = sinceId, + ["srcPath"] = srcPath, + ["untilId"] = untilId, + ["whitespace"] = whitespace, + ["withComments"] = BitbucketHelpers.BoolToString(withComments), + }; + } + + private static async IAsyncEnumerable DeserializePullRequestDiffsAsync(Stream responseStream, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + await foreach (var diff in DeserializeDiffsFromStreamAsync(responseStream, cancellationToken).ConfigureAwait(false)) + { + yield return diff; + } + } + + /// + /// Deserializes diff entries from a JSON stream containing a "diffs" array. + /// Used by all diff streaming methods (commit, repository, compare, pull request). + /// Uses zero-copy deserialization directly from JsonElement to avoid intermediate string allocations. + /// + private static async IAsyncEnumerable DeserializeDiffsFromStreamAsync(Stream responseStream, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + using var doc = await JsonDocument.ParseAsync(responseStream, cancellationToken: cancellationToken).ConfigureAwait(false); + + if (!doc.RootElement.TryGetProperty("diffs", out var diffsArray) || diffsArray.ValueKind != JsonValueKind.Array) + { + yield break; + } + + foreach (var diffElement in diffsArray.EnumerateArray()) + { + cancellationToken.ThrowIfCancellationRequested(); + + // Zero-copy: Deserialize directly from JsonElement instead of GetRawText() string allocation + var diff = diffElement.Deserialize(s_jsonOptions); + if (diff is not null) + { + yield return diff; + } + } + } + + // Note: MoveToDiffArrayAsync is no longer needed with System.Text.Json approach + +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequests.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequests.cs new file mode 100644 index 0000000..c964b2d --- /dev/null +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.PullRequests.cs @@ -0,0 +1,552 @@ +using Bitbucket.Net.Common; +using Bitbucket.Net.Common.Models; +using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Core.Users; +using Flurl.Http; + +namespace Bitbucket.Net; + +public partial class BitbucketClient +{ + /// + /// Retrieves participants related to pull requests in a repository. + /// + /// The project key. + /// The repository slug. + /// Direction of pull requests to consider. + /// Optional filter string. + /// Optional role filter. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Cancellation token. + /// A collection of identities. + public async Task> GetRepositoryParticipantsAsync(string projectKey, string repositorySlug, + PullRequestDirections direction = PullRequestDirections.Incoming, + string? filter = null, + Roles? role = null, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["direction"] = BitbucketHelpers.PullRequestDirectionToString(direction), + ["filter"] = filter, + ["role"] = BitbucketHelpers.RoleToString(role), + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/participants") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Retrieves pull requests for a repository with optional filtering. + /// + /// The project key. + /// The repository slug. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Pull request direction filter. + /// Optional branch filter. + /// Pull request state. + /// Ordering option. + /// Whether to include attributes. + /// Whether to include properties. + /// Cancellation token. + /// A collection of pull requests. + public async Task> GetPullRequestsAsync(string projectKey, string repositorySlug, + int? maxPages = null, + int? limit = null, + int? start = null, + PullRequestDirections direction = PullRequestDirections.Incoming, + string? branchId = null, + PullRequestStates state = PullRequestStates.Open, + PullRequestOrders order = PullRequestOrders.Newest, + bool withAttributes = true, + bool withProperties = true, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["direction"] = BitbucketHelpers.PullRequestDirectionToString(direction), + ["at"] = branchId, + ["state"] = BitbucketHelpers.PullRequestStateToString(state), + ["order"] = BitbucketHelpers.PullRequestOrderToString(order), + ["withAttributes"] = BitbucketHelpers.BoolToString(withAttributes), + ["withProperties"] = BitbucketHelpers.BoolToString(withProperties), + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/pull-requests") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Streams all pull requests for a repository as an IAsyncEnumerable. + /// + public IAsyncEnumerable GetPullRequestsStreamAsync(string projectKey, string repositorySlug, + int? maxPages = null, + int? limit = null, + int? start = null, + PullRequestDirections direction = PullRequestDirections.Incoming, + string? branchId = null, + PullRequestStates state = PullRequestStates.Open, + PullRequestOrders order = PullRequestOrders.Newest, + bool withAttributes = true, + bool withProperties = true, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["direction"] = BitbucketHelpers.PullRequestDirectionToString(direction), + ["at"] = branchId, + ["state"] = BitbucketHelpers.PullRequestStateToString(state), + ["order"] = BitbucketHelpers.PullRequestOrderToString(order), + ["withAttributes"] = BitbucketHelpers.BoolToString(withAttributes), + ["withProperties"] = BitbucketHelpers.BoolToString(withProperties), + }; + + return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/pull-requests") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken); + } + + /// + /// Creates a pull request in a repository. + /// + /// The project key. + /// The repository slug. + /// The pull request payload. + /// Cancellation token. + /// The created pull request. + public async Task CreatePullRequestAsync(string projectKey, string repositorySlug, PullRequestInfo pullRequestInfo, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/pull-requests") + .SendAsync(HttpMethod.Post, CreateJsonContent(pullRequestInfo), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves a pull request by ID. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// Cancellation token. + /// The requested pull request. + public async Task GetPullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}") + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Updates a pull request. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// The update payload. + /// Cancellation token. + /// The updated pull request. + public async Task UpdatePullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, PullRequestUpdate pullRequestUpdate, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}") + .SendAsync(HttpMethod.Put, CreateJsonContent(pullRequestUpdate), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Deletes a pull request. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// Version info for optimistic concurrency. + /// Cancellation token. + /// true if the pull request was deleted; otherwise, false. + public async Task DeletePullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, VersionInfo versionInfo, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}") + .SendAsync(HttpMethod.Delete, CreateJsonContent(versionInfo), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + /// + /// Declines a pull request. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// Optional version for optimistic concurrency. + /// Cancellation token. + /// true if the pull request was declined; otherwise, false. + public async Task DeclinePullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, int version = -1, CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["version"] = version, + }; + + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/decline") + .SetQueryParams(queryParamValues) + .SendAsync(HttpMethod.Post, new StringContent(string.Empty), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves the merge state for a pull request. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// Optional version for optimistic concurrency. + /// Cancellation token. + /// The merge state. + public async Task GetPullRequestMergeStateAsync(string projectKey, string repositorySlug, long pullRequestId, int version = -1, CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["version"] = version, + }; + + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/merge") + .SetQueryParams(queryParamValues) + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Gets the merge base (common ancestor) commit for a pull request. + /// This is the best common ancestor between the latest commits of the source and target branches. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// Cancellation token. + /// The merge base commit, or null if not found (HTTP 204 - no common ancestor exists). + /// + /// This endpoint is useful for creating line-specific comments on pull requests. + /// The returned commit ID can be used as the fromHash parameter when creating anchored comments, + /// while the toHash can be obtained from on the pull request's FromRef. + /// + public async Task GetPullRequestMergeBaseAsync(string projectKey, string repositorySlug, long pullRequestId, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/merge-base") + .AllowHttpStatus(204) + .GetAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + // HTTP 204 indicates no common ancestor exists (e.g., unrelated histories) + if (response.StatusCode == 204) + { + return null; + } + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Merges a pull request. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// Optional version for optimistic concurrency. + /// Cancellation token. + /// The merged pull request. + public async Task MergePullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, int version = -1, CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["version"] = version, + }; + + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/merge") + .SetQueryParams(queryParamValues) + .SendAsync(HttpMethod.Post, new StringContent(string.Empty), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Reopens a declined pull request. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// Optional version for optimistic concurrency. + /// Cancellation token. + /// The reopened pull request. + public async Task ReopenPullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, int version = -1, CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["version"] = version, + }; + + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/reopen") + .SetQueryParams(queryParamValues) + .SendAsync(HttpMethod.Post, new StringContent(string.Empty), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Approves a pull request. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// Cancellation token. + /// The reviewer entry reflecting the approval. + public async Task ApprovePullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/approve") + .SendAsync(HttpMethod.Post, new StringContent(string.Empty), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Removes an approval from a pull request. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// Cancellation token. + /// The reviewer entry after removal. + public async Task DeletePullRequestApprovalAsync(string projectKey, string repositorySlug, long pullRequestId, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/approve") + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves participants for a pull request. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Optional avatar size. + /// Cancellation token. + /// A collection of participants. + public async Task> GetPullRequestParticipantsAsync(string projectKey, string repositorySlug, long pullRequestId, + int? maxPages = null, + int? limit = null, + int? start = null, + int? avatarSize = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["avatarSize"] = avatarSize, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug) + .AppendPathSegment($"/pull-requests/{pullRequestId}/participants") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Streams participants for a pull request, yielding items as they are retrieved. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Optional avatar size. + /// Token to cancel the operation. + /// An async enumerable of participants. + public IAsyncEnumerable GetPullRequestParticipantsStreamAsync(string projectKey, string repositorySlug, long pullRequestId, + int? maxPages = null, + int? limit = null, + int? start = null, + int? avatarSize = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["avatarSize"] = avatarSize, + }; + + return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug) + .AppendPathSegment($"/pull-requests/{pullRequestId}/participants") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken); + } + + /// + /// Assigns a role to a user in a pull request. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// The user to assign. + /// The role to assign. + /// Cancellation token. + /// The created participant entry. + public async Task AssignUserRoleToPullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, + Named named, + Roles role, + CancellationToken cancellationToken = default) + { + var data = new + { + user = named, + role = BitbucketHelpers.RoleToString(role), + }; + + var response = await GetProjectsReposUrl(projectKey, repositorySlug) + .AppendPathSegment($"/pull-requests/{pullRequestId}/participants") + .SendAsync(HttpMethod.Post, CreateJsonContent(data), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Deletes a participant from a pull request by username. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// The username to remove. + /// Cancellation token. + /// true if removal succeeded; otherwise, false. + public async Task DeletePullRequestParticipantAsync(string projectKey, string repositorySlug, long pullRequestId, string userName, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug) + .AppendPathSegment($"/pull-requests/{pullRequestId}/participants") + .SetQueryParam("username", userName) + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Updates a participant's approval status on a pull request. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// The user slug to update. + /// The user identity. + /// Whether the participant approves the PR. + /// The participant status. + /// Cancellation token. + /// The updated participant entry. + public async Task UpdatePullRequestParticipantStatus(string projectKey, string repositorySlug, long pullRequestId, + string userSlug, + Named named, + bool approved, + ParticipantStatus participantStatus, + CancellationToken cancellationToken = default) + { + var data = new + { + user = named, + approved = BitbucketHelpers.BoolToString(approved), + status = BitbucketHelpers.ParticipantStatusToString(participantStatus), + }; + + var response = await GetProjectsReposUrl(projectKey, repositorySlug) + .AppendPathSegment($"/pull-requests/{pullRequestId}/participants/{userSlug}") + .SendAsync(HttpMethod.Put, CreateJsonContent(data), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Removes a participant from a pull request by user slug. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// The user slug to remove. + /// Cancellation token. + /// true if removal succeeded; otherwise, false. + public async Task UnassignUserFromPullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, string userSlug, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug) + .AppendPathSegment($"/pull-requests/{pullRequestId}/participants/{userSlug}") + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Repositories.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Repositories.cs new file mode 100644 index 0000000..5fbb358 --- /dev/null +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Repositories.cs @@ -0,0 +1,547 @@ +using Bitbucket.Net.Common; +using Bitbucket.Net.Common.Models; +using Bitbucket.Net.Models.Core.Admin; +using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Core.Users; +using Flurl.Http; + +namespace Bitbucket.Net; + +public partial class BitbucketClient +{ + /// + /// Retrieves repositories for a project. + /// + /// The project key. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Token to cancel the operation. + /// A collection of repositories. + public async Task> GetProjectRepositoriesAsync(string projectKey, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsUrl($"/{projectKey}/repos") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Streams all repositories for a project as an . + /// + public IAsyncEnumerable GetProjectRepositoriesStreamAsync(string projectKey, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + }; + + return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsUrl($"/{projectKey}/repos") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken); + } + + /// + /// Creates a repository within a project. + /// + /// The project key. + /// The repository name. + /// Optional SCM identifier (default is git). + /// Token to cancel the operation. + /// The created repository. + public async Task CreateProjectRepositoryAsync(string projectKey, string repositoryName, string scmId = "git", CancellationToken cancellationToken = default) + { + var data = new + { + name = repositoryName, + scmId, + }; + + var response = await GetProjectsUrl($"/{projectKey}/repos") + .SendAsync(HttpMethod.Post, CreateJsonContent(data), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves a repository within a project. + /// + /// The project key. + /// The repository slug. + /// Token to cancel the operation. + /// The requested repository. + public async Task GetProjectRepositoryAsync(string projectKey, string repositorySlug, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug) + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Creates a fork of a repository, optionally targeting another project or slug. + /// + /// The source project key. + /// The source repository slug. + /// Optional target project key for the fork. + /// Optional target repository slug. + /// Optional display name for the fork. + /// Token to cancel the operation. + /// The created repository fork. + public async Task CreateProjectRepositoryForkAsync(string projectKey, string repositorySlug, string? targetProjectKey = null, string? targetSlug = null, string? targetName = null, CancellationToken cancellationToken = default) + { + var data = new + { + slug = targetSlug ?? repositorySlug, + name = targetName, + project = targetProjectKey == null ? null : new ProjectRef { Key = targetProjectKey }, + }; + + var response = await GetProjectsReposUrl(projectKey, repositorySlug) + .SendAsync(HttpMethod.Post, CreateJsonContent(data), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Schedules a repository for deletion. + /// + /// The project key. + /// The repository slug. + /// Token to cancel the operation. + /// true if the repository was scheduled for deletion; otherwise, false. + public async Task ScheduleProjectRepositoryForDeletionAsync(string projectKey, string repositorySlug, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug) + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Updates repository metadata such as name, forkability, project, or visibility. + /// + /// The project key. + /// The repository slug. + /// Optional new repository name. + /// Optional forkable flag. + /// Optional target project key. + /// Optional public visibility flag. + /// Token to cancel the operation. + /// The updated repository. + public async Task UpdateProjectRepositoryAsync(string projectKey, string repositorySlug, + string? targetName = null, + bool? isForkable = null, + string? targetProjectKey = null, + bool? isPublic = null, + CancellationToken cancellationToken = default) + { + var data = new + { + name = targetName, + forkable = isForkable, + project = targetProjectKey == null ? null : new ProjectRef { Key = targetProjectKey }, + @public = isPublic, + }; + + var response = await GetProjectsReposUrl(projectKey, repositorySlug) + .SendAsync(HttpMethod.Put, CreateJsonContent(data), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves forks of a repository. + /// + /// The project key. + /// The repository slug. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Token to cancel the operation. + /// A collection of repository forks. + public async Task> GetProjectRepositoryForksAsync(string projectKey, string repositorySlug, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/forks") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Recreates a repository in place. + /// + /// The project key. + /// The repository slug. + /// Token to cancel the operation. + /// The recreated repository. + public async Task RecreateProjectRepositoryAsync(string projectKey, string repositorySlug, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/recreate") + .SendAsync(HttpMethod.Post, new StringContent(string.Empty), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves repositories related to the specified repository (e.g., forks). + /// + /// The project key. + /// The repository slug. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Token to cancel the operation. + /// A collection of related repository forks. + public async Task> GetRelatedProjectRepositoriesAsync(string projectKey, string repositorySlug, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/related") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Retrieves an archive (zip/tar) of a repository at a specific ref. + /// + /// The project key. + /// The repository slug. + /// The ref (commit/branch/tag) to archive. + /// The archive file name. + /// The archive format. + /// Optional path filter. + /// Optional archive prefix. + /// Token to cancel the operation. + /// Archive bytes. + public async Task GetProjectRepositoryArchiveAsync(string projectKey, string repositorySlug, + string at, + string fileName, + ArchiveFormats archiveFormat, + string path, + string prefix, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["at"] = at, + ["fileName"] = fileName, + ["format"] = BitbucketHelpers.ArchiveFormatToString(archiveFormat), + ["path"] = path, + ["prefix"] = prefix, + }; + + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/archive") + .SetQueryParams(queryParamValues) + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + await HandleErrorsAsync(response, cancellationToken).ConfigureAwait(false); + return await ReadResponseBytesAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves group permissions for a repository. + /// + /// The project key. + /// The repository slug. + /// Optional filter string. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Token to cancel the operation. + /// A collection of group permissions. + public async Task> GetProjectRepositoryGroupPermissionsAsync(string projectKey, string repositorySlug, + string? filter = null, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["filter"] = filter, + ["limit"] = limit, + ["start"] = start, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/permissions/groups") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Updates a group's permissions for a repository. + /// + /// The project key. + /// The repository slug. + /// The permission to grant. + /// The group name. + /// Token to cancel the operation. + /// true if the update succeeded; otherwise, false. + public async Task UpdateProjectRepositoryGroupPermissionsAsync(string projectKey, string repositorySlug, Permissions permission, string name, CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["permission"] = BitbucketHelpers.PermissionToString(permission), + ["name"] = name, + }; + + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/permissions/groups") + .SetQueryParams(queryParamValues) + .SendAsync(HttpMethod.Put, new StringContent(string.Empty), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Removes a group's permissions from a repository. + /// + /// The project key. + /// The repository slug. + /// The group name. + /// Token to cancel the operation. + /// true if the permissions were removed; otherwise, false. + public async Task DeleteProjectRepositoryGroupPermissionsAsync(string projectKey, string repositorySlug, string name, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/permissions/groups") + .SetQueryParam("name", name) + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves groups or users without permissions on a repository. + /// + /// The project key. + /// The repository slug. + /// Optional filter string. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Token to cancel the operation. + /// A collection of removable group or user entries. + public async Task> GetProjectRepositoryGroupPermissionsNoneAsync(string projectKey, string repositorySlug, + string? filter = null, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["filter"] = filter, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/permissions/groups/none") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Retrieves user permissions for a repository. + /// + /// The project key. + /// The repository slug. + /// Optional filter string. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Optional avatar size. + /// Token to cancel the operation. + /// A collection of user permissions. + public async Task> GetProjectRepositoryUserPermissionsAsync(string projectKey, string repositorySlug, + string? filter = null, + int? maxPages = null, + int? limit = null, + int? start = null, + int? avatarSize = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["filter"] = filter, + ["limit"] = limit, + ["start"] = start, + ["avatarSize"] = avatarSize, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/permissions/users") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Updates a user's permissions for a repository. + /// + /// The project key. + /// The repository slug. + /// The permission to grant. + /// The user name. + /// Token to cancel the operation. + /// true if the update succeeded; otherwise, false. + public async Task UpdateProjectRepositoryUserPermissionsAsync(string projectKey, string repositorySlug, Permissions permission, string name, CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["permission"] = BitbucketHelpers.PermissionToString(permission), + ["name"] = name, + }; + + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/permissions/users") + .SetQueryParams(queryParamValues) + .SendAsync(HttpMethod.Put, new StringContent(string.Empty), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Removes a user's permissions from a repository. + /// + /// The project key. + /// The repository slug. + /// The user name. + /// Optional avatar size. + /// Token to cancel the operation. + /// true if the permissions were removed; otherwise, false. + public async Task DeleteProjectRepositoryUserPermissionsAsync(string projectKey, string repositorySlug, string name, + int? avatarSize = null, + CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/permissions/users") + .SetQueryParam("name", name) + .SetQueryParam("avatarSize", avatarSize) + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves users who have no permissions on a repository. + /// + /// The project key. + /// The repository slug. + /// Optional filter string. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Token to cancel the operation. + /// A collection of users without repository permissions. + public async Task> GetProjectRepositoryUserPermissionsNoneAsync(string projectKey, string repositorySlug, + string? filter = null, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["filter"] = filter, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/permissions/users/none") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.RepositorySettings.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.RepositorySettings.cs new file mode 100644 index 0000000..aff4026 --- /dev/null +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.RepositorySettings.cs @@ -0,0 +1,595 @@ +using Bitbucket.Net.Common; +using Bitbucket.Net.Common.Models; +using Bitbucket.Net.Models.Core.Admin; +using Bitbucket.Net.Models.Core.Projects; +using Flurl.Http; + +namespace Bitbucket.Net; + +public partial class BitbucketClient +{ + /// + /// Retrieves raw content from a file path in a repository. + /// + /// The project key. + /// The repository slug. + /// The file path to fetch. + /// Optional ref (branch, tag, commit). + /// Whether to render markup. + /// Whether to hard wrap the output. + /// Whether to HTML-escape the output. + /// Cancellation token. + /// A stream containing the raw content. + public async Task RetrieveRawContentAsync(string projectKey, string repositorySlug, string path, + string? at = null, + bool markup = false, + bool hardWrap = true, + bool htmlEscape = true, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["at"] = at, + ["markup"] = BitbucketHelpers.BoolToString(markup), + ["hardWrap"] = BitbucketHelpers.BoolToString(hardWrap), + ["htmlEscape"] = BitbucketHelpers.BoolToString(htmlEscape), + }; + + var response = await GetProjectsReposUrl(projectKey, repositorySlug) + .AppendPathSegment($"/raw/{path}") + .SetQueryParams(queryParamValues) + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + await HandleErrorsAsync(response, cancellationToken).ConfigureAwait(false); + return await ReadResponseStreamAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves pull request settings for a repository. + /// + /// The project key. + /// The repository slug. + /// Cancellation token. + /// The pull request settings. + public async Task GetProjectRepositoryPullRequestSettingsAsync(string projectKey, string repositorySlug, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/settings/pull-requests") + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Updates pull request settings for a repository. + /// + /// The project key. + /// The repository slug. + /// The settings payload. + /// Cancellation token. + /// The updated pull request settings. + public async Task UpdateProjectRepositoryPullRequestSettingsAsync(string projectKey, string repositorySlug, + PullRequestSettings pullRequestSettings, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/settings/pull-requests") + .SendAsync(HttpMethod.Post, CreateJsonContent(pullRequestSettings), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves repository hooks. + /// + /// The project key. + /// The repository slug. + /// Optional hook type filter. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Cancellation token. + /// A collection of hooks. + public async Task> GetProjectRepositoryHooksSettingsAsync(string projectKey, string repositorySlug, + HookTypes? hookType = null, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["type"] = hookType, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/settings/hooks") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Retrieves a specific hook's settings. + /// + /// The project key. + /// The repository slug. + /// The hook key. + /// Cancellation token. + /// The hook configuration. + public async Task GetProjectRepositoryHookSettingsAsync(string projectKey, string repositorySlug, string hookKey, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/settings/hooks/{hookKey}") + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Deletes a repository hook. + /// + /// The project key. + /// The repository slug. + /// The hook key. + /// Cancellation token. + /// true if deletion succeeded; otherwise, false. + public async Task DeleteProjectRepositoryHookSettingsAsync(string projectKey, string repositorySlug, string hookKey, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/settings/hooks/{hookKey}") + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Enables a repository hook, optionally providing settings. + /// + /// The project key. + /// The repository slug. + /// The hook key. + /// Optional hook settings. + /// Cancellation token. + /// The enabled hook. + public async Task EnableProjectRepositoryHookAsync(string projectKey, string repositorySlug, string hookKey, object? hookSettings = null, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/settings/hooks/{hookKey}/enabled") + .SendAsync(HttpMethod.Put, CreateJsonContent(hookSettings), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Disables a repository hook. + /// + /// The project key. + /// The repository slug. + /// The hook key. + /// Cancellation token. + /// The disabled hook. + public async Task DisableProjectRepositoryHookAsync(string projectKey, string repositorySlug, string hookKey, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/settings/hooks/{hookKey}/enabled") + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves all settings for a repository hook. + /// + /// The project key. + /// The repository slug. + /// The hook key. + /// Cancellation token. + /// A dictionary of hook settings. + public async Task> GetProjectRepositoryHookAllSettingsAsync(string projectKey, string repositorySlug, string hookKey, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/settings/hooks/{hookKey}/settings") + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Updates all settings for a repository hook. + /// + /// The project key. + /// The repository slug. + /// The hook key. + /// The settings payload. + /// Cancellation token. + /// The updated settings. + public async Task> UpdateProjectRepositoryHookAllSettingsAsync(string projectKey, string repositorySlug, string hookKey, + Dictionary allSettings, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/settings/hooks/{hookKey}/settings") + .SendAsync(HttpMethod.Put, CreateJsonContent(allSettings), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves merge strategies for pull requests within a project SCM. + /// + /// The project key. + /// The SCM identifier. + /// Cancellation token. + /// The pull request settings. + public async Task GetProjectPullRequestsMergeStrategiesAsync(string projectKey, string scmId, CancellationToken cancellationToken = default) + { + var response = await GetProjectUrl(projectKey) + .AppendPathSegment($"/settings/pull-requests/{scmId}") + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Updates merge strategies for pull requests within a project SCM. + /// + /// The project key. + /// The SCM identifier. + /// The merge strategies payload. + /// Cancellation token. + /// The updated merge strategies. + public async Task UpdateProjectPullRequestsMergeStrategiesAsync(string projectKey, string scmId, MergeStrategies mergeStrategies, CancellationToken cancellationToken = default) + { + var response = await GetProjectUrl(projectKey) + .AppendPathSegment($"/settings/pull-requests/{scmId}") + .SendAsync(HttpMethod.Post, CreateJsonContent(mergeStrategies), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves tags from a repository. + /// + /// The project key. + /// The repository slug. + /// Filter text for tag names. + /// Ordering option. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Cancellation token. + /// A collection of tags. + public async Task> GetProjectRepositoryTagsAsync(string projectKey, string repositorySlug, + string filterText, + BranchOrderBy orderBy, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["filterText"] = filterText, + ["orderBy"] = BitbucketHelpers.BranchOrderByToString(orderBy), + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/tags") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Streams tags for a repository, yielding items as they are retrieved. + /// + /// The project key. + /// The repository slug. + /// Filter text for tag names. + /// Ordering option. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Token to cancel the operation. + /// An async enumerable of tags. + public IAsyncEnumerable GetProjectRepositoryTagsStreamAsync(string projectKey, string repositorySlug, + string filterText, + BranchOrderBy orderBy, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["filterText"] = filterText, + ["orderBy"] = BitbucketHelpers.BranchOrderByToString(orderBy), + }; + + return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/tags") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken); + } + + /// + /// Creates a tag in a repository. + /// + /// The project key. + /// The repository slug. + /// The tag name. + /// The starting commit or ref. + /// The tag message. + /// Cancellation token. + /// The created tag. + public async Task CreateProjectRepositoryTagAsync(string projectKey, string repositorySlug, + string name, + string startPoint, + string message, + CancellationToken cancellationToken = default) + { + var data = new + { + name, + startPoint, + message, + }; + + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/tags") + .SendAsync(HttpMethod.Post, CreateJsonContent(data), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves a tag by name. + /// + /// The project key. + /// The repository slug. + /// The tag name. + /// Cancellation token. + /// The requested tag. + public async Task GetProjectRepositoryTagAsync(string projectKey, string repositorySlug, string tagName, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/tags/{tagName}") + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves webhooks for a repository. + /// + /// The project key. + /// The repository slug. + /// Optional event filter. + /// Whether to include statistics. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Cancellation token. + /// A collection of webhooks. + public async Task> GetProjectRepositoryWebHooksAsync(string projectKey, string repositorySlug, + string? @event = null, + bool statistics = false, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["event"] = @event, + ["statistics"] = BitbucketHelpers.BoolToString(statistics), + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/webhooks") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Creates a webhook for a repository. + /// + /// The project key. + /// The repository slug. + /// The webhook payload. + /// Cancellation token. + /// The created webhook. + public async Task CreateProjectRepositoryWebHookAsync(string projectKey, string repositorySlug, WebHook webHook, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/webhooks") + .SendAsync(HttpMethod.Post, CreateJsonContent(webHook), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Tests a webhook delivery for a repository. + /// + /// The project key. + /// The repository slug. + /// The URL to test. + /// Cancellation token. + /// The webhook test response. + public async Task TestProjectRepositoryWebHookAsync(string projectKey, string repositorySlug, string url, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/webhooks/test") + .SetQueryParam("url", url) + .SendAsync(HttpMethod.Post, new StringContent(string.Empty), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves a webhook by ID. + /// + /// The project key. + /// The repository slug. + /// The webhook ID. + /// Whether to include statistics. + /// Cancellation token. + /// The webhook. + public async Task GetProjectRepositoryWebHookAsync(string projectKey, string repositorySlug, + string webHookId, + bool statistics = false, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["statistics"] = BitbucketHelpers.BoolToString(statistics), + }; + + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/webhooks/{webHookId}") + .SetQueryParams(queryParamValues) + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Updates a webhook. + /// + /// The project key. + /// The repository slug. + /// The webhook ID. + /// The webhook payload. + /// Cancellation token. + /// The updated webhook. + public async Task UpdateProjectRepositoryWebHookAsync(string projectKey, string repositorySlug, + string webHookId, WebHook webHook, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/webhooks/{webHookId}") + .SendAsync(HttpMethod.Put, CreateJsonContent(webHook), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Deletes a webhook. + /// + /// The project key. + /// The repository slug. + /// The webhook ID. + /// Cancellation token. + /// true if deletion succeeded; otherwise, false. + public async Task DeleteProjectRepositoryWebHookAsync(string projectKey, string repositorySlug, + string webHookId, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/webhooks/{webHookId}") + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + //public async Task GetProjectRepositoryWebHookLatestAsync(string projectKey, string repositorySlug, + /// + /// Retrieves the latest webhook invocation summary as a string. + /// + /// The project key. + /// The repository slug. + /// The webhook ID. + /// Optional event filter. + /// Optional outcome filter. + /// Cancellation token. + /// The latest invocation payload. + public async Task GetProjectRepositoryWebHookLatestAsync(string projectKey, string repositorySlug, + string webHookId, + string? @event = null, + WebHookOutcomes? outcome = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["event"] = @event, + ["outcome"] = BitbucketHelpers.WebHookOutcomeToString(outcome), + }; + + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/webhooks/{webHookId}/latest") + .SetQueryParams(queryParamValues) + //.GetJsonAsync() + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, s => s, cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves webhook statistics. + /// + /// The project key. + /// The repository slug. + /// The webhook ID. + /// Optional event filter. + /// Cancellation token. + /// Webhook statistics. + public async Task GetProjectRepositoryWebHookStatisticsAsync(string projectKey, string repositorySlug, + string webHookId, + string? @event = null, + CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/webhooks/{webHookId}/statistics") + .SetQueryParam("event", @event) + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves a statistics summary for a webhook. + /// + /// The project key. + /// The repository slug. + /// The webhook ID. + /// Cancellation token. + /// A dictionary of webhook statistics counts. + public async Task> GetProjectRepositoryWebHookStatisticsSummaryAsync(string projectKey, string repositorySlug, + string webHookId, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/webhooks/{webHookId}/statistics/summary") + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.Tasks.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Tasks.cs new file mode 100644 index 0000000..f1bcb72 --- /dev/null +++ b/src/Bitbucket.Net/Core/Projects/BitbucketClient.Tasks.cs @@ -0,0 +1,507 @@ +using Bitbucket.Net.Common; +using Bitbucket.Net.Common.Exceptions; +using Bitbucket.Net.Common.Models; +using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Core.Tasks; +using Flurl.Http; + +namespace Bitbucket.Net; + +public partial class BitbucketClient +{ + /// + /// Gets tasks for a pull request using the legacy tasks endpoint. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// Maximum number of pages to retrieve. + /// Maximum number of results per page. + /// Pagination start index. + /// Avatar size for user avatars. + /// Cancellation token. + /// Collection of tasks. + /// + /// + /// Deprecation Notice: This endpoint was deprecated in Bitbucket Server 9.0 and returns 404 Not Found on servers version 9.0+. + /// + /// + /// For Bitbucket Server 9.0+, use instead. + /// For cross-version compatibility, use . + /// + /// + [Obsolete("This endpoint is deprecated in Bitbucket Server 9.0+. Use GetPullRequestBlockerCommentsAsync for 9.0+ or GetPullRequestTasksWithFallbackAsync for cross-version compatibility.")] + public async Task> GetPullRequestTasksAsync(string projectKey, string repositorySlug, long pullRequestId, + int? maxPages = null, + int? limit = null, + int? start = null, + int? avatarSize = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["avatarSize"] = avatarSize, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/tasks") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Streams tasks for a pull request, yielding items as they are retrieved. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Optional avatar size. + /// Token to cancel the operation. + /// An async enumerable of tasks. + /// + /// + /// Deprecation Notice: This endpoint was deprecated in Bitbucket Server 9.0 and returns 404 Not Found on servers version 9.0+. + /// + /// + /// For Bitbucket Server 9.0+, use instead. + /// + /// + [Obsolete("This endpoint is deprecated in Bitbucket Server 9.0+. Use GetPullRequestBlockerCommentsStreamAsync for 9.0+ compatibility.")] + public IAsyncEnumerable GetPullRequestTasksStreamAsync(string projectKey, string repositorySlug, long pullRequestId, + int? maxPages = null, + int? limit = null, + int? start = null, + int? avatarSize = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["avatarSize"] = avatarSize, + }; + + return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/tasks") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken); + } + + /// + /// Gets the task count for a pull request using the legacy tasks endpoint. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// Cancellation token. + /// The task count. + /// + /// + /// Deprecation Notice: This endpoint was deprecated in Bitbucket Server 9.0 and may return 404 Not Found on servers version 9.0+. + /// + /// + /// For Bitbucket Server 9.0+, use and count the results. + /// + /// + [Obsolete("This endpoint is deprecated in Bitbucket Server 9.0+. Use GetPullRequestBlockerCommentsAsync and count the results for 9.0+ compatibility.")] + public async Task GetPullRequestTaskCountAsync(string projectKey, string repositorySlug, long pullRequestId, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug) + .AppendPathSegment($"/pull-requests/{pullRequestId}/tasks/count") + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + #region Blocker Comments (Bitbucket Server 9.0+) + + /// + /// Gets blocker comments (tasks) for a pull request. + /// This endpoint is available in Bitbucket Server 9.0+ and replaces the legacy tasks endpoint. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// Optional filter: , , or null for all. + /// Maximum number of pages to retrieve. + /// Maximum number of results per page. + /// Pagination start index. + /// Cancellation token. + /// Collection of blocker comments. + /// + /// + /// In Bitbucket Server 9.0+, tasks have been replaced by blocker comments. + /// A blocker comment is a comment with severity: 'BLOCKER' that must be resolved before the pull request can be merged. + /// + /// + /// For servers prior to 9.0, use instead. + /// + /// + public async Task> GetPullRequestBlockerCommentsAsync( + string projectKey, + string repositorySlug, + long pullRequestId, + BlockerCommentState? state = null, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["state"] = BitbucketHelpers.BlockerCommentStateToString(state), + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/blocker-comments") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Streams blocker comments for a pull request, yielding items as they are retrieved. + /// This endpoint is available in Bitbucket Server 9.0+. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// Optional blocker comment state filter. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Token to cancel the operation. + /// An async enumerable of blocker comments. + public IAsyncEnumerable GetPullRequestBlockerCommentsStreamAsync( + string projectKey, + string repositorySlug, + long pullRequestId, + BlockerCommentState? state = null, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["state"] = BitbucketHelpers.BlockerCommentStateToString(state), + }; + + return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/blocker-comments") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken); + } + + /// + /// Gets a single blocker comment by ID. + /// This endpoint is available in Bitbucket Server 9.0+. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// The blocker comment ID. + /// Cancellation token. + /// The blocker comment. + public async Task GetPullRequestBlockerCommentAsync( + string projectKey, + string repositorySlug, + long pullRequestId, + long blockerCommentId, + CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug) + .AppendPathSegment($"/pull-requests/{pullRequestId}/blocker-comments/{blockerCommentId}") + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Creates a blocker comment (task) on a pull request. + /// This endpoint is available in Bitbucket Server 9.0+. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// The blocker comment text. + /// Optional anchor for file/line-specific blockers. + /// Cancellation token. + /// The created blocker comment. + public async Task CreatePullRequestBlockerCommentAsync( + string projectKey, + string repositorySlug, + long pullRequestId, + string text, + CommentAnchor? anchor = null, + CancellationToken cancellationToken = default) + { + var data = new + { + text, + severity = "BLOCKER", + anchor, + }; + + var response = await GetProjectsReposUrl(projectKey, repositorySlug) + .AppendPathSegment($"/pull-requests/{pullRequestId}/blocker-comments") + .SendAsync(HttpMethod.Post, CreateJsonContent(data), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Updates a blocker comment's text. + /// This endpoint is available in Bitbucket Server 9.0+. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// The blocker comment ID. + /// The updated blocker comment text. + /// The version of the blocker comment (for optimistic locking). + /// Cancellation token. + /// The updated blocker comment. + public async Task UpdatePullRequestBlockerCommentAsync( + string projectKey, + string repositorySlug, + long pullRequestId, + long blockerCommentId, + string text, + int version, + CancellationToken cancellationToken = default) + { + var data = new + { + text, + version, + }; + + var response = await GetProjectsReposUrl(projectKey, repositorySlug) + .AppendPathSegment($"/pull-requests/{pullRequestId}/blocker-comments/{blockerCommentId}") + .SendAsync(HttpMethod.Put, CreateJsonContent(data), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Deletes a blocker comment. + /// This endpoint is available in Bitbucket Server 9.0+. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// The blocker comment ID. + /// The version of the blocker comment (for optimistic locking). + /// Cancellation token. + /// True if the blocker comment was deleted successfully. + public async Task DeletePullRequestBlockerCommentAsync( + string projectKey, + string repositorySlug, + long pullRequestId, + long blockerCommentId, + int version, + CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug) + .AppendPathSegment($"/pull-requests/{pullRequestId}/blocker-comments/{blockerCommentId}") + .SetQueryParam("version", version) + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Resolves a blocker comment (marks the task as complete). + /// This endpoint is available in Bitbucket Server 9.0+. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// The blocker comment ID. + /// The version of the blocker comment (for optimistic locking). + /// Cancellation token. + /// The resolved blocker comment. + public async Task ResolvePullRequestBlockerCommentAsync( + string projectKey, + string repositorySlug, + long pullRequestId, + long blockerCommentId, + int version, + CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug) + .AppendPathSegment($"/pull-requests/{pullRequestId}/blocker-comments/{blockerCommentId}/resolve") + .SetQueryParam("version", version) + .PutAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Reopens a resolved blocker comment. + /// This endpoint is available in Bitbucket Server 9.0+. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// The blocker comment ID. + /// The version of the blocker comment (for optimistic locking). + /// Cancellation token. + /// The reopened blocker comment. + public async Task ReopenPullRequestBlockerCommentAsync( + string projectKey, + string repositorySlug, + long pullRequestId, + long blockerCommentId, + int version, + CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug) + .AppendPathSegment($"/pull-requests/{pullRequestId}/blocker-comments/{blockerCommentId}/reopen") + .SetQueryParam("version", version) + .PutAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Gets pull request tasks with automatic fallback for cross-version compatibility. + /// + /// + /// + /// This method provides backward compatibility across Bitbucket Server versions: + /// + /// + /// Bitbucket Server 9.0+: Uses the new /blocker-comments endpoint. + /// Bitbucket Server < 9.0: Falls back to the legacy /tasks endpoint. + /// + /// + /// The method first tries the new blocker-comments endpoint. If it returns 404 (Not Found), + /// it automatically falls back to the legacy tasks endpoint. + /// + /// + /// For new code targeting Bitbucket Server 9.0+, prefer using + /// directly for better type safety. + /// + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// Maximum number of pages to retrieve. + /// Maximum number of results per page. + /// Pagination start index. + /// Cancellation token. + /// + /// A collection of blocker comments () on Bitbucket 9.0+, + /// or legacy tasks () on older versions. + /// + public async Task> GetPullRequestTasksWithFallbackAsync( + string projectKey, + string repositorySlug, + long pullRequestId, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + try + { + // Try new blocker-comments endpoint first (Bitbucket 9.0+) + var blockerComments = await GetPullRequestBlockerCommentsAsync( + projectKey, repositorySlug, pullRequestId, + maxPages: maxPages, limit: limit, start: start, + cancellationToken: cancellationToken).ConfigureAwait(false); + + return blockerComments.Cast(); + } + catch (BitbucketNotFoundException) + { + // Fall back to legacy tasks endpoint (Bitbucket < 9.0) +#pragma warning disable CS0618 // Type or member is obsolete - intentional fallback + var tasks = await GetPullRequestTasksAsync( + projectKey, repositorySlug, pullRequestId, + maxPages: maxPages, limit: limit, start: start, + cancellationToken: cancellationToken).ConfigureAwait(false); +#pragma warning restore CS0618 + + return tasks.Cast(); + } + } + + #endregion + + /// + /// Subscribes the current user to watch a pull request. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// Cancellation token. + /// true if the watch was added; otherwise, false. + public async Task WatchPullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug) + .AppendPathSegment($"/pull-requests/{pullRequestId}/watch") + .SendAsync(HttpMethod.Post, new StringContent(string.Empty), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Unsubscribes the current user from watching a pull request. + /// + /// The project key. + /// The repository slug. + /// The pull request ID. + /// Cancellation token. + /// true if the watch was removed; otherwise, false. + public async Task UnwatchPullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, CancellationToken cancellationToken = default) + { + var response = await GetProjectsReposUrl(projectKey, repositorySlug) + .AppendPathSegment($"/pull-requests/{pullRequestId}/watch") + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Projects/BitbucketClient.cs b/src/Bitbucket.Net/Core/Projects/BitbucketClient.cs deleted file mode 100644 index ac9c267..0000000 --- a/src/Bitbucket.Net/Core/Projects/BitbucketClient.cs +++ /dev/null @@ -1,2745 +0,0 @@ -using System; -using System.Buffers; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Bitbucket.Net.Common; -using Bitbucket.Net.Common.Exceptions; -using Bitbucket.Net.Common.Models; -using Bitbucket.Net.Models.Core.Admin; -using Bitbucket.Net.Models.Core.Projects; -using Bitbucket.Net.Models.Core.Tasks; -using Bitbucket.Net.Models.Core.Users; -using Flurl.Http; - -namespace Bitbucket.Net -{ - public partial class BitbucketClient - { - private IFlurlRequest GetProjectsUrl() => GetBaseUrl() - .AppendPathSegment("/projects"); - - private IFlurlRequest GetProjectsUrl(string path) => GetProjectsUrl() - .AppendPathSegment(path); - - private IFlurlRequest GetProjectUrl(string projectKey) => GetProjectsUrl() - .AppendPathSegment($"/{projectKey}"); - - private IFlurlRequest GetProjectsReposUrl(string projectKey, string repositorySlug) => GetProjectsUrl($"/{projectKey}/repos/{repositorySlug}"); - - private IFlurlRequest GetProjectsReposUrl(string projectKey, string repositorySlug, string path) => GetProjectsReposUrl(projectKey, repositorySlug) - .AppendPathSegment(path); - - public async Task> GetProjectsAsync( - int? maxPages = null, - int? limit = null, - int? start = null, - string? name = null, - Permissions? permission = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["name"] = name, - ["permission"] = BitbucketHelpers.PermissionToString(permission) - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsUrl() - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - /// - /// Streams all projects as an IAsyncEnumerable, yielding items as they are retrieved. - /// This is more memory-efficient for large result sets. - /// - public IAsyncEnumerable GetProjectsStreamAsync( - int? maxPages = null, - int? limit = null, - int? start = null, - string? name = null, - Permissions? permission = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["name"] = name, - ["permission"] = BitbucketHelpers.PermissionToString(permission) - }; - - return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsUrl() - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken); - } - - public async Task CreateProjectAsync(ProjectDefinition projectDefinition, CancellationToken cancellationToken = default) - { - var response = await GetProjectsUrl() - .PostJsonAsync(projectDefinition, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task DeleteProjectAsync(string projectKey, CancellationToken cancellationToken = default) - { - var response = await GetProjectsUrl($"/{projectKey}") - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - public async Task UpdateProjectAsync(string projectKey, ProjectDefinition projectDefinition, CancellationToken cancellationToken = default) - { - var response = await GetProjectsUrl($"/{projectKey}") - .PutJsonAsync(projectDefinition, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task GetProjectAsync(string projectKey, CancellationToken cancellationToken = default) - { - var response = await GetProjectsUrl($"/{projectKey}") - .GetAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task> GetProjectUserPermissionsAsync(string projectKey, string? filter = null, - int? maxPages = null, - int? limit = null, - int? start = null, - int? avatarSize = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["filter"] = filter, - ["avatarSize"] = avatarSize - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsUrl($"/{projectKey}/permissions/users") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task DeleteProjectUserPermissionsAsync(string projectKey, string userName, CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["name"] = userName - }; - - var response = await GetProjectsUrl($"/{projectKey}/permissions/users") - .SetQueryParams(queryParamValues) - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - public async Task UpdateProjectUserPermissionsAsync(string projectKey, string userName, Permissions permission, CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["name"] = userName, - ["permission"] = BitbucketHelpers.PermissionToString(permission) - }; - - var response = await GetProjectsUrl($"/{projectKey}/permissions/users") - .SetQueryParams(queryParamValues) - .PutAsync(new StringContent(""), cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - public async Task> GetProjectUserPermissionsNoneAsync(string projectKey, string? filter = null, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["filter"] = filter - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsUrl($"/{projectKey}/permissions/users/none") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task> GetProjectGroupPermissionsAsync(string projectKey, string? filter = null, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["filter"] = filter - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsUrl($"/{projectKey}/permissions/groups") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task DeleteProjectGroupPermissionsAsync(string projectKey, string groupName, CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["name"] = groupName - }; - - var response = await GetProjectsUrl($"/{projectKey}/permissions/groups") - .SetQueryParams(queryParamValues) - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - public async Task UpdateProjectGroupPermissionsAsync(string projectKey, string groupName, Permissions permission, CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["name"] = groupName, - ["permission"] = BitbucketHelpers.PermissionToString(permission) - }; - - var response = await GetProjectsUrl($"/{projectKey}/permissions/groups") - .SetQueryParams(queryParamValues) - .PutAsync(new StringContent(""), cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - public async Task> GetProjectGroupPermissionsNoneAsync(string projectKey, string? filter = null, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["filter"] = filter - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsUrl($"/{projectKey}/permissions/groups/none") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task IsProjectDefaultPermissionAsync(string projectKey, Permissions permission, CancellationToken cancellationToken = default) - { - var response = await GetProjectsUrl($"/{projectKey}/permissions/{BitbucketHelpers.PermissionToString(permission)}/all") - .GetAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, s => - { - using var doc = JsonDocument.Parse(s); - return doc.RootElement.GetProperty("permitted").GetBoolean(); - }, cancellationToken) - .ConfigureAwait(false); - } - - private async Task SetProjectDefaultPermissionAsync(string projectKey, Permissions permission, bool allow, CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["allow"] = BitbucketHelpers.BoolToString(allow) - }; - - var response = await GetProjectsUrl($"/{projectKey}/permissions/{BitbucketHelpers.PermissionToString(permission)}/all") - .SetQueryParams(queryParamValues) - .PostJsonAsync(new StringContent(""), cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - public async Task GrantProjectPermissionToAllAsync(string projectKey, Permissions permission, CancellationToken cancellationToken = default) - { - return await SetProjectDefaultPermissionAsync(projectKey, permission, true, cancellationToken).ConfigureAwait(false); - } - - public async Task RevokeProjectPermissionFromAllAsync(string projectKey, Permissions permission, CancellationToken cancellationToken = default) - { - return await SetProjectDefaultPermissionAsync(projectKey, permission, false, cancellationToken).ConfigureAwait(false); - } - - public async Task> GetProjectRepositoriesAsync(string projectKey, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsUrl($"/{projectKey}/repos") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - /// - /// Streams all repositories for a project as an IAsyncEnumerable. - /// - public IAsyncEnumerable GetProjectRepositoriesStreamAsync(string projectKey, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start - }; - - return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsUrl($"/{projectKey}/repos") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken); - } - - public async Task CreateProjectRepositoryAsync(string projectKey, string repositoryName, string scmId = "git", CancellationToken cancellationToken = default) - { - var data = new - { - name = repositoryName, - scmId - }; - - var response = await GetProjectsUrl($"/{projectKey}/repos") - .PostJsonAsync(data, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task GetProjectRepositoryAsync(string projectKey, string repositorySlug, CancellationToken cancellationToken = default) - { - return await GetProjectsReposUrl(projectKey, repositorySlug) - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - public async Task CreateProjectRepositoryForkAsync(string projectKey, string repositorySlug, string? targetProjectKey = null, string? targetSlug = null, string? targetName = null, CancellationToken cancellationToken = default) - { - var data = new - { - slug = targetSlug ?? repositorySlug, - name = targetName, - project = targetProjectKey == null ? null : new ProjectRef { Key = targetProjectKey } - }; - - var response = await GetProjectsReposUrl(projectKey, repositorySlug) - .PostJsonAsync(data, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task ScheduleProjectRepositoryForDeletionAsync(string projectKey, string repositorySlug, CancellationToken cancellationToken = default) - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug) - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - public async Task UpdateProjectRepositoryAsync(string projectKey, string repositorySlug, - string? targetName = null, - bool? isForkable = null, - string? targetProjectKey = null, - bool? isPublic = null, - CancellationToken cancellationToken = default) - { - var data = new - { - name = targetName, - forkable = isForkable, - project = targetProjectKey == null ? null : new ProjectRef { Key = targetProjectKey }, - @public = isPublic - }; - - var response = await GetProjectsReposUrl(projectKey, repositorySlug) - .PutJsonAsync(data, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task> GetProjectRepositoryForksAsync(string projectKey, string repositorySlug, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsReposUrl(projectKey, repositorySlug, "/forks") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task RecreateProjectRepositoryAsync(string projectKey, string repositorySlug, CancellationToken cancellationToken = default) - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/recreate") - .PostJsonAsync(new StringContent(""), cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task> GetRelatedProjectRepositoriesAsync(string projectKey, string repositorySlug, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsReposUrl(projectKey, repositorySlug, "/related") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task GetProjectRepositoryArchiveAsync(string projectKey, string repositorySlug, - string at, - string fileName, - ArchiveFormats archiveFormat, - string path, - string prefix, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["at"] = at, - ["fileName"] = fileName, - ["format"] = BitbucketHelpers.ArchiveFormatToString(archiveFormat), - ["path"] = path, - ["prefix"] = prefix - }; - - return await GetProjectsReposUrl(projectKey, repositorySlug, "/archive") - .SetQueryParams(queryParamValues) - .GetBytesAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - public async Task> GetProjectRepositoryGroupPermissionsAsync(string projectKey, string repositorySlug, - string? filter = null, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["filter"] = filter, - ["limit"] = limit, - ["start"] = start - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsReposUrl(projectKey, repositorySlug, "/permissions/groups") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task UpdateProjectRepositoryGroupPermissionsAsync(string projectKey, string repositorySlug, Permissions permission, string name, CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["permission"] = BitbucketHelpers.PermissionToString(permission), - ["name"] = name - }; - - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/permissions/groups") - .SetQueryParams(queryParamValues) - .PutJsonAsync(new StringContent(""), cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - public async Task DeleteProjectRepositoryGroupPermissionsAsync(string projectKey, string repositorySlug, string name, CancellationToken cancellationToken = default) - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/permissions/groups") - .SetQueryParam("name", name) - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - public async Task> GetProjectRepositoryGroupPermissionsNoneAsync(string projectKey, string repositorySlug, - string? filter = null, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["filter"] = filter - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsReposUrl(projectKey, repositorySlug, "/permissions/groups/none") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task> GetProjectRepositoryUserPermissionsAsync(string projectKey, string repositorySlug, - string? filter = null, - int? maxPages = null, - int? limit = null, - int? start = null, - int? avatarSize = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["filter"] = filter, - ["limit"] = limit, - ["start"] = start, - ["avatarSize"] = avatarSize - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsReposUrl(projectKey, repositorySlug, "/permissions/users") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task UpdateProjectRepositoryUserPermissionsAsync(string projectKey, string repositorySlug, Permissions permission, string name, CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["permission"] = BitbucketHelpers.PermissionToString(permission), - ["name"] = name - }; - - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/permissions/users") - .SetQueryParams(queryParamValues) - .PutJsonAsync(new StringContent(""), cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - public async Task DeleteProjectRepositoryUserPermissionsAsync(string projectKey, string repositorySlug, string name, - int? avatarSize = null, - CancellationToken cancellationToken = default) - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/permissions/users") - .SetQueryParam("name", name) - .SetQueryParam("avatarSize", avatarSize) - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - public async Task> GetProjectRepositoryUserPermissionsNoneAsync(string projectKey, string repositorySlug, - string? filter = null, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["filter"] = filter - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsReposUrl(projectKey, repositorySlug, "/permissions/users/none") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task> GetBranchesAsync(string projectKey, string repositorySlug, - int? maxPages = null, - int? limit = null, - int? start = null, - string? baseBranchOrTag = null, - bool? details = null, - string? filterText = null, - BranchOrderBy? orderBy = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["base"] = baseBranchOrTag, - ["details"] = details.HasValue ? BitbucketHelpers.BoolToString(details.Value) : null, - ["filterText"] = filterText, - ["orderBy"] = orderBy.HasValue ? BitbucketHelpers.BranchOrderByToString(orderBy.Value) : null - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsReposUrl(projectKey, repositorySlug, "/branches") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - /// - /// Streams all branches for a repository as an IAsyncEnumerable. - /// - public IAsyncEnumerable GetBranchesStreamAsync(string projectKey, string repositorySlug, - int? maxPages = null, - int? limit = null, - int? start = null, - string? baseBranchOrTag = null, - bool? details = null, - string? filterText = null, - BranchOrderBy? orderBy = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["base"] = baseBranchOrTag, - ["details"] = details.HasValue ? BitbucketHelpers.BoolToString(details.Value) : null, - ["filterText"] = filterText, - ["orderBy"] = orderBy.HasValue ? BitbucketHelpers.BranchOrderByToString(orderBy.Value) : null - }; - - return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsReposUrl(projectKey, repositorySlug, "/branches") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken); - } - - public async Task CreateBranchAsync(string projectKey, string repositorySlug, BranchInfo branchInfo, CancellationToken cancellationToken = default) - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/branches") - .PostJsonAsync(branchInfo, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task GetDefaultBranchAsync(string projectKey, string repositorySlug, CancellationToken cancellationToken = default) - { - return await GetProjectsReposUrl(projectKey, repositorySlug, "/branches/default") - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - public async Task SetDefaultBranchAsync(string projectKey, string repositorySlug, BranchRef branchRef, CancellationToken cancellationToken = default) - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/branches") - .PutJsonAsync(branchRef, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - public async Task BrowseProjectRepositoryAsync(string projectKey, string repositorySlug, string at, bool type = false, - bool blame = false, - bool noContent = false, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["at"] = at, - ["type"] = BitbucketHelpers.BoolToString(type) - }; - if (blame) - { - queryParamValues.Add("blame", null); - } - if (blame && noContent) - { - queryParamValues.Add("noContent", null); - } - - return await GetProjectsReposUrl(projectKey, repositorySlug, "/browse") - .SetQueryParams(queryParamValues, Flurl.NullValueHandling.NameOnly) - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - public async Task BrowseProjectRepositoryPathAsync(string projectKey, string repositorySlug, string path, string at, bool type = false, - bool blame = false, - bool noContent = false, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["at"] = at, - ["type"] = BitbucketHelpers.BoolToString(type) - }; - if (blame) - { - queryParamValues.Add("blame", null); - } - if (blame && noContent) - { - queryParamValues.Add("noContent", null); - } - - return await GetProjectsReposUrl(projectKey, repositorySlug, $"/browse/{path}") - .SetQueryParams(queryParamValues, Flurl.NullValueHandling.NameOnly) - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - /// - /// Gets the raw content of a file as a stream. This is optimal for large files as it doesn't buffer the entire content in memory. - /// - /// The project key. - /// The repository slug. - /// The file path within the repository. - /// Optional ref (branch, tag, or commit) to get the file content at. Defaults to default branch. - /// Cancellation token. - /// A stream containing the raw file content. Caller is responsible for disposing. - public async Task GetRawFileContentStreamAsync(string projectKey, string repositorySlug, string path, - string? at = null, - CancellationToken cancellationToken = default) - { - var request = GetProjectsReposUrl(projectKey, repositorySlug, $"/raw/{path}"); - - if (!string.IsNullOrEmpty(at)) - { - request = request.SetQueryParam("at", at); - } - - return await request - .GetStreamAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - /// - /// Streams the raw content of a file line by line. This is optimal for large text files. - /// - /// The project key. - /// The repository slug. - /// The file path within the repository. - /// Optional ref (branch, tag, or commit) to get the file content at. Defaults to default branch. - /// Cancellation token. - /// An async enumerable of lines from the file. - public async IAsyncEnumerable GetRawFileContentLinesStreamAsync(string projectKey, string repositorySlug, string path, - string? at = null, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - var stream = await GetRawFileContentStreamAsync(projectKey, repositorySlug, path, at, cancellationToken).ConfigureAwait(false); - - await using (stream.ConfigureAwait(false)) - { - using var reader = new StreamReader(stream); - while (!reader.EndOfStream) - { - cancellationToken.ThrowIfCancellationRequested(); - var line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false); - if (line is not null) - { - yield return line; - } - } - } - } - - /// - /// Updates a file at the specified path in the repository. - /// Uses ArrayPool<byte> for zero-copy buffer management to minimize heap allocations. - /// - public async Task UpdateProjectRepositoryPathAsync(string projectKey, string repositorySlug, string path, - string fileName, - string branch, - string? message = null, - string? sourceCommitId = null, - string? sourceBranch = null, - CancellationToken cancellationToken = default) - { - if (!File.Exists(fileName)) - { - throw new ArgumentException($"File doesn't exist: {fileName}", nameof(fileName)); - } - - var fileInfo = new FileInfo(fileName); - int fileSize = checked((int)fileInfo.Length); - - // Use ArrayPool to rent a buffer instead of allocating new array - byte[] buffer = ArrayPool.Shared.Rent(fileSize); - try - { - int bytesRead; - var stm = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 4096, useAsync: true); - await using (stm.ConfigureAwait(false)) - { - bytesRead = await stm.ReadAsync(buffer.AsMemory(0, fileSize), cancellationToken).ConfigureAwait(false); - } - - // Create MemoryStream over the exact bytes read (not the rented buffer size) - using var memoryStream = new MemoryStream(buffer, 0, bytesRead, writable: false); - - var data = new DynamicMultipartFormDataContent - { - { new StreamContent(memoryStream), "content" }, - { new StringContent(branch), "branch" }, - { message, message == null ? null : new StringContent(message), "message" }, - { sourceCommitId, sourceCommitId == null ? null : new StringContent(sourceCommitId), "sourceCommitId" }, - { sourceBranch, sourceBranch == null ? null : new StringContent(sourceBranch), "sourceBranch" } - }; - - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/browse/{path}") - .PutAsync(data.ToMultipartFormDataContent(), cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - finally - { - // Always return the buffer to the pool - ArrayPool.Shared.Return(buffer); - } - } - - public async Task> GetChangesAsync(string projectKey, string repositorySlug, string until, string? since = null, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["since"] = since, - ["until"] = until - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsReposUrl(projectKey, repositorySlug, "/changes") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task> GetCommitsAsync(string projectKey, string repositorySlug, - string until, - bool followRenames = false, - bool ignoreMissing = false, - MergeCommits merges = MergeCommits.Exclude, - string? path = null, - string? since = null, - bool withCounts = false, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["followRenames"] = BitbucketHelpers.BoolToString(followRenames), - ["ignoreMissing"] = BitbucketHelpers.BoolToString(ignoreMissing), - ["merges"] = BitbucketHelpers.MergeCommitsToString(merges), - ["path"] = path, - ["since"] = since, - ["until"] = until, - ["withCounts"] = BitbucketHelpers.BoolToString(withCounts) - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsReposUrl(projectKey, repositorySlug, "/commits") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - /// - /// Streams all commits for a repository as an IAsyncEnumerable. - /// - public IAsyncEnumerable GetCommitsStreamAsync(string projectKey, string repositorySlug, - string until, - bool followRenames = false, - bool ignoreMissing = false, - MergeCommits merges = MergeCommits.Exclude, - string? path = null, - string? since = null, - bool withCounts = false, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["followRenames"] = BitbucketHelpers.BoolToString(followRenames), - ["ignoreMissing"] = BitbucketHelpers.BoolToString(ignoreMissing), - ["merges"] = BitbucketHelpers.MergeCommitsToString(merges), - ["path"] = path, - ["since"] = since, - ["until"] = until, - ["withCounts"] = BitbucketHelpers.BoolToString(withCounts) - }; - - return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsReposUrl(projectKey, repositorySlug, "/commits") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken); - } - - public async Task GetCommitAsync(string projectKey, string repositorySlug, string commitId, string? path = null, CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["path"] = path - }; - - return await GetProjectsReposUrl(projectKey, repositorySlug, $"/commits/{commitId}") - .SetQueryParams(queryParamValues) - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - public async Task> GetCommitChangesAsync(string projectKey, string repositorySlug, string commitId, - string? since = null, - bool withComments = true, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["since"] = since, - ["withComments"] = BitbucketHelpers.BoolToString(withComments) - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsReposUrl(projectKey, repositorySlug, $"/commits/{commitId}/changes") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task> GetCommitCommentsAsync(string projectKey, string repositorySlug, string commitId, - string path, - string? since = null, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["path"] = path, - ["since"] = since - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsReposUrl(projectKey, repositorySlug, $"/commits/{commitId}/comments") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task CreateCommitCommentAsync(string projectKey, string repositorySlug, string commitId, - CommentInfo commentInfo, string? since = null, CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["since"] = since - }; - - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/commits/{commitId}/comments") - .SetQueryParams(queryParamValues) - .PostJsonAsync(commentInfo, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task GetCommitCommentAsync(string projectKey, string repositorySlug, string commitId, long commentId, - int? avatarSize = null, - CancellationToken cancellationToken = default) - { - return await GetProjectsReposUrl(projectKey, repositorySlug, $"/commits/{commitId}/comments/{commentId}") - .SetQueryParam("avatarSize", avatarSize) - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - public async Task UpdateCommitCommentAsync(string projectKey, string repositorySlug, string commitId, long commentId, - CommentText commentText, CancellationToken cancellationToken = default) - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/commits/{commitId}/comments/{commentId}") - .PutJsonAsync(commentText, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task DeleteCommitCommentAsync(string projectKey, string repositorySlug, string commitId, long commentId, - int version = -1, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["version"] = version - }; - - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/commits/{commitId}/comments/{commentId}") - .SetQueryParams(queryParamValues) - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - public async Task GetCommitDiffAsync(string projectKey, string repositorySlug, string commitId, - bool autoSrcPath = false, - int contextLines = -1, - string? since = null, - string? srcPath = null, - string whitespace = "ignore-all", - bool withComments = true, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["autoSrcPath"] = BitbucketHelpers.BoolToString(autoSrcPath), - ["contextLines"] = contextLines, - ["since"] = since, - ["srcPath"] = srcPath, - ["whitespace"] = whitespace, - ["withComments"] = BitbucketHelpers.BoolToString(withComments) - }; - - return await GetProjectsReposUrl(projectKey, repositorySlug, $"/commits/{commitId}/diff") - .SetQueryParams(queryParamValues) - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - /// - /// Streams the diff for a specific commit, yielding individual diff entries as they are parsed. - /// This is more memory-efficient for large diffs. - /// - /// The project key. - /// The repository slug. - /// The commit ID. - /// Auto source path. - /// Number of context lines. - /// Since commit. - /// Source path filter. - /// Whitespace handling. - /// Include comments. - /// Cancellation token. - /// An async enumerable of diffs. - public async IAsyncEnumerable GetCommitDiffStreamAsync(string projectKey, string repositorySlug, string commitId, - bool autoSrcPath = false, - int contextLines = -1, - string? since = null, - string? srcPath = null, - string whitespace = "ignore-all", - bool withComments = true, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["autoSrcPath"] = BitbucketHelpers.BoolToString(autoSrcPath), - ["contextLines"] = contextLines, - ["since"] = since, - ["srcPath"] = srcPath, - ["whitespace"] = whitespace, - ["withComments"] = BitbucketHelpers.BoolToString(withComments) - }; - - var responseStream = await GetProjectsReposUrl(projectKey, repositorySlug, $"/commits/{commitId}/diff") - .SetQueryParams(queryParamValues) - .GetStreamAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - await using (responseStream.ConfigureAwait(false)) - { - await foreach (var diff in DeserializeDiffsFromStreamAsync(responseStream, cancellationToken).ConfigureAwait(false)) - { - yield return diff; - } - } - } - - public async Task CreateCommitWatchAsync(string projectKey, string repositorySlug, string commitId, CancellationToken cancellationToken = default) - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/commits/{commitId}/watch") - .PostJsonAsync(new StringContent(""), cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - public async Task DeleteCommitWatchAsync(string projectKey, string repositorySlug, string commitId, CancellationToken cancellationToken = default) - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/commits/{commitId}/watch") - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - public async Task> GetRepositoryCompareChangesAsync(string projectKey, string repositorySlug, string from, string to, - string? fromRepo = null, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["from"] = from, - ["to"] = to, - ["fromRepo"] = fromRepo - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsReposUrl(projectKey, repositorySlug, "/compare/changes") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task GetRepositoryCompareDiffAsync(string projectKey, string repositorySlug, string from, string to, - string? fromRepo = null, - string? srcPath = null, - int contextLines = -1, - string whitespace = "ignore-all", - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["from"] = from, - ["to"] = to, - ["fromRepo"] = fromRepo, - ["srcPath"] = srcPath, - ["contextLines"] = contextLines, - ["whitespace"] = whitespace - }; - - return await GetProjectsReposUrl(projectKey, repositorySlug, "/compare/diff") - .SetQueryParams(queryParamValues) - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - /// - /// Streams the compare diff between two refs, yielding individual diff entries as they are parsed. - /// This is more memory-efficient for large diffs. - /// - /// The project key. - /// The repository slug. - /// The source ref (branch, tag, or commit). - /// The target ref (branch, tag, or commit). - /// Optional source repository if comparing across forks. - /// Source path filter. - /// Number of context lines. - /// Whitespace handling. - /// Cancellation token. - /// An async enumerable of diffs. - public async IAsyncEnumerable GetRepositoryCompareDiffStreamAsync(string projectKey, string repositorySlug, string from, string to, - string? fromRepo = null, - string? srcPath = null, - int contextLines = -1, - string whitespace = "ignore-all", - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["from"] = from, - ["to"] = to, - ["fromRepo"] = fromRepo, - ["srcPath"] = srcPath, - ["contextLines"] = contextLines, - ["whitespace"] = whitespace - }; - - var responseStream = await GetProjectsReposUrl(projectKey, repositorySlug, "/compare/diff") - .SetQueryParams(queryParamValues) - .GetStreamAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - await using (responseStream.ConfigureAwait(false)) - { - await foreach (var diff in DeserializeDiffsFromStreamAsync(responseStream, cancellationToken).ConfigureAwait(false)) - { - yield return diff; - } - } - } - - public async Task> GetRepositoryCompareCommitsAsync(string projectKey, string repositorySlug, string from, string to, - string? fromRepo = null, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["from"] = from, - ["to"] = to, - ["fromRepo"] = fromRepo - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsReposUrl(projectKey, repositorySlug, "/compare/commits") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task GetRepositoryDiffAsync(string projectKey, string repositorySlug, string until, - int contextLines = -1, - string? since = null, - string? srcPath = null, - string whitespace = "ignore-all", - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["contextLines"] = contextLines, - ["since"] = since, - ["srcPath"] = srcPath, - ["until"] = until, - ["whitespace"] = whitespace - }; - - return await GetProjectsReposUrl(projectKey, repositorySlug, "/diff") - .SetQueryParams(queryParamValues) - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - /// - /// Streams the repository diff, yielding individual diff entries as they are parsed. - /// This is more memory-efficient for large diffs. - /// - /// The project key. - /// The repository slug. - /// The commit ID to diff until. - /// Number of context lines. - /// The commit ID to diff since. - /// Source path filter. - /// Whitespace handling. - /// Cancellation token. - /// An async enumerable of diffs. - public async IAsyncEnumerable GetRepositoryDiffStreamAsync(string projectKey, string repositorySlug, string until, - int contextLines = -1, - string? since = null, - string? srcPath = null, - string whitespace = "ignore-all", - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["contextLines"] = contextLines, - ["since"] = since, - ["srcPath"] = srcPath, - ["until"] = until, - ["whitespace"] = whitespace - }; - - var responseStream = await GetProjectsReposUrl(projectKey, repositorySlug, "/diff") - .SetQueryParams(queryParamValues) - .GetStreamAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - await using (responseStream.ConfigureAwait(false)) - { - await foreach (var diff in DeserializeDiffsFromStreamAsync(responseStream, cancellationToken).ConfigureAwait(false)) - { - yield return diff; - } - } - } - - public async Task> GetRepositoryFilesAsync(string projectKey, string repositorySlug, string? at = null, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["at"] = at - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsReposUrl(projectKey, repositorySlug, "/files") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task GetProjectRepositoryLastModifiedAsync(string projectKey, string repositorySlug, string at, CancellationToken cancellationToken = default) - { - return await GetProjectsReposUrl(projectKey, repositorySlug, "/last-modified") - .SetQueryParam("at", at) - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - public async Task> GetRepositoryParticipantsAsync(string projectKey, string repositorySlug, - PullRequestDirections direction = PullRequestDirections.Incoming, - string? filter = null, - Roles? role = null, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["direction"] = BitbucketHelpers.PullRequestDirectionToString(direction), - ["filter"] = filter, - ["role"] = BitbucketHelpers.RoleToString(role) - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsReposUrl(projectKey, repositorySlug, "/participants") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task> GetPullRequestsAsync(string projectKey, string repositorySlug, - int? maxPages = null, - int? limit = null, - int? start = null, - PullRequestDirections direction = PullRequestDirections.Incoming, - string? branchId = null, - PullRequestStates state = PullRequestStates.Open, - PullRequestOrders order = PullRequestOrders.Newest, - bool withAttributes = true, - bool withProperties = true, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["direction"] = BitbucketHelpers.PullRequestDirectionToString(direction), - ["at"] = branchId, - ["state"] = BitbucketHelpers.PullRequestStateToString(state), - ["order"] = BitbucketHelpers.PullRequestOrderToString(order), - ["withAttributes"] = BitbucketHelpers.BoolToString(withAttributes), - ["withProperties"] = BitbucketHelpers.BoolToString(withProperties) - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsReposUrl(projectKey, repositorySlug, "/pull-requests") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - /// - /// Streams all pull requests for a repository as an IAsyncEnumerable. - /// - public IAsyncEnumerable GetPullRequestsStreamAsync(string projectKey, string repositorySlug, - int? maxPages = null, - int? limit = null, - int? start = null, - PullRequestDirections direction = PullRequestDirections.Incoming, - string? branchId = null, - PullRequestStates state = PullRequestStates.Open, - PullRequestOrders order = PullRequestOrders.Newest, - bool withAttributes = true, - bool withProperties = true, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["direction"] = BitbucketHelpers.PullRequestDirectionToString(direction), - ["at"] = branchId, - ["state"] = BitbucketHelpers.PullRequestStateToString(state), - ["order"] = BitbucketHelpers.PullRequestOrderToString(order), - ["withAttributes"] = BitbucketHelpers.BoolToString(withAttributes), - ["withProperties"] = BitbucketHelpers.BoolToString(withProperties) - }; - - return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsReposUrl(projectKey, repositorySlug, "/pull-requests") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken); - } - - public async Task CreatePullRequestAsync(string projectKey, string repositorySlug, PullRequestInfo pullRequestInfo, CancellationToken cancellationToken = default) - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/pull-requests") - .PostJsonAsync(pullRequestInfo, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task GetPullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, CancellationToken cancellationToken = default) - { - return await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}") - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - public async Task UpdatePullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, PullRequestUpdate pullRequestUpdate, CancellationToken cancellationToken = default) - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}") - .PutJsonAsync(pullRequestUpdate, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task DeletePullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, VersionInfo versionInfo, CancellationToken cancellationToken = default) - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}") - .SendJsonAsync(HttpMethod.Delete, versionInfo, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - public async Task> GetPullRequestActivitiesAsync(string projectKey, string repositorySlug, long pullRequestId, - long? fromId = null, - PullRequestFromTypes? fromType = null, - int? maxPages = null, - int? limit = null, - int? start = null, - int? avatarSize = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["fromId"] = fromId, - ["fromType"] = BitbucketHelpers.PullRequestFromTypeToString(fromType), - ["avatarSize"] = avatarSize - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/activities") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task DeclinePullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, int version = -1, CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["version"] = version - }; - - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/decline") - .SetQueryParams(queryParamValues) - .PostJsonAsync(new StringContent(""), cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - public async Task GetPullRequestMergeStateAsync(string projectKey, string repositorySlug, long pullRequestId, int version = -1, CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["version"] = version - }; - - return await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/merge") - .SetQueryParams(queryParamValues) - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - /// - /// Gets the merge base (common ancestor) commit for a pull request. - /// This is the best common ancestor between the latest commits of the source and target branches. - /// - /// The project key. - /// The repository slug. - /// The pull request ID. - /// Cancellation token. - /// The merge base commit, or null if not found (HTTP 204 - no common ancestor exists). - /// - /// This endpoint is useful for creating line-specific comments on pull requests. - /// The returned commit ID can be used as the fromHash parameter when creating anchored comments, - /// while the toHash can be obtained from on the pull request's FromRef. - /// - public async Task GetPullRequestMergeBaseAsync(string projectKey, string repositorySlug, long pullRequestId, CancellationToken cancellationToken = default) - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/merge-base") - .AllowHttpStatus(204) - .GetAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - // HTTP 204 indicates no common ancestor exists (e.g., unrelated histories) - if (response.StatusCode == 204) - { - return null; - } - - return await response.GetJsonAsync().ConfigureAwait(false); - } - - public async Task MergePullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, int version = -1, CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["version"] = version - }; - - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/merge") - .SetQueryParams(queryParamValues) - .PostJsonAsync(new StringContent(""), cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task ReopenPullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, int version = -1, CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["version"] = version - }; - - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/reopen") - .SetQueryParams(queryParamValues) - .PostJsonAsync(new StringContent(""), cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task ApprovePullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, CancellationToken cancellationToken = default) - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/approve") - .PostJsonAsync(new StringContent(""), cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task DeletePullRequestApprovalAsync(string projectKey, string repositorySlug, long pullRequestId, CancellationToken cancellationToken = default) - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/approve") - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task> GetPullRequestChangesAsync(string projectKey, string repositorySlug, long pullRequestId, - ChangeScopes changeScope = ChangeScopes.All, - string? sinceId = null, - string? untilId = null, - bool withComments = true, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["changeScope"] = BitbucketHelpers.ChangeScopeToString(changeScope), - ["sinceId"] = sinceId, - ["untilId"] = untilId, - ["withComments"] = BitbucketHelpers.BoolToString(withComments) - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/changes") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task CreatePullRequestCommentAsync(string projectKey, string repositorySlug, long pullRequestId, - string text, - string? parentId = null, - DiffTypes? diffType = null, - string? fromHash = null, - string? path = null, - string? srcPath = null, - string? toHash = null, - int? line = null, - FileTypes? fileType = null, - LineTypes? lineType = null, - CancellationToken cancellationToken = default) - { - // Build the comment payload dynamically to avoid sending empty anchor objects - // which Bitbucket Server 9.0 rejects with HTTP 500. - // See: BUG-003 - add_pull_request_comment returns 500 error - var data = new Dictionary - { - ["text"] = text - }; - - if (!string.IsNullOrEmpty(parentId)) - { - data["parent"] = new { id = parentId }; - } - - // Only include anchor if at least one anchor-related field is specified - // Empty anchor objects cause HTTP 500 on Bitbucket Server 9.0 - var hasAnchorData = diffType.HasValue - || !string.IsNullOrEmpty(fromHash) - || !string.IsNullOrEmpty(path) - || !string.IsNullOrEmpty(srcPath) - || !string.IsNullOrEmpty(toHash) - || line.HasValue - || fileType.HasValue - || lineType.HasValue; - - if (hasAnchorData) - { - data["anchor"] = new - { - diffType = BitbucketHelpers.DiffTypeToString(diffType), - fromHash, - path, - srcPath, - toHash, - line, - fileType = BitbucketHelpers.FileTypeToString(fileType), - lineType = BitbucketHelpers.LineTypeToString(lineType) - }; - } - - var response = await GetProjectsReposUrl(projectKey, repositorySlug) - .AppendPathSegment($"/pull-requests/{pullRequestId}/comments") - .PostJsonAsync(data, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task> GetPullRequestCommentsAsync(string projectKey, string repositorySlug, long pullRequestId, - string path, - AnchorStates anchorState = AnchorStates.Active, - DiffTypes diffType = DiffTypes.Effective, - string? fromHash = null, - string? toHash = null, - int? maxPages = null, - int? limit = null, - int? start = null, - int? avatarSize = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["avatarSize"] = avatarSize, - ["path"] = path, - ["anchorState"] = BitbucketHelpers.AnchorStateToString(anchorState), - ["diffType"] = BitbucketHelpers.DiffTypeToString(diffType), - ["fromHash"] = fromHash, - ["toHash"] = toHash - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/comments") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task GetPullRequestCommentAsync(string projectKey, string repositorySlug, long pullRequestId, long commentId, - int? avatarSize = null, - CancellationToken cancellationToken = default) - { - return await GetProjectsReposUrl(projectKey, repositorySlug) - .AppendPathSegment($"/pull-requests/{pullRequestId}/comments/{commentId}") - .SetQueryParam("avatarSize", avatarSize) - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - public async Task UpdatePullRequestCommentAsync(string projectKey, string repositorySlug, long pullRequestId, long commentId, - int version, string text, CancellationToken cancellationToken = default) - { - var data = new - { - version, - text - }; - - var response = await GetProjectsReposUrl(projectKey, repositorySlug) - .AppendPathSegment($"/pull-requests/{pullRequestId}/comments/{commentId}") - .SetQueryParam("version", version) - .PutJsonAsync(data, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task DeletePullRequestCommentAsync(string projectKey, string repositorySlug, long pullRequestId, long commentId, - int version = -1, - CancellationToken cancellationToken = default) - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug) - .AppendPathSegment($"/pull-requests/{pullRequestId}/comments/{commentId}") - .SetQueryParam("version", version) - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - public async Task> GetPullRequestCommitsAsync(string projectKey, string repositorySlug, long pullRequestId, - bool withCounts = false, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["withCounts"] = BitbucketHelpers.BoolToString(withCounts) - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/commits") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - /// - /// Streams all commits for a pull request as an IAsyncEnumerable. - /// - public IAsyncEnumerable GetPullRequestCommitsStreamAsync(string projectKey, string repositorySlug, long pullRequestId, - bool withCounts = false, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["withCounts"] = BitbucketHelpers.BoolToString(withCounts) - }; - - return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/commits") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken); - } - - public async Task GetPullRequestDiffAsync(string projectKey, string repositorySlug, long pullRequestId, - int contextLines = -1, - DiffTypes diffType = DiffTypes.Effective, - string? sinceId = null, - string? srcPath = null, - string? untilId = null, - string whitespace = "ignore-all", - bool withComments = true, - CancellationToken cancellationToken = default) - { - var queryParamValues = CreatePullRequestDiffQueryParams(contextLines, diffType, sinceId, srcPath, untilId, whitespace, withComments); - - return await GetProjectsReposUrl(projectKey, repositorySlug) - .AppendPathSegment($"/pull-requests/{pullRequestId}/diff") - .SetQueryParams(queryParamValues) - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - public async IAsyncEnumerable GetPullRequestDiffStreamAsync(string projectKey, string repositorySlug, long pullRequestId, - int contextLines = -1, - DiffTypes diffType = DiffTypes.Effective, - string? sinceId = null, - string? srcPath = null, - string? untilId = null, - string whitespace = "ignore-all", - bool withComments = true, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - var queryParamValues = CreatePullRequestDiffQueryParams(contextLines, diffType, sinceId, srcPath, untilId, whitespace, withComments); - var responseStream = await GetProjectsReposUrl(projectKey, repositorySlug) - .AppendPathSegment($"/pull-requests/{pullRequestId}/diff") - .SetQueryParams(queryParamValues) - .GetStreamAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - try - { - await foreach (var diff in DeserializePullRequestDiffsAsync(responseStream, cancellationToken).ConfigureAwait(false)) - { - yield return diff; - } - } - finally - { - responseStream.Dispose(); - } - } - - public async Task GetPullRequestDiffPathAsync(string projectKey, string repositorySlug, long pullRequestId, - string path, - int contextLines = -1, - DiffTypes diffType = DiffTypes.Effective, - string? sinceId = null, - string? srcPath = null, - string? untilId = null, - string whitespace = "ignore-all", - bool withComments = true, - CancellationToken cancellationToken = default) - { - var queryParamValues = CreatePullRequestDiffQueryParams(contextLines, diffType, sinceId, srcPath, untilId, whitespace, withComments); - - return await GetProjectsReposUrl(projectKey, repositorySlug) - .AppendPathSegment($"/pull-requests/{pullRequestId}/diff/{path}") - .SetQueryParams(queryParamValues) - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - private static Dictionary CreatePullRequestDiffQueryParams(int contextLines, DiffTypes diffType, string? sinceId, - string? srcPath, string? untilId, string whitespace, bool withComments) - { - return new Dictionary - { - ["contextLines"] = contextLines, - ["diffType"] = BitbucketHelpers.DiffTypeToString(diffType), - ["sinceId"] = sinceId, - ["srcPath"] = srcPath, - ["untilId"] = untilId, - ["whitespace"] = whitespace, - ["withComments"] = BitbucketHelpers.BoolToString(withComments) - }; - } - - private static async IAsyncEnumerable DeserializePullRequestDiffsAsync(Stream responseStream, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - await foreach (var diff in DeserializeDiffsFromStreamAsync(responseStream, cancellationToken).ConfigureAwait(false)) - { - yield return diff; - } - } - - /// - /// Deserializes diff entries from a JSON stream containing a "diffs" array. - /// Used by all diff streaming methods (commit, repository, compare, pull request). - /// Uses zero-copy deserialization directly from JsonElement to avoid intermediate string allocations. - /// - private static async IAsyncEnumerable DeserializeDiffsFromStreamAsync(Stream responseStream, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - using var doc = await JsonDocument.ParseAsync(responseStream, cancellationToken: cancellationToken).ConfigureAwait(false); - - if (!doc.RootElement.TryGetProperty("diffs", out var diffsArray) || diffsArray.ValueKind != JsonValueKind.Array) - { - yield break; - } - - foreach (var diffElement in diffsArray.EnumerateArray()) - { - cancellationToken.ThrowIfCancellationRequested(); - - // Zero-copy: Deserialize directly from JsonElement instead of GetRawText() string allocation - var diff = diffElement.Deserialize(s_jsonOptions); - if (diff is not null) - { - yield return diff; - } - } - } - - // Note: MoveToDiffArrayAsync is no longer needed with System.Text.Json approach - - public async Task> GetPullRequestParticipantsAsync(string projectKey, string repositorySlug, long pullRequestId, - int? maxPages = null, - int? limit = null, - int? start = null, - int? avatarSize = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["avatarSize"] = avatarSize - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsReposUrl(projectKey, repositorySlug) - .AppendPathSegment($"/pull-requests/{pullRequestId}/participants") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task AssignUserRoleToPullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, - Named named, - Roles role, - CancellationToken cancellationToken = default) - { - var data = new - { - user = named, - role = BitbucketHelpers.RoleToString(role) - }; - - var response = await GetProjectsReposUrl(projectKey, repositorySlug) - .AppendPathSegment($"/pull-requests/{pullRequestId}/participants") - .PostJsonAsync(data, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task DeletePullRequestParticipantAsync(string projectKey, string repositorySlug, long pullRequestId, string userName, CancellationToken cancellationToken = default) - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug) - .AppendPathSegment($"/pull-requests/{pullRequestId}/participants") - .SetQueryParam("username", userName) - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - public async Task UpdatePullRequestParticipantStatus(string projectKey, string repositorySlug, long pullRequestId, - string userSlug, - Named named, - bool approved, - ParticipantStatus participantStatus, - CancellationToken cancellationToken = default) - { - var data = new - { - user = named, - approved = BitbucketHelpers.BoolToString(approved), - status = BitbucketHelpers.ParticipantStatusToString(participantStatus) - }; - - var response = await GetProjectsReposUrl(projectKey, repositorySlug) - .AppendPathSegment($"/pull-requests/{pullRequestId}/participants/{userSlug}") - .PutJsonAsync(data, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task UnassignUserFromPullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, string userSlug, CancellationToken cancellationToken = default) - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug) - .AppendPathSegment($"/pull-requests/{pullRequestId}/participants/{userSlug}") - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - /// - /// Gets tasks for a pull request using the legacy tasks endpoint. - /// - /// The project key. - /// The repository slug. - /// The pull request ID. - /// Maximum number of pages to retrieve. - /// Maximum number of results per page. - /// Pagination start index. - /// Avatar size for user avatars. - /// Cancellation token. - /// Collection of tasks. - /// - /// - /// Deprecation Notice: This endpoint was deprecated in Bitbucket Server 9.0 and returns 404 Not Found on servers version 9.0+. - /// - /// - /// For Bitbucket Server 9.0+, use instead. - /// For cross-version compatibility, use . - /// - /// - [Obsolete("This endpoint is deprecated in Bitbucket Server 9.0+. Use GetPullRequestBlockerCommentsAsync for 9.0+ or GetPullRequestTasksWithFallbackAsync for cross-version compatibility.")] - public async Task> GetPullRequestTasksAsync(string projectKey, string repositorySlug, long pullRequestId, - int? maxPages = null, - int? limit = null, - int? start = null, - int? avatarSize = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["avatarSize"] = avatarSize - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/tasks") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - /// - /// Gets the task count for a pull request using the legacy tasks endpoint. - /// - /// The project key. - /// The repository slug. - /// The pull request ID. - /// Cancellation token. - /// The task count. - /// - /// - /// Deprecation Notice: This endpoint was deprecated in Bitbucket Server 9.0 and may return 404 Not Found on servers version 9.0+. - /// - /// - /// For Bitbucket Server 9.0+, use and count the results. - /// - /// - [Obsolete("This endpoint is deprecated in Bitbucket Server 9.0+. Use GetPullRequestBlockerCommentsAsync and count the results for 9.0+ compatibility.")] - public async Task GetPullRequestTaskCountAsync(string projectKey, string repositorySlug, long pullRequestId, CancellationToken cancellationToken = default) - { - return await GetProjectsReposUrl(projectKey, repositorySlug) - .AppendPathSegment($"/pull-requests/{pullRequestId}/tasks/count") - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - #region Blocker Comments (Bitbucket Server 9.0+) - - /// - /// Gets blocker comments (tasks) for a pull request. - /// This endpoint is available in Bitbucket Server 9.0+ and replaces the legacy tasks endpoint. - /// - /// The project key. - /// The repository slug. - /// The pull request ID. - /// Optional filter: , , or null for all. - /// Maximum number of pages to retrieve. - /// Maximum number of results per page. - /// Pagination start index. - /// Cancellation token. - /// Collection of blocker comments. - /// - /// - /// In Bitbucket Server 9.0+, tasks have been replaced by blocker comments. - /// A blocker comment is a comment with severity: 'BLOCKER' that must be resolved before the pull request can be merged. - /// - /// - /// For servers prior to 9.0, use instead. - /// - /// - public async Task> GetPullRequestBlockerCommentsAsync( - string projectKey, - string repositorySlug, - long pullRequestId, - BlockerCommentState? state = null, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["state"] = BitbucketHelpers.BlockerCommentStateToString(state) - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsReposUrl(projectKey, repositorySlug, $"/pull-requests/{pullRequestId}/blocker-comments") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - /// - /// Gets a single blocker comment by ID. - /// This endpoint is available in Bitbucket Server 9.0+. - /// - /// The project key. - /// The repository slug. - /// The pull request ID. - /// The blocker comment ID. - /// Cancellation token. - /// The blocker comment. - public async Task GetPullRequestBlockerCommentAsync( - string projectKey, - string repositorySlug, - long pullRequestId, - long blockerCommentId, - CancellationToken cancellationToken = default) - { - return await GetProjectsReposUrl(projectKey, repositorySlug) - .AppendPathSegment($"/pull-requests/{pullRequestId}/blocker-comments/{blockerCommentId}") - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - /// - /// Creates a blocker comment (task) on a pull request. - /// This endpoint is available in Bitbucket Server 9.0+. - /// - /// The project key. - /// The repository slug. - /// The pull request ID. - /// The blocker comment text. - /// Optional anchor for file/line-specific blockers. - /// Cancellation token. - /// The created blocker comment. - public async Task CreatePullRequestBlockerCommentAsync( - string projectKey, - string repositorySlug, - long pullRequestId, - string text, - CommentAnchor? anchor = null, - CancellationToken cancellationToken = default) - { - var data = new - { - text, - severity = "BLOCKER", - anchor - }; - - var response = await GetProjectsReposUrl(projectKey, repositorySlug) - .AppendPathSegment($"/pull-requests/{pullRequestId}/blocker-comments") - .PostJsonAsync(data, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - /// - /// Updates a blocker comment's text. - /// This endpoint is available in Bitbucket Server 9.0+. - /// - /// The project key. - /// The repository slug. - /// The pull request ID. - /// The blocker comment ID. - /// The updated blocker comment text. - /// The version of the blocker comment (for optimistic locking). - /// Cancellation token. - /// The updated blocker comment. - public async Task UpdatePullRequestBlockerCommentAsync( - string projectKey, - string repositorySlug, - long pullRequestId, - long blockerCommentId, - string text, - int version, - CancellationToken cancellationToken = default) - { - var data = new - { - text, - version - }; - - var response = await GetProjectsReposUrl(projectKey, repositorySlug) - .AppendPathSegment($"/pull-requests/{pullRequestId}/blocker-comments/{blockerCommentId}") - .PutJsonAsync(data, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - /// - /// Deletes a blocker comment. - /// This endpoint is available in Bitbucket Server 9.0+. - /// - /// The project key. - /// The repository slug. - /// The pull request ID. - /// The blocker comment ID. - /// The version of the blocker comment (for optimistic locking). - /// Cancellation token. - /// True if the blocker comment was deleted successfully. - public async Task DeletePullRequestBlockerCommentAsync( - string projectKey, - string repositorySlug, - long pullRequestId, - long blockerCommentId, - int version, - CancellationToken cancellationToken = default) - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug) - .AppendPathSegment($"/pull-requests/{pullRequestId}/blocker-comments/{blockerCommentId}") - .SetQueryParam("version", version) - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - /// - /// Resolves a blocker comment (marks the task as complete). - /// This endpoint is available in Bitbucket Server 9.0+. - /// - /// The project key. - /// The repository slug. - /// The pull request ID. - /// The blocker comment ID. - /// The version of the blocker comment (for optimistic locking). - /// Cancellation token. - /// The resolved blocker comment. - public async Task ResolvePullRequestBlockerCommentAsync( - string projectKey, - string repositorySlug, - long pullRequestId, - long blockerCommentId, - int version, - CancellationToken cancellationToken = default) - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug) - .AppendPathSegment($"/pull-requests/{pullRequestId}/blocker-comments/{blockerCommentId}/resolve") - .SetQueryParam("version", version) - .PutAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - /// - /// Reopens a resolved blocker comment. - /// This endpoint is available in Bitbucket Server 9.0+. - /// - /// The project key. - /// The repository slug. - /// The pull request ID. - /// The blocker comment ID. - /// The version of the blocker comment (for optimistic locking). - /// Cancellation token. - /// The reopened blocker comment. - public async Task ReopenPullRequestBlockerCommentAsync( - string projectKey, - string repositorySlug, - long pullRequestId, - long blockerCommentId, - int version, - CancellationToken cancellationToken = default) - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug) - .AppendPathSegment($"/pull-requests/{pullRequestId}/blocker-comments/{blockerCommentId}/reopen") - .SetQueryParam("version", version) - .PutAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - /// - /// Gets pull request tasks with automatic fallback for cross-version compatibility. - /// - /// - /// - /// This method provides backward compatibility across Bitbucket Server versions: - /// - /// - /// Bitbucket Server 9.0+: Uses the new /blocker-comments endpoint. - /// Bitbucket Server < 9.0: Falls back to the legacy /tasks endpoint. - /// - /// - /// The method first tries the new blocker-comments endpoint. If it returns 404 (Not Found), - /// it automatically falls back to the legacy tasks endpoint. - /// - /// - /// For new code targeting Bitbucket Server 9.0+, prefer using - /// directly for better type safety. - /// - /// - /// The project key. - /// The repository slug. - /// The pull request ID. - /// Maximum number of pages to retrieve. - /// Maximum number of results per page. - /// Pagination start index. - /// Cancellation token. - /// - /// A collection of blocker comments () on Bitbucket 9.0+, - /// or legacy tasks () on older versions. - /// - public async Task> GetPullRequestTasksWithFallbackAsync( - string projectKey, - string repositorySlug, - long pullRequestId, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) - { - try - { - // Try new blocker-comments endpoint first (Bitbucket 9.0+) - var blockerComments = await GetPullRequestBlockerCommentsAsync( - projectKey, repositorySlug, pullRequestId, - maxPages: maxPages, limit: limit, start: start, - cancellationToken: cancellationToken).ConfigureAwait(false); - - return blockerComments.Cast(); - } - catch (BitbucketNotFoundException) - { - // Fall back to legacy tasks endpoint (Bitbucket < 9.0) -#pragma warning disable CS0618 // Type or member is obsolete - intentional fallback - var tasks = await GetPullRequestTasksAsync( - projectKey, repositorySlug, pullRequestId, - maxPages: maxPages, limit: limit, start: start, - cancellationToken: cancellationToken).ConfigureAwait(false); -#pragma warning restore CS0618 - - return tasks.Cast(); - } - } - - #endregion - - public async Task WatchPullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, CancellationToken cancellationToken = default) - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug) - .AppendPathSegment($"/pull-requests/{pullRequestId}/watch") - .PostJsonAsync(new StringContent(""), cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - public async Task UnwatchPullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, CancellationToken cancellationToken = default) - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug) - .AppendPathSegment($"/pull-requests/{pullRequestId}/watch") - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - public async Task RetrieveRawContentAsync(string projectKey, string repositorySlug, string path, - string? at = null, - bool markup = false, - bool hardWrap = true, - bool htmlEscape = true, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["at"] = at, - ["markup"] = BitbucketHelpers.BoolToString(markup), - ["hardWrap"] = BitbucketHelpers.BoolToString(hardWrap), - ["htmlEscape"] = BitbucketHelpers.BoolToString(htmlEscape) - }; - - return await GetProjectsReposUrl(projectKey, repositorySlug) - .AppendPathSegment($"/raw/{path}") - .SetQueryParams(queryParamValues) - .GetStreamAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - public async Task GetProjectRepositoryPullRequestSettingsAsync(string projectKey, string repositorySlug, CancellationToken cancellationToken = default) - { - return await GetProjectsReposUrl(projectKey, repositorySlug, "/settings/pull-requests") - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - public async Task UpdateProjectRepositoryPullRequestSettingsAsync(string projectKey, string repositorySlug, - PullRequestSettings pullRequestSettings, CancellationToken cancellationToken = default) - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/settings/pull-requests") - .PostJsonAsync(pullRequestSettings, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task> GetProjectRepositoryHooksSettingsAsync(string projectKey, string repositorySlug, - HookTypes? hookType = null, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["type"] = hookType - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsReposUrl(projectKey, repositorySlug, "/settings/hooks") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task GetProjectRepositoryHookSettingsAsync(string projectKey, string repositorySlug, string hookKey, CancellationToken cancellationToken = default) - { - return await GetProjectsReposUrl(projectKey, repositorySlug, $"/settings/hooks/{hookKey}") - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - public async Task DeleteProjectRepositoryHookSettingsAsync(string projectKey, string repositorySlug, string hookKey, CancellationToken cancellationToken = default) - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/settings/hooks/{hookKey}") - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - public async Task EnableProjectRepositoryHookAsync(string projectKey, string repositorySlug, string hookKey, object? hookSettings = null, CancellationToken cancellationToken = default) - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/settings/hooks/{hookKey}/enabled") - .PutJsonAsync(hookSettings, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task DisableProjectRepositoryHookAsync(string projectKey, string repositorySlug, string hookKey, CancellationToken cancellationToken = default) - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/settings/hooks/{hookKey}/enabled") - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task> GetProjectRepositoryHookAllSettingsAsync(string projectKey, string repositorySlug, string hookKey, CancellationToken cancellationToken = default) - { - return await GetProjectsReposUrl(projectKey, repositorySlug, $"/settings/hooks/{hookKey}/settings") - .GetJsonAsync>(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - public async Task> UpdateProjectRepositoryHookAllSettingsAsync(string projectKey, string repositorySlug, string hookKey, - Dictionary allSettings, CancellationToken cancellationToken = default) - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/settings/hooks/{hookKey}/settings") - .PutJsonAsync(allSettings, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task GetProjectPullRequestsMergeStrategiesAsync(string projectKey, string scmId, CancellationToken cancellationToken = default) - { - return await GetProjectUrl(projectKey) - .AppendPathSegment($"/settings/pull-requests/{scmId}") - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - public async Task UpdateProjectPullRequestsMergeStrategiesAsync(string projectKey, string scmId, MergeStrategies mergeStrategies, CancellationToken cancellationToken = default) - { - var response = await GetProjectUrl(projectKey) - .AppendPathSegment($"/settings/pull-requests/{scmId}") - .PostJsonAsync(mergeStrategies, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task> GetProjectRepositoryTagsAsync(string projectKey, string repositorySlug, - string filterText, - BranchOrderBy orderBy, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["filterText"] = filterText, - ["orderBy"] = BitbucketHelpers.BranchOrderByToString(orderBy) - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsReposUrl(projectKey, repositorySlug, "/tags") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task CreateProjectRepositoryTagAsync(string projectKey, string repositorySlug, - string name, - string startPoint, - string message, - CancellationToken cancellationToken = default) - { - var data = new - { - name, - startPoint, - message - }; - - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/tags") - .PostJsonAsync(data, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task GetProjectRepositoryTagAsync(string projectKey, string repositorySlug, string tagName, CancellationToken cancellationToken = default) - { - return await GetProjectsReposUrl(projectKey, repositorySlug, $"/tags/{tagName}") - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - public async Task> GetProjectRepositoryWebHooksAsync(string projectKey, string repositorySlug, - string? @event = null, - bool statistics = false, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["event"] = @event, - ["statistics"] = BitbucketHelpers.BoolToString(statistics) - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetProjectsReposUrl(projectKey, repositorySlug, "/webhooks") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task CreateProjectRepositoryWebHookAsync(string projectKey, string repositorySlug, WebHook webHook, CancellationToken cancellationToken = default) - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/webhooks") - .PostJsonAsync(webHook, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task TestProjectRepositoryWebHookAsync(string projectKey, string repositorySlug, string url, CancellationToken cancellationToken = default) - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, "/webhooks/test") - .SetQueryParam("url", url) - .PostJsonAsync(new StringContent(""), cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task GetProjectRepositoryWebHookAsync(string projectKey, string repositorySlug, - string webHookId, - bool statistics = false, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["statistics"] = BitbucketHelpers.BoolToString(statistics) - }; - - return await GetProjectsReposUrl(projectKey, repositorySlug, $"/webhooks/{webHookId}") - .SetQueryParams(queryParamValues) - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - public async Task UpdateProjectRepositoryWebHookAsync(string projectKey, string repositorySlug, - string webHookId, WebHook webHook, CancellationToken cancellationToken = default) - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/webhooks/{webHookId}") - .PutJsonAsync(webHook, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task DeleteProjectRepositoryWebHookAsync(string projectKey, string repositorySlug, - string webHookId, CancellationToken cancellationToken = default) - { - var response = await GetProjectsReposUrl(projectKey, repositorySlug, $"/webhooks/{webHookId}") - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - //public async Task GetProjectRepositoryWebHookLatestAsync(string projectKey, string repositorySlug, - public async Task GetProjectRepositoryWebHookLatestAsync(string projectKey, string repositorySlug, - string webHookId, - string? @event = null, - WebHookOutcomes? outcome = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["event"] = @event, - ["outcome"] = BitbucketHelpers.WebHookOutcomeToString(outcome) - }; - - return await GetProjectsReposUrl(projectKey, repositorySlug, $"/webhooks/{webHookId}/latest") - .SetQueryParams(queryParamValues) - //.GetJsonAsync() - .GetStringAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - public async Task GetProjectRepositoryWebHookStatisticsAsync(string projectKey, string repositorySlug, - string webHookId, - string? @event = null, - CancellationToken cancellationToken = default) - { - return await GetProjectsReposUrl(projectKey, repositorySlug, $"/webhooks/{webHookId}/statistics") - .SetQueryParam("event", @event) - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - public async Task> GetProjectRepositoryWebHookStatisticsSummaryAsync(string projectKey, string repositorySlug, - string webHookId, CancellationToken cancellationToken = default) - { - return await GetProjectsReposUrl(projectKey, repositorySlug, $"/webhooks/{webHookId}/statistics/summary") - .GetJsonAsync>(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - } -} diff --git a/src/Bitbucket.Net/Core/Repos/BitbucketClient.cs b/src/Bitbucket.Net/Core/Repos/BitbucketClient.cs index 308eb6d..e5e7e5e 100644 --- a/src/Bitbucket.Net/Core/Repos/BitbucketClient.cs +++ b/src/Bitbucket.Net/Core/Repos/BitbucketClient.cs @@ -1,75 +1,107 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; using Bitbucket.Net.Common; using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Core.Admin; using Bitbucket.Net.Models.Core.Projects; using Flurl.Http; -namespace Bitbucket.Net +namespace Bitbucket.Net; + +/// +/// Provides repository listing Bitbucket API operations. +/// +public partial class BitbucketClient { - public partial class BitbucketClient - { - private IFlurlRequest GetReposUrl() => GetBaseUrl() - .AppendPathSegment("/repos"); + /// + /// Gets the base repositories URL. + /// + /// An targeting the repos endpoint. + private IFlurlRequest GetReposUrl() => GetBaseUrl() + .AppendPathSegment("/repos"); - public async Task> GetRepositoriesAsync( - int? maxPages = null, - int? limit = null, - int? start = null, - string? name = null, - string? projectName = null, - Permissions? permission = null, - bool isPublic = false, - CancellationToken cancellationToken = default) + /// + /// Retrieves repositories accessible to the current user. + /// + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Optional repository name filter. + /// Optional project name filter. + /// Optional permission filter. + /// Whether to include only public repositories. + /// Token to cancel the operation. + /// A collection of repositories. + public async Task> GetRepositoriesAsync( + int? maxPages = null, + int? limit = null, + int? start = null, + string? name = null, + string? projectName = null, + Permissions? permission = null, + bool isPublic = false, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["name"] = name, - ["projectname"] = projectName, - ["permission"] = BitbucketHelpers.PermissionToString(permission), - ["visibility"] = isPublic ? "public" : "private" - }; + ["limit"] = limit, + ["start"] = start, + ["name"] = name, + ["projectname"] = projectName, + ["permission"] = BitbucketHelpers.PermissionToString(permission), + ["visibility"] = isPublic ? "public" : "private", + }; - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetReposUrl() + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetReposUrl() .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } + .GetAsync(ct) + .ConfigureAwait(false); - /// - /// Streams all repositories as an IAsyncEnumerable, yielding items as they are retrieved. - /// - public IAsyncEnumerable GetRepositoriesStreamAsync( - int? maxPages = null, - int? limit = null, - int? start = null, - string? name = null, - string? projectName = null, - Permissions? permission = null, - bool isPublic = false, - CancellationToken cancellationToken = default) + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Streams all repositories accessible to the current user, yielding items as they are retrieved. + /// + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Optional repository name filter. + /// Optional project name filter. + /// Optional permission filter. + /// Whether to include only public repositories. + /// Token to cancel the operation. + /// An async enumerable of repositories. + public IAsyncEnumerable GetRepositoriesStreamAsync( + int? maxPages = null, + int? limit = null, + int? start = null, + string? name = null, + string? projectName = null, + Permissions? permission = null, + bool isPublic = false, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["name"] = name, - ["projectname"] = projectName, - ["permission"] = BitbucketHelpers.PermissionToString(permission), - ["visibility"] = isPublic ? "public" : "private" - }; + ["limit"] = limit, + ["start"] = start, + ["name"] = name, + ["projectname"] = projectName, + ["permission"] = BitbucketHelpers.PermissionToString(permission), + ["visibility"] = isPublic ? "public" : "private", + }; - return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetReposUrl() + return GetPagedResultsStreamAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetReposUrl() .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken); - } + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Tasks/BitbucketClient.cs b/src/Bitbucket.Net/Core/Tasks/BitbucketClient.cs index d49a0ef..839636e 100644 --- a/src/Bitbucket.Net/Core/Tasks/BitbucketClient.cs +++ b/src/Bitbucket.Net/Core/Tasks/BitbucketClient.cs @@ -1,57 +1,95 @@ -using System.Threading; -using System.Threading.Tasks; -using Bitbucket.Net.Models.Core.Tasks; -using Flurl.Http; - -namespace Bitbucket.Net -{ - public partial class BitbucketClient - { - private IFlurlRequest GetTasksUrl() => GetBaseUrl() - .AppendPathSegment("/tasks"); - - private IFlurlRequest GetTasksUrl(string path) => GetTasksUrl() - .AppendPathSegment(path); - - public async Task CreateTaskAsync(TaskInfo taskInfo, CancellationToken cancellationToken = default) - { - var response = await GetTasksUrl() - .PostJsonAsync(taskInfo, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task GetTaskAsync(long taskId, int? avatarSize = null, CancellationToken cancellationToken = default) - { - return await GetTasksUrl($"/{taskId}") - .SetQueryParam("avatarSize", avatarSize) - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - public async Task UpdateTaskAsync(long taskId, string text, CancellationToken cancellationToken = default) - { - var obj = new - { - id = taskId, - text - }; - - var response = await GetTasksUrl($"/{taskId}") - .PutJsonAsync(obj, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task DeleteTaskAsync(long taskId, CancellationToken cancellationToken = default) - { - var response = await GetTasksUrl($"/{taskId}") - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - } -} +using Bitbucket.Net.Common; +using Bitbucket.Net.Models.Core.Tasks; +using Flurl.Http; + +namespace Bitbucket.Net; + +/// +/// Provides task-related Bitbucket API operations. +/// +public partial class BitbucketClient +{ + /// + /// Gets the base tasks URL. + /// + /// An targeting the tasks endpoint. + private IFlurlRequest GetTasksUrl() => GetBaseUrl() + .AppendPathSegment("/tasks"); + + /// + /// Gets the tasks URL for the specified path. + /// + /// The path to append to the tasks endpoint. + /// An pointing to the tasks path. + private IFlurlRequest GetTasksUrl(string path) => GetTasksUrl() + .AppendPathSegment(path); + + /// + /// Creates a task. + /// + /// The task information. + /// Token to cancel the operation. + /// The created task. + public async Task CreateTaskAsync(TaskInfo taskInfo, CancellationToken cancellationToken = default) + { + var response = await GetTasksUrl() + .SendAsync(HttpMethod.Post, CreateJsonContent(taskInfo), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves a task by identifier. + /// + /// The task identifier. + /// Optional avatar size for returned users. + /// Token to cancel the operation. + /// The requested task. + public async Task GetTaskAsync(long taskId, int? avatarSize = null, CancellationToken cancellationToken = default) + { + var response = await GetTasksUrl($"/{taskId}") + .SetQueryParam("avatarSize", avatarSize) + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Updates a task's text. + /// + /// The task identifier. + /// The updated task text. + /// Token to cancel the operation. + /// The updated task. + public async Task UpdateTaskAsync(long taskId, string text, CancellationToken cancellationToken = default) + { + var obj = new + { + id = taskId, + text, + }; + + var response = await GetTasksUrl($"/{taskId}") + .SendAsync(HttpMethod.Put, CreateJsonContent(obj), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Deletes a task. + /// + /// The task identifier. + /// Token to cancel the operation. + /// true if the task was deleted; otherwise, false. + public async Task DeleteTaskAsync(long taskId, CancellationToken cancellationToken = default) + { + var response = await GetTasksUrl($"/{taskId}") + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/Users/BitbucketClient.cs b/src/Bitbucket.Net/Core/Users/BitbucketClient.cs index 329d51a..b301b4d 100644 --- a/src/Bitbucket.Net/Core/Users/BitbucketClient.cs +++ b/src/Bitbucket.Net/Core/Users/BitbucketClient.cs @@ -1,111 +1,177 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; +using Bitbucket.Net.Common; using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Core.Users; using Flurl.Http; -namespace Bitbucket.Net +namespace Bitbucket.Net; + +/// +/// Provides user-related Bitbucket API operations. +/// +public partial class BitbucketClient { - public partial class BitbucketClient + /// + /// Gets the base users URL. + /// + /// An targeting the users endpoint. + private IFlurlRequest GetUsersUrl() => GetBaseUrl() + .AppendPathSegment("/users"); + + /// + /// Gets the users URL for the specified path. + /// + /// The path to append to the users endpoint. + /// An pointing to the users path. + private IFlurlRequest GetUsersUrl(string path) => GetUsersUrl() + .AppendPathSegment(path); + + /// + /// Retrieves users with optional filters. + /// + /// Optional search filter. + /// Optional group filter. + /// Optional permission filter. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Optional avatar size for returned users. + /// Token to cancel the operation. + /// Additional permission filters. + /// A collection of users. + public async Task> GetUsersAsync(string? filter = null, string? group = null, string? permission = null, + int? maxPages = null, + int? limit = null, + int? start = null, + int? avatarSize = null, + CancellationToken cancellationToken = default, + params string[] permissionN) { - private IFlurlRequest GetUsersUrl() => GetBaseUrl() - .AppendPathSegment("/users"); - - private IFlurlRequest GetUsersUrl(string path) => GetUsersUrl() - .AppendPathSegment(path); - - public async Task> GetUsersAsync(string? filter = null, string? group = null, string? permission = null, - int? maxPages = null, - int? limit = null, - int? start = null, - int? avatarSize = null, - CancellationToken cancellationToken = default, - params string[] permissionN) + var queryParamValues = new Dictionary(StringComparer.Ordinal) { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["avatarSize"] = avatarSize, - ["filter"] = filter, - ["group"] = group, - ["permission"] = permission - }; - - int permissionNCounter = 0; - foreach (string perm in permissionN) - { - permissionNCounter++; - queryParamValues.Add($"permission.{permissionNCounter}", perm); - } - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetUsersUrl() - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); + ["limit"] = limit, + ["start"] = start, + ["avatarSize"] = avatarSize, + ["filter"] = filter, + ["group"] = group, + ["permission"] = permission, + }; + + int permissionNCounter = 0; + foreach (string perm in permissionN) + { + permissionNCounter++; + queryParamValues.Add($"permission.{permissionNCounter}", perm); } - public async Task UpdateUserAsync(string? email = null, string? displayName = null, CancellationToken cancellationToken = default) - { - var obj = new + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => { - displayName, - email - }; + var response = await GetUsersUrl() + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } - var response = await GetUsersUrl() - .PutJsonAsync(obj, cancellationToken: cancellationToken) - .ConfigureAwait(false); + /// + /// Updates the current user's profile fields. + /// + /// Optional email address. + /// Optional display name. + /// Token to cancel the operation. + /// The updated user. + public async Task UpdateUserAsync(string? email = null, string? displayName = null, CancellationToken cancellationToken = default) + { + var obj = new + { + displayName, + email, + }; - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } + var response = await GetUsersUrl() + .SendAsync(HttpMethod.Put, CreateJsonContent(obj), cancellationToken: cancellationToken) + .ConfigureAwait(false); - public async Task UpdateUserCredentialsAsync(PasswordChange passwordChange, CancellationToken cancellationToken = default) - { - var response = await GetUsersUrl("/credentials") - .PutJsonAsync(passwordChange, cancellationToken: cancellationToken) - .ConfigureAwait(false); + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } + /// + /// Updates the current user's credentials. + /// + /// The password change payload. + /// Token to cancel the operation. + /// true if the update succeeded; otherwise, false. + public async Task UpdateUserCredentialsAsync(PasswordChange passwordChange, CancellationToken cancellationToken = default) + { + var response = await GetUsersUrl("/credentials") + .SendAsync(HttpMethod.Put, CreateJsonContent(passwordChange), cancellationToken: cancellationToken) + .ConfigureAwait(false); - public async Task GetUserAsync(string userSlug, int? avatarSize = null, CancellationToken cancellationToken = default) - { - return await GetUsersUrl($"/{userSlug}") - .SetQueryParam("avatarSize", avatarSize) - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } - public async Task DeleteUserAvatarAsync(string userSlug, CancellationToken cancellationToken = default) - { - var response = await GetUsersUrl($"/{userSlug}/avatar.png") - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); + /// + /// Retrieves a user by slug. + /// + /// The user slug. + /// Optional avatar size for the returned user. + /// Token to cancel the operation. + /// The requested user. + public async Task GetUserAsync(string userSlug, int? avatarSize = null, CancellationToken cancellationToken = default) + { + var response = await GetUsersUrl($"/{userSlug}") + .SetQueryParam("avatarSize", avatarSize) + .GetAsync(cancellationToken) + .ConfigureAwait(false); - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } - public async Task> GetUserSettingsAsync(string userSlug, CancellationToken cancellationToken = default) - { - var response = await GetUsersUrl($"/{userSlug}/settings") - .GetJsonAsync>(cancellationToken: cancellationToken) - .ConfigureAwait(false); + /// + /// Deletes a user's avatar. + /// + /// The user slug. + /// Token to cancel the operation. + /// true if the avatar was deleted; otherwise, false. + public async Task DeleteUserAvatarAsync(string userSlug, CancellationToken cancellationToken = default) + { + var response = await GetUsersUrl($"/{userSlug}/avatar.png") + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); - return response; - } + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } - public async Task UpdateUserSettingsAsync(string userSlug, IDictionary userSettings, CancellationToken cancellationToken = default) - { - var response = await GetUsersUrl($"/{userSlug}/settings") - .PostJsonAsync(userSettings, cancellationToken: cancellationToken) - .ConfigureAwait(false); + /// + /// Retrieves user settings. + /// + /// The user slug. + /// Token to cancel the operation. + /// A dictionary of user settings. + public async Task> GetUserSettingsAsync(string userSlug, CancellationToken cancellationToken = default) + { + var response = await GetUsersUrl($"/{userSlug}/settings") + .GetAsync(cancellationToken) + .ConfigureAwait(false); - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } + return await HandleResponseAsync>(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Updates user settings. + /// + /// The user slug. + /// The settings to update. + /// Token to cancel the operation. + /// true if the settings were updated; otherwise, false. + public async Task UpdateUserSettingsAsync(string userSlug, IDictionary userSettings, CancellationToken cancellationToken = default) + { + var response = await GetUsersUrl($"/{userSlug}/settings") + .SendAsync(HttpMethod.Post, CreateJsonContent(userSettings), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Core/WhoAmI/BitbucketClient.cs b/src/Bitbucket.Net/Core/WhoAmI/BitbucketClient.cs index b41196d..e58901a 100644 --- a/src/Bitbucket.Net/Core/WhoAmI/BitbucketClient.cs +++ b/src/Bitbucket.Net/Core/WhoAmI/BitbucketClient.cs @@ -1,72 +1,72 @@ -using System.Threading; -using System.Threading.Tasks; using Bitbucket.Net.Common; using Flurl; using Flurl.Http; -namespace Bitbucket.Net +namespace Bitbucket.Net; + +/// +/// Provides user identity helper operations. +/// +public partial class BitbucketClient { - public partial class BitbucketClient + /// + /// Gets the username of the currently authenticated user. + /// Uses the /plugins/servlet/applinks/whoami endpoint which returns + /// just the username as plain text. + /// + /// Cancellation token. + /// The username of the authenticated user, or null if not authenticated. + /// + /// This endpoint is essential for MCP servers and other integrations that need to + /// identify the current user context. Unlike GetUsersAsync(), this returns the + /// authenticated user specifically, not a list of all users. + /// + /// Usage example: + /// + /// var client = new BitbucketClient(url, () => token); + /// + /// // Get the authenticated user's username + /// var username = await client.GetWhoAmIAsync(); + /// + /// // Then fetch full user details if needed + /// if (username != null) + /// { + /// var currentUser = await client.GetUserAsync(username); + /// } + /// + /// + public async Task GetWhoAmIAsync(CancellationToken cancellationToken = default) { - /// - /// Gets the username of the currently authenticated user. - /// Uses the /plugins/servlet/applinks/whoami endpoint which returns - /// just the username as plain text. - /// - /// Cancellation token. - /// The username of the authenticated user, or null if not authenticated. - /// - /// This endpoint is essential for MCP servers and other integrations that need to - /// identify the current user context. Unlike GetUsersAsync(), this returns the - /// authenticated user specifically, not a list of all users. - /// - /// Usage example: - /// - /// var client = new BitbucketClient(url, () => token); - /// - /// // Get the authenticated user's username - /// var username = await client.GetWhoAmIAsync(); - /// - /// // Then fetch full user details if needed - /// if (username != null) - /// { - /// var currentUser = await client.GetUserAsync(username); - /// } - /// - /// - public async Task GetWhoAmIAsync(CancellationToken cancellationToken = default) + string response; + + // Handle DI constructor scenario (injected IFlurlClient or HttpClient) + if (_injectedClient != null) { - string response; + var request = _injectedClient + .Request() + .AppendPathSegment("/plugins/servlet/applinks/whoami"); - // Handle DI constructor scenario (injected IFlurlClient or HttpClient) - if (_injectedClient != null) + // Apply token authentication if provided + if (_getToken != null) { - var request = _injectedClient - .Request() - .AppendPathSegment("/plugins/servlet/applinks/whoami"); - - // Apply token authentication if provided - if (_getToken != null) - { - request = request.WithOAuthBearerToken(_getToken()); - } - - response = await request - .GetStringAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - else - { - // Original behavior for non-DI scenarios - // Construct full URL and convert to IFlurlRequest for authentication - var fullUrl = new Url(_url).AppendPathSegment("/plugins/servlet/applinks/whoami"); - response = await new FlurlRequest(fullUrl) - .WithAuthentication(_getToken, _userName, _password) - .GetStringAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); + request = request.WithOAuthBearerToken(_getToken()); } - return string.IsNullOrWhiteSpace(response) ? null : response.Trim(); + response = await request + .GetStringAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + else + { + // Original behavior for non-DI scenarios + // Construct full URL and convert to IFlurlRequest for authentication + var fullUrl = new Url(_url).AppendPathSegment("/plugins/servlet/applinks/whoami"); + response = await new FlurlRequest(fullUrl) + .WithAuthentication(_getToken, _userName, _password) + .GetStringAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); } + + return string.IsNullOrWhiteSpace(response) ? null : response.Trim(); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/DefaultReviewers/BitbucketClient.cs b/src/Bitbucket.Net/DefaultReviewers/BitbucketClient.cs index 28cbb77..5af1890 100644 --- a/src/Bitbucket.Net/DefaultReviewers/BitbucketClient.cs +++ b/src/Bitbucket.Net/DefaultReviewers/BitbucketClient.cs @@ -1,118 +1,200 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Bitbucket.Net.Models.Core.Users; -using Bitbucket.Net.Models.DefaultReviewers; -using Flurl.Http; - -namespace Bitbucket.Net -{ - public partial class BitbucketClient - { - private IFlurlRequest GetDefaultReviewersUrl() => GetBaseUrl("/default-reviewers"); - - private IFlurlRequest GetDefaultReviewersUrl(string path) => GetDefaultReviewersUrl() - .AppendPathSegment(path); - - public async Task> GetDefaultReviewerConditionsAsync(string projectKey, - int? avatarSize = null, CancellationToken cancellationToken = default) - { - var response = await GetDefaultReviewersUrl($"/projects/{projectKey}/conditions") - .SetQueryParam("avatarSize", avatarSize) - .GetAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task CreateDefaultReviewerConditionAsync(string projectKey, DefaultReviewerPullRequestCondition condition, CancellationToken cancellationToken = default) - { - var response = await GetDefaultReviewersUrl($"/projects/{projectKey}/conditions") - .PostJsonAsync(condition, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task UpdateDefaultReviewerConditionAsync(string projectKey, string defaultReviewerPullRequestConditionId, DefaultReviewerPullRequestCondition condition, CancellationToken cancellationToken = default) - { - var response = await GetDefaultReviewersUrl($"/projects/{projectKey}/conditions/{defaultReviewerPullRequestConditionId}") - .PutJsonAsync(condition, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task DeleteDefaultReviewerConditionAsync(string projectKey, string defaultReviewerPullRequestConditionId, CancellationToken cancellationToken = default) - { - var response = await GetDefaultReviewersUrl($"/projects/{projectKey}/conditions/{defaultReviewerPullRequestConditionId}") - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - public async Task> GetDefaultReviewerConditionsAsync(string projectKey, string repositorySlug, - int? avatarSize = null, CancellationToken cancellationToken = default) - { - var response = await GetDefaultReviewersUrl($"/projects/{projectKey}/repos/{repositorySlug}/conditions") - .SetQueryParam("avatarSize", avatarSize) - .GetAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task CreateDefaultReviewerConditionAsync(string projectKey, string repositorySlug, DefaultReviewerPullRequestCondition condition, CancellationToken cancellationToken = default) - { - var response = await GetDefaultReviewersUrl($"/projects/{projectKey}/repos/{repositorySlug}/conditions") - .PostJsonAsync(condition, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task UpdateDefaultReviewerConditionAsync(string projectKey, string repositorySlug, string defaultReviewerPullRequestConditionId, DefaultReviewerPullRequestCondition condition, CancellationToken cancellationToken = default) - { - var response = await GetDefaultReviewersUrl($"/projects/{projectKey}/repos/{repositorySlug}/conditions/{defaultReviewerPullRequestConditionId}") - .PutJsonAsync(condition, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task DeleteDefaultReviewerConditionAsync(string projectKey, string repositorySlug, string defaultReviewerPullRequestConditionId, CancellationToken cancellationToken = default) - { - var response = await GetDefaultReviewersUrl($"/projects/{projectKey}/repos/{repositorySlug}/conditions/{defaultReviewerPullRequestConditionId}") - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - public async Task> GetDefaultReviewersAsync(string projectKey, string repositorySlug, - int? sourceRepoId = null, - int? targetRepoId = null, - string? sourceRefId = null, - string? targetRefId = null, - int? avatarSize = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["sourceRepoId"] = sourceRepoId, - ["targetRepoId"] = targetRepoId, - ["sourceRefId"] = sourceRefId, - ["targetRefId"] = targetRefId, - ["avatarSize"] = avatarSize - }; - - var response = await GetDefaultReviewersUrl($"/projects/{projectKey}/repos/{repositorySlug}/reviewers") - .SetQueryParams(queryParamValues) - .GetAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - } -} +using Bitbucket.Net.Models.Core.Users; +using Bitbucket.Net.Models.DefaultReviewers; +using Flurl.Http; + +namespace Bitbucket.Net; + +/// +/// Provides default reviewer related Bitbucket API operations. +/// +public partial class BitbucketClient +{ + /// + /// Gets the base default reviewers URL. + /// + /// An targeting the default reviewers root. + private IFlurlRequest GetDefaultReviewersUrl() => GetBaseUrl("/default-reviewers"); + + /// + /// Gets the default reviewers URL for the specified path. + /// + /// The path to append to the default reviewers root. + /// An pointing to the requested default reviewers path. + private IFlurlRequest GetDefaultReviewersUrl(string path) => GetDefaultReviewersUrl() + .AppendPathSegment(path); + + /// + /// Retrieves default reviewer conditions for a project. + /// + /// The project key. + /// Optional avatar size for returned users. + /// Token to cancel the operation. + /// A collection of default reviewer conditions. + public async Task> GetDefaultReviewerConditionsAsync(string projectKey, + int? avatarSize = null, CancellationToken cancellationToken = default) + { + var response = await GetDefaultReviewersUrl($"/projects/{projectKey}/conditions") + .SetQueryParam("avatarSize", avatarSize) + .GetAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Creates a default reviewer condition for a project. + /// + /// The project key. + /// The condition to create. + /// Token to cancel the operation. + /// The created default reviewer condition. + public async Task CreateDefaultReviewerConditionAsync(string projectKey, DefaultReviewerPullRequestCondition condition, CancellationToken cancellationToken = default) + { + var response = await GetDefaultReviewersUrl($"/projects/{projectKey}/conditions") + .SendAsync(HttpMethod.Post, CreateJsonContent(condition), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Updates a default reviewer condition for a project. + /// + /// The project key. + /// The condition identifier. + /// The updated condition. + /// Token to cancel the operation. + /// The updated default reviewer condition. + public async Task UpdateDefaultReviewerConditionAsync(string projectKey, string defaultReviewerPullRequestConditionId, DefaultReviewerPullRequestCondition condition, CancellationToken cancellationToken = default) + { + var response = await GetDefaultReviewersUrl($"/projects/{projectKey}/conditions/{defaultReviewerPullRequestConditionId}") + .SendAsync(HttpMethod.Put, CreateJsonContent(condition), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Deletes a default reviewer condition from a project. + /// + /// The project key. + /// The condition identifier. + /// Token to cancel the operation. + /// true if the condition was deleted; otherwise, false. + public async Task DeleteDefaultReviewerConditionAsync(string projectKey, string defaultReviewerPullRequestConditionId, CancellationToken cancellationToken = default) + { + var response = await GetDefaultReviewersUrl($"/projects/{projectKey}/conditions/{defaultReviewerPullRequestConditionId}") + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves default reviewer conditions for a repository. + /// + /// The project key. + /// The repository slug. + /// Optional avatar size for returned users. + /// Token to cancel the operation. + /// A collection of default reviewer conditions. + public async Task> GetDefaultReviewerConditionsAsync(string projectKey, string repositorySlug, + int? avatarSize = null, CancellationToken cancellationToken = default) + { + var response = await GetDefaultReviewersUrl($"/projects/{projectKey}/repos/{repositorySlug}/conditions") + .SetQueryParam("avatarSize", avatarSize) + .GetAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Creates a default reviewer condition for a repository. + /// + /// The project key. + /// The repository slug. + /// The condition to create. + /// Token to cancel the operation. + /// The created default reviewer condition. + public async Task CreateDefaultReviewerConditionAsync(string projectKey, string repositorySlug, DefaultReviewerPullRequestCondition condition, CancellationToken cancellationToken = default) + { + var response = await GetDefaultReviewersUrl($"/projects/{projectKey}/repos/{repositorySlug}/conditions") + .SendAsync(HttpMethod.Post, CreateJsonContent(condition), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Updates a default reviewer condition for a repository. + /// + /// The project key. + /// The repository slug. + /// The condition identifier. + /// The updated condition. + /// Token to cancel the operation. + /// The updated default reviewer condition. + public async Task UpdateDefaultReviewerConditionAsync(string projectKey, string repositorySlug, string defaultReviewerPullRequestConditionId, DefaultReviewerPullRequestCondition condition, CancellationToken cancellationToken = default) + { + var response = await GetDefaultReviewersUrl($"/projects/{projectKey}/repos/{repositorySlug}/conditions/{defaultReviewerPullRequestConditionId}") + .SendAsync(HttpMethod.Put, CreateJsonContent(condition), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Deletes a default reviewer condition from a repository. + /// + /// The project key. + /// The repository slug. + /// The condition identifier. + /// Token to cancel the operation. + /// true if the condition was deleted; otherwise, false. + public async Task DeleteDefaultReviewerConditionAsync(string projectKey, string repositorySlug, string defaultReviewerPullRequestConditionId, CancellationToken cancellationToken = default) + { + var response = await GetDefaultReviewersUrl($"/projects/{projectKey}/repos/{repositorySlug}/conditions/{defaultReviewerPullRequestConditionId}") + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves default reviewers for a repository. + /// + /// The project key. + /// The repository slug. + /// Optional source repository identifier. + /// Optional target repository identifier. + /// Optional source reference identifier. + /// Optional target reference identifier. + /// Optional avatar size for returned users. + /// Token to cancel the operation. + /// A collection of default reviewers. + public async Task> GetDefaultReviewersAsync(string projectKey, string repositorySlug, + int? sourceRepoId = null, + int? targetRepoId = null, + string? sourceRefId = null, + string? targetRefId = null, + int? avatarSize = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["sourceRepoId"] = sourceRepoId, + ["targetRepoId"] = targetRepoId, + ["sourceRefId"] = sourceRefId, + ["targetRefId"] = targetRefId, + ["avatarSize"] = avatarSize, + }; + + var response = await GetDefaultReviewersUrl($"/projects/{projectKey}/repos/{repositorySlug}/reviewers") + .SetQueryParams(queryParamValues) + .GetAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Git/BitbucketClient.cs b/src/Bitbucket.Net/Git/BitbucketClient.cs index 0b62bb2..3e6a7b1 100644 --- a/src/Bitbucket.Net/Git/BitbucketClient.cs +++ b/src/Bitbucket.Net/Git/BitbucketClient.cs @@ -1,61 +1,105 @@ -using System.Threading; -using System.Threading.Tasks; using Bitbucket.Net.Common; using Bitbucket.Net.Models.Core.Projects; using Bitbucket.Net.Models.Git; using Flurl.Http; -namespace Bitbucket.Net +namespace Bitbucket.Net; + +/// +/// Provides Git-related Bitbucket API operations. +/// +public partial class BitbucketClient { - public partial class BitbucketClient - { - private IFlurlRequest GetGitUrl() => GetBaseUrl("/git"); + /// + /// Gets the base Git URL. + /// + /// An targeting the Git root. + private IFlurlRequest GetGitUrl() => GetBaseUrl("/git"); - private IFlurlRequest GetGitUrl(string path) => GetGitUrl() - .AppendPathSegment(path); + /// + /// Gets the Git URL for the specified path. + /// + /// The path to append to the Git root. + /// An pointing to the Git path. + private IFlurlRequest GetGitUrl(string path) => GetGitUrl() + .AppendPathSegment(path); - public async Task GetCanRebasePullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, CancellationToken cancellationToken = default) - { - var response = await GetGitUrl($"/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}/rebase") - .GetAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); + /// + /// Determines whether a pull request can be rebased. + /// + /// The project key. + /// The repository slug. + /// The pull request identifier. + /// Token to cancel the operation. + /// The rebase eligibility details. + public async Task GetCanRebasePullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, CancellationToken cancellationToken = default) + { + var response = await GetGitUrl($"/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}/rebase") + .GetAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } - public async Task RebasePullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, int version, CancellationToken cancellationToken = default) - { - var data = new { version }; - var response = await GetGitUrl($"/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}/rebase") - .PostJsonAsync(data, cancellationToken: cancellationToken) - .ConfigureAwait(false); + /// + /// Rebases a pull request to the latest target branch state. + /// + /// The project key. + /// The repository slug. + /// The pull request identifier. + /// The pull request version for concurrency control. + /// Token to cancel the operation. + /// The updated pull request. + public async Task RebasePullRequestAsync(string projectKey, string repositorySlug, long pullRequestId, int version, CancellationToken cancellationToken = default) + { + var data = new { version }; + var response = await GetGitUrl($"/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}/rebase") + .SendAsync(HttpMethod.Post, CreateJsonContent(data), cancellationToken: cancellationToken) + .ConfigureAwait(false); - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } - public async Task CreateTagAsync(string projectKey, string repositorySlug, TagTypes tagType, string tagName, string startPoint, CancellationToken cancellationToken = default) + /// + /// Creates a tag in a repository. + /// + /// The project key. + /// The repository slug. + /// The type of tag to create. + /// The name of the tag. + /// The commit or ref where the tag should point. + /// Token to cancel the operation. + /// The created tag. + public async Task CreateTagAsync(string projectKey, string repositorySlug, TagTypes tagType, string tagName, string startPoint, CancellationToken cancellationToken = default) + { + var data = new { - var data = new - { - type = BitbucketHelpers.TagTypeToString(tagType), - name = tagName, - startPoint - }; + type = BitbucketHelpers.TagTypeToString(tagType), + name = tagName, + startPoint, + }; - var response = await GetGitUrl($"/projects/{projectKey}/repos/{repositorySlug}/tags") - .PostJsonAsync(data, cancellationToken: cancellationToken) - .ConfigureAwait(false); + var response = await GetGitUrl($"/projects/{projectKey}/repos/{repositorySlug}/tags") + .SendAsync(HttpMethod.Post, CreateJsonContent(data), cancellationToken: cancellationToken) + .ConfigureAwait(false); - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } - public async Task DeleteTagAsync(string projectKey, string repositorySlug, string tagName, CancellationToken cancellationToken = default) - { - var response = await GetGitUrl($"/projects/{projectKey}/repos/{repositorySlug}/tags/{tagName}") - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); + /// + /// Deletes a tag from a repository. + /// + /// The project key. + /// The repository slug. + /// The name of the tag to delete. + /// Token to cancel the operation. + /// true if the tag was deleted; otherwise, false. + public async Task DeleteTagAsync(string projectKey, string repositorySlug, string tagName, CancellationToken cancellationToken = default) + { + var response = await GetGitUrl($"/projects/{projectKey}/repos/{repositorySlug}/tags/{tagName}") + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Jira/BitbucketClient.cs b/src/Bitbucket.Net/Jira/BitbucketClient.cs index 7b03427..21f7dec 100644 --- a/src/Bitbucket.Net/Jira/BitbucketClient.cs +++ b/src/Bitbucket.Net/Jira/BitbucketClient.cs @@ -1,65 +1,105 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; +using Bitbucket.Net.Common; using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Builds; using Bitbucket.Net.Models.Jira; using Flurl.Http; -namespace Bitbucket.Net +namespace Bitbucket.Net; + +/// +/// Provides Jira-related Bitbucket API operations. +/// +public partial class BitbucketClient { - public partial class BitbucketClient - { - private IFlurlRequest GetJiraUrl() => GetBaseUrl("/jira"); + /// + /// Gets the base Jira URL. + /// + /// An targeting the Jira root. + private IFlurlRequest GetJiraUrl() => GetBaseUrl("/jira"); - private IFlurlRequest GetJiraUrl(string path) => GetJiraUrl() - .AppendPathSegment(path); + /// + /// Gets the Jira URL for the specified path. + /// + /// The path to append to the Jira root. + /// An pointing to the Jira path. + private IFlurlRequest GetJiraUrl(string path) => GetJiraUrl() + .AppendPathSegment(path); - public async Task> GetChangeSetsAsync(string issueKey, int maxChanges = 10, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) + /// + /// Retrieves changesets linked to a Jira issue. + /// + /// The Jira issue key. + /// Maximum number of changes per commit to include. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Token to cancel the operation. + /// A collection of changesets. + public async Task> GetChangeSetsAsync(string issueKey, int maxChanges = 10, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) { - var queryParamValues = new Dictionary + ["limit"] = limit, + ["start"] = start, + ["maxChanges"] = maxChanges, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => { - ["limit"] = limit, - ["start"] = start, - ["maxChanges"] = maxChanges - }; + var response = await GetJiraUrl($"/issues/{issueKey}/commits") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetJiraUrl($"/issues/{issueKey}/commits") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } - public async Task CreateJiraIssueAsync(string pullRequestCommentId, string applicationId, string title, string type, CancellationToken cancellationToken = default) + /// + /// Creates a Jira issue linked to a pull request comment. + /// + /// The pull request comment identifier. + /// The application identifier. + /// The issue title. + /// The issue type. + /// Token to cancel the operation. + /// The created Jira issue. + public async Task CreateJiraIssueAsync(string pullRequestCommentId, string applicationId, string title, string type, CancellationToken cancellationToken = default) + { + var data = new { - var data = new - { - id = "https://docs.atlassian.com/jira/REST/schema/string#", - title, - type - }; + id = "https://docs.atlassian.com/jira/REST/schema/string#", + title, + type, + }; - var response = await GetJiraUrl($"/comments/{pullRequestCommentId}/issues") - .SetQueryParam("applicationId", applicationId) - .PostJsonAsync(data, cancellationToken: cancellationToken) - .ConfigureAwait(false); + var response = await GetJiraUrl($"/comments/{pullRequestCommentId}/issues") + .SetQueryParam("applicationId", applicationId) + .SendAsync(HttpMethod.Post, CreateJsonContent(data), cancellationToken: cancellationToken) + .ConfigureAwait(false); - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } - public async Task> GetJiraIssuesAsync(string projectKey, string repositorySlug, long pullRequestId, CancellationToken cancellationToken = default) - { - var response = await GetJiraUrl($"/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}/issues") - .GetAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); + /// + /// Retrieves Jira issues linked to a pull request. + /// + /// The project key. + /// The repository slug. + /// The pull request identifier. + /// Token to cancel the operation. + /// A collection of Jira issue links. + public async Task> GetJiraIssuesAsync(string projectKey, string repositorySlug, long pullRequestId, CancellationToken cancellationToken = default) + { + var response = await GetJiraUrl($"/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}/issues") + .GetAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); - return await HandleResponseAsync>(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } + return await HandleResponseAsync>(response, cancellationToken: cancellationToken).ConfigureAwait(false); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Audit/AuditEvent.cs b/src/Bitbucket.Net/Models/Audit/AuditEvent.cs index 02702e4..30c925c 100644 --- a/src/Bitbucket.Net/Models/Audit/AuditEvent.cs +++ b/src/Bitbucket.Net/Models/Audit/AuditEvent.cs @@ -1,12 +1,11 @@ using Bitbucket.Net.Models.Core.Users; -namespace Bitbucket.Net.Models.Audit +namespace Bitbucket.Net.Models.Audit; + +public class AuditEvent { - public class AuditEvent - { - public string Action { get; set; } - public long Timestamp { get; set; } - public string Details { get; set; } - public User User { get; set; } - } -} + public string? Action { get; set; } + public long Timestamp { get; set; } + public string? Details { get; set; } + public User? User { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Branches/BranchModel.cs b/src/Bitbucket.Net/Models/Branches/BranchModel.cs index ae3f5ab..737cd00 100644 --- a/src/Bitbucket.Net/Models/Branches/BranchModel.cs +++ b/src/Bitbucket.Net/Models/Branches/BranchModel.cs @@ -1,12 +1,10 @@ -using System.Collections.Generic; -using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Core.Projects; -namespace Bitbucket.Net.Models.Branches +namespace Bitbucket.Net.Models.Branches; + +public class BranchModel { - public class BranchModel - { - public Branch Development { get; set; } - public Branch Production { get; set; } - public List Types { get; set; } - } -} + public Branch? Development { get; set; } + public Branch? Production { get; set; } + public List? Types { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Branches/BranchModelType.cs b/src/Bitbucket.Net/Models/Branches/BranchModelType.cs index a1c8b39..c894ab4 100644 --- a/src/Bitbucket.Net/Models/Branches/BranchModelType.cs +++ b/src/Bitbucket.Net/Models/Branches/BranchModelType.cs @@ -1,9 +1,8 @@ -namespace Bitbucket.Net.Models.Branches +namespace Bitbucket.Net.Models.Branches; + +public class BranchModelType { - public class BranchModelType - { - public string Id { get; set; } - public string DisplayName { get; set; } - public string Prefix { get; set; } - } -} + public string? Id { get; set; } + public string? DisplayName { get; set; } + public string? Prefix { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Builds/BuildStats.cs b/src/Bitbucket.Net/Models/Builds/BuildStats.cs index d802374..66439ae 100644 --- a/src/Bitbucket.Net/Models/Builds/BuildStats.cs +++ b/src/Bitbucket.Net/Models/Builds/BuildStats.cs @@ -1,9 +1,8 @@ -namespace Bitbucket.Net.Models.Builds +namespace Bitbucket.Net.Models.Builds; + +public class BuildStats { - public class BuildStats - { - public int Successful { get; set; } - public int InProgress { get; set; } - public int Failed { get; set; } - } -} + public int Successful { get; set; } + public int InProgress { get; set; } + public int Failed { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Builds/BuildStatus.cs b/src/Bitbucket.Net/Models/Builds/BuildStatus.cs index e4657b7..bdb3c3e 100644 --- a/src/Bitbucket.Net/Models/Builds/BuildStatus.cs +++ b/src/Bitbucket.Net/Models/Builds/BuildStatus.cs @@ -1,10 +1,9 @@ -namespace Bitbucket.Net.Models.Builds +namespace Bitbucket.Net.Models.Builds; + +public class BuildStatus : KeyedUrl { - public class BuildStatus : KeyedUrl - { - public string State { get; set; } - public string Name { get; set; } - public string Description { get; set; } - public long DateAdded { get; set; } - } -} + public string? State { get; set; } + public string? Name { get; set; } + public string? Description { get; set; } + public long DateAdded { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Builds/KeyedUrl.cs b/src/Bitbucket.Net/Models/Builds/KeyedUrl.cs index 26ad1e7..445268b 100644 --- a/src/Bitbucket.Net/Models/Builds/KeyedUrl.cs +++ b/src/Bitbucket.Net/Models/Builds/KeyedUrl.cs @@ -1,8 +1,7 @@ -namespace Bitbucket.Net.Models.Builds +namespace Bitbucket.Net.Models.Builds; + +public class KeyedUrl { - public class KeyedUrl - { - public string Key { get; set; } - public string Url { get; set; } - } + public string? Key { get; set; } + public string? Url { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Admin/Address.cs b/src/Bitbucket.Net/Models/Core/Admin/Address.cs index a49833a..822cc0a 100644 --- a/src/Bitbucket.Net/Models/Core/Admin/Address.cs +++ b/src/Bitbucket.Net/Models/Core/Admin/Address.cs @@ -1,8 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Admin +namespace Bitbucket.Net.Models.Core.Admin; + +public class Address { - public class Address - { - public string HostName { get; set; } - public int Port { get; set; } - } + public string? HostName { get; set; } + public int Port { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Admin/Cluster.cs b/src/Bitbucket.Net/Models/Core/Admin/Cluster.cs index 1a3d8dd..531f9cd 100644 --- a/src/Bitbucket.Net/Models/Core/Admin/Cluster.cs +++ b/src/Bitbucket.Net/Models/Core/Admin/Cluster.cs @@ -1,11 +1,8 @@ -using System.Collections.Generic; +namespace Bitbucket.Net.Models.Core.Admin; -namespace Bitbucket.Net.Models.Core.Admin +public class Cluster { - public class Cluster - { - public Node LocalNode { get; set; } - public List Nodes { get; set; } - public bool Running { get; set; } - } -} + public Node? LocalNode { get; set; } + public List? Nodes { get; set; } + public bool Running { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Admin/DeletableGroupOrUser.cs b/src/Bitbucket.Net/Models/Core/Admin/DeletableGroupOrUser.cs index edcab79..c2b72f2 100644 --- a/src/Bitbucket.Net/Models/Core/Admin/DeletableGroupOrUser.cs +++ b/src/Bitbucket.Net/Models/Core/Admin/DeletableGroupOrUser.cs @@ -1,9 +1,8 @@ using Bitbucket.Net.Models.Core.Users; -namespace Bitbucket.Net.Models.Core.Admin +namespace Bitbucket.Net.Models.Core.Admin; + +public class DeletableGroupOrUser : Named { - public class DeletableGroupOrUser : Named - { - public bool Deletable { get; set; } - } -} + public bool Deletable { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Admin/GroupPermission.cs b/src/Bitbucket.Net/Models/Core/Admin/GroupPermission.cs index f5e67a7..6b83bdd 100644 --- a/src/Bitbucket.Net/Models/Core/Admin/GroupPermission.cs +++ b/src/Bitbucket.Net/Models/Core/Admin/GroupPermission.cs @@ -2,14 +2,13 @@ using Bitbucket.Net.Models.Core.Users; using System.Text.Json.Serialization; -namespace Bitbucket.Net.Models.Core.Admin +namespace Bitbucket.Net.Models.Core.Admin; + +public class GroupPermission { - public class GroupPermission - { - public Named Group { get; set; } - [JsonConverter(typeof(PermissionsConverter))] - public Permissions Permission { get; set; } + public Named? Group { get; set; } + [JsonConverter(typeof(PermissionsConverter))] + public Permissions Permission { get; set; } - public override string ToString() => $"{Permission} - {Group}"; - } -} + public override string ToString() => $"{Permission} - {Group}"; +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Admin/GroupUsers.cs b/src/Bitbucket.Net/Models/Core/Admin/GroupUsers.cs index 8d91594..2253d00 100644 --- a/src/Bitbucket.Net/Models/Core/Admin/GroupUsers.cs +++ b/src/Bitbucket.Net/Models/Core/Admin/GroupUsers.cs @@ -1,10 +1,7 @@ -using System.Collections.Generic; +namespace Bitbucket.Net.Models.Core.Admin; -namespace Bitbucket.Net.Models.Core.Admin +public class GroupUsers { - public class GroupUsers - { - public string Group { get; set; } - public List Users { get; set; } - } -} + public string? Group { get; set; } + public List? Users { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Admin/LicenseDetails.cs b/src/Bitbucket.Net/Models/Core/Admin/LicenseDetails.cs index b307ddc..d50ee1f 100644 --- a/src/Bitbucket.Net/Models/Core/Admin/LicenseDetails.cs +++ b/src/Bitbucket.Net/Models/Core/Admin/LicenseDetails.cs @@ -1,28 +1,26 @@ -using System; using Bitbucket.Net.Common.Converters; using System.Text.Json.Serialization; -namespace Bitbucket.Net.Models.Core.Admin +namespace Bitbucket.Net.Models.Core.Admin; + +public class LicenseDetails : LicenseInfo { - public class LicenseDetails : LicenseInfo - { - [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset? CreationDate { get; set; } - [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset? PurchaseDate { get; set; } - [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset? ExpiryDate { get; set; } - public int NumberOfDaysBeforeExpiry { get; set; } - [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset? MaintenanceExpiryDate { get; set; } - public int NumberOfDaysBeforeMaintenanceExpiry { get; set; } - [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset? GracePeriodEndDate { get; set; } - public int NumberOfDaysBeforeGracePeriodExpiry { get; set; } - public int MaximumNumberOfUsers { get; set; } - public bool UnlimitedNumberOfUsers { get; set; } - public string ServerId { get; set; } - public string SupportEntitlementNumber { get; set; } - public LicenseStatus Status { get; set; } - } -} + [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] + public DateTimeOffset? CreationDate { get; set; } + [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] + public DateTimeOffset? PurchaseDate { get; set; } + [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] + public DateTimeOffset? ExpiryDate { get; set; } + public int NumberOfDaysBeforeExpiry { get; set; } + [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] + public DateTimeOffset? MaintenanceExpiryDate { get; set; } + public int NumberOfDaysBeforeMaintenanceExpiry { get; set; } + [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] + public DateTimeOffset? GracePeriodEndDate { get; set; } + public int NumberOfDaysBeforeGracePeriodExpiry { get; set; } + public int MaximumNumberOfUsers { get; set; } + public bool UnlimitedNumberOfUsers { get; set; } + public string? ServerId { get; set; } + public string? SupportEntitlementNumber { get; set; } + public LicenseStatus? Status { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Admin/LicenseInfo.cs b/src/Bitbucket.Net/Models/Core/Admin/LicenseInfo.cs index 58f9842..3de91d4 100644 --- a/src/Bitbucket.Net/Models/Core/Admin/LicenseInfo.cs +++ b/src/Bitbucket.Net/Models/Core/Admin/LicenseInfo.cs @@ -1,7 +1,6 @@ -namespace Bitbucket.Net.Models.Core.Admin +namespace Bitbucket.Net.Models.Core.Admin; + +public class LicenseInfo { - public class LicenseInfo - { - public string License { get; set; } - } + public string? License { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Admin/LicenseStatus.cs b/src/Bitbucket.Net/Models/Core/Admin/LicenseStatus.cs index 4a45e74..4fb5606 100644 --- a/src/Bitbucket.Net/Models/Core/Admin/LicenseStatus.cs +++ b/src/Bitbucket.Net/Models/Core/Admin/LicenseStatus.cs @@ -1,8 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Admin +namespace Bitbucket.Net.Models.Core.Admin; + +public class LicenseStatus { - public class LicenseStatus - { - public string ServerId { get; set; } - public int CurrentNumberOfUsers { get; set; } - } + public string? ServerId { get; set; } + public int CurrentNumberOfUsers { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Admin/MailServerConfiguration.cs b/src/Bitbucket.Net/Models/Core/Admin/MailServerConfiguration.cs index d5ec6b2..faf5d65 100644 --- a/src/Bitbucket.Net/Models/Core/Admin/MailServerConfiguration.cs +++ b/src/Bitbucket.Net/Models/Core/Admin/MailServerConfiguration.cs @@ -1,14 +1,12 @@ -namespace Bitbucket.Net.Models.Core.Admin -{ - public class MailServerConfiguration - { - public string HostName { get; set; } - public int Port { get; set; } - public string Protocol { get; set; } - public bool UseStartTls { get; set; } - public bool RequireStartTls { get; set; } - public string UserName { get; set; } - public string SenderAddress { get; set; } - } +namespace Bitbucket.Net.Models.Core.Admin; -} +public class MailServerConfiguration +{ + public string? HostName { get; set; } + public int Port { get; set; } + public string? Protocol { get; set; } + public bool UseStartTls { get; set; } + public bool RequireStartTls { get; set; } + public string? UserName { get; set; } + public string? SenderAddress { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Admin/MergeStrategies.cs b/src/Bitbucket.Net/Models/Core/Admin/MergeStrategies.cs index ca6fa6f..ddec15c 100644 --- a/src/Bitbucket.Net/Models/Core/Admin/MergeStrategies.cs +++ b/src/Bitbucket.Net/Models/Core/Admin/MergeStrategies.cs @@ -1,13 +1,10 @@ -using System.Collections.Generic; +namespace Bitbucket.Net.Models.Core.Admin; -namespace Bitbucket.Net.Models.Core.Admin +public class MergeStrategies { - public class MergeStrategies - { - public MergeStrategy DefaultStrategy { get; set; } + public MergeStrategy? DefaultStrategy { get; set; } - public List Strategies { get; set; } + public List? Strategies { get; set; } - public string Type { get; set; } - } -} + public string? Type { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Admin/MergeStrategy.cs b/src/Bitbucket.Net/Models/Core/Admin/MergeStrategy.cs index ceb7cbe..1b33f34 100644 --- a/src/Bitbucket.Net/Models/Core/Admin/MergeStrategy.cs +++ b/src/Bitbucket.Net/Models/Core/Admin/MergeStrategy.cs @@ -1,11 +1,10 @@ -namespace Bitbucket.Net.Models.Core.Admin +namespace Bitbucket.Net.Models.Core.Admin; + +public class MergeStrategy { - public class MergeStrategy - { - public string Description { get; set; } - public bool Enabled { get; set; } - public string Flag { get; set; } - public string Id { get; set; } - public string Name { get; set; } - } + public string? Description { get; set; } + public bool Enabled { get; set; } + public string? Flag { get; set; } + public string? Id { get; set; } + public string? Name { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Admin/Node.cs b/src/Bitbucket.Net/Models/Core/Admin/Node.cs index f0cc014..df4e021 100644 --- a/src/Bitbucket.Net/Models/Core/Admin/Node.cs +++ b/src/Bitbucket.Net/Models/Core/Admin/Node.cs @@ -1,10 +1,9 @@ -namespace Bitbucket.Net.Models.Core.Admin +namespace Bitbucket.Net.Models.Core.Admin; + +public class Node { - public class Node - { - public string Id { get; set; } - public string Name { get; set; } - public Address Address { get; set; } - public bool Local { get; set; } - } + public string? Id { get; set; } + public string? Name { get; set; } + public Address? Address { get; set; } + public bool Local { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Admin/PasswordBasic.cs b/src/Bitbucket.Net/Models/Core/Admin/PasswordBasic.cs index 6d7175c..d8880f1 100644 --- a/src/Bitbucket.Net/Models/Core/Admin/PasswordBasic.cs +++ b/src/Bitbucket.Net/Models/Core/Admin/PasswordBasic.cs @@ -1,8 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Admin +namespace Bitbucket.Net.Models.Core.Admin; + +public abstract class PasswordBasic { - public abstract class PasswordBasic - { - public string Password { get; set; } - public string PasswordConfirm { get; set; } - } -} + public string? Password { get; set; } + public string? PasswordConfirm { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Admin/PasswordChange.cs b/src/Bitbucket.Net/Models/Core/Admin/PasswordChange.cs index c8a3f05..d560ed0 100644 --- a/src/Bitbucket.Net/Models/Core/Admin/PasswordChange.cs +++ b/src/Bitbucket.Net/Models/Core/Admin/PasswordChange.cs @@ -1,7 +1,6 @@ -namespace Bitbucket.Net.Models.Core.Admin +namespace Bitbucket.Net.Models.Core.Admin; + +public class PasswordChange : PasswordBasic { - public class PasswordChange : PasswordBasic - { - public string Name { get; set; } - } -} + public string? Name { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Admin/Permissions.cs b/src/Bitbucket.Net/Models/Core/Admin/Permissions.cs index 23424c4..01cff67 100644 --- a/src/Bitbucket.Net/Models/Core/Admin/Permissions.cs +++ b/src/Bitbucket.Net/Models/Core/Admin/Permissions.cs @@ -1,17 +1,16 @@ -namespace Bitbucket.Net.Models.Core.Admin +namespace Bitbucket.Net.Models.Core.Admin; + +public enum Permissions { - public enum Permissions - { - Admin, - LicensedUser, - ProjectAdmin, - ProjectCreate, - ProjectRead, - ProjectView, - ProjectWrite, - RepoAdmin, - RepoRead, - RepoWrite, - SysAdmin - } -} + Admin, + LicensedUser, + ProjectAdmin, + ProjectCreate, + ProjectRead, + ProjectView, + ProjectWrite, + RepoAdmin, + RepoRead, + RepoWrite, + SysAdmin, +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Admin/UserGroups.cs b/src/Bitbucket.Net/Models/Core/Admin/UserGroups.cs index 3e2d796..3a1757a 100644 --- a/src/Bitbucket.Net/Models/Core/Admin/UserGroups.cs +++ b/src/Bitbucket.Net/Models/Core/Admin/UserGroups.cs @@ -1,10 +1,7 @@ -using System.Collections.Generic; +namespace Bitbucket.Net.Models.Core.Admin; -namespace Bitbucket.Net.Models.Core.Admin +public class UserGroups { - public class UserGroups - { - public string User { get; set; } - public List Groups { get; set; } - } -} + public string? User { get; set; } + public List? Groups { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Admin/UserInfo.cs b/src/Bitbucket.Net/Models/Core/Admin/UserInfo.cs index 96e4c62..c86c105 100644 --- a/src/Bitbucket.Net/Models/Core/Admin/UserInfo.cs +++ b/src/Bitbucket.Net/Models/Core/Admin/UserInfo.cs @@ -1,14 +1,12 @@ using Bitbucket.Net.Models.Core.Users; -namespace Bitbucket.Net.Models.Core.Admin -{ - public class UserInfo : User - { - public string DirectoryName { get; set; } - public bool Deletable { get; set; } - public long LastAuthenticationTimestamp { get; set; } - public bool MutableDetails { get; set; } - public bool MutableGroups { get; set; } - } +namespace Bitbucket.Net.Models.Core.Admin; -} +public class UserInfo : User +{ + public string? DirectoryName { get; set; } + public bool Deletable { get; set; } + public long LastAuthenticationTimestamp { get; set; } + public bool MutableDetails { get; set; } + public bool MutableGroups { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Admin/UserPermission.cs b/src/Bitbucket.Net/Models/Core/Admin/UserPermission.cs index a23bd2e..652e037 100644 --- a/src/Bitbucket.Net/Models/Core/Admin/UserPermission.cs +++ b/src/Bitbucket.Net/Models/Core/Admin/UserPermission.cs @@ -2,14 +2,13 @@ using Bitbucket.Net.Models.Core.Users; using System.Text.Json.Serialization; -namespace Bitbucket.Net.Models.Core.Admin +namespace Bitbucket.Net.Models.Core.Admin; + +public class UserPermission { - public class UserPermission - { - public User User { get; set; } - [JsonConverter(typeof(PermissionsConverter))] - public Permissions Permission { get; set; } + public User? User { get; set; } + [JsonConverter(typeof(PermissionsConverter))] + public Permissions Permission { get; set; } - public override string ToString() => $"{Permission} - {User?.DisplayName}"; - } -} + public override string ToString() => $"{Permission} - {User?.DisplayName}"; +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Admin/UserRename.cs b/src/Bitbucket.Net/Models/Core/Admin/UserRename.cs index 84a1143..a01a1e7 100644 --- a/src/Bitbucket.Net/Models/Core/Admin/UserRename.cs +++ b/src/Bitbucket.Net/Models/Core/Admin/UserRename.cs @@ -1,8 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Admin +namespace Bitbucket.Net.Models.Core.Admin; + +public class UserRename { - public class UserRename - { - public string Name { get; set; } - public string NewName { get; set; } - } -} + public string? Name { get; set; } + public string? NewName { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Logs/LogLevels.cs b/src/Bitbucket.Net/Models/Core/Logs/LogLevels.cs index 00c2ad7..78ddf4b 100644 --- a/src/Bitbucket.Net/Models/Core/Logs/LogLevels.cs +++ b/src/Bitbucket.Net/Models/Core/Logs/LogLevels.cs @@ -1,11 +1,10 @@ -namespace Bitbucket.Net.Models.Core.Logs +namespace Bitbucket.Net.Models.Core.Logs; + +public enum LogLevels { - public enum LogLevels - { - Trace, - Debug, - Info, - Warn, - Error - } -} + Trace, + Debug, + Info, + Warn, + Error, +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/AheadBehindMetaData.cs b/src/Bitbucket.Net/Models/Core/Projects/AheadBehindMetaData.cs index c124a9b..05cf78c 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/AheadBehindMetaData.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/AheadBehindMetaData.cs @@ -1,8 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class AheadBehindMetaData { - public class AheadBehindMetaData - { - public int Ahead { get; set; } - public int Behind { get; set; } - } -} + public int Ahead { get; set; } + public int Behind { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/AnchorStates.cs b/src/Bitbucket.Net/Models/Core/Projects/AnchorStates.cs index 05b9525..b3e76ab 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/AnchorStates.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/AnchorStates.cs @@ -1,9 +1,8 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public enum AnchorStates { - public enum AnchorStates - { - Active, - Orphaned, - All - } -} + Active, + Orphaned, + All, +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/ArchiveFormats.cs b/src/Bitbucket.Net/Models/Core/Projects/ArchiveFormats.cs index bfd966f..f759bc4 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/ArchiveFormats.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/ArchiveFormats.cs @@ -1,10 +1,9 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public enum ArchiveFormats { - public enum ArchiveFormats - { - Zip, - Tar, - TarGz, - Tgz - } -} + Zip, + Tar, + TarGz, + Tgz, +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Author.cs b/src/Bitbucket.Net/Models/Core/Projects/Author.cs index 066db48..630e4ad 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Author.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Author.cs @@ -1,8 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class Author { - public class Author - { - public string Name { get; set; } - public string EmailAddress { get; set; } - } + public string? Name { get; set; } + public string? EmailAddress { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/BlockerComment.cs b/src/Bitbucket.Net/Models/Core/Projects/BlockerComment.cs index 4534a77..3fca691 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/BlockerComment.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/BlockerComment.cs @@ -1,102 +1,100 @@ -using System; using Bitbucket.Net.Common.Converters; using Bitbucket.Net.Models.Core.Users; using System.Text.Json.Serialization; -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +/// +/// Represents a blocker comment (task) in Bitbucket Server 9.0+. +/// Blocker comments are comments with severity +/// that must be resolved before the pull request can be merged. +/// +/// +/// +/// In Bitbucket Server 9.0+, the legacy /pull-requests/{id}/tasks endpoint was deprecated +/// and replaced with the blocker comments model. Tasks are now represented as comments with +/// severity: 'BLOCKER' and accessed via the /blocker-comments endpoint. +/// +/// +/// Use to retrieve blocker comments +/// from Bitbucket Server 9.0+. +/// +/// +public class BlockerComment { /// - /// Represents a blocker comment (task) in Bitbucket Server 9.0+. - /// Blocker comments are comments with severity - /// that must be resolved before the pull request can be merged. + /// The unique identifier of the blocker comment. /// - /// - /// - /// In Bitbucket Server 9.0+, the legacy /pull-requests/{id}/tasks endpoint was deprecated - /// and replaced with the blocker comments model. Tasks are now represented as comments with - /// severity: 'BLOCKER' and accessed via the /blocker-comments endpoint. - /// - /// - /// Use to retrieve blocker comments - /// from Bitbucket Server 9.0+. - /// - /// - public class BlockerComment - { - /// - /// The unique identifier of the blocker comment. - /// - public int Id { get; set; } + public int Id { get; set; } - /// - /// The version of the blocker comment, used for optimistic locking. - /// - public int Version { get; set; } + /// + /// The version of the blocker comment, used for optimistic locking. + /// + public int Version { get; set; } - /// - /// The text content of the blocker comment. - /// - public string Text { get; set; } = string.Empty; + /// + /// The text content of the blocker comment. + /// + public string Text { get; set; } = string.Empty; - /// - /// The user who created the blocker comment. - /// - public User? Author { get; set; } + /// + /// The user who created the blocker comment. + /// + public User? Author { get; set; } - /// - /// When the blocker comment was created. - /// - [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset? CreatedDate { get; set; } + /// + /// When the blocker comment was created. + /// + [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] + public DateTimeOffset? CreatedDate { get; set; } - /// - /// When the blocker comment was last updated. - /// - [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset? UpdatedDate { get; set; } + /// + /// When the blocker comment was last updated. + /// + [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] + public DateTimeOffset? UpdatedDate { get; set; } - /// - /// The severity level of the comment. For blocker comments, this is always . - /// - [JsonConverter(typeof(CommentSeverityConverter))] - public CommentSeverity Severity { get; set; } = CommentSeverity.Blocker; + /// + /// The severity level of the comment. For blocker comments, this is always . + /// + [JsonConverter(typeof(CommentSeverityConverter))] + public CommentSeverity Severity { get; set; } = CommentSeverity.Blocker; - /// - /// The state of the blocker comment. - /// - [JsonConverter(typeof(BlockerCommentStateConverter))] - public BlockerCommentState State { get; set; } = BlockerCommentState.Open; + /// + /// The state of the blocker comment. + /// + [JsonConverter(typeof(BlockerCommentStateConverter))] + public BlockerCommentState State { get; set; } = BlockerCommentState.Open; - /// - /// The user who resolved the blocker comment, if resolved. - /// - public User? Resolver { get; set; } + /// + /// The user who resolved the blocker comment, if resolved. + /// + public User? Resolver { get; set; } - /// - /// When the blocker comment was resolved. - /// - [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset? ResolvedDate { get; set; } + /// + /// When the blocker comment was resolved. + /// + [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] + public DateTimeOffset? ResolvedDate { get; set; } - /// - /// The anchor point for the comment (file, line number, etc.). - /// Null for general pull request-level blocker comments. - /// - public CommentAnchor? Anchor { get; set; } + /// + /// The anchor point for the comment (file, line number, etc.). + /// Null for general pull request-level blocker comments. + /// + public CommentAnchor? Anchor { get; set; } - /// - /// The parent comment this blocker is attached to, if any. - /// - public CommentRef? Parent { get; set; } + /// + /// The parent comment this blocker is attached to, if any. + /// + public CommentRef? Parent { get; set; } - /// - /// The permitted operations the current user can perform on this blocker comment. - /// - public Permittedoperations? PermittedOperations { get; set; } + /// + /// The permitted operations the current user can perform on this blocker comment. + /// + public Permittedoperations? PermittedOperations { get; set; } - /// - /// Additional properties associated with the blocker comment. - /// - public Properties? Properties { get; set; } - } -} + /// + /// Additional properties associated with the blocker comment. + /// + public Properties? Properties { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/BlockerCommentState.cs b/src/Bitbucket.Net/Models/Core/Projects/BlockerCommentState.cs index 03fdda6..8750a90 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/BlockerCommentState.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/BlockerCommentState.cs @@ -1,18 +1,17 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +/// +/// Represents the state of a blocker comment (task) in Bitbucket Server 9.0+. +/// +public enum BlockerCommentState { /// - /// Represents the state of a blocker comment (task) in Bitbucket Server 9.0+. + /// The blocker comment is open and must be addressed before merging. /// - public enum BlockerCommentState - { - /// - /// The blocker comment is open and must be addressed before merging. - /// - Open, + Open, - /// - /// The blocker comment has been resolved. - /// - Resolved - } -} + /// + /// The blocker comment has been resolved. + /// + Resolved, +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Branch.cs b/src/Bitbucket.Net/Models/Core/Projects/Branch.cs index 0ec6dc6..904400a 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Branch.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Branch.cs @@ -1,73 +1,95 @@ using System.Text.Json; using System.Text.Json.Serialization; -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +/// +/// Full Bitbucket branch. Extends with commit info, default status, and parsed metadata. +/// +public class Branch : BranchBase { - public class Branch : BranchBase + private BranchMetaData? _branchMetadata; + private static readonly JsonSerializerOptions s_jsonOptions = new() { - private BranchMetaData? _branchMetadata; - private static readonly JsonSerializerOptions s_jsonOptions = new() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - }; + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }; + + /// + /// Gets or sets the SHA of the latest commit on this branch. + /// + public string? LatestCommit { get; set; } + + /// + /// Gets or sets the changeset identifier of the latest change on this branch. + /// + public string? LatestChangeset { get; set; } - public string LatestCommit { get; set; } - public string LatestChangeset { get; set; } - public bool IsDefault { get; set; } + /// + /// Gets or sets a value indicating whether this is the repository's default branch. + /// + public bool IsDefault { get; set; } - public BranchMetaData? BranchMetadata + /// + /// Gets parsed branch metadata (ahead/behind counts, build status, outgoing pull requests) from the raw JSON. + /// + public BranchMetaData? BranchMetadata + { + get { - get + if (_branchMetadata != null) + { + return _branchMetadata; + } + + if (Metadata == null || Metadata.Value.ValueKind != JsonValueKind.Array) + { + return null; + } + + _branchMetadata = new BranchMetaData(); + + foreach (var metadata in Metadata.Value.EnumerateArray()) { - if (_branchMetadata != null) + if (!metadata.TryGetProperty("Name", out var nameElement) && !metadata.TryGetProperty("name", out nameElement)) { - return _branchMetadata; + continue; } - if (Metadata == null || Metadata.Value.ValueKind != JsonValueKind.Array) + var name = nameElement.GetString(); + if (!metadata.TryGetProperty("Value", out var valueElement) && !metadata.TryGetProperty("value", out valueElement)) { - return null; + continue; } - _branchMetadata = new BranchMetaData(); + var valueJson = valueElement.GetRawText(); - foreach (var metadata in Metadata.Value.EnumerateArray()) + if (string.Equals(name, "com.atlassian.bitbucket.server.bitbucket-branch:ahead-behind-metadata-provider", StringComparison.Ordinal)) { - if (!metadata.TryGetProperty("Name", out var nameElement) && !metadata.TryGetProperty("name", out nameElement)) - { - continue; - } - - var name = nameElement.GetString(); - if (!metadata.TryGetProperty("Value", out var valueElement) && !metadata.TryGetProperty("value", out valueElement)) - { - continue; - } - - var valueJson = valueElement.GetRawText(); - - if (name == "com.atlassian.bitbucket.server.bitbucket-branch:ahead-behind-metadata-provider") - { - _branchMetadata.AheadBehind = JsonSerializer.Deserialize(valueJson, s_jsonOptions); - } - else if (name == "com.atlassian.bitbucket.server.bitbucket-build:build-status-metadata") - { - _branchMetadata.BuildStatus = JsonSerializer.Deserialize(valueJson, s_jsonOptions); - } - else if (name == "com.atlassian.bitbucket.server.bitbucket-ref-metadata:outgoing-pull-request-metadata") - { - _branchMetadata.OutgoingPullRequest = JsonSerializer.Deserialize(valueJson, s_jsonOptions); - } + _branchMetadata.AheadBehind = JsonSerializer.Deserialize(valueJson, s_jsonOptions); + } + else if (string.Equals(name, "com.atlassian.bitbucket.server.bitbucket-build:build-status-metadata", StringComparison.Ordinal)) + { + _branchMetadata.BuildStatus = JsonSerializer.Deserialize(valueJson, s_jsonOptions); + } + else if (string.Equals(name, "com.atlassian.bitbucket.server.bitbucket-ref-metadata:outgoing-pull-request-metadata", StringComparison.Ordinal)) + { + _branchMetadata.OutgoingPullRequest = JsonSerializer.Deserialize(valueJson, s_jsonOptions); } - - return _branchMetadata; } + + return _branchMetadata; } + } - [JsonPropertyName("metadata")] - public JsonElement? Metadata { get; set; } + /// + /// Gets or sets the raw JSON metadata array returned by Bitbucket Server for this branch. + /// + [JsonPropertyName("metadata")] + public JsonElement? Metadata { get; set; } - public override string ToString() => DisplayId; - } -} + /// + /// Returns the branch display identifier. + /// + public override string ToString() => DisplayId ?? string.Empty; +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/BranchBase.cs b/src/Bitbucket.Net/Models/Core/Projects/BranchBase.cs index e5e88f9..55577e2 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/BranchBase.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/BranchBase.cs @@ -1,8 +1,17 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +/// +/// Base branch reference. Extends with a display identifier and ref type. +/// +public class BranchBase : WithId { - public class BranchBase : WithId - { - public string DisplayId { get; set; } - public string Type { get; set; } - } -} + /// + /// Gets or sets the short display name of the branch (e.g. "main"). + /// + public string? DisplayId { get; set; } + + /// + /// Gets or sets the ref type (e.g. "BRANCH" or "TAG"). + /// + public string? Type { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/BranchInfo.cs b/src/Bitbucket.Net/Models/Core/Projects/BranchInfo.cs index 8c67348..6572976 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/BranchInfo.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/BranchInfo.cs @@ -1,9 +1,8 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class BranchInfo { - public class BranchInfo - { - public string Name { get; set; } - public string StartPoint { get; set; } - public string Message { get; set; } - } + public string? Name { get; set; } + public string? StartPoint { get; set; } + public string? Message { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/BranchMetaData.cs b/src/Bitbucket.Net/Models/Core/Projects/BranchMetaData.cs index 8cd619e..930379c 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/BranchMetaData.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/BranchMetaData.cs @@ -1,16 +1,15 @@ using System.Text.Json.Serialization; -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class BranchMetaData { - public class BranchMetaData - { - [JsonPropertyName("com.atlassian.bitbucket.server.bitbucket-branch:ahead-behind-metadata-provider")] - public AheadBehindMetaData? AheadBehind { get; set; } + [JsonPropertyName("com.atlassian.bitbucket.server.bitbucket-branch:ahead-behind-metadata-provider")] + public AheadBehindMetaData? AheadBehind { get; set; } - [JsonPropertyName("com.atlassian.bitbucket.server.bitbucket-build:build-status-metadata")] - public BuildStatusMetadata? BuildStatus { get; set; } + [JsonPropertyName("com.atlassian.bitbucket.server.bitbucket-build:build-status-metadata")] + public BuildStatusMetadata? BuildStatus { get; set; } - [JsonPropertyName("com.atlassian.bitbucket.server.bitbucket-ref-metadata:outgoing-pull-request-metadata")] - public PullRequestMetadata? OutgoingPullRequest { get; set; } - } -} + [JsonPropertyName("com.atlassian.bitbucket.server.bitbucket-ref-metadata:outgoing-pull-request-metadata")] + public PullRequestMetadata? OutgoingPullRequest { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/BranchOrderBy.cs b/src/Bitbucket.Net/Models/Core/Projects/BranchOrderBy.cs index 6c15d8f..719108f 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/BranchOrderBy.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/BranchOrderBy.cs @@ -1,8 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public enum BranchOrderBy { - public enum BranchOrderBy - { - Alphabetical, - Modification - } + Alphabetical, + Modification, } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/BranchRef.cs b/src/Bitbucket.Net/Models/Core/Projects/BranchRef.cs index ddbaa17..ac41fd9 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/BranchRef.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/BranchRef.cs @@ -1,5 +1,4 @@ -namespace Bitbucket.Net.Models.Core.Projects -{ - public class BranchRef : WithId - { } -} \ No newline at end of file +namespace Bitbucket.Net.Models.Core.Projects; + +public class BranchRef : WithId +{ } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/BrowseItem.cs b/src/Bitbucket.Net/Models/Core/Projects/BrowseItem.cs index a970559..37143e4 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/BrowseItem.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/BrowseItem.cs @@ -1,11 +1,10 @@ using Bitbucket.Net.Common.Models; -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class BrowseItem { - public class BrowseItem - { - public Path Path { get; set; } - public string Revision { get; set; } - public PagedResults Children { get; set; } - } -} + public Path? Path { get; set; } + public string? Revision { get; set; } + public PagedResults? Children { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/BrowsePathItem.cs b/src/Bitbucket.Net/Models/Core/Projects/BrowsePathItem.cs index a142311..a7a8781 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/BrowsePathItem.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/BrowsePathItem.cs @@ -1,10 +1,8 @@ -using System.Collections.Generic; -using Bitbucket.Net.Common.Models; +using Bitbucket.Net.Common.Models; -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class BrowsePathItem : PagedResultsBase { - public class BrowsePathItem : PagedResultsBase - { - public List Lines { get; set; } - } -} + public List? Lines { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/BuildStatusMetadata.cs b/src/Bitbucket.Net/Models/Core/Projects/BuildStatusMetadata.cs index efce208..cc10510 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/BuildStatusMetadata.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/BuildStatusMetadata.cs @@ -1,9 +1,8 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class BuildStatusMetadata { - public class BuildStatusMetadata - { - public int Successful { get; set; } - public int InProgress { get; set; } - public int Failed { get; set; } - } + public int Successful { get; set; } + public int InProgress { get; set; } + public int Failed { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Change.cs b/src/Bitbucket.Net/Models/Core/Projects/Change.cs index ff951f0..a95b41b 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Change.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Change.cs @@ -1,16 +1,57 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +/// +/// Represents a file change in a Bitbucket commit or pull request. +/// +public class Change { - public class Change - { - public string ContentId { get; set; } - public string FromContentId { get; set; } - public Path Path { get; set; } - public bool Executable { get; set; } - public int PercentUnchanged { get; set; } - public string Type { get; set; } - public string NodeType { get; set; } - public Path SrcPath { get; set; } - public bool SrcExecutable { get; set; } - public Links Links { get; set; } - } -} + /// + /// Gets or sets the content hash of the file after the change. + /// + public string? ContentId { get; set; } + + /// + /// Gets or sets the content hash of the file before the change. + /// + public string? FromContentId { get; set; } + + /// + /// Gets or sets the file path after the change. + /// + public Path? Path { get; set; } + + /// + /// Gets or sets a value indicating whether the file is executable after the change. + /// + public bool Executable { get; set; } + + /// + /// Gets or sets the percentage of the file that is unchanged. + /// + public int PercentUnchanged { get; set; } + + /// + /// Gets or sets the change type (e.g. "ADD", "MODIFY", "DELETE", "MOVE", "COPY"). + /// + public string? Type { get; set; } + + /// + /// Gets or sets the node type (e.g. "FILE" or "DIRECTORY"). + /// + public string? NodeType { get; set; } + + /// + /// Gets or sets the original file path before a move or copy. + /// + public Path? SrcPath { get; set; } + + /// + /// Gets or sets a value indicating whether the file was executable before the change. + /// + public bool SrcExecutable { get; set; } + + /// + /// Gets or sets the hypermedia links for this change. + /// + public Links? Links { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/ChangeScopes.cs b/src/Bitbucket.Net/Models/Core/Projects/ChangeScopes.cs index 8d8a19f..2884747 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/ChangeScopes.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/ChangeScopes.cs @@ -1,9 +1,8 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public enum ChangeScopes { - public enum ChangeScopes - { - All, - Unreviewed, - Range - } -} + All, + Unreviewed, + Range, +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/CloneLink.cs b/src/Bitbucket.Net/Models/Core/Projects/CloneLink.cs index 7e593c8..cbd3129 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/CloneLink.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/CloneLink.cs @@ -1,7 +1,6 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class CloneLink : Link { - public class CloneLink : Link - { - public string Name { get; set; } - } -} + public string? Name { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/CloneLinks.cs b/src/Bitbucket.Net/Models/Core/Projects/CloneLinks.cs index e6fc151..09f8e3c 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/CloneLinks.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/CloneLinks.cs @@ -1,9 +1,6 @@ -using System.Collections.Generic; +namespace Bitbucket.Net.Models.Core.Projects; -namespace Bitbucket.Net.Models.Core.Projects +public class CloneLinks : Links { - public class CloneLinks : Links - { - public List Clone { get; set; } - } -} + public List? Clone { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Comment.cs b/src/Bitbucket.Net/Models/Core/Projects/Comment.cs index 5096eae..8b24dba 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Comment.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Comment.cs @@ -1,52 +1,87 @@ -using System; -using System.Collections.Generic; using Bitbucket.Net.Common.Converters; using Bitbucket.Net.Models.Core.Tasks; using Bitbucket.Net.Models.Core.Users; using System.Text.Json.Serialization; -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +/// +/// A comment on a Bitbucket pull request, file, or commit. +/// +public class Comment { - public class Comment : PullRequestInfo - { - public int Id { get; set; } - public int Version { get; set; } - public string Text { get; set; } - - /// - /// Bitbucket Server comment state. - /// Common values: OPEN, PENDING, RESOLVED. - /// - /// - /// This intentionally hides . Although inheriting from - /// is not ideal for a comment model, using the same CLR member name avoids System.Text.Json property-name collisions. - /// - public new string? State { get; set; } - - /// - /// Indicates whether the whole comment thread is resolved. - /// When true, Bitbucket UI will typically collapse/hide the thread as resolved. - /// - public bool? ThreadResolved { get; set; } - - /// - /// The user who resolved the comment thread (when resolved). - /// - public User? Resolver { get; set; } - - /// - /// When the comment thread was resolved (when resolved). - /// - [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset? ResolvedDate { get; set; } - [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset? CreatedDate { get; set; } - [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset? UpdatedDate { get; set; } - public User Author { get; set; } - public List Comments { get; set; } - public List Tasks { get; set; } - public List Participants { get; set; } - public Links Links { get; set; } - } -} + /// + /// Gets or sets the server-assigned comment identifier. + /// + public int Id { get; set; } + + /// + /// Gets or sets the version number for optimistic locking on updates. + /// + public int Version { get; set; } + + /// + /// Gets or sets the comment body text. + /// + public string? Text { get; set; } + + /// + /// Bitbucket Server comment state. + /// Common values: OPEN, PENDING, RESOLVED. + /// + public string? State { get; set; } + + /// + /// Indicates whether the whole comment thread is resolved. + /// When true, Bitbucket UI will typically collapse/hide the thread as resolved. + /// + public bool? ThreadResolved { get; set; } + + /// + /// The user who resolved the comment thread (when resolved). + /// + public User? Resolver { get; set; } + + /// + /// When the comment thread was resolved (when resolved). + /// + [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] + public DateTimeOffset? ResolvedDate { get; set; } + + /// + /// Gets or sets the date and time when the comment was created. + /// + [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] + public DateTimeOffset? CreatedDate { get; set; } + + /// + /// Gets or sets the date and time when the comment was last updated. + /// + [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] + public DateTimeOffset? UpdatedDate { get; set; } + + /// + /// Gets or sets the user who authored the comment. + /// + public User? Author { get; set; } + + /// + /// Gets or sets the nested reply comments. + /// + public List? Comments { get; set; } + + /// + /// Gets or sets the tasks associated with this comment. + /// + public List? Tasks { get; set; } + + /// + /// Gets or sets the participants in the comment thread. + /// + public List? Participants { get; set; } + + /// + /// Gets or sets the hypermedia links for this comment. + /// + public Links? Links { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/CommentAnchor.cs b/src/Bitbucket.Net/Models/Core/Projects/CommentAnchor.cs index 394e136..05e6f6c 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/CommentAnchor.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/CommentAnchor.cs @@ -1,18 +1,47 @@ using Bitbucket.Net.Common.Converters; using System.Text.Json.Serialization; -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +/// +/// Anchor location for an inline comment on a specific file and line in a pull request diff. +/// +public class CommentAnchor { - public class CommentAnchor - { - public int? Line { get; set; } - [JsonConverter(typeof(LineTypesConverter))] - public LineTypes LineType { get; set; } - [JsonConverter(typeof(FileTypesConverter))] - public FileTypes FileType { get; set; } - public string FromHash { get; set; } - public string ToHash { get; set; } - public string Path { get; set; } - public string SrcPath { get; set; } - } + /// + /// Gets or sets the line number the comment is anchored to. + /// + public int? Line { get; set; } + + /// + /// Gets or sets the line type (e.g. ADDED, REMOVED, CONTEXT). + /// + [JsonConverter(typeof(LineTypesConverter))] + public LineTypes LineType { get; set; } + + /// + /// Gets or sets the file type (e.g. FROM for source, TO for destination). + /// + [JsonConverter(typeof(FileTypesConverter))] + public FileTypes FileType { get; set; } + + /// + /// Gets or sets the commit hash of the source side of the diff. + /// + public string? FromHash { get; set; } + + /// + /// Gets or sets the commit hash of the destination side of the diff. + /// + public string? ToHash { get; set; } + + /// + /// Gets or sets the file path the comment is anchored to. + /// + public string? Path { get; set; } + + /// + /// Gets or sets the original file path before a rename or move. + /// + public string? SrcPath { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/CommentId.cs b/src/Bitbucket.Net/Models/Core/Projects/CommentId.cs index 399edaa..89db9cf 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/CommentId.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/CommentId.cs @@ -1,7 +1,6 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class CommentId { - public class CommentId - { - public int? Id { get; set; } - } + public int? Id { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/CommentInfo.cs b/src/Bitbucket.Net/Models/Core/Projects/CommentInfo.cs index c891d20..70b1da8 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/CommentInfo.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/CommentInfo.cs @@ -1,9 +1,8 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class CommentInfo { - public class CommentInfo - { - public string Text { get; set; } - public CommentId Parent { get; set; } - public CommentAnchor Anchor { get; set; } - } -} + public string? Text { get; set; } + public CommentId? Parent { get; set; } + public CommentAnchor? Anchor { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/CommentRef.cs b/src/Bitbucket.Net/Models/Core/Projects/CommentRef.cs index 74287cc..826fbb5 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/CommentRef.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/CommentRef.cs @@ -1,25 +1,64 @@ -using System; -using System.Collections.Generic; using Bitbucket.Net.Common.Converters; using Bitbucket.Net.Models.Core.Tasks; using Bitbucket.Net.Models.Core.Users; using System.Text.Json.Serialization; -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +/// +/// Lightweight comment reference used in task anchors and nested comment structures. +/// +public class CommentRef { - public class CommentRef - { - public Properties Properties { get; set; } - public int Id { get; set; } - public int Version { get; set; } - public string Text { get; set; } - public User Author { get; set; } - [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset? CreatedDate { get; set; } - [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset? UpdatedDate { get; set; } - public List Comments { get; set; } - public List Tasks { get; set; } - public Permittedoperations PermittedOperations { get; set; } - } + /// + /// Gets or sets the additional properties bag. + /// + public Properties? Properties { get; set; } + + /// + /// Gets or sets the server-assigned comment identifier. + /// + public int Id { get; set; } + + /// + /// Gets or sets the version number for optimistic locking on updates. + /// + public int Version { get; set; } + + /// + /// Gets or sets the comment body text. + /// + public string? Text { get; set; } + + /// + /// Gets or sets the user who authored the comment. + /// + public User? Author { get; set; } + + /// + /// Gets or sets the date and time when the comment was created. + /// + [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] + public DateTimeOffset? CreatedDate { get; set; } + + /// + /// Gets or sets the date and time when the comment was last updated. + /// + [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] + public DateTimeOffset? UpdatedDate { get; set; } + + /// + /// Gets or sets the nested reply comment references. + /// + public List? Comments { get; set; } + + /// + /// Gets or sets the tasks associated with this comment. + /// + public List? Tasks { get; set; } + + /// + /// Gets or sets the operations the current user is permitted to perform on this comment. + /// + public Permittedoperations? PermittedOperations { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/CommentSeverity.cs b/src/Bitbucket.Net/Models/Core/Projects/CommentSeverity.cs index f9f6ffa..ac05d7e 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/CommentSeverity.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/CommentSeverity.cs @@ -1,18 +1,17 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +/// +/// Represents the severity of a comment in Bitbucket Server 9.0+. +/// +public enum CommentSeverity { /// - /// Represents the severity of a comment in Bitbucket Server 9.0+. + /// A normal comment with no special behavior. /// - public enum CommentSeverity - { - /// - /// A normal comment with no special behavior. - /// - Normal, + Normal, - /// - /// A blocker comment (task) that must be resolved before merging. - /// - Blocker - } -} + /// + /// A blocker comment (task) that must be resolved before merging. + /// + Blocker, +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/CommentText.cs b/src/Bitbucket.Net/Models/Core/Projects/CommentText.cs index a29d217..d1360c9 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/CommentText.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/CommentText.cs @@ -1,8 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class CommentText { - public class CommentText - { - public int Version { get; set; } - public string Text { get; set; } - } -} + public int Version { get; set; } + public string? Text { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Commit.cs b/src/Bitbucket.Net/Models/Core/Projects/Commit.cs index ff96fb8..fb6bc7d 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Commit.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Commit.cs @@ -1,21 +1,52 @@ -using System; -using System.Collections.Generic; using Bitbucket.Net.Common.Converters; using System.Text.Json.Serialization; -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +/// +/// Full Git commit. Extends with author, committer, message, and parent references. +/// +public class Commit : CommitParent { - public class Commit : CommitParent - { - public Author Author { get; set; } - [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset AuthorTimestamp { get; set; } - public Author Committer { get; set; } - [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset CommitterTimestamp { get; set; } - public string Message { get; set; } - public List Parents { get; set; } - public int AuthorCount { get; set; } - public int TotalCount { get; set; } - } -} + /// + /// Gets or sets the commit author. + /// + public Author? Author { get; set; } + + /// + /// Gets or sets the author timestamp (Unix epoch milliseconds from the Bitbucket API). + /// + [JsonConverter(typeof(UnixDateTimeOffsetConverter))] + public DateTimeOffset AuthorTimestamp { get; set; } + + /// + /// Gets or sets the committer (may differ from author in cherry-picks or patches). + /// + public Author? Committer { get; set; } + + /// + /// Gets or sets the committer timestamp (Unix epoch milliseconds from the Bitbucket API). + /// + [JsonConverter(typeof(UnixDateTimeOffsetConverter))] + public DateTimeOffset CommitterTimestamp { get; set; } + + /// + /// Gets or sets the commit message. + /// + public string? Message { get; set; } + + /// + /// Gets or sets the parent commits. + /// + public List? Parents { get; set; } + + /// + /// Gets or sets the number of unique authors in the commit range. + /// + public int AuthorCount { get; set; } + + /// + /// Gets or sets the total number of commits in the range. + /// + public int TotalCount { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/CommitParent.cs b/src/Bitbucket.Net/Models/Core/Projects/CommitParent.cs index 0d71089..7f23edd 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/CommitParent.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/CommitParent.cs @@ -1,8 +1,17 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +/// +/// Lightweight commit reference containing the full and abbreviated SHA. +/// +public class CommitParent { - public class CommitParent - { - public string Id { get; set; } - public string DisplayId { get; set; } - } + /// + /// Gets or sets the full commit SHA hash. + /// + public string? Id { get; set; } + + /// + /// Gets or sets the abbreviated commit SHA shown in the Bitbucket UI. + /// + public string? DisplayId { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/ContentItem.cs b/src/Bitbucket.Net/Models/Core/Projects/ContentItem.cs index 0a1528c..21dc427 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/ContentItem.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/ContentItem.cs @@ -1,10 +1,9 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class ContentItem { - public class ContentItem - { - public Path Path { get; set; } - public string ContentId { get; set; } - public string Type { get; set; } - public int Size { get; set; } - } + public Path? Path { get; set; } + public string? ContentId { get; set; } + public string? Type { get; set; } + public int Size { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Diff.cs b/src/Bitbucket.Net/Models/Core/Projects/Diff.cs index b963a46..bb78453 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Diff.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Diff.cs @@ -1,11 +1,22 @@ -using System.Collections.Generic; +namespace Bitbucket.Net.Models.Core.Projects; -namespace Bitbucket.Net.Models.Core.Projects +/// +/// A file-level diff. Extends with source/destination paths and hunks. +/// +public class Diff : DiffInfo { - public class Diff : DiffInfo - { - public Path Source { get; set; } - public Path Destination { get; set; } - public List Hunks { get; set; } - } + /// + /// Gets or sets the source (before) file path. + /// + public Path? Source { get; set; } + + /// + /// Gets or sets the destination (after) file path. + /// + public Path? Destination { get; set; } + + /// + /// Gets or sets the list of diff hunks containing the actual line changes. + /// + public List? Hunks { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/DiffHunk.cs b/src/Bitbucket.Net/Models/Core/Projects/DiffHunk.cs index c13d715..17142a9 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/DiffHunk.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/DiffHunk.cs @@ -1,14 +1,37 @@ -using System.Collections.Generic; +namespace Bitbucket.Net.Models.Core.Projects; -namespace Bitbucket.Net.Models.Core.Projects +/// +/// A hunk within a file diff, describing a contiguous block of changes. +/// +public class DiffHunk { - public class DiffHunk - { - public int SourceLine { get; set; } - public int SourceSpan { get; set; } - public int DestinationLine { get; set; } - public int DestinationSpan { get; set; } - public List Segments { get; set; } - public bool Truncated { get; set; } - } + /// + /// Gets or sets the starting line number in the source (before) file. + /// + public int SourceLine { get; set; } + + /// + /// Gets or sets the number of lines from the source file included in this hunk. + /// + public int SourceSpan { get; set; } + + /// + /// Gets or sets the starting line number in the destination (after) file. + /// + public int DestinationLine { get; set; } + + /// + /// Gets or sets the number of lines from the destination file included in this hunk. + /// + public int DestinationSpan { get; set; } + + /// + /// Gets or sets the segments (groups of added, removed, or context lines) in this hunk. + /// + public List? Segments { get; set; } + + /// + /// Gets or sets a value indicating whether this hunk was truncated by the server. + /// + public bool Truncated { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/DiffInfo.cs b/src/Bitbucket.Net/Models/Core/Projects/DiffInfo.cs index 1ba4235..e928bdc 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/DiffInfo.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/DiffInfo.cs @@ -1,16 +1,15 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public abstract class DiffInfo { - public abstract class DiffInfo - { - /// - /// Indicates whether the diff was truncated by the server. - /// Note: Bitbucket Server 9.0+ returns boolean; older versions may return string. - /// - public bool Truncated { get; set; } + /// + /// Indicates whether the diff was truncated by the server. + /// Note: Bitbucket Server 9.0+ returns boolean; older versions may return string. + /// + public bool Truncated { get; set; } - public string ContextLines { get; set; } - public string FromHash { get; set; } - public string ToHash { get; set; } - public string WhiteSpace { get; set; } - } + public string? ContextLines { get; set; } + public string? FromHash { get; set; } + public string? ToHash { get; set; } + public string? WhiteSpace { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/DiffTypes.cs b/src/Bitbucket.Net/Models/Core/Projects/DiffTypes.cs index 3601ac5..6c9c1e9 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/DiffTypes.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/DiffTypes.cs @@ -1,9 +1,8 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public enum DiffTypes { - public enum DiffTypes - { - Effective, - Range, - Commit - } -} + Effective, + Range, + Commit, +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Differences.cs b/src/Bitbucket.Net/Models/Core/Projects/Differences.cs index ea339f1..0ed6755 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Differences.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Differences.cs @@ -1,9 +1,6 @@ -using System.Collections.Generic; +namespace Bitbucket.Net.Models.Core.Projects; -namespace Bitbucket.Net.Models.Core.Projects +public class Differences : DiffInfo { - public class Differences : DiffInfo - { - public List Diffs { get; set; } - } -} + public List? Diffs { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/FileTypes.cs b/src/Bitbucket.Net/Models/Core/Projects/FileTypes.cs index 46429d5..8c08acf 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/FileTypes.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/FileTypes.cs @@ -1,8 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public enum FileTypes { - public enum FileTypes - { - From, - To - } -} + From, + To, +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/FromToRef.cs b/src/Bitbucket.Net/Models/Core/Projects/FromToRef.cs index 189d24c..5e48c3c 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/FromToRef.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/FromToRef.cs @@ -1,34 +1,33 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +/// +/// Represents a reference (branch/tag) in a pull request's source or target. +/// +public class FromToRef { /// - /// Represents a reference (branch/tag) in a pull request's source or target. + /// The full ref ID (e.g., "refs/heads/feature-branch"). /// - public class FromToRef - { - /// - /// The full ref ID (e.g., "refs/heads/feature-branch"). - /// - public string Id { get; set; } + public string? Id { get; set; } - /// - /// The display-friendly ref ID (e.g., "feature-branch"). - /// - public string? DisplayId { get; set; } + /// + /// The display-friendly ref ID (e.g., "feature-branch"). + /// + public string? DisplayId { get; set; } - /// - /// The SHA of the latest commit on this ref. - /// This is useful for creating line-specific comments on pull requests. - /// - public string? LatestCommit { get; set; } + /// + /// The SHA of the latest commit on this ref. + /// This is useful for creating line-specific comments on pull requests. + /// + public string? LatestCommit { get; set; } - /// - /// The type of ref (e.g., "BRANCH", "TAG"). - /// - public string? Type { get; set; } + /// + /// The type of ref (e.g., "BRANCH", "TAG"). + /// + public string? Type { get; set; } - /// - /// The repository containing this ref. - /// - public RepositoryRef Repository { get; set; } - } + /// + /// The repository containing this ref. + /// + public RepositoryRef? Repository { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Hook.cs b/src/Bitbucket.Net/Models/Core/Projects/Hook.cs index f048bc8..536de41 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Hook.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Hook.cs @@ -1,10 +1,9 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class Hook { - public class Hook - { - public HookDetails Details { get; set; } - public bool Enabled { get; set; } - public bool Configured { get; set; } - public HookScope Scope { get; set; } - } -} + public HookDetails? Details { get; set; } + public bool Enabled { get; set; } + public bool Configured { get; set; } + public HookScope? Scope { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/HookDetails.cs b/src/Bitbucket.Net/Models/Core/Projects/HookDetails.cs index cbd297e..43ffb54 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/HookDetails.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/HookDetails.cs @@ -1,18 +1,16 @@ -using System.Collections.Generic; using Bitbucket.Net.Common.Converters; using System.Text.Json.Serialization; -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class HookDetails { - public class HookDetails - { - public string Key { get; set; } - public string Name { get; set; } - [JsonConverter(typeof(HookTypesConverter))] - public HookTypes Type { get; set; } - public string Description { get; set; } - public string Version { get; set; } - public object ConfigFormKey { get; set; } - public List ScopeTypes { get; set; } - } + public string? Key { get; set; } + public string? Name { get; set; } + [JsonConverter(typeof(HookTypesConverter))] + public HookTypes Type { get; set; } + public string? Description { get; set; } + public string? Version { get; set; } + public object? ConfigFormKey { get; set; } + public List? ScopeTypes { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/HookScope.cs b/src/Bitbucket.Net/Models/Core/Projects/HookScope.cs index 1d9ce6f..a5634ac 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/HookScope.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/HookScope.cs @@ -1,12 +1,11 @@ using Bitbucket.Net.Common.Converters; using System.Text.Json.Serialization; -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class HookScope { - public class HookScope - { - public int ResourceId { get; set; } - [JsonConverter(typeof(ScopeTypesConverter))] - public ScopeTypes Type { get; set; } - } + public int ResourceId { get; set; } + [JsonConverter(typeof(ScopeTypesConverter))] + public ScopeTypes Type { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/HookTypes.cs b/src/Bitbucket.Net/Models/Core/Projects/HookTypes.cs index 7c74852..36396b4 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/HookTypes.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/HookTypes.cs @@ -1,9 +1,8 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public enum HookTypes { - public enum HookTypes - { - PreReceive, - PostReceive, - PrePullRequestMerge - } -} + PreReceive, + PostReceive, + PrePullRequestMerge, +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/LastModified.cs b/src/Bitbucket.Net/Models/Core/Projects/LastModified.cs index d997a4e..0a68846 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/LastModified.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/LastModified.cs @@ -1,10 +1,7 @@ -using System.Collections.Generic; +namespace Bitbucket.Net.Models.Core.Projects; -namespace Bitbucket.Net.Models.Core.Projects +public class LastModified { - public class LastModified - { - public Dictionary Files { get; set; } - public Commit LatestCommit { get; set; } - } -} + public Dictionary? Files { get; set; } + public Commit? LatestCommit { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/LicensedUser.cs b/src/Bitbucket.Net/Models/Core/Projects/LicensedUser.cs index e3895b8..447a901 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/LicensedUser.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/LicensedUser.cs @@ -1,8 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class LicensedUser { - public class LicensedUser - { - public string Name { get; set; } - public bool Deletable { get; set; } - } -} + public string? Name { get; set; } + public bool Deletable { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Line.cs b/src/Bitbucket.Net/Models/Core/Projects/Line.cs index b2ad451..62bd824 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Line.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Line.cs @@ -1,7 +1,6 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class Line { - public class Line - { - public string Text { get; set; } - } + public string? Text { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/LineRef.cs b/src/Bitbucket.Net/Models/Core/Projects/LineRef.cs index 392f690..ba111e8 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/LineRef.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/LineRef.cs @@ -1,10 +1,27 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +/// +/// A single line in a diff segment with source and destination line numbers. +/// +public class LineRef { - public class LineRef - { - public int Destination { get; set; } - public int Source { get; set; } - public string Line { get; set; } - public bool Truncated { get; set; } - } + /// + /// Gets or sets the line number in the destination (after) file. + /// + public int Destination { get; set; } + + /// + /// Gets or sets the line number in the source (before) file. + /// + public int Source { get; set; } + + /// + /// Gets or sets the text content of the line. + /// + public string? Line { get; set; } + + /// + /// Gets or sets a value indicating whether this line was truncated by the server. + /// + public bool Truncated { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/LineTypes.cs b/src/Bitbucket.Net/Models/Core/Projects/LineTypes.cs index 49305a9..cee9aa1 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/LineTypes.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/LineTypes.cs @@ -1,9 +1,8 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public enum LineTypes { - public enum LineTypes - { - Added, - Removed, - Context - } -} + Added, + Removed, + Context, +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Link.cs b/src/Bitbucket.Net/Models/Core/Projects/Link.cs index c9c0743..e357710 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Link.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Link.cs @@ -1,9 +1,17 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +/// +/// A single hyperlink in the Bitbucket REST API response. +/// +public class Link { - public class Link - { - public string Href { get; set; } + /// + /// Gets or sets the URL of the link. + /// + public string? Href { get; set; } - public override string ToString() => Href; - } + /// + /// Returns the link URL or an empty string when not set. + /// + public override string ToString() => Href ?? string.Empty; } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Links.cs b/src/Bitbucket.Net/Models/Core/Projects/Links.cs index d2b50d8..7d8811c 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Links.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Links.cs @@ -1,9 +1,12 @@ -using System.Collections.Generic; +namespace Bitbucket.Net.Models.Core.Projects; -namespace Bitbucket.Net.Models.Core.Projects +/// +/// Hypermedia links for a Bitbucket resource. +/// +public class Links { - public class Links - { - public List Self { get; set; } - } + /// + /// Gets or sets the self-referencing links. + /// + public List? Self { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/MergeCheckRequiredBuilds.cs b/src/Bitbucket.Net/Models/Core/Projects/MergeCheckRequiredBuilds.cs index 19e0fb7..51ae255 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/MergeCheckRequiredBuilds.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/MergeCheckRequiredBuilds.cs @@ -1,8 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class MergeCheckRequiredBuilds { - public class MergeCheckRequiredBuilds - { - public bool Enable { get; set; } - public int Count { get; set; } - } + public bool Enable { get; set; } + public int Count { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/MergeCommits.cs b/src/Bitbucket.Net/Models/Core/Projects/MergeCommits.cs index d3b2ccd..08458a1 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/MergeCommits.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/MergeCommits.cs @@ -1,9 +1,8 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public enum MergeCommits { - public enum MergeCommits - { - Exclude, - Include, - Only - } -} + Exclude, + Include, + Only, +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/MergeHookRequiredApprovers.cs b/src/Bitbucket.Net/Models/Core/Projects/MergeHookRequiredApprovers.cs index a65794d..5af97a6 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/MergeHookRequiredApprovers.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/MergeHookRequiredApprovers.cs @@ -1,8 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class MergeHookRequiredApprovers { - public class MergeHookRequiredApprovers - { - public bool Enable { get; set; } - public int Count { get; set; } - } + public bool Enable { get; set; } + public int Count { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Participant.cs b/src/Bitbucket.Net/Models/Core/Projects/Participant.cs index dddbc70..1a89859 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Participant.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Participant.cs @@ -2,17 +2,37 @@ using Bitbucket.Net.Models.Core.Users; using System.Text.Json.Serialization; -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +/// +/// A participant in a Bitbucket pull request (author, reviewer, or watcher). +/// +public class Participant { - public class Participant - { - public User User { get; set; } - [JsonConverter(typeof(RolesConverter))] - public Roles Role { get; set; } - public bool Approved { get; set; } - [JsonConverter(typeof(ParticipantStatusConverter))] - public ParticipantStatus Status { get; set; } + /// + /// Gets or sets the participant's user details. + /// + public User? User { get; set; } + + /// + /// Gets or sets the participant's role (e.g. AUTHOR, REVIEWER, PARTICIPANT). + /// + [JsonConverter(typeof(RolesConverter))] + public Roles Role { get; set; } + + /// + /// Gets or sets a value indicating whether the participant has approved the pull request. + /// + public bool Approved { get; set; } + + /// + /// Gets or sets the participant's review status (e.g. APPROVED, UNAPPROVED, NEEDS_WORK). + /// + [JsonConverter(typeof(ParticipantStatusConverter))] + public ParticipantStatus Status { get; set; } - public override string ToString() => User.DisplayName; - } -} + /// + /// Returns the participant's display name when available. + /// + public override string ToString() => User?.DisplayName ?? "Unknown"; +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/ParticipantStatus.cs b/src/Bitbucket.Net/Models/Core/Projects/ParticipantStatus.cs index 00c85c6..9e70d2f 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/ParticipantStatus.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/ParticipantStatus.cs @@ -1,9 +1,8 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public enum ParticipantStatus { - public enum ParticipantStatus - { - Unapproved, - NeedsWork, - Approved - } -} + Unapproved, + NeedsWork, + Approved, +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Path.cs b/src/Bitbucket.Net/Models/Core/Projects/Path.cs index 6590e9f..78393e0 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Path.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Path.cs @@ -1,58 +1,55 @@ -using System.Collections.Generic; +namespace Bitbucket.Net.Models.Core.Projects; -namespace Bitbucket.Net.Models.Core.Projects +/// +/// Represents a file path in a Bitbucket repository. +/// +public class Path { /// - /// Represents a file path in a Bitbucket repository. + /// The path components (directory and file name parts). /// - public class Path + public List? Components { get; set; } + + /// + /// The parent directory path. + /// + public string? Parent { get; set; } + + /// + /// The file or directory name. + /// + public string? Name { get; set; } + + /// + /// The file extension (if any). + /// + public string? Extension { get; set; } + + /// + /// The full path as a string, as returned by the Bitbucket API. + /// Note: This property name is lowercase to match the JSON response. + /// + // ReSharper disable once InconsistentNaming + public string? toString { get; set; } + + /// + /// Returns the full path string representation. + /// + /// + /// The path string from the API if available; otherwise, + /// constructs the path from Components or falls back to Name. + /// + public override string ToString() { - /// - /// The path components (directory and file name parts). - /// - public List Components { get; set; } - - /// - /// The parent directory path. - /// - public string Parent { get; set; } - - /// - /// The file or directory name. - /// - public string Name { get; set; } - - /// - /// The file extension (if any). - /// - public string Extension { get; set; } - - /// - /// The full path as a string, as returned by the Bitbucket API. - /// Note: This property name is lowercase to match the JSON response. - /// - // ReSharper disable once InconsistentNaming - public string toString { get; set; } - - /// - /// Returns the full path string representation. - /// - /// - /// The path string from the API if available; otherwise, - /// constructs the path from Components or falls back to Name. - /// - public override string ToString() - { - // Prefer the API-provided toString property - if (!string.IsNullOrEmpty(toString)) - return toString; - - // Build from components if available - if (Components is { Count: > 0 }) - return string.Join("/", Components); - - // Fallback to name, then type name (shouldn't happen in practice) - return Name ?? "(unknown path)"; - } + // Prefer the API-provided toString property + if (!string.IsNullOrEmpty(toString)) + return toString; + + // Build from components if available + if (Components is { Count: > 0 }) + return string.Join('/', Components); + + // Fallback to name, then type name (shouldn't happen in practice) + return Name ?? "(unknown path)"; } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Permittedoperations.cs b/src/Bitbucket.Net/Models/Core/Projects/Permittedoperations.cs index 07e8017..4444410 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Permittedoperations.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Permittedoperations.cs @@ -1,9 +1,8 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class Permittedoperations { - public class Permittedoperations - { - public bool Editable { get; set; } - public bool Deletable { get; set; } - public bool Transitionable { get; set; } - } + public bool Editable { get; set; } + public bool Deletable { get; set; } + public bool Transitionable { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Project.cs b/src/Bitbucket.Net/Models/Core/Projects/Project.cs index 72e474e..998f60b 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Project.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Project.cs @@ -1,12 +1,32 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +/// +/// Full Bitbucket project. Extends with server-assigned identity and metadata. +/// +public class Project : ProjectDefinition { - public class Project : ProjectDefinition - { - public int Id { get; set; } - public bool Public { get; set; } - public string Type { get; set; } - public Links Links { get; set; } - - public override string ToString() => Name; - } -} + /// + /// Gets or sets the server-assigned project identifier. + /// + public int Id { get; set; } + + /// + /// Gets or sets a value indicating whether the project is publicly accessible. + /// + public bool Public { get; set; } + + /// + /// Gets or sets the project type (e.g. "NORMAL" or "PERSONAL"). + /// + public string? Type { get; set; } + + /// + /// Gets or sets the hypermedia links for this project. + /// + public Links? Links { get; set; } + + /// + /// Returns the project name, when available. + /// + public override string ToString() => Name ?? string.Empty; +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/ProjectDefinition.cs b/src/Bitbucket.Net/Models/Core/Projects/ProjectDefinition.cs index 7ecaef3..c45f711 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/ProjectDefinition.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/ProjectDefinition.cs @@ -1,9 +1,17 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +/// +/// Extends with a human-readable name and description. +/// +public class ProjectDefinition : ProjectRef { - public class ProjectDefinition : ProjectRef - { - public string Name { get; set; } - public string Description { get; set; } - //public string Avatar { get; set; } - } -} + /// + /// Gets or sets the project display name. + /// + public string? Name { get; set; } + + /// + /// Gets or sets the project description. + /// + public string? Description { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/ProjectRef.cs b/src/Bitbucket.Net/Models/Core/Projects/ProjectRef.cs index 2dbe271..2e8fdcc 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/ProjectRef.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/ProjectRef.cs @@ -1,7 +1,12 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +/// +/// Lightweight reference to a Bitbucket project, identified by its key. +/// +public class ProjectRef { - public class ProjectRef - { - public string Key { get; set; } - } -} + /// + /// Gets or sets the unique project key (e.g. "PRJ"). + /// + public string? Key { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Properties.cs b/src/Bitbucket.Net/Models/Core/Projects/Properties.cs index a5fd1b5..820eb56 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Properties.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Properties.cs @@ -1,7 +1,6 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class Properties { - public class Properties - { - public string Key { get; set; } - } + public string? Key { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/PullRequest.cs b/src/Bitbucket.Net/Models/Core/Projects/PullRequest.cs index 2c85328..4123cc9 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/PullRequest.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/PullRequest.cs @@ -1,22 +1,52 @@ -using System; -using System.Collections.Generic; using Bitbucket.Net.Common.Converters; using System.Text.Json.Serialization; -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +/// +/// Full Bitbucket pull request. Extends with server-assigned identity, timestamps, and participants. +/// +public class PullRequest : PullRequestInfo { - public class PullRequest : PullRequestInfo - { - public int Id { get; set; } - public int Version { get; set; } - [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset? CreatedDate { get; set; } - [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset? UpdatedDate { get; set; } - public Participant Author { get; set; } - public List Participants { get; set; } - public Links Links { get; set; } - - public override string ToString() => $"{Author.User.DisplayName}: {Title ?? "(untitled)"}"; - } -} + /// + /// Gets or sets the server-assigned pull request identifier. + /// + public int Id { get; set; } + + /// + /// Gets or sets the version number for optimistic locking on updates. + /// + public int Version { get; set; } + + /// + /// Gets or sets the date and time when the pull request was created. + /// + [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] + public DateTimeOffset? CreatedDate { get; set; } + + /// + /// Gets or sets the date and time when the pull request was last updated. + /// + [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] + public DateTimeOffset? UpdatedDate { get; set; } + + /// + /// Gets or sets the pull request author. + /// + public Participant? Author { get; set; } + + /// + /// Gets or sets the list of participants (author, reviewers, and watchers). + /// + public List? Participants { get; set; } + + /// + /// Gets or sets the hypermedia links for this pull request. + /// + public Links? Links { get; set; } + + /// + /// Returns a human-readable label combining the author display name and title. + /// + public override string ToString() => $"{Author?.User?.DisplayName ?? "Unknown"}: {Title ?? "(untitled)"}"; +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/PullRequestActivity.cs b/src/Bitbucket.Net/Models/Core/Projects/PullRequestActivity.cs index 1c5fe0e..a378678 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/PullRequestActivity.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/PullRequestActivity.cs @@ -1,20 +1,52 @@ using Bitbucket.Net.Common.Converters; using Bitbucket.Net.Models.Core.Users; using System.Text.Json.Serialization; -using System; -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +/// +/// An activity event on a Bitbucket pull request (e.g. comment, approval, merge). +/// +public class PullRequestActivity { - public class PullRequestActivity - { - public int Id { get; set; } - [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset? CreatedDate { get; set; } - public User User { get; set; } - public string Action { get; set; } - public string CommentAction { get; set; } - public Comment Comment { get; set; } - public CommentAnchor CommentAnchor { get; set; } - public Commit Commit { get; set; } - } -} + /// + /// Gets or sets the server-assigned activity identifier. + /// + public int Id { get; set; } + + /// + /// Gets or sets the date and time when the activity occurred. + /// + [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] + public DateTimeOffset? CreatedDate { get; set; } + + /// + /// Gets or sets the user who performed the activity. + /// + public User? User { get; set; } + + /// + /// Gets or sets the activity action type (e.g. "COMMENTED", "APPROVED", "MERGED", "OPENED"). + /// + public string? Action { get; set; } + + /// + /// Gets or sets the comment-specific action (e.g. "ADDED", "UPDATED", "DELETED") when the activity involves a comment. + /// + public string? CommentAction { get; set; } + + /// + /// Gets or sets the comment associated with this activity, if any. + /// + public Comment? Comment { get; set; } + + /// + /// Gets or sets the anchor location for an inline comment, if applicable. + /// + public CommentAnchor? CommentAnchor { get; set; } + + /// + /// Gets or sets the commit associated with this activity, if any. + /// + public Commit? Commit { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/PullRequestDirections.cs b/src/Bitbucket.Net/Models/Core/Projects/PullRequestDirections.cs index 33605cd..ce22521 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/PullRequestDirections.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/PullRequestDirections.cs @@ -1,8 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public enum PullRequestDirections { - public enum PullRequestDirections - { - Incoming, - Outgoing - } + Incoming, + Outgoing, } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/PullRequestFromTypes.cs b/src/Bitbucket.Net/Models/Core/Projects/PullRequestFromTypes.cs index 49874a6..f60e796 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/PullRequestFromTypes.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/PullRequestFromTypes.cs @@ -1,8 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public enum PullRequestFromTypes { - public enum PullRequestFromTypes - { - Comment, - Activity - } -} + Comment, + Activity, +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/PullRequestInfo.cs b/src/Bitbucket.Net/Models/Core/Projects/PullRequestInfo.cs index e0fc94a..71cf44c 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/PullRequestInfo.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/PullRequestInfo.cs @@ -1,20 +1,56 @@ -using System.Collections.Generic; using Bitbucket.Net.Common.Converters; using System.Text.Json.Serialization; -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +/// +/// Core pull request data used when creating or updating a pull request. +/// +public class PullRequestInfo { - public class PullRequestInfo - { - public string Title { get; set; } - public string Description { get; set; } - [JsonConverter(typeof(PullRequestStatesConverter))] - public PullRequestStates State { get; set; } - public bool Open { get; set; } - public bool Closed { get; set; } - public FromToRef FromRef { get; set; } - public FromToRef ToRef { get; set; } - public bool Locked { get; set; } - public List Reviewers { get; set; } - } -} + /// + /// Gets or sets the pull request title. + /// + public string? Title { get; set; } + + /// + /// Gets or sets the pull request description (supports Markdown). + /// + public string? Description { get; set; } + + /// + /// Gets or sets the pull request state (e.g. OPEN, MERGED, DECLINED). + /// + [JsonConverter(typeof(PullRequestStatesConverter))] + public PullRequestStates State { get; set; } + + /// + /// Gets or sets a value indicating whether the pull request is open. + /// + public bool Open { get; set; } + + /// + /// Gets or sets a value indicating whether the pull request is closed. + /// + public bool Closed { get; set; } + + /// + /// Gets or sets the source branch reference. + /// + public FromToRef? FromRef { get; set; } + + /// + /// Gets or sets the target branch reference. + /// + public FromToRef? ToRef { get; set; } + + /// + /// Gets or sets a value indicating whether the pull request is locked from further changes. + /// + public bool Locked { get; set; } + + /// + /// Gets or sets the list of assigned reviewers. + /// + public List? Reviewers { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/PullRequestMergeState.cs b/src/Bitbucket.Net/Models/Core/Projects/PullRequestMergeState.cs index d174c20..f13b2c7 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/PullRequestMergeState.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/PullRequestMergeState.cs @@ -1,12 +1,9 @@ -using System.Collections.Generic; +namespace Bitbucket.Net.Models.Core.Projects; -namespace Bitbucket.Net.Models.Core.Projects +public class PullRequestMergeState { - public class PullRequestMergeState - { - public bool CanMerge { get; set; } - public bool Conflicted { get; set; } - public string Outcome { get; set; } - public List Vetoes { get; set; } - } -} + public bool CanMerge { get; set; } + public bool Conflicted { get; set; } + public string? Outcome { get; set; } + public List? Vetoes { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/PullRequestMetadata.cs b/src/Bitbucket.Net/Models/Core/Projects/PullRequestMetadata.cs index fdea2ac..b0c1ee6 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/PullRequestMetadata.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/PullRequestMetadata.cs @@ -1,7 +1,6 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class PullRequestMetadata { - public class PullRequestMetadata - { - public PullRequest PullRequest { get; set; } - } + public PullRequest? PullRequest { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/PullRequestOrders.cs b/src/Bitbucket.Net/Models/Core/Projects/PullRequestOrders.cs index f22e516..6ada768 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/PullRequestOrders.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/PullRequestOrders.cs @@ -1,10 +1,9 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public enum PullRequestOrders { - public enum PullRequestOrders - { - Newest, - Oldest, - ParticipantStatus, - ClosedDate - } + Newest, + Oldest, + ParticipantStatus, + ClosedDate, } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/PullRequestSettings.cs b/src/Bitbucket.Net/Models/Core/Projects/PullRequestSettings.cs index 07e3cc7..c8cfe0b 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/PullRequestSettings.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/PullRequestSettings.cs @@ -1,18 +1,17 @@ using Bitbucket.Net.Models.Core.Admin; using System.Text.Json.Serialization; -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class PullRequestSettings { - public class PullRequestSettings - { - public MergeStrategies MergeConfig { get; set; } - public bool RequiredAllApprovers { get; set; } - public bool RequiredAllTasksComplete { get; set; } - [JsonPropertyName("com.atlassian.bitbucket.server.bitbucket-bundled-hooks:requiredApprovers")] - public MergeHookRequiredApprovers ComatlassianbitbucketserverbundledhooksrequiredApprovers { get; set; } - public int RequiredApprovers { get; set; } - [JsonPropertyName("com.atlassian.bitbucket.server.bitbucket-build:requiredBuilds")] - public MergeCheckRequiredBuilds ComatlassianbitbucketserverbitbucketbuildrequiredBuilds { get; set; } - public int RequiredSuccessfulBuilds { get; set; } - } -} + public MergeStrategies? MergeConfig { get; set; } + public bool RequiredAllApprovers { get; set; } + public bool RequiredAllTasksComplete { get; set; } + [JsonPropertyName("com.atlassian.bitbucket.server.bitbucket-bundled-hooks:requiredApprovers")] + public MergeHookRequiredApprovers? ComatlassianbitbucketserverbundledhooksrequiredApprovers { get; set; } + public int RequiredApprovers { get; set; } + [JsonPropertyName("com.atlassian.bitbucket.server.bitbucket-build:requiredBuilds")] + public MergeCheckRequiredBuilds? ComatlassianbitbucketserverbitbucketbuildrequiredBuilds { get; set; } + public int RequiredSuccessfulBuilds { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/PullRequestStates.cs b/src/Bitbucket.Net/Models/Core/Projects/PullRequestStates.cs index 4091411..40d6fbe 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/PullRequestStates.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/PullRequestStates.cs @@ -1,10 +1,9 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public enum PullRequestStates { - public enum PullRequestStates - { - Open, - Declined, - Merged, - All - } + Open, + Declined, + Merged, + All, } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/PullRequestSuggestion.cs b/src/Bitbucket.Net/Models/Core/Projects/PullRequestSuggestion.cs index 18638fd..626a6ac 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/PullRequestSuggestion.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/PullRequestSuggestion.cs @@ -1,16 +1,14 @@ -using System; using Bitbucket.Net.Common.Converters; using System.Text.Json.Serialization; -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class PullRequestSuggestion { - public class PullRequestSuggestion - { - [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset ChangeTime { get; set; } - public RefChange RefChange { get; set; } - public Repository Repository { get; set; } - public Ref FromRef { get; set; } - public Ref ToRef { get; set; } - } -} + [JsonConverter(typeof(UnixDateTimeOffsetConverter))] + public DateTimeOffset ChangeTime { get; set; } + public RefChange? RefChange { get; set; } + public Repository? Repository { get; set; } + public Ref? FromRef { get; set; } + public Ref? ToRef { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/PullRequestUpdate.cs b/src/Bitbucket.Net/Models/Core/Projects/PullRequestUpdate.cs index 064b172..e82c7bf 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/PullRequestUpdate.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/PullRequestUpdate.cs @@ -1,13 +1,10 @@ -using System.Collections.Generic; +namespace Bitbucket.Net.Models.Core.Projects; -namespace Bitbucket.Net.Models.Core.Projects +public class PullRequestUpdate { - public class PullRequestUpdate - { - public int Id { get; set; } - public int Version { get; set; } - public string Title { get; set; } - public string Description { get; set; } - public List Reviewers { get; set; } - } -} + public int Id { get; set; } + public int Version { get; set; } + public string? Title { get; set; } + public string? Description { get; set; } + public List? Reviewers { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Ref.cs b/src/Bitbucket.Net/Models/Core/Projects/Ref.cs index 49fa0e5..e8fa224 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Ref.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Ref.cs @@ -1,9 +1,8 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class Ref { - public class Ref - { - public string Id { get; set; } - public string DisplayId { get; set; } - public string Type { get; set; } - } + public string? Id { get; set; } + public string? DisplayId { get; set; } + public string? Type { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/RefChange.cs b/src/Bitbucket.Net/Models/Core/Projects/RefChange.cs index 3f04143..5e1a82f 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/RefChange.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/RefChange.cs @@ -1,11 +1,10 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class RefChange { - public class RefChange - { - public Ref Ref { get; set; } - public string RefId { get; set; } - public string FromHash { get; set; } - public string ToHash { get; set; } - public string Type { get; set; } - } + public Ref? Ref { get; set; } + public string? RefId { get; set; } + public string? FromHash { get; set; } + public string? ToHash { get; set; } + public string? Type { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Repository.cs b/src/Bitbucket.Net/Models/Core/Projects/Repository.cs index f818589..b901be7 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Repository.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Repository.cs @@ -1,15 +1,47 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +/// +/// Full Bitbucket repository. Extends with server-assigned identity and metadata. +/// +public class Repository : RepositoryRef { - public class Repository : RepositoryRef - { - public int Id { get; set; } - public string ScmId { get; set; } - public string State { get; set; } - public string StatusMessage { get; set; } - public bool Forkable { get; set; } - public bool Public { get; set; } - public CloneLinks Links { get; set; } - - public override string ToString() => Name; - } -} + /// + /// Gets or sets the server-assigned repository identifier. + /// + public int Id { get; set; } + + /// + /// Gets or sets the SCM type identifier (e.g. "git"). + /// + public string? ScmId { get; set; } + + /// + /// Gets or sets the repository state (e.g. "AVAILABLE"). + /// + public string? State { get; set; } + + /// + /// Gets or sets a human-readable status message for the repository. + /// + public string? StatusMessage { get; set; } + + /// + /// Gets or sets a value indicating whether the repository can be forked. + /// + public bool Forkable { get; set; } + + /// + /// Gets or sets a value indicating whether the repository is publicly accessible. + /// + public bool Public { get; set; } + + /// + /// Gets or sets the clone URLs and other hypermedia links for this repository. + /// + public CloneLinks? Links { get; set; } + + /// + /// Returns the repository name, when available. + /// + public override string ToString() => Name ?? string.Empty; +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/RepositoryFork.cs b/src/Bitbucket.Net/Models/Core/Projects/RepositoryFork.cs index c337473..def5f2d 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/RepositoryFork.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/RepositoryFork.cs @@ -1,7 +1,6 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class RepositoryFork : RepositoryOrigin { - public class RepositoryFork : RepositoryOrigin - { - public RepositoryOrigin Origin { get; set; } - } -} + public RepositoryOrigin? Origin { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/RepositoryOrigin.cs b/src/Bitbucket.Net/Models/Core/Projects/RepositoryOrigin.cs index 2645250..064eec8 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/RepositoryOrigin.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/RepositoryOrigin.cs @@ -1,16 +1,15 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class RepositoryOrigin { - public class RepositoryOrigin - { - public string Slug { get; set; } - public int Id { get; set; } - public string Name { get; set; } - public string ScmId { get; set; } - public string State { get; set; } - public string StatusMessage { get; set; } - public bool Forkable { get; set; } - public Project Project { get; set; } - public bool Public { get; set; } - public Links Links { get; set; } - } + public string? Slug { get; set; } + public int Id { get; set; } + public string? Name { get; set; } + public string? ScmId { get; set; } + public string? State { get; set; } + public string? StatusMessage { get; set; } + public bool Forkable { get; set; } + public Project? Project { get; set; } + public bool Public { get; set; } + public Links? Links { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/RepositoryRef.cs b/src/Bitbucket.Net/Models/Core/Projects/RepositoryRef.cs index ddcd4a5..5f50cfb 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/RepositoryRef.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/RepositoryRef.cs @@ -1,9 +1,22 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +/// +/// Lightweight reference to a Bitbucket repository, identified by slug and parent project. +/// +public class RepositoryRef { - public class RepositoryRef - { - public string Slug { get; set; } - public string Name { get; set; } - public ProjectRef Project { get; set; } - } -} + /// + /// Gets or sets the URL-friendly repository identifier. + /// + public string? Slug { get; set; } + + /// + /// Gets or sets the repository display name. + /// + public string? Name { get; set; } + + /// + /// Gets or sets the parent project reference. + /// + public ProjectRef? Project { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Reviewer.cs b/src/Bitbucket.Net/Models/Core/Projects/Reviewer.cs index 30fde09..6095f34 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Reviewer.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Reviewer.cs @@ -1,7 +1,12 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +/// +/// A pull request reviewer. Extends with the last-reviewed commit reference. +/// +public class Reviewer : Participant { - public class Reviewer : Participant - { - public string LastReviewedCommit { get; set; } - } + /// + /// Gets or sets the SHA of the last commit the reviewer has reviewed. + /// + public string? LastReviewedCommit { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Roles.cs b/src/Bitbucket.Net/Models/Core/Projects/Roles.cs index f2dee8a..159d792 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Roles.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Roles.cs @@ -1,9 +1,8 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public enum Roles { - public enum Roles - { - Author, - Reviewer, - Participant - } -} + Author, + Reviewer, + Participant, +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/ScopeTypes.cs b/src/Bitbucket.Net/Models/Core/Projects/ScopeTypes.cs index 3660a15..6c73632 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/ScopeTypes.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/ScopeTypes.cs @@ -1,8 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public enum ScopeTypes { - public enum ScopeTypes - { - Project, - Repository - } -} + Project, + Repository, +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Segment.cs b/src/Bitbucket.Net/Models/Core/Projects/Segment.cs index c11fb88..4f0d314 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Segment.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Segment.cs @@ -1,11 +1,22 @@ -using System.Collections.Generic; +namespace Bitbucket.Net.Models.Core.Projects; -namespace Bitbucket.Net.Models.Core.Projects +/// +/// A segment within a diff hunk, grouping consecutive lines of the same type. +/// +public class Segment { - public class Segment - { - public string Type { get; set; } - public List Lines { get; set; } - public bool Truncated { get; set; } - } + /// + /// Gets or sets the segment type (e.g. "ADDED", "REMOVED", or "CONTEXT"). + /// + public string? Type { get; set; } + + /// + /// Gets or sets the lines in this segment. + /// + public List? Lines { get; set; } + + /// + /// Gets or sets a value indicating whether this segment was truncated by the server. + /// + public bool Truncated { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Tag.cs b/src/Bitbucket.Net/Models/Core/Projects/Tag.cs index 39870b1..be724f5 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Tag.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Tag.cs @@ -1,12 +1,37 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +/// +/// A Git tag in a Bitbucket repository. +/// +public class Tag { - public class Tag - { - public string Id { get; set; } - public string DisplayId { get; set; } - public string Type { get; set; } - public string LatestCommit { get; set; } - public string LatestChangeset { get; set; } - public string Hash { get; set; } - } -} + /// + /// Gets or sets the full tag ref path (e.g. "refs/tags/v1.0"). + /// + public string? Id { get; set; } + + /// + /// Gets or sets the short tag name shown in the Bitbucket UI. + /// + public string? DisplayId { get; set; } + + /// + /// Gets or sets the ref type (typically "TAG"). + /// + public string? Type { get; set; } + + /// + /// Gets or sets the SHA of the latest commit this tag points to. + /// + public string? LatestCommit { get; set; } + + /// + /// Gets or sets the changeset identifier of the latest change. + /// + public string? LatestChangeset { get; set; } + + /// + /// Gets or sets the tag object hash (for annotated tags). + /// + public string? Hash { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/TimeWindow.cs b/src/Bitbucket.Net/Models/Core/Projects/TimeWindow.cs index 7b2729f..9d10a11 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/TimeWindow.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/TimeWindow.cs @@ -1,13 +1,11 @@ -using System; using Bitbucket.Net.Common.Converters; using System.Text.Json.Serialization; -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class TimeWindow { - public class TimeWindow - { - [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset Start { get; set; } - public long Duration { get; set; } - } + [JsonConverter(typeof(UnixDateTimeOffsetConverter))] + public DateTimeOffset Start { get; set; } + public long Duration { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/VersionInfo.cs b/src/Bitbucket.Net/Models/Core/Projects/VersionInfo.cs index e7f748e..7396527 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/VersionInfo.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/VersionInfo.cs @@ -1,7 +1,6 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class VersionInfo { - public class VersionInfo - { - public int Version { get; set; } - } -} + public int Version { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/Veto.cs b/src/Bitbucket.Net/Models/Core/Projects/Veto.cs index c284d04..a9cc7df 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/Veto.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/Veto.cs @@ -1,8 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class Veto { - public class Veto - { - public string SummaryMessage { get; set; } - public string DetailedMessage { get; set; } - } + public string? SummaryMessage { get; set; } + public string? DetailedMessage { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/WebHook.cs b/src/Bitbucket.Net/Models/Core/Projects/WebHook.cs index 8a9a392..efe2355 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/WebHook.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/WebHook.cs @@ -1,21 +1,18 @@ -using System; -using System.Collections.Generic; using Bitbucket.Net.Common.Converters; using System.Text.Json.Serialization; -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class WebHook { - public class WebHook - { - public int Id { get; set; } - public string Name { get; set; } - [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset CreatedDate { get; set; } - [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset UpdatedDate { get; set; } - public List Events { get; set; } - public Dictionary Configuration { get; set; } - public string Url { get; set; } - public bool Active { get; set; } - } -} + public int Id { get; set; } + public string? Name { get; set; } + [JsonConverter(typeof(UnixDateTimeOffsetConverter))] + public DateTimeOffset CreatedDate { get; set; } + [JsonConverter(typeof(UnixDateTimeOffsetConverter))] + public DateTimeOffset UpdatedDate { get; set; } + public List? Events { get; set; } + public Dictionary? Configuration { get; set; } + public string? Url { get; set; } + public bool Active { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/WebHookInvocation.cs b/src/Bitbucket.Net/Models/Core/Projects/WebHookInvocation.cs index 93f5b94..3feec75 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/WebHookInvocation.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/WebHookInvocation.cs @@ -1,19 +1,17 @@ -using System; using Bitbucket.Net.Common.Converters; using System.Text.Json.Serialization; -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class WebHookInvocation { - public class WebHookInvocation - { - public int Id { get; set; } - public string Event { get; set; } - public int Duration { get; set; } - [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset Start { get; set; } - [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset Finish { get; set; } - public WebHookRequest Request { get; set; } - public WebHookResult Result { get; set; } - } + public int Id { get; set; } + public string? Event { get; set; } + public int Duration { get; set; } + [JsonConverter(typeof(UnixDateTimeOffsetConverter))] + public DateTimeOffset Start { get; set; } + [JsonConverter(typeof(UnixDateTimeOffsetConverter))] + public DateTimeOffset Finish { get; set; } + public WebHookRequest? Request { get; set; } + public WebHookResult? Result { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/WebHookOutcomes.cs b/src/Bitbucket.Net/Models/Core/Projects/WebHookOutcomes.cs index 450bd4c..279c83e 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/WebHookOutcomes.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/WebHookOutcomes.cs @@ -1,9 +1,8 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public enum WebHookOutcomes { - public enum WebHookOutcomes - { - Success, - Failure, - Error - } -} + Success, + Failure, + Error, +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/WebHookRequest.cs b/src/Bitbucket.Net/Models/Core/Projects/WebHookRequest.cs index df59095..6dd5c3e 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/WebHookRequest.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/WebHookRequest.cs @@ -1,8 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class WebHookRequest { - public class WebHookRequest - { - public string Url { get; set; } - public string Method { get; set; } - } -} + public string? Url { get; set; } + public string? Method { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/WebHookResult.cs b/src/Bitbucket.Net/Models/Core/Projects/WebHookResult.cs index 875e905..e8b7399 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/WebHookResult.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/WebHookResult.cs @@ -1,12 +1,11 @@ using Bitbucket.Net.Common.Converters; using System.Text.Json.Serialization; -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class WebHookResult { - public class WebHookResult - { - public string Description { get; set; } - [JsonConverter(typeof(WebHookOutcomesConverter))] - public WebHookOutcomes Outcome { get; set; } - } -} + public string? Description { get; set; } + [JsonConverter(typeof(WebHookOutcomesConverter))] + public WebHookOutcomes Outcome { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/WebHookStatistics.cs b/src/Bitbucket.Net/Models/Core/Projects/WebHookStatistics.cs index afe3939..41aa17b 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/WebHookStatistics.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/WebHookStatistics.cs @@ -1,7 +1,6 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class WebHookStatistics { - public class WebHookStatistics - { - public WebHookStatisticsCounts Counts { get; set; } - } -} + public WebHookStatisticsCounts? Counts { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/WebHookStatisticsCounts.cs b/src/Bitbucket.Net/Models/Core/Projects/WebHookStatisticsCounts.cs index ad9a0d4..ecc6cc6 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/WebHookStatisticsCounts.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/WebHookStatisticsCounts.cs @@ -1,10 +1,9 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class WebHookStatisticsCounts { - public class WebHookStatisticsCounts - { - public int Errors { get; set; } - public int Failures { get; set; } - public int Successes { get; set; } - public TimeWindow Window { get; set; } - } + public int Errors { get; set; } + public int Failures { get; set; } + public int Successes { get; set; } + public TimeWindow? Window { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/WebHookStatisticsSummary.cs b/src/Bitbucket.Net/Models/Core/Projects/WebHookStatisticsSummary.cs index 79f321e..aae165f 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/WebHookStatisticsSummary.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/WebHookStatisticsSummary.cs @@ -1,10 +1,9 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class WebHookStatisticsSummary { - public class WebHookStatisticsSummary - { - public WebHookInvocation LastSuccess { get; set; } - public WebHookInvocation LastFailure { get; set; } - public WebHookInvocation LastError { get; set; } - public int Counts { get; set; } - } -} + public WebHookInvocation? LastSuccess { get; set; } + public WebHookInvocation? LastFailure { get; set; } + public WebHookInvocation? LastError { get; set; } + public int Counts { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/WebHookTestRequest.cs b/src/Bitbucket.Net/Models/Core/Projects/WebHookTestRequest.cs index 093bf1c..5a09992 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/WebHookTestRequest.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/WebHookTestRequest.cs @@ -1,10 +1,7 @@ -using System.Collections.Generic; +namespace Bitbucket.Net.Models.Core.Projects; -namespace Bitbucket.Net.Models.Core.Projects +public class WebHookTestRequest : WebHookRequest { - public class WebHookTestRequest : WebHookRequest - { - public string Body { get; set; } - public List Headers { get; set; } - } + public string? Body { get; set; } + public List? Headers { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/WebHookTestRequestResponse.cs b/src/Bitbucket.Net/Models/Core/Projects/WebHookTestRequestResponse.cs index cfee4f2..3588983 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/WebHookTestRequestResponse.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/WebHookTestRequestResponse.cs @@ -1,8 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +public class WebHookTestRequestResponse { - public class WebHookTestRequestResponse - { - public WebHookTestRequest Request { get; set; } - public WebHookTestResponse Response { get; set; } - } -} + public WebHookTestRequest? Request { get; set; } + public WebHookTestResponse? Response { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/WebHookTestResponse.cs b/src/Bitbucket.Net/Models/Core/Projects/WebHookTestResponse.cs index ae17e58..51cc559 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/WebHookTestResponse.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/WebHookTestResponse.cs @@ -1,11 +1,8 @@ -using System.Collections.Generic; +namespace Bitbucket.Net.Models.Core.Projects; -namespace Bitbucket.Net.Models.Core.Projects +public class WebHookTestResponse { - public class WebHookTestResponse - { - public int Status { get; set; } - public List Headers { get; set; } - public string Body { get; set; } - } + public int Status { get; set; } + public List? Headers { get; set; } + public string? Body { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Projects/WithId.cs b/src/Bitbucket.Net/Models/Core/Projects/WithId.cs index b65155d..f2f7c5e 100644 --- a/src/Bitbucket.Net/Models/Core/Projects/WithId.cs +++ b/src/Bitbucket.Net/Models/Core/Projects/WithId.cs @@ -1,7 +1,12 @@ -namespace Bitbucket.Net.Models.Core.Projects +namespace Bitbucket.Net.Models.Core.Projects; + +/// +/// Base class for Bitbucket entities identified by a string identifier. +/// +public class WithId { - public class WithId - { - public string Id { get; set; } - } -} + /// + /// Gets or sets the unique identifier (typically a Git ref path such as "refs/heads/main"). + /// + public string? Id { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Tasks/BitbucketTask.cs b/src/Bitbucket.Net/Models/Core/Tasks/BitbucketTask.cs index 4811029..25449d6 100644 --- a/src/Bitbucket.Net/Models/Core/Tasks/BitbucketTask.cs +++ b/src/Bitbucket.Net/Models/Core/Tasks/BitbucketTask.cs @@ -1,8 +1,17 @@ -namespace Bitbucket.Net.Models.Core.Tasks +namespace Bitbucket.Net.Models.Core.Tasks; + +/// +/// A Bitbucket pull request task. Extends with an anchor and state. +/// +public class BitbucketTask : TaskRef { - public class BitbucketTask : TaskRef - { - public TaskAnchor Anchor { get; set; } - public string State { get; set; } - } -} + /// + /// Gets or sets the comment anchor this task is attached to. + /// + public TaskAnchor? Anchor { get; set; } + + /// + /// Gets or sets the task state (e.g. "OPEN" or "RESOLVED"). + /// + public string? State { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Tasks/BitbucketTaskCount.cs b/src/Bitbucket.Net/Models/Core/Tasks/BitbucketTaskCount.cs index 3492af3..1f1f749 100644 --- a/src/Bitbucket.Net/Models/Core/Tasks/BitbucketTaskCount.cs +++ b/src/Bitbucket.Net/Models/Core/Tasks/BitbucketTaskCount.cs @@ -1,9 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Tasks -{ - public class BitbucketTaskCount - { - public int Open { get; set; } - public int Resolved { get; set; } - } +namespace Bitbucket.Net.Models.Core.Tasks; -} +public class BitbucketTaskCount +{ + public int Open { get; set; } + public int Resolved { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Tasks/TaskAnchor.cs b/src/Bitbucket.Net/Models/Core/Tasks/TaskAnchor.cs index 82f65fd..2dae176 100644 --- a/src/Bitbucket.Net/Models/Core/Tasks/TaskAnchor.cs +++ b/src/Bitbucket.Net/Models/Core/Tasks/TaskAnchor.cs @@ -1,17 +1,32 @@ -using System; -using System.Collections.Generic; using Bitbucket.Net.Common.Converters; using Bitbucket.Net.Models.Core.Projects; using System.Text.Json.Serialization; -namespace Bitbucket.Net.Models.Core.Tasks +namespace Bitbucket.Net.Models.Core.Tasks; + +/// +/// A task anchor that represents the comment a task is attached to. Extends with versioning and nested content. +/// +public class TaskAnchor : TaskRef { - public class TaskAnchor : TaskRef - { - public int Version { get; set; } - [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset? UpdatedDate { get; set; } - public List Comments { get; set; } - public List Tasks { get; set; } - } + /// + /// Gets or sets the version number for optimistic locking on updates. + /// + public int Version { get; set; } + + /// + /// Gets or sets the date and time when the anchor was last updated. + /// + [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] + public DateTimeOffset? UpdatedDate { get; set; } + + /// + /// Gets or sets the nested comment references on this anchor. + /// + public List? Comments { get; set; } + + /// + /// Gets or sets the tasks associated with this anchor. + /// + public List? Tasks { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Tasks/TaskBasicAnchor.cs b/src/Bitbucket.Net/Models/Core/Tasks/TaskBasicAnchor.cs index 29cd79c..d2edc8f 100644 --- a/src/Bitbucket.Net/Models/Core/Tasks/TaskBasicAnchor.cs +++ b/src/Bitbucket.Net/Models/Core/Tasks/TaskBasicAnchor.cs @@ -1,8 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Tasks +namespace Bitbucket.Net.Models.Core.Tasks; + +public class TaskBasicAnchor { - public class TaskBasicAnchor - { - public int Id { get; set; } - public string Type { get; set; } - } + public int Id { get; set; } + public string? Type { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Tasks/TaskInfo.cs b/src/Bitbucket.Net/Models/Core/Tasks/TaskInfo.cs index 7fb669b..be6c45a 100644 --- a/src/Bitbucket.Net/Models/Core/Tasks/TaskInfo.cs +++ b/src/Bitbucket.Net/Models/Core/Tasks/TaskInfo.cs @@ -1,8 +1,7 @@ -namespace Bitbucket.Net.Models.Core.Tasks +namespace Bitbucket.Net.Models.Core.Tasks; + +public class TaskInfo { - public class TaskInfo - { - public TaskBasicAnchor Anchor { get; set; } - public string Text { get; set; } - } -} + public TaskBasicAnchor? Anchor { get; set; } + public string? Text { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Tasks/TaskRef.cs b/src/Bitbucket.Net/Models/Core/Tasks/TaskRef.cs index cad5b79..a7ab03e 100644 --- a/src/Bitbucket.Net/Models/Core/Tasks/TaskRef.cs +++ b/src/Bitbucket.Net/Models/Core/Tasks/TaskRef.cs @@ -1,19 +1,43 @@ -using System; using Bitbucket.Net.Common.Converters; using Bitbucket.Net.Models.Core.Projects; using Bitbucket.Net.Models.Core.Users; using System.Text.Json.Serialization; -namespace Bitbucket.Net.Models.Core.Tasks +namespace Bitbucket.Net.Models.Core.Tasks; + +/// +/// Abstract base class for Bitbucket tasks with identity, text, author, and creation date. +/// +public abstract class TaskRef { - public abstract class TaskRef - { - public Properties Properties { get; set; } - public int Id { get; set; } - public string Text { get; set; } - public User Author { get; set; } - [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset? CreatedDate { get; set; } - public Permittedoperations PermittedOperations { get; set; } - } + /// + /// Gets or sets the additional properties bag. + /// + public Properties? Properties { get; set; } + + /// + /// Gets or sets the server-assigned task identifier. + /// + public int Id { get; set; } + + /// + /// Gets or sets the task description text. + /// + public string? Text { get; set; } + + /// + /// Gets or sets the user who created the task. + /// + public User? Author { get; set; } + + /// + /// Gets or sets the date and time when the task was created. + /// + [JsonConverter(typeof(NullableUnixDateTimeOffsetConverter))] + public DateTimeOffset? CreatedDate { get; set; } + + /// + /// Gets or sets the operations the current user is permitted to perform on this task. + /// + public Permittedoperations? PermittedOperations { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Users/Identity.cs b/src/Bitbucket.Net/Models/Core/Users/Identity.cs index 12ad5b0..a54e30a 100644 --- a/src/Bitbucket.Net/Models/Core/Users/Identity.cs +++ b/src/Bitbucket.Net/Models/Core/Users/Identity.cs @@ -1,7 +1,12 @@ -namespace Bitbucket.Net.Models.Core.Users +namespace Bitbucket.Net.Models.Core.Users; + +/// +/// Extends with an email address. +/// +public class Identity : Named { - public class Identity : Named - { - public string EmailAddress { get; set; } - } + /// + /// Gets or sets the email address associated with the identity. + /// + public string? EmailAddress { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Users/Named.cs b/src/Bitbucket.Net/Models/Core/Users/Named.cs index 1b9e1ce..83cb021 100644 --- a/src/Bitbucket.Net/Models/Core/Users/Named.cs +++ b/src/Bitbucket.Net/Models/Core/Users/Named.cs @@ -1,9 +1,14 @@ -namespace Bitbucket.Net.Models.Core.Users +namespace Bitbucket.Net.Models.Core.Users; + +/// +/// Base class for Bitbucket entities that have a name. +/// +public class Named { - public class Named - { - public string Name { get; set; } + /// + /// Gets or sets the entity name (typically the username for users). + /// + public string? Name { get; set; } - public override string ToString() => Name; - } -} + public override string ToString() => Name ?? string.Empty; +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Users/PasswordChange.cs b/src/Bitbucket.Net/Models/Core/Users/PasswordChange.cs index 6fd5ae8..fdfb9d1 100644 --- a/src/Bitbucket.Net/Models/Core/Users/PasswordChange.cs +++ b/src/Bitbucket.Net/Models/Core/Users/PasswordChange.cs @@ -1,9 +1,8 @@ using Bitbucket.Net.Models.Core.Admin; -namespace Bitbucket.Net.Models.Core.Users +namespace Bitbucket.Net.Models.Core.Users; + +public class PasswordChange : PasswordBasic { - public class PasswordChange : PasswordBasic - { - public string OldPassword { get; set; } - } -} + public string? OldPassword { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Core/Users/User.cs b/src/Bitbucket.Net/Models/Core/Users/User.cs index 616ccbc..a4390b1 100644 --- a/src/Bitbucket.Net/Models/Core/Users/User.cs +++ b/src/Bitbucket.Net/Models/Core/Users/User.cs @@ -1,14 +1,42 @@ -namespace Bitbucket.Net.Models.Core.Users +namespace Bitbucket.Net.Models.Core.Users; + +/// +/// Full Bitbucket user. Extends with server-assigned identity, display name, and account state. +/// +public class User : Identity { - public class User : Identity - { - public int Id { get; set; } - public string DisplayName { get; set; } - public bool Active { get; set; } - public string Slug { get; set; } - public string Type { get; set; } - public string AvatarUrl { get; set; } - - public override string ToString() => DisplayName; - } + /// + /// Gets or sets the server-assigned user identifier. + /// + public int Id { get; set; } + + /// + /// Gets or sets the user's display name. + /// + public string? DisplayName { get; set; } + + /// + /// Gets or sets a value indicating whether the user account is active. + /// + public bool Active { get; set; } + + /// + /// Gets or sets the URL-friendly user identifier. + /// + public string? Slug { get; set; } + + /// + /// Gets or sets the user type (e.g. "NORMAL" or "SERVICE"). + /// + public string? Type { get; set; } + + /// + /// Gets or sets the URL of the user's avatar image. + /// + public string? AvatarUrl { get; set; } + + /// + /// Returns the user's display name when available. + /// + public override string ToString() => DisplayName ?? string.Empty; } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/DefaultReviewers/DefaultReviewerPullRequestCondition.cs b/src/Bitbucket.Net/Models/DefaultReviewers/DefaultReviewerPullRequestCondition.cs index b034270..05212f2 100644 --- a/src/Bitbucket.Net/Models/DefaultReviewers/DefaultReviewerPullRequestCondition.cs +++ b/src/Bitbucket.Net/Models/DefaultReviewers/DefaultReviewerPullRequestCondition.cs @@ -1,15 +1,13 @@ -using System.Collections.Generic; -using Bitbucket.Net.Models.Core.Users; +using Bitbucket.Net.Models.Core.Users; -namespace Bitbucket.Net.Models.DefaultReviewers +namespace Bitbucket.Net.Models.DefaultReviewers; + +public class DefaultReviewerPullRequestCondition { - public class DefaultReviewerPullRequestCondition - { - public int Id { get; set; } - public DefaultReviewerPullRequestConditionScope Scope { get; set; } - public RefMatcher SourceRefMatcher { get; set; } - public RefMatcher TargetRefMatcher { get; set; } - public List Reviewers { get; set; } - public int RequiredApprovals { get; set; } - } -} + public int Id { get; set; } + public DefaultReviewerPullRequestConditionScope? Scope { get; set; } + public RefMatcher? SourceRefMatcher { get; set; } + public RefMatcher? TargetRefMatcher { get; set; } + public List? Reviewers { get; set; } + public int RequiredApprovals { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/DefaultReviewers/DefaultReviewerPullRequestConditionScope.cs b/src/Bitbucket.Net/Models/DefaultReviewers/DefaultReviewerPullRequestConditionScope.cs index 3bce9f4..f32bee2 100644 --- a/src/Bitbucket.Net/Models/DefaultReviewers/DefaultReviewerPullRequestConditionScope.cs +++ b/src/Bitbucket.Net/Models/DefaultReviewers/DefaultReviewerPullRequestConditionScope.cs @@ -1,8 +1,7 @@ -namespace Bitbucket.Net.Models.DefaultReviewers +namespace Bitbucket.Net.Models.DefaultReviewers; + +public class DefaultReviewerPullRequestConditionScope { - public class DefaultReviewerPullRequestConditionScope - { - public string Type { get; set; } - public int ResourceId { get; set; } - } + public string? Type { get; set; } + public int ResourceId { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/DefaultReviewers/DefaultReviewerPullRequestConditionType.cs b/src/Bitbucket.Net/Models/DefaultReviewers/DefaultReviewerPullRequestConditionType.cs index cfdda3a..9267530 100644 --- a/src/Bitbucket.Net/Models/DefaultReviewers/DefaultReviewerPullRequestConditionType.cs +++ b/src/Bitbucket.Net/Models/DefaultReviewers/DefaultReviewerPullRequestConditionType.cs @@ -1,8 +1,7 @@ -namespace Bitbucket.Net.Models.DefaultReviewers +namespace Bitbucket.Net.Models.DefaultReviewers; + +public class DefaultReviewerPullRequestConditionType { - public class DefaultReviewerPullRequestConditionType - { - public string Id { get; set; } - public string Name { get; set; } - } + public string? Id { get; set; } + public string? Name { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/DefaultReviewers/RefMatcher.cs b/src/Bitbucket.Net/Models/DefaultReviewers/RefMatcher.cs index af35739..b852734 100644 --- a/src/Bitbucket.Net/Models/DefaultReviewers/RefMatcher.cs +++ b/src/Bitbucket.Net/Models/DefaultReviewers/RefMatcher.cs @@ -1,10 +1,9 @@ -namespace Bitbucket.Net.Models.DefaultReviewers +namespace Bitbucket.Net.Models.DefaultReviewers; + +public class RefMatcher { - public class RefMatcher - { - public bool Active { get; set; } - public string Id { get; set; } - public string DisplayId { get; set; } - public DefaultReviewerPullRequestConditionType Type { get; set; } - } + public bool Active { get; set; } + public string? Id { get; set; } + public string? DisplayId { get; set; } + public DefaultReviewerPullRequestConditionType? Type { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Git/RebasePullRequestCondition.cs b/src/Bitbucket.Net/Models/Git/RebasePullRequestCondition.cs index 9648ed0..43607e6 100644 --- a/src/Bitbucket.Net/Models/Git/RebasePullRequestCondition.cs +++ b/src/Bitbucket.Net/Models/Git/RebasePullRequestCondition.cs @@ -1,11 +1,8 @@ -using System.Collections.Generic; +namespace Bitbucket.Net.Models.Git; -namespace Bitbucket.Net.Models.Git +public class RebasePullRequestCondition { - public class RebasePullRequestCondition - { - public bool CanRebase { get; set; } - public bool CanWrite { get; set; } - public List Vetoes { get; set; } - } -} + public bool CanRebase { get; set; } + public bool CanWrite { get; set; } + public List? Vetoes { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Git/TagTypes.cs b/src/Bitbucket.Net/Models/Git/TagTypes.cs index c73fe4e..c6fa3ea 100644 --- a/src/Bitbucket.Net/Models/Git/TagTypes.cs +++ b/src/Bitbucket.Net/Models/Git/TagTypes.cs @@ -1,8 +1,7 @@ -namespace Bitbucket.Net.Models.Git +namespace Bitbucket.Net.Models.Git; + +public enum TagTypes { - public enum TagTypes - { - LightWeight, - Annotated - } -} + LightWeight, + Annotated, +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Git/Veto.cs b/src/Bitbucket.Net/Models/Git/Veto.cs index 8c45efc..93d271c 100644 --- a/src/Bitbucket.Net/Models/Git/Veto.cs +++ b/src/Bitbucket.Net/Models/Git/Veto.cs @@ -1,8 +1,7 @@ -namespace Bitbucket.Net.Models.Git +namespace Bitbucket.Net.Models.Git; + +public class Veto { - public class Veto - { - public string SummaryMessage { get; set; } - public string DetailedMessage { get; set; } - } + public string? SummaryMessage { get; set; } + public string? DetailedMessage { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Jira/ChangeSet.cs b/src/Bitbucket.Net/Models/Jira/ChangeSet.cs index a71d8cb..e6d86a8 100644 --- a/src/Bitbucket.Net/Models/Jira/ChangeSet.cs +++ b/src/Bitbucket.Net/Models/Jira/ChangeSet.cs @@ -1,13 +1,12 @@ using Bitbucket.Net.Models.Core.Projects; -namespace Bitbucket.Net.Models.Jira -{ - public class ChangeSet +namespace Bitbucket.Net.Models.Jira; + +public class ChangeSet { - public CommitParent FromCommit { get; set; } - public Commit ToCommit { get; set; } - public Changes Changes { get; set; } - public Links Links { get; set; } - public Repository Repository { get; set; } - } -} + public CommitParent? FromCommit { get; set; } + public Commit? ToCommit { get; set; } + public Changes? Changes { get; set; } + public Links? Links { get; set; } + public Repository? Repository { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Jira/Changes.cs b/src/Bitbucket.Net/Models/Jira/Changes.cs index b99192b..223dcb7 100644 --- a/src/Bitbucket.Net/Models/Jira/Changes.cs +++ b/src/Bitbucket.Net/Models/Jira/Changes.cs @@ -1,14 +1,12 @@ -using System.Collections.Generic; -using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Core.Projects; -namespace Bitbucket.Net.Models.Jira +namespace Bitbucket.Net.Models.Jira; + +public class Changes { - public class Changes - { - public int Size { get; set; } - public int Limit { get; set; } - public bool IsLastPage { get; set; } - public List Values { get; set; } - public int Start { get; set; } - } + public int Size { get; set; } + public int Limit { get; set; } + public bool IsLastPage { get; set; } + public List? Values { get; set; } + public int Start { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Jira/JiraIssue.cs b/src/Bitbucket.Net/Models/Jira/JiraIssue.cs index 644dfc8..405b1ee 100644 --- a/src/Bitbucket.Net/Models/Jira/JiraIssue.cs +++ b/src/Bitbucket.Net/Models/Jira/JiraIssue.cs @@ -1,8 +1,7 @@ -namespace Bitbucket.Net.Models.Jira +namespace Bitbucket.Net.Models.Jira; + +public class JiraIssue { - public class JiraIssue - { - public int CommentId { get; set; } - public string IssueKey { get; set; } - } -} + public int CommentId { get; set; } + public string? IssueKey { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/PersonalAccessTokens/AccessToken.cs b/src/Bitbucket.Net/Models/PersonalAccessTokens/AccessToken.cs index c1245c7..84a597a 100644 --- a/src/Bitbucket.Net/Models/PersonalAccessTokens/AccessToken.cs +++ b/src/Bitbucket.Net/Models/PersonalAccessTokens/AccessToken.cs @@ -1,19 +1,17 @@ -using System; using Bitbucket.Net.Common.Converters; using Bitbucket.Net.Models.Core.Users; using System.Text.Json.Serialization; -namespace Bitbucket.Net.Models.PersonalAccessTokens +namespace Bitbucket.Net.Models.PersonalAccessTokens; + +public class AccessToken : AccessTokenCreate { - public class AccessToken : AccessTokenCreate - { - public string Id { get; set; } - [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset CreatedDate { get; set; } - [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset LastAuthenticated { get; set; } - public User User { get; set; } - [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset ExpiryDate { get; set; } - } -} + public string? Id { get; set; } + [JsonConverter(typeof(UnixDateTimeOffsetConverter))] + public DateTimeOffset CreatedDate { get; set; } + [JsonConverter(typeof(UnixDateTimeOffsetConverter))] + public DateTimeOffset LastAuthenticated { get; set; } + public User? User { get; set; } + [JsonConverter(typeof(UnixDateTimeOffsetConverter))] + public DateTimeOffset ExpiryDate { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/PersonalAccessTokens/AccessTokenCreate.cs b/src/Bitbucket.Net/Models/PersonalAccessTokens/AccessTokenCreate.cs index 9c6262d..233e5bf 100644 --- a/src/Bitbucket.Net/Models/PersonalAccessTokens/AccessTokenCreate.cs +++ b/src/Bitbucket.Net/Models/PersonalAccessTokens/AccessTokenCreate.cs @@ -1,14 +1,12 @@ -using System.Collections.Generic; using Bitbucket.Net.Common.Converters; using Bitbucket.Net.Models.Core.Admin; using System.Text.Json.Serialization; -namespace Bitbucket.Net.Models.PersonalAccessTokens +namespace Bitbucket.Net.Models.PersonalAccessTokens; + +public class AccessTokenCreate { - public class AccessTokenCreate - { - public string Name { get; set; } - [JsonConverter(typeof(PermissionsConverter))] - public List Permissions { get; set; } - } + public string? Name { get; set; } + [JsonConverter(typeof(PermissionsListConverter))] + public List? Permissions { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/PersonalAccessTokens/FullAccessToken.cs b/src/Bitbucket.Net/Models/PersonalAccessTokens/FullAccessToken.cs index 8896556..e851c02 100644 --- a/src/Bitbucket.Net/Models/PersonalAccessTokens/FullAccessToken.cs +++ b/src/Bitbucket.Net/Models/PersonalAccessTokens/FullAccessToken.cs @@ -1,7 +1,6 @@ -namespace Bitbucket.Net.Models.PersonalAccessTokens +namespace Bitbucket.Net.Models.PersonalAccessTokens; + +public class FullAccessToken : AccessToken { - public class FullAccessToken : AccessToken - { - public string Token { get; set; } - } + public string? Token { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/RefRestrictions/AccessKey.cs b/src/Bitbucket.Net/Models/RefRestrictions/AccessKey.cs index df76380..1c3a694 100644 --- a/src/Bitbucket.Net/Models/RefRestrictions/AccessKey.cs +++ b/src/Bitbucket.Net/Models/RefRestrictions/AccessKey.cs @@ -1,7 +1,6 @@ -namespace Bitbucket.Net.Models.RefRestrictions +namespace Bitbucket.Net.Models.RefRestrictions; + +public class AccessKey { - public class AccessKey - { - public Key Key { get; set; } - } + public Key? Key { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/RefRestrictions/Key.cs b/src/Bitbucket.Net/Models/RefRestrictions/Key.cs index b7e7586..1fb6c97 100644 --- a/src/Bitbucket.Net/Models/RefRestrictions/Key.cs +++ b/src/Bitbucket.Net/Models/RefRestrictions/Key.cs @@ -1,9 +1,8 @@ -namespace Bitbucket.Net.Models.RefRestrictions +namespace Bitbucket.Net.Models.RefRestrictions; + +public class Key { - public class Key - { - public int Id { get; set; } - public string Text { get; set; } - public string Label { get; set; } - } + public int Id { get; set; } + public string? Text { get; set; } + public string? Label { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/RefRestrictions/RefMatcherTypes.cs b/src/Bitbucket.Net/Models/RefRestrictions/RefMatcherTypes.cs index 30d2eff..12dfe0f 100644 --- a/src/Bitbucket.Net/Models/RefRestrictions/RefMatcherTypes.cs +++ b/src/Bitbucket.Net/Models/RefRestrictions/RefMatcherTypes.cs @@ -1,10 +1,9 @@ -namespace Bitbucket.Net.Models.RefRestrictions +namespace Bitbucket.Net.Models.RefRestrictions; + +public enum RefMatcherTypes { - public enum RefMatcherTypes - { - Branch, - Pattern, - ModelCategory, - ModelBranch - } -} + Branch, + Pattern, + ModelCategory, + ModelBranch, +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/RefRestrictions/RefRestriction.cs b/src/Bitbucket.Net/Models/RefRestrictions/RefRestriction.cs index c37131b..428c99d 100644 --- a/src/Bitbucket.Net/Models/RefRestrictions/RefRestriction.cs +++ b/src/Bitbucket.Net/Models/RefRestrictions/RefRestriction.cs @@ -1,14 +1,12 @@ -using System.Collections.Generic; -using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Core.Projects; using Bitbucket.Net.Models.Core.Users; -namespace Bitbucket.Net.Models.RefRestrictions +namespace Bitbucket.Net.Models.RefRestrictions; + +public class RefRestriction : RefRestrictionBase { - public class RefRestriction : RefRestrictionBase - { - public int Id { get; set; } - public HookScope Scope { get; set; } - public List Users { get; set; } - public List AccessKeys { get; set; } - } -} + public int Id { get; set; } + public HookScope? Scope { get; set; } + public List? Users { get; set; } + public List? AccessKeys { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/RefRestrictions/RefRestrictionBase.cs b/src/Bitbucket.Net/Models/RefRestrictions/RefRestrictionBase.cs index 4cb9fb6..63ecddc 100644 --- a/src/Bitbucket.Net/Models/RefRestrictions/RefRestrictionBase.cs +++ b/src/Bitbucket.Net/Models/RefRestrictions/RefRestrictionBase.cs @@ -1,15 +1,13 @@ -using System.Collections.Generic; using Bitbucket.Net.Common.Converters; using Bitbucket.Net.Models.DefaultReviewers; using System.Text.Json.Serialization; -namespace Bitbucket.Net.Models.RefRestrictions +namespace Bitbucket.Net.Models.RefRestrictions; + +public abstract class RefRestrictionBase { - public abstract class RefRestrictionBase - { - [JsonConverter(typeof(RefRestrictionTypesConverter))] - public RefRestrictionTypes Type { get; set; } - public RefMatcher Matcher { get; set; } - public List Groups { get; set; } - } + [JsonConverter(typeof(RefRestrictionTypesConverter))] + public RefRestrictionTypes Type { get; set; } + public RefMatcher? Matcher { get; set; } + public List? Groups { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/RefRestrictions/RefRestrictionCreate.cs b/src/Bitbucket.Net/Models/RefRestrictions/RefRestrictionCreate.cs index d594fc6..97a467c 100644 --- a/src/Bitbucket.Net/Models/RefRestrictions/RefRestrictionCreate.cs +++ b/src/Bitbucket.Net/Models/RefRestrictions/RefRestrictionCreate.cs @@ -1,10 +1,7 @@ -using System.Collections.Generic; +namespace Bitbucket.Net.Models.RefRestrictions; -namespace Bitbucket.Net.Models.RefRestrictions +public class RefRestrictionCreate : RefRestrictionBase { - public class RefRestrictionCreate : RefRestrictionBase - { - public List Users { get; set; } - public List AccessKeys { get; set; } - } -} + public List? Users { get; set; } + public List? AccessKeys { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/RefRestrictions/RefRestrictionTypes.cs b/src/Bitbucket.Net/Models/RefRestrictions/RefRestrictionTypes.cs index 841a602..d8eef60 100644 --- a/src/Bitbucket.Net/Models/RefRestrictions/RefRestrictionTypes.cs +++ b/src/Bitbucket.Net/Models/RefRestrictions/RefRestrictionTypes.cs @@ -1,10 +1,9 @@ -namespace Bitbucket.Net.Models.RefRestrictions +namespace Bitbucket.Net.Models.RefRestrictions; + +public enum RefRestrictionTypes { - public enum RefRestrictionTypes - { - AllChanges, - RewritingHistory, - Deletion, - ChangesWithoutPullRequest - } -} + AllChanges, + RewritingHistory, + Deletion, + ChangesWithoutPullRequest, +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/RefSync/FullRef.cs b/src/Bitbucket.Net/Models/RefSync/FullRef.cs index 15f3bc5..82c3c44 100644 --- a/src/Bitbucket.Net/Models/RefSync/FullRef.cs +++ b/src/Bitbucket.Net/Models/RefSync/FullRef.cs @@ -1,10 +1,9 @@ using Bitbucket.Net.Models.Core.Projects; -namespace Bitbucket.Net.Models.RefSync +namespace Bitbucket.Net.Models.RefSync; + +public class FullRef : Ref { - public class FullRef : Ref - { - public string State { get; set; } - public bool Tag { get; set; } - } + public string? State { get; set; } + public bool Tag { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/RefSync/RepositorySynchronizationStatus.cs b/src/Bitbucket.Net/Models/RefSync/RepositorySynchronizationStatus.cs index 8ee4926..a3b1f2e 100644 --- a/src/Bitbucket.Net/Models/RefSync/RepositorySynchronizationStatus.cs +++ b/src/Bitbucket.Net/Models/RefSync/RepositorySynchronizationStatus.cs @@ -1,18 +1,15 @@ -using System; -using System.Collections.Generic; using Bitbucket.Net.Common.Converters; using System.Text.Json.Serialization; -namespace Bitbucket.Net.Models.RefSync +namespace Bitbucket.Net.Models.RefSync; + +public class RepositorySynchronizationStatus { - public class RepositorySynchronizationStatus - { - public bool Available { get; set; } - public bool Enabled { get; set; } - [JsonConverter(typeof(UnixDateTimeOffsetConverter))] - public DateTimeOffset LastSync { get; set; } - public List AheadRefs { get; set; } - public List DivergedRefs { get; set; } - public List OrphanedRefs { get; set; } - } -} + public bool Available { get; set; } + public bool Enabled { get; set; } + [JsonConverter(typeof(UnixDateTimeOffsetConverter))] + public DateTimeOffset LastSync { get; set; } + public List? AheadRefs { get; set; } + public List? DivergedRefs { get; set; } + public List? OrphanedRefs { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/RefSync/Synchronize.cs b/src/Bitbucket.Net/Models/RefSync/Synchronize.cs index 32e04cf..180930f 100644 --- a/src/Bitbucket.Net/Models/RefSync/Synchronize.cs +++ b/src/Bitbucket.Net/Models/RefSync/Synchronize.cs @@ -1,13 +1,12 @@ using Bitbucket.Net.Common.Converters; using System.Text.Json.Serialization; -namespace Bitbucket.Net.Models.RefSync +namespace Bitbucket.Net.Models.RefSync; + +public class Synchronize { - public class Synchronize - { - public string RefId { get; set; } - [JsonConverter(typeof(SynchronizeActionsConverter))] - public SynchronizeActions Action { get; set; } - public SynchronizeContext Context { get; set; } - } -} + public string? RefId { get; set; } + [JsonConverter(typeof(SynchronizeActionsConverter))] + public SynchronizeActions Action { get; set; } + public SynchronizeContext? Context { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/RefSync/SynchronizeActions.cs b/src/Bitbucket.Net/Models/RefSync/SynchronizeActions.cs index df83f32..9ed4b3f 100644 --- a/src/Bitbucket.Net/Models/RefSync/SynchronizeActions.cs +++ b/src/Bitbucket.Net/Models/RefSync/SynchronizeActions.cs @@ -1,8 +1,7 @@ -namespace Bitbucket.Net.Models.RefSync +namespace Bitbucket.Net.Models.RefSync; + +public enum SynchronizeActions { - public enum SynchronizeActions - { - Merge, - Discard - } -} + Merge, + Discard, +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/RefSync/SynchronizeContext.cs b/src/Bitbucket.Net/Models/RefSync/SynchronizeContext.cs index 3a2fb5f..2086aa4 100644 --- a/src/Bitbucket.Net/Models/RefSync/SynchronizeContext.cs +++ b/src/Bitbucket.Net/Models/RefSync/SynchronizeContext.cs @@ -1,7 +1,6 @@ -namespace Bitbucket.Net.Models.RefSync +namespace Bitbucket.Net.Models.RefSync; + +public class SynchronizeContext { - public class SynchronizeContext - { - public string CommitMessage { get; set; } - } + public string? CommitMessage { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Ssh/Accesskeys.cs b/src/Bitbucket.Net/Models/Ssh/Accesskeys.cs index d032c0e..f557845 100644 --- a/src/Bitbucket.Net/Models/Ssh/Accesskeys.cs +++ b/src/Bitbucket.Net/Models/Ssh/Accesskeys.cs @@ -1,7 +1,6 @@ -namespace Bitbucket.Net.Models.Ssh +namespace Bitbucket.Net.Models.Ssh; + +public class Accesskeys { - public class Accesskeys - { - public bool Enabled { get; set; } - } + public bool Enabled { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Ssh/Fingerprint.cs b/src/Bitbucket.Net/Models/Ssh/Fingerprint.cs index 8736000..b54158e 100644 --- a/src/Bitbucket.Net/Models/Ssh/Fingerprint.cs +++ b/src/Bitbucket.Net/Models/Ssh/Fingerprint.cs @@ -1,8 +1,7 @@ -namespace Bitbucket.Net.Models.Ssh +namespace Bitbucket.Net.Models.Ssh; + +public class Fingerprint { - public class Fingerprint - { - public string Algorithm { get; set; } - public string Value { get; set; } - } + public string? Algorithm { get; set; } + public string? Value { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Ssh/KeyBase.cs b/src/Bitbucket.Net/Models/Ssh/KeyBase.cs index fdbadf7..e477df5 100644 --- a/src/Bitbucket.Net/Models/Ssh/KeyBase.cs +++ b/src/Bitbucket.Net/Models/Ssh/KeyBase.cs @@ -3,12 +3,11 @@ using Bitbucket.Net.Models.RefRestrictions; using System.Text.Json.Serialization; -namespace Bitbucket.Net.Models.Ssh +namespace Bitbucket.Net.Models.Ssh; + +public abstract class KeyBase { - public abstract class KeyBase - { - public Key Key { get; set; } - [JsonConverter(typeof(PermissionsConverter))] - public Permissions Permission { get; set; } - } + public Key? Key { get; set; } + [JsonConverter(typeof(PermissionsConverter))] + public Permissions Permission { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Ssh/ProjectKey.cs b/src/Bitbucket.Net/Models/Ssh/ProjectKey.cs index 06e07a9..0125d4d 100644 --- a/src/Bitbucket.Net/Models/Ssh/ProjectKey.cs +++ b/src/Bitbucket.Net/Models/Ssh/ProjectKey.cs @@ -1,9 +1,8 @@ using Bitbucket.Net.Models.Core.Projects; -namespace Bitbucket.Net.Models.Ssh +namespace Bitbucket.Net.Models.Ssh; + +public class ProjectKey : KeyBase { - public class ProjectKey : KeyBase - { - public Project Project { get; set; } - } -} + public Project? Project { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Ssh/RepositoryKey.cs b/src/Bitbucket.Net/Models/Ssh/RepositoryKey.cs index 4515b21..7d6fb27 100644 --- a/src/Bitbucket.Net/Models/Ssh/RepositoryKey.cs +++ b/src/Bitbucket.Net/Models/Ssh/RepositoryKey.cs @@ -1,9 +1,8 @@ using Bitbucket.Net.Models.Core.Projects; -namespace Bitbucket.Net.Models.Ssh +namespace Bitbucket.Net.Models.Ssh; + +public class RepositoryKey : KeyBase { - public class RepositoryKey : KeyBase - { - public Repository Repository { get; set; } - } + public Repository? Repository { get; set; } } \ No newline at end of file diff --git a/src/Bitbucket.Net/Models/Ssh/SshSettings.cs b/src/Bitbucket.Net/Models/Ssh/SshSettings.cs index 4b30efa..928cc09 100644 --- a/src/Bitbucket.Net/Models/Ssh/SshSettings.cs +++ b/src/Bitbucket.Net/Models/Ssh/SshSettings.cs @@ -1,11 +1,10 @@ -namespace Bitbucket.Net.Models.Ssh +namespace Bitbucket.Net.Models.Ssh; + +public class SshSettings { - public class SshSettings - { - public Accesskeys AccessKeys { get; set; } - public string BaseUrl { get; set; } - public bool Enabled { get; set; } - public Fingerprint Fingerprint { get; set; } - public int Port { get; set; } - } -} + public Accesskeys? AccessKeys { get; set; } + public string? BaseUrl { get; set; } + public bool Enabled { get; set; } + public Fingerprint? Fingerprint { get; set; } + public int Port { get; set; } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/PersonalAccessTokens/BitbucketClient.cs b/src/Bitbucket.Net/PersonalAccessTokens/BitbucketClient.cs index a0d1876..8843715 100644 --- a/src/Bitbucket.Net/PersonalAccessTokens/BitbucketClient.cs +++ b/src/Bitbucket.Net/PersonalAccessTokens/BitbucketClient.cs @@ -1,74 +1,129 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Bitbucket.Net.Common.Models; -using Bitbucket.Net.Models.PersonalAccessTokens; -using Flurl.Http; - -namespace Bitbucket.Net -{ - public partial class BitbucketClient - { - private IFlurlRequest GetPatUrl() => GetBaseUrl("/access-tokens"); - - private IFlurlRequest GetPatUrl(string path) => GetPatUrl() - .AppendPathSegment(path); - - public async Task> GetUserAccessTokensAsync(string userSlug, - int? maxPages = null, - int? limit = null, - int? start = null, - int? avatarSize = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["limit"] = limit, - ["start"] = start, - ["avatarSize"] = avatarSize - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetPatUrl($"/users/{userSlug}") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task CreateAccessTokenAsync(string userSlug, AccessTokenCreate accessToken, CancellationToken cancellationToken = default) - { - var response = await GetPatUrl($"/users/{userSlug}") - .PutJsonAsync(accessToken, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task GetUserAccessTokenAsync(string userSlug, string tokenId, int? avatarSize = null, CancellationToken cancellationToken = default) - { - return await GetPatUrl($"/users/{userSlug}/{tokenId}") - .SetQueryParam("avatarSize", avatarSize) - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - public async Task ChangeUserAccessTokenAsync(string userSlug, string tokenId, AccessTokenCreate accessToken, CancellationToken cancellationToken = default) - { - var response = await GetPatUrl($"/users/{userSlug}/{tokenId}") - .PostJsonAsync(accessToken, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task DeleteUserAccessTokenAsync(string userSlug, string tokenId, CancellationToken cancellationToken = default) - { - var response = await GetPatUrl($"/users/{userSlug}/{tokenId}") - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - } -} +using Bitbucket.Net.Common; +using Bitbucket.Net.Common.Models; +using Bitbucket.Net.Models.PersonalAccessTokens; +using Flurl.Http; + +namespace Bitbucket.Net; + +/// +/// Provides personal access token related Bitbucket API operations. +/// +public partial class BitbucketClient +{ + /// + /// Gets the base personal access token URL. + /// + /// An targeting the PAT root. + private IFlurlRequest GetPatUrl() => GetBaseUrl("/access-tokens"); + + /// + /// Gets the personal access token URL for the specified path. + /// + /// The path to append to the PAT root. + /// An pointing to the PAT path. + private IFlurlRequest GetPatUrl(string path) => GetPatUrl() + .AppendPathSegment(path); + + /// + /// Retrieves access tokens for a user. + /// + /// The user slug. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Optional avatar size for returned users. + /// Token to cancel the operation. + /// A collection of access tokens. + public async Task> GetUserAccessTokensAsync(string userSlug, + int? maxPages = null, + int? limit = null, + int? start = null, + int? avatarSize = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["limit"] = limit, + ["start"] = start, + ["avatarSize"] = avatarSize, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetPatUrl($"/users/{userSlug}") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Creates a personal access token for a user. + /// + /// The user slug. + /// The token creation payload. + /// Token to cancel the operation. + /// The created access token including secret. + public async Task CreateAccessTokenAsync(string userSlug, AccessTokenCreate accessToken, CancellationToken cancellationToken = default) + { + var response = await GetPatUrl($"/users/{userSlug}") + .SendAsync(HttpMethod.Put, CreateJsonContent(accessToken), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves a personal access token by identifier. + /// + /// The user slug. + /// The token identifier. + /// Optional avatar size for returned users. + /// Token to cancel the operation. + /// The access token details. + public async Task GetUserAccessTokenAsync(string userSlug, string tokenId, int? avatarSize = null, CancellationToken cancellationToken = default) + { + var response = await GetPatUrl($"/users/{userSlug}/{tokenId}") + .SetQueryParam("avatarSize", avatarSize) + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Updates a personal access token. + /// + /// The user slug. + /// The token identifier. + /// The updated token payload. + /// Token to cancel the operation. + /// The updated access token details. + public async Task ChangeUserAccessTokenAsync(string userSlug, string tokenId, AccessTokenCreate accessToken, CancellationToken cancellationToken = default) + { + var response = await GetPatUrl($"/users/{userSlug}/{tokenId}") + .SendAsync(HttpMethod.Post, CreateJsonContent(accessToken), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Deletes a personal access token. + /// + /// The user slug. + /// The token identifier. + /// Token to cancel the operation. + /// true if the token was deleted; otherwise, false. + public async Task DeleteUserAccessTokenAsync(string userSlug, string tokenId, CancellationToken cancellationToken = default) + { + var response = await GetPatUrl($"/users/{userSlug}/{tokenId}") + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/RefRestrictions/BitbucketClient.cs b/src/Bitbucket.Net/RefRestrictions/BitbucketClient.cs index aad11a0..9f13adf 100644 --- a/src/Bitbucket.Net/RefRestrictions/BitbucketClient.cs +++ b/src/Bitbucket.Net/RefRestrictions/BitbucketClient.cs @@ -1,157 +1,279 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Bitbucket.Net.Common; -using Bitbucket.Net.Common.Models; -using Bitbucket.Net.Models.RefRestrictions; -using Flurl.Http; - -namespace Bitbucket.Net -{ - public partial class BitbucketClient - { - private IFlurlRequest GetRefRestrictionsUrl() => GetBaseUrl("/branch-permissions", "2.0"); - - private IFlurlRequest GetRefRestrictionsUrl(string path) => GetRefRestrictionsUrl() - .AppendPathSegment(path); - - public async Task> GetProjectRefRestrictionsAsync(string projectKey, - RefRestrictionTypes? type = null, - RefMatcherTypes? matcherType = null, - string? matcherId = null, - int? maxPages = null, - int? limit = null, - int? start = null, - int? avatarSize = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["type"] = BitbucketHelpers.RefRestrictionTypeToString(type), - ["matcherType"] = BitbucketHelpers.RefMatcherTypeToString(matcherType), - ["matcherId"] = matcherId, - ["limit"] = limit, - ["start"] = start, - ["avatarSize"] = avatarSize - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetRefRestrictionsUrl($"/projects/{projectKey}/restrictions") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task> CreateProjectRefRestrictionsAsync(string projectKey, CancellationToken cancellationToken, params RefRestrictionCreate[] refRestrictions) - { - var response = await GetRefRestrictionsUrl($"/projects/{projectKey}/restrictions") - .WithHeader("Accept", "application/vnd.atl.bitbucket.bulk+json") - .PostJsonAsync(refRestrictions, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task> CreateProjectRefRestrictionsAsync(string projectKey, params RefRestrictionCreate[] refRestrictions) - { - return await CreateProjectRefRestrictionsAsync(projectKey, default, refRestrictions).ConfigureAwait(false); - } - - public async Task CreateProjectRefRestrictionAsync(string projectKey, RefRestrictionCreate refRestriction, CancellationToken cancellationToken = default) - { - var response = await GetRefRestrictionsUrl($"/projects/{projectKey}/restrictions") - .PostJsonAsync(refRestriction, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task GetProjectRefRestrictionAsync(string projectKey, int refRestrictionId, int? avatarSize = null, CancellationToken cancellationToken = default) - { - return await GetRefRestrictionsUrl($"/projects/{projectKey}/restrictions/{refRestrictionId}") - .SetQueryParam("avatarSize", avatarSize) - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - public async Task DeleteProjectRefRestrictionAsync(string projectKey, int refRestrictionId, CancellationToken cancellationToken = default) - { - var response = await GetRefRestrictionsUrl($"/projects/{projectKey}/restrictions/{refRestrictionId}") - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - - public async Task> GetRepositoryRefRestrictionsAsync(string projectKey, string repositorySlug, - RefRestrictionTypes? type = null, - RefMatcherTypes? matcherType = null, - string? matcherId = null, - int? maxPages = null, - int? limit = null, - int? start = null, - int? avatarSize = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary - { - ["type"] = BitbucketHelpers.RefRestrictionTypeToString(type), - ["matcherType"] = BitbucketHelpers.RefMatcherTypeToString(matcherType), - ["matcherId"] = matcherId, - ["limit"] = limit, - ["start"] = start, - ["avatarSize"] = avatarSize - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetRefRestrictionsUrl($"/projects/{projectKey}/repos/{repositorySlug}/restrictions") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task> CreateRepositoryRefRestrictionsAsync(string projectKey, string repositorySlug, CancellationToken cancellationToken, params RefRestrictionCreate[] refRestrictions) - { - var response = await GetRefRestrictionsUrl($"/projects/{projectKey}/repos/{repositorySlug}/restrictions") - .WithHeader("Accept", "application/vnd.atl.bitbucket.bulk+json") - .PostJsonAsync(refRestrictions, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync>(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task> CreateRepositoryRefRestrictionsAsync(string projectKey, string repositorySlug, params RefRestrictionCreate[] refRestrictions) - { - return await CreateRepositoryRefRestrictionsAsync(projectKey, repositorySlug, default, refRestrictions).ConfigureAwait(false); - } - - public async Task CreateRepositoryRefRestrictionAsync(string projectKey, string repositorySlug, RefRestrictionCreate refRestriction, CancellationToken cancellationToken = default) - { - var response = await GetRefRestrictionsUrl($"/projects/{projectKey}/repos/{repositorySlug}/restrictions") - .PostJsonAsync(refRestriction, cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task GetRepositoryRefRestrictionAsync(string projectKey, string repositorySlug, int refRestrictionId, - int? avatarSize = null, CancellationToken cancellationToken = default) - { - return await GetRefRestrictionsUrl($"/projects/{projectKey}/repos/{repositorySlug}/restrictions/{refRestrictionId}") - .SetQueryParam("avatarSize", avatarSize) - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } - - public async Task DeleteRepositoryRefRestrictionAsync(string projectKey, string repositorySlug, int refRestrictionId, CancellationToken cancellationToken = default) - { - var response = await GetRefRestrictionsUrl($"/projects/{projectKey}/repos/{repositorySlug}/restrictions/{refRestrictionId}") - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } - } -} +using Bitbucket.Net.Common; +using Bitbucket.Net.Common.Models; +using Bitbucket.Net.Models.RefRestrictions; +using Flurl.Http; + +namespace Bitbucket.Net; + +/// +/// Provides reference restriction (branch permissions) Bitbucket API operations. +/// +public partial class BitbucketClient +{ + /// + /// Gets the base ref restrictions URL. + /// + /// An targeting the branch permissions root. + private IFlurlRequest GetRefRestrictionsUrl() => GetBaseUrl("/branch-permissions", "2.0"); + + /// + /// Gets the ref restrictions URL for the specified path. + /// + /// The path to append to the branch permissions root. + /// An pointing to the requested branch permissions path. + private IFlurlRequest GetRefRestrictionsUrl(string path) => GetRefRestrictionsUrl() + .AppendPathSegment(path); + + /// + /// Retrieves reference restrictions for a project. + /// + /// The project key. + /// Optional restriction type filter. + /// Optional matcher type filter. + /// Optional matcher identifier filter. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Optional avatar size for returned users. + /// Token to cancel the operation. + /// A collection of reference restrictions. + public async Task> GetProjectRefRestrictionsAsync(string projectKey, + RefRestrictionTypes? type = null, + RefMatcherTypes? matcherType = null, + string? matcherId = null, + int? maxPages = null, + int? limit = null, + int? start = null, + int? avatarSize = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["type"] = BitbucketHelpers.RefRestrictionTypeToString(type), + ["matcherType"] = BitbucketHelpers.RefMatcherTypeToString(matcherType), + ["matcherId"] = matcherId, + ["limit"] = limit, + ["start"] = start, + ["avatarSize"] = avatarSize, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetRefRestrictionsUrl($"/projects/{projectKey}/restrictions") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Creates multiple reference restrictions for a project. + /// + /// The project key. + /// Token to cancel the operation. + /// The reference restrictions to create. + /// The created reference restrictions. + public async Task> CreateProjectRefRestrictionsAsync(string projectKey, CancellationToken cancellationToken, params RefRestrictionCreate[] refRestrictions) + { + var response = await GetRefRestrictionsUrl($"/projects/{projectKey}/restrictions") + .WithHeader("Accept", "application/vnd.atl.bitbucket.bulk+json") + .SendAsync(HttpMethod.Post, CreateJsonContent(refRestrictions), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Creates multiple reference restrictions for a project using the default cancellation token. + /// + /// The project key. + /// The reference restrictions to create. + /// The created reference restrictions. + public async Task> CreateProjectRefRestrictionsAsync(string projectKey, params RefRestrictionCreate[] refRestrictions) + { + return await CreateProjectRefRestrictionsAsync(projectKey, default, refRestrictions).ConfigureAwait(false); + } + + /// + /// Creates a single reference restriction for a project. + /// + /// The project key. + /// The reference restriction to create. + /// Token to cancel the operation. + /// The created reference restriction. + public async Task CreateProjectRefRestrictionAsync(string projectKey, RefRestrictionCreate refRestriction, CancellationToken cancellationToken = default) + { + var response = await GetRefRestrictionsUrl($"/projects/{projectKey}/restrictions") + .SendAsync(HttpMethod.Post, CreateJsonContent(refRestriction), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves a specific project reference restriction. + /// + /// The project key. + /// The restriction identifier. + /// Optional avatar size for returned users. + /// Token to cancel the operation. + /// The requested reference restriction. + public async Task GetProjectRefRestrictionAsync(string projectKey, int refRestrictionId, int? avatarSize = null, CancellationToken cancellationToken = default) + { + var response = await GetRefRestrictionsUrl($"/projects/{projectKey}/restrictions/{refRestrictionId}") + .SetQueryParam("avatarSize", avatarSize) + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Deletes a project reference restriction. + /// + /// The project key. + /// The restriction identifier. + /// Token to cancel the operation. + /// true if the restriction was deleted; otherwise, false. + public async Task DeleteProjectRefRestrictionAsync(string projectKey, int refRestrictionId, CancellationToken cancellationToken = default) + { + var response = await GetRefRestrictionsUrl($"/projects/{projectKey}/restrictions/{refRestrictionId}") + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves reference restrictions for a repository. + /// + /// The project key. + /// The repository slug. + /// Optional restriction type filter. + /// Optional matcher type filter. + /// Optional matcher identifier filter. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Optional avatar size for returned users. + /// Token to cancel the operation. + /// A collection of reference restrictions. + public async Task> GetRepositoryRefRestrictionsAsync(string projectKey, string repositorySlug, + RefRestrictionTypes? type = null, + RefMatcherTypes? matcherType = null, + string? matcherId = null, + int? maxPages = null, + int? limit = null, + int? start = null, + int? avatarSize = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) + { + ["type"] = BitbucketHelpers.RefRestrictionTypeToString(type), + ["matcherType"] = BitbucketHelpers.RefMatcherTypeToString(matcherType), + ["matcherId"] = matcherId, + ["limit"] = limit, + ["start"] = start, + ["avatarSize"] = avatarSize, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => + { + var response = await GetRefRestrictionsUrl($"/projects/{projectKey}/repos/{repositorySlug}/restrictions") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Creates multiple reference restrictions for a repository. + /// + /// The project key. + /// The repository slug. + /// Token to cancel the operation. + /// The reference restrictions to create. + /// The created reference restrictions. + public async Task> CreateRepositoryRefRestrictionsAsync(string projectKey, string repositorySlug, CancellationToken cancellationToken, params RefRestrictionCreate[] refRestrictions) + { + var response = await GetRefRestrictionsUrl($"/projects/{projectKey}/repos/{repositorySlug}/restrictions") + .WithHeader("Accept", "application/vnd.atl.bitbucket.bulk+json") + .SendAsync(HttpMethod.Post, CreateJsonContent(refRestrictions), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Creates multiple reference restrictions for a repository using the default cancellation token. + /// + /// The project key. + /// The repository slug. + /// The reference restrictions to create. + /// The created reference restrictions. + public async Task> CreateRepositoryRefRestrictionsAsync(string projectKey, string repositorySlug, params RefRestrictionCreate[] refRestrictions) + { + return await CreateRepositoryRefRestrictionsAsync(projectKey, repositorySlug, default, refRestrictions).ConfigureAwait(false); + } + + /// + /// Creates a single reference restriction for a repository. + /// + /// The project key. + /// The repository slug. + /// The reference restriction to create. + /// Token to cancel the operation. + /// The created reference restriction. + public async Task CreateRepositoryRefRestrictionAsync(string projectKey, string repositorySlug, RefRestrictionCreate refRestriction, CancellationToken cancellationToken = default) + { + var response = await GetRefRestrictionsUrl($"/projects/{projectKey}/repos/{repositorySlug}/restrictions") + .SendAsync(HttpMethod.Post, CreateJsonContent(refRestriction), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves a specific repository reference restriction. + /// + /// The project key. + /// The repository slug. + /// The restriction identifier. + /// Optional avatar size for returned users. + /// Token to cancel the operation. + /// The requested reference restriction. + public async Task GetRepositoryRefRestrictionAsync(string projectKey, string repositorySlug, int refRestrictionId, + int? avatarSize = null, CancellationToken cancellationToken = default) + { + var response = await GetRefRestrictionsUrl($"/projects/{projectKey}/repos/{repositorySlug}/restrictions/{refRestrictionId}") + .SetQueryParam("avatarSize", avatarSize) + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Deletes a repository reference restriction. + /// + /// The project key. + /// The repository slug. + /// The restriction identifier. + /// Token to cancel the operation. + /// true if the restriction was deleted; otherwise, false. + public async Task DeleteRepositoryRefRestrictionAsync(string projectKey, string repositorySlug, int refRestrictionId, CancellationToken cancellationToken = default) + { + var response = await GetRefRestrictionsUrl($"/projects/{projectKey}/repos/{repositorySlug}/restrictions/{refRestrictionId}") + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/src/Bitbucket.Net/RefSync/BitbucketClient.cs b/src/Bitbucket.Net/RefSync/BitbucketClient.cs index 2f385f4..abbb0ab 100644 --- a/src/Bitbucket.Net/RefSync/BitbucketClient.cs +++ b/src/Bitbucket.Net/RefSync/BitbucketClient.cs @@ -1,50 +1,83 @@ -using System.Threading; -using System.Threading.Tasks; using Bitbucket.Net.Common; using Bitbucket.Net.Models.RefSync; using Flurl.Http; -namespace Bitbucket.Net +namespace Bitbucket.Net; + +/// +/// Provides repository synchronization Bitbucket API operations. +/// +public partial class BitbucketClient { - public partial class BitbucketClient - { - private IFlurlRequest GetRefSyncUrl() => GetBaseUrl("/sync"); + /// + /// Gets the base reference synchronization URL. + /// + /// An targeting the sync root. + private IFlurlRequest GetRefSyncUrl() => GetBaseUrl("/sync"); - private IFlurlRequest GetRefSyncUrl(string path) => GetRefSyncUrl() - .AppendPathSegment(path); + /// + /// Gets the reference synchronization URL for the specified path. + /// + /// The path to append to the sync root. + /// An pointing to the sync path. + private IFlurlRequest GetRefSyncUrl(string path) => GetRefSyncUrl() + .AppendPathSegment(path); - public async Task GetRepositorySynchronizationStatusAsync(string projectKey, string repositorySlug, - string? at = null, CancellationToken cancellationToken = default) - { - var response = await GetRefSyncUrl($"/projects/{projectKey}/repos/{repositorySlug}") - .SetQueryParam("at", at) - .GetAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); + /// + /// Retrieves the synchronization status for a repository. + /// + /// The project key. + /// The repository slug. + /// Optional reference to scope the status. + /// Token to cancel the operation. + /// The repository synchronization status. + public async Task GetRepositorySynchronizationStatusAsync(string projectKey, string repositorySlug, + string? at = null, CancellationToken cancellationToken = default) + { + var response = await GetRefSyncUrl($"/projects/{projectKey}/repos/{repositorySlug}") + .SetQueryParam("at", at) + .GetAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } - public async Task EnableRepositorySynchronizationAsync(string projectKey, string repositorySlug, bool enabled, CancellationToken cancellationToken = default) + /// + /// Enables or disables repository synchronization. + /// + /// The project key. + /// The repository slug. + /// Whether synchronization should be enabled. + /// Token to cancel the operation. + /// The updated repository synchronization status. + public async Task EnableRepositorySynchronizationAsync(string projectKey, string repositorySlug, bool enabled, CancellationToken cancellationToken = default) + { + var data = new { - var data = new - { - enabled = BitbucketHelpers.BoolToString(enabled) - }; + enabled = BitbucketHelpers.BoolToString(enabled), + }; - var response = await GetRefSyncUrl($"/projects/{projectKey}/repos/{repositorySlug}") - .PostJsonAsync(data, cancellationToken: cancellationToken) - .ConfigureAwait(false); + var response = await GetRefSyncUrl($"/projects/{projectKey}/repos/{repositorySlug}") + .SendAsync(HttpMethod.Post, CreateJsonContent(data), cancellationToken: cancellationToken) + .ConfigureAwait(false); - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } - public async Task SynchronizeRepositoryAsync(string projectKey, string repositorySlug, Synchronize synchronize, CancellationToken cancellationToken = default) - { - var response = await GetRefSyncUrl($"/projects/{projectKey}/repos/{repositorySlug}") - .PostJsonAsync(synchronize, cancellationToken: cancellationToken) - .ConfigureAwait(false); + /// + /// Triggers synchronization for a repository. + /// + /// The project key. + /// The repository slug. + /// The synchronization payload. + /// Token to cancel the operation. + /// The result of the synchronization. + public async Task SynchronizeRepositoryAsync(string projectKey, string repositorySlug, Synchronize synchronize, CancellationToken cancellationToken = default) + { + var response = await GetRefSyncUrl($"/projects/{projectKey}/repos/{repositorySlug}") + .SendAsync(HttpMethod.Post, CreateJsonContent(synchronize), cancellationToken: cancellationToken) + .ConfigureAwait(false); - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); } -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Serialization/BitbucketJsonContext.cs b/src/Bitbucket.Net/Serialization/BitbucketJsonContext.cs index 27d3444..07b49db 100644 --- a/src/Bitbucket.Net/Serialization/BitbucketJsonContext.cs +++ b/src/Bitbucket.Net/Serialization/BitbucketJsonContext.cs @@ -1,52 +1,34 @@ -using System.Collections.Generic; -using System.Text.Json.Serialization; - using Bitbucket.Net.Common.Models; - // Audit using Bitbucket.Net.Models.Audit; - // Branches using Bitbucket.Net.Models.Branches; - // Builds using Bitbucket.Net.Models.Builds; - // Core - Admin using Bitbucket.Net.Models.Core.Admin; - // Core - Logs -using Bitbucket.Net.Models.Core.Logs; - // Core - Projects using Bitbucket.Net.Models.Core.Projects; - // Core - Tasks using Bitbucket.Net.Models.Core.Tasks; - // Core - Users using Bitbucket.Net.Models.Core.Users; - // DefaultReviewers using Bitbucket.Net.Models.DefaultReviewers; - // Git using Bitbucket.Net.Models.Git; - // Jira using Bitbucket.Net.Models.Jira; - // PersonalAccessTokens using Bitbucket.Net.Models.PersonalAccessTokens; - // RefRestrictions using Bitbucket.Net.Models.RefRestrictions; - // RefSync using Bitbucket.Net.Models.RefSync; - // Ssh using Bitbucket.Net.Models.Ssh; +using System.Text.Json.Serialization; namespace Bitbucket.Net.Serialization; @@ -148,7 +130,7 @@ namespace Bitbucket.Net.Serialization; [JsonSerializable(typeof(MergeStrategy))] [JsonSerializable(typeof(Node))] [JsonSerializable(typeof(PasswordBasic))] -[JsonSerializable(typeof(Bitbucket.Net.Models.Core.Admin.PasswordChange))] +[JsonSerializable(typeof(Bitbucket.Net.Models.Core.Admin.PasswordChange), TypeInfoPropertyName = "AdminPasswordChange")] [JsonSerializable(typeof(UserGroups))] [JsonSerializable(typeof(UserInfo))] [JsonSerializable(typeof(UserPermission))] @@ -198,7 +180,7 @@ namespace Bitbucket.Net.Serialization; [JsonSerializable(typeof(MergeCommits))] [JsonSerializable(typeof(MergeHookRequiredApprovers))] [JsonSerializable(typeof(Participant))] -[JsonSerializable(typeof(Path))] +[JsonSerializable(typeof(Bitbucket.Net.Models.Core.Projects.Path))] [JsonSerializable(typeof(Permittedoperations))] [JsonSerializable(typeof(Project))] [JsonSerializable(typeof(ProjectDefinition))] @@ -223,7 +205,7 @@ namespace Bitbucket.Net.Serialization; [JsonSerializable(typeof(Tag))] [JsonSerializable(typeof(TimeWindow))] [JsonSerializable(typeof(VersionInfo))] -[JsonSerializable(typeof(Bitbucket.Net.Models.Core.Projects.Veto))] +[JsonSerializable(typeof(Bitbucket.Net.Models.Core.Projects.Veto), TypeInfoPropertyName = "ProjectVeto")] [JsonSerializable(typeof(WebHook))] [JsonSerializable(typeof(WebHookInvocation))] [JsonSerializable(typeof(WebHookRequest))] @@ -251,7 +233,7 @@ namespace Bitbucket.Net.Serialization; // ============================================================================ [JsonSerializable(typeof(Identity))] [JsonSerializable(typeof(Named))] -[JsonSerializable(typeof(Bitbucket.Net.Models.Core.Users.PasswordChange))] +[JsonSerializable(typeof(Bitbucket.Net.Models.Core.Users.PasswordChange), TypeInfoPropertyName = "UserPasswordChange")] [JsonSerializable(typeof(User))] // ============================================================================ @@ -265,7 +247,7 @@ namespace Bitbucket.Net.Serialization; // Git Models // ============================================================================ [JsonSerializable(typeof(RebasePullRequestCondition))] -[JsonSerializable(typeof(Bitbucket.Net.Models.Git.Veto))] +[JsonSerializable(typeof(Bitbucket.Net.Models.Git.Veto), TypeInfoPropertyName = "GitVeto")] // ============================================================================ // Jira Models @@ -320,6 +302,8 @@ namespace Bitbucket.Net.Serialization; [JsonSerializable(typeof(List))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(List))] +[JsonSerializable(typeof(List), TypeInfoPropertyName = "ProjectVetoList")] +[JsonSerializable(typeof(List), TypeInfoPropertyName = "GitVetoList")] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(List))] @@ -327,19 +311,6 @@ namespace Bitbucket.Net.Serialization; [JsonSerializable(typeof(List))] [JsonSerializable(typeof(Dictionary))] -/// -/// Provides the source-generated JSON serialization context for Bitbucket.Net. -/// This context enables AOT compilation, trimming support, and improved serialization performance. -/// -/// -/// Consumers can use this context directly for advanced scenarios: -/// -/// var options = new JsonSerializerOptions -/// { -/// TypeInfoResolver = BitbucketJsonContext.Default -/// }; -/// -/// public partial class BitbucketJsonContext : JsonSerializerContext { -} +} \ No newline at end of file diff --git a/src/Bitbucket.Net/Ssh/BitbucketClient.cs b/src/Bitbucket.Net/Ssh/BitbucketClient.cs index d51b9a1..fa1605f 100644 --- a/src/Bitbucket.Net/Ssh/BitbucketClient.cs +++ b/src/Bitbucket.Net/Ssh/BitbucketClient.cs @@ -1,275 +1,465 @@ -using System.Collections.Generic; -using System.Net.Http; -using System.Text; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; using Bitbucket.Net.Common; using Bitbucket.Net.Common.Models; using Bitbucket.Net.Models.Core.Admin; using Bitbucket.Net.Models.RefRestrictions; using Bitbucket.Net.Models.Ssh; using Flurl.Http; +using System.Text; +using System.Text.Json; + +namespace Bitbucket.Net; -namespace Bitbucket.Net +/// +/// Provides SSH key management Bitbucket API operations. +/// +public partial class BitbucketClient { - public partial class BitbucketClient + /// + /// Gets the base SSH keys URL. + /// + /// An targeting the keys root. + private IFlurlRequest GetKeysUrl() => GetBaseUrl("/keys"); + + /// + /// Gets the SSH keys URL for the specified path. + /// + /// The path to append to the keys root. + /// An pointing to the keys path. + private IFlurlRequest GetKeysUrl(string path) => GetKeysUrl() + .AppendPathSegment(path); + + /// + /// Gets the base SSH URL. + /// + /// An targeting the SSH root. + private IFlurlRequest GetSshUrl() => GetBaseUrl("/ssh"); + + /// + /// Gets the SSH URL for the specified path. + /// + /// The path to append to the SSH root. + /// An pointing to the SSH path. + private IFlurlRequest GetSshUrl(string path) => GetSshUrl() + .AppendPathSegment(path); + + /// + /// Deletes an SSH key from multiple projects or repositories. + /// + /// The SSH key identifier. + /// Token to cancel the operation. + /// Project or repository identifiers. + /// true if deletion succeeded; otherwise, false. + public async Task DeleteProjectsReposKeysAsync(int keyId, CancellationToken cancellationToken, params string[] projectsOrRepos) { - private IFlurlRequest GetKeysUrl() => GetBaseUrl("/keys"); + var json = JsonSerializer.Serialize(projectsOrRepos); + var response = await GetKeysUrl($"/ssh/{keyId}") + .WithHeader("Content-Type", "application/json") + .SendAsync(HttpMethod.Delete, new StringContent(json, Encoding.UTF8, "application/json"), cancellationToken: cancellationToken) + .ConfigureAwait(false); - private IFlurlRequest GetKeysUrl(string path) => GetKeysUrl() - .AppendPathSegment(path); - - private IFlurlRequest GetSshUrl() => GetBaseUrl("/ssh"); + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } - private IFlurlRequest GetSshUrl(string path) => GetSshUrl() - .AppendPathSegment(path); + /// + /// Deletes an SSH key from multiple projects or repositories using the default cancellation token. + /// + /// The SSH key identifier. + /// Project or repository identifiers. + /// true if deletion succeeded; otherwise, false. + public async Task DeleteProjectsReposKeysAsync(int keyId, params string[] projectsOrRepos) + { + return await DeleteProjectsReposKeysAsync(keyId, default, projectsOrRepos).ConfigureAwait(false); + } - public async Task DeleteProjectsReposKeysAsync(int keyId, CancellationToken cancellationToken, params string[] projectsOrRepos) + /// + /// Retrieves project keys associated with an SSH key identifier. + /// + /// The SSH key identifier. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Token to cancel the operation. + /// A collection of project keys. + public async Task> GetProjectKeysAsync(int keyId, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) { - var json = JsonSerializer.Serialize(projectsOrRepos); - var response = await GetKeysUrl($"/ssh/{keyId}") - .WithHeader("Content-Type", "application/json") - .SendAsync(HttpMethod.Delete, new StringContent(json, Encoding.UTF8, "application/json"), cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } + ["limit"] = limit, + ["start"] = start, + }; - public async Task DeleteProjectsReposKeysAsync(int keyId, params string[] projectsOrRepos) - { - return await DeleteProjectsReposKeysAsync(keyId, default, projectsOrRepos).ConfigureAwait(false); - } - - public async Task> GetProjectKeysAsync(int keyId, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) - { - var queryParamValues = new Dictionary + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => { - ["limit"] = limit, - ["start"] = start - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetKeysUrl($"/ssh/{keyId}/projects") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task> GetProjectKeysAsync(string projectKey, - string? filter = null, - Permissions? permission = null, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) + var response = await GetKeysUrl($"/ssh/{keyId}/projects") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Retrieves project SSH keys within a project. + /// + /// The project key. + /// Optional filter for search. + /// Optional permission filter. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Token to cancel the operation. + /// A collection of project keys. + public async Task> GetProjectKeysAsync(string projectKey, + string? filter = null, + Permissions? permission = null, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) { - var queryParamValues = new Dictionary + ["limit"] = limit, + ["start"] = start, + ["filter"] = filter, + ["permission"] = BitbucketHelpers.PermissionToString(permission), + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => { - ["limit"] = limit, - ["start"] = start, - ["filter"] = filter, - ["permission"] = BitbucketHelpers.PermissionToString(permission) - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetKeysUrl($"/projects/{projectKey}/ssh") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task CreateProjectKeyAsync(string projectKey, string keyText, Permissions permission, CancellationToken cancellationToken = default) + var response = await GetKeysUrl($"/projects/{projectKey}/ssh") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Creates an SSH key for a project. + /// + /// The project key. + /// The public key text. + /// The permission to grant. + /// Token to cancel the operation. + /// The created project key. + public async Task CreateProjectKeyAsync(string projectKey, string keyText, Permissions permission, CancellationToken cancellationToken = default) + { + var data = new { - var data = new - { - key = new { text = keyText }, - permission = BitbucketHelpers.PermissionToString(permission) - }; + key = new { text = keyText }, + permission = BitbucketHelpers.PermissionToString(permission), + }; - var response = await GetKeysUrl($"/projects/{projectKey}/ssh") - .PostJsonAsync(data, cancellationToken: cancellationToken) - .ConfigureAwait(false); + var response = await GetKeysUrl($"/projects/{projectKey}/ssh") + .SendAsync(HttpMethod.Post, CreateJsonContent(data), cancellationToken: cancellationToken) + .ConfigureAwait(false); - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } - public async Task GetProjectKeyAsync(string projectKey, int keyId, CancellationToken cancellationToken = default) - { - var response = await GetKeysUrl($"/projects/{projectKey}/ssh/{keyId}") - .GetAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); + /// + /// Retrieves a specific project SSH key. + /// + /// The project key. + /// The key identifier. + /// Token to cancel the operation. + /// The requested project key. + public async Task GetProjectKeyAsync(string projectKey, int keyId, CancellationToken cancellationToken = default) + { + var response = await GetKeysUrl($"/projects/{projectKey}/ssh/{keyId}") + .GetAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } - public async Task DeleteProjectKeyAsync(string projectKey, int keyId, CancellationToken cancellationToken = default) - { - var response = await GetKeysUrl($"/projects/{projectKey}/ssh/{keyId}") - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); + /// + /// Deletes a project SSH key. + /// + /// The project key. + /// The key identifier. + /// Token to cancel the operation. + /// true if the key was deleted; otherwise, false. + public async Task DeleteProjectKeyAsync(string projectKey, int keyId, CancellationToken cancellationToken = default) + { + var response = await GetKeysUrl($"/projects/{projectKey}/ssh/{keyId}") + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } - public async Task UpdateProjectKeyPermissionAsync(string projectKey, int keyId, Permissions permission, CancellationToken cancellationToken = default) - { - var response = await GetKeysUrl($"/projects/{projectKey}/ssh/{keyId}/permissions/{BitbucketHelpers.PermissionToString(permission)}") - .PutAsync(new StringContent(""), cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task> GetRepoKeysAsync(int keyId, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) + /// + /// Updates the permission of a project SSH key. + /// + /// The project key. + /// The key identifier. + /// The permission to apply. + /// Token to cancel the operation. + /// The updated project key. + public async Task UpdateProjectKeyPermissionAsync(string projectKey, int keyId, Permissions permission, CancellationToken cancellationToken = default) + { + var response = await GetKeysUrl($"/projects/{projectKey}/ssh/{keyId}/permissions/{BitbucketHelpers.PermissionToString(permission)}") + .PutAsync(new StringContent(""), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves repository keys associated with an SSH key identifier. + /// + /// The SSH key identifier. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Token to cancel the operation. + /// A collection of repository keys. + public async Task> GetRepoKeysAsync(int keyId, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) { - var queryParamValues = new Dictionary + ["limit"] = limit, + ["start"] = start, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => { - ["limit"] = limit, - ["start"] = start - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetKeysUrl($"/ssh/{keyId}/repos") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task> GetRepoKeysAsync(string projectKey, string repositorySlug, - string? filter = null, - bool? effective = null, - Permissions? permission = null, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) + var response = await GetKeysUrl($"/ssh/{keyId}/repos") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Retrieves repository SSH keys within a repository. + /// + /// The project key. + /// The repository slug. + /// Optional filter for search. + /// Whether to include effective permissions. + /// Optional permission filter. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Token to cancel the operation. + /// A collection of repository keys. + public async Task> GetRepoKeysAsync(string projectKey, string repositorySlug, + string? filter = null, + bool? effective = null, + Permissions? permission = null, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) { - var queryParamValues = new Dictionary + ["limit"] = limit, + ["start"] = start, + ["filter"] = filter, + ["effective"] = BitbucketHelpers.BoolToString(effective), + ["permission"] = BitbucketHelpers.PermissionToString(permission), + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => { - ["limit"] = limit, - ["start"] = start, - ["filter"] = filter, - ["effective"] = BitbucketHelpers.BoolToString(effective), - ["permission"] = BitbucketHelpers.PermissionToString(permission) - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetKeysUrl($"/projects/{projectKey}/repos/{repositorySlug}/ssh") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task CreateRepoKeyAsync(string projectKey, string repositorySlug, string keyText, Permissions permission, CancellationToken cancellationToken = default) + var response = await GetKeysUrl($"/projects/{projectKey}/repos/{repositorySlug}/ssh") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Creates an SSH key for a repository. + /// + /// The project key. + /// The repository slug. + /// The public key text. + /// The permission to grant. + /// Token to cancel the operation. + /// The created repository key. + public async Task CreateRepoKeyAsync(string projectKey, string repositorySlug, string keyText, Permissions permission, CancellationToken cancellationToken = default) + { + var data = new { - var data = new - { - key = new { text = keyText }, - permission = BitbucketHelpers.PermissionToString(permission) - }; + key = new { text = keyText }, + permission = BitbucketHelpers.PermissionToString(permission), + }; - var response = await GetKeysUrl($"/projects/{projectKey}/repos/{repositorySlug}/ssh") - .PostJsonAsync(data, cancellationToken: cancellationToken) - .ConfigureAwait(false); + var response = await GetKeysUrl($"/projects/{projectKey}/repos/{repositorySlug}/ssh") + .SendAsync(HttpMethod.Post, CreateJsonContent(data), cancellationToken: cancellationToken) + .ConfigureAwait(false); - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } - public async Task GetRepoKeyAsync(string projectKey, string repositorySlug, int keyId, CancellationToken cancellationToken = default) - { - var response = await GetKeysUrl($"/projects/{projectKey}/repos/{repositorySlug}/ssh/{keyId}") - .GetAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); + /// + /// Retrieves a specific repository SSH key. + /// + /// The project key. + /// The repository slug. + /// The key identifier. + /// Token to cancel the operation. + /// The requested repository key. + public async Task GetRepoKeyAsync(string projectKey, string repositorySlug, int keyId, CancellationToken cancellationToken = default) + { + var response = await GetKeysUrl($"/projects/{projectKey}/repos/{repositorySlug}/ssh/{keyId}") + .GetAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } - public async Task DeleteRepoKeyAsync(string projectKey, string repositorySlug, int keyId, CancellationToken cancellationToken = default) - { - var response = await GetKeysUrl($"/projects/{projectKey}/repos/{repositorySlug}/ssh/{keyId}") - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); + /// + /// Deletes a repository SSH key. + /// + /// The project key. + /// The repository slug. + /// The key identifier. + /// Token to cancel the operation. + /// true if the key was deleted; otherwise, false. + public async Task DeleteRepoKeyAsync(string projectKey, string repositorySlug, int keyId, CancellationToken cancellationToken = default) + { + var response = await GetKeysUrl($"/projects/{projectKey}/repos/{repositorySlug}/ssh/{keyId}") + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } - public async Task UpdateRepoKeyPermissionAsync(string projectKey, string repositorySlug, int keyId, Permissions permission, CancellationToken cancellationToken = default) - { - var response = await GetKeysUrl($"/projects/{projectKey}/repos/{repositorySlug}/ssh/{keyId}/permissions/{BitbucketHelpers.PermissionToString(permission)}") - .PutAsync(new StringContent(""), cancellationToken: cancellationToken) - .ConfigureAwait(false); - - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - public async Task> GetUserKeysAsync(string? userSlug = null, - int? maxPages = null, - int? limit = null, - int? start = null, - CancellationToken cancellationToken = default) + /// + /// Updates the permission of a repository SSH key. + /// + /// The project key. + /// The repository slug. + /// The key identifier. + /// The permission to apply. + /// Token to cancel the operation. + /// The updated repository key. + public async Task UpdateRepoKeyPermissionAsync(string projectKey, string repositorySlug, int keyId, Permissions permission, CancellationToken cancellationToken = default) + { + var response = await GetKeysUrl($"/projects/{projectKey}/repos/{repositorySlug}/ssh/{keyId}/permissions/{BitbucketHelpers.PermissionToString(permission)}") + .PutAsync(new StringContent(""), cancellationToken: cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves SSH keys for a user. + /// + /// Optional user slug. If null, retrieves keys for the current user. + /// Optional maximum number of pages to retrieve. + /// Optional page size. + /// Optional starting index for pagination. + /// Token to cancel the operation. + /// A collection of SSH keys. + public async Task> GetUserKeysAsync(string? userSlug = null, + int? maxPages = null, + int? limit = null, + int? start = null, + CancellationToken cancellationToken = default) + { + var queryParamValues = new Dictionary(StringComparer.Ordinal) { - var queryParamValues = new Dictionary + ["limit"] = limit, + ["start"] = start, + ["user"] = userSlug, + }; + + return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => { - ["limit"] = limit, - ["start"] = start, - ["user"] = userSlug - }; - - return await GetPagedResultsAsync(maxPages, queryParamValues, async (qpv, ct) => - await GetSshUrl("/keys") - .SetQueryParams(qpv) - .GetJsonAsync>(cancellationToken: ct) - .ConfigureAwait(false), cancellationToken) - .ConfigureAwait(false); - } - - public async Task CreateUserKeyAsync(string keyText, string? userSlug = null, CancellationToken cancellationToken = default) - { - var response = await GetSshUrl("/keys") - .SetQueryParam("user", userSlug) - .PostJsonAsync(new { text = keyText }, cancellationToken: cancellationToken) - .ConfigureAwait(false); + var response = await GetSshUrl("/keys") + .SetQueryParams(qpv) + .GetAsync(ct) + .ConfigureAwait(false); + + return await HandleResponseAsync>(response, cancellationToken: ct).ConfigureAwait(false); + }, cancellationToken) + .ConfigureAwait(false); + } - return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); - } + /// + /// Creates an SSH key for a user. + /// + /// The public key text. + /// Optional user slug. If null, applies to the current user. + /// Token to cancel the operation. + /// The created SSH key. + public async Task CreateUserKeyAsync(string keyText, string? userSlug = null, CancellationToken cancellationToken = default) + { + var response = await GetSshUrl("/keys") + .SetQueryParam("user", userSlug) + .SendAsync(HttpMethod.Post, CreateJsonContent(new { text = keyText }), cancellationToken: cancellationToken) + .ConfigureAwait(false); - public async Task DeleteUserKeysAsync(string? userSlug = null, CancellationToken cancellationToken = default) - { - var response = await GetSshUrl("/keys") - .SetQueryParam("user", userSlug) - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); + } - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } + /// + /// Deletes all SSH keys for a user. + /// + /// Optional user slug. If null, deletes keys for the current user. + /// Token to cancel the operation. + /// true if keys were deleted; otherwise, false. + public async Task DeleteUserKeysAsync(string? userSlug = null, CancellationToken cancellationToken = default) + { + var response = await GetSshUrl("/keys") + .SetQueryParam("user", userSlug) + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); - public async Task DeleteUserKeyAsync(int keyId, CancellationToken cancellationToken = default) - { - var response = await GetSshUrl($"/keys/{keyId}") - .DeleteAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } - return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); - } + /// + /// Deletes a specific SSH key for the current user. + /// + /// The key identifier. + /// Token to cancel the operation. + /// true if the key was deleted; otherwise, false. + public async Task DeleteUserKeyAsync(int keyId, CancellationToken cancellationToken = default) + { + var response = await GetSshUrl($"/keys/{keyId}") + .DeleteAsync(cancellationToken: cancellationToken) + .ConfigureAwait(false); - public async Task GetSshSettingsAsync(CancellationToken cancellationToken = default) - { - return await GetSshUrl("/settings") - .GetJsonAsync(cancellationToken: cancellationToken) - .ConfigureAwait(false); - } + return await HandleResponseAsync(response, cancellationToken).ConfigureAwait(false); + } + + /// + /// Retrieves SSH settings. + /// + /// Token to cancel the operation. + /// The SSH settings. + public async Task GetSshSettingsAsync(CancellationToken cancellationToken = default) + { + var response = await GetSshUrl("/settings") + .GetAsync(cancellationToken) + .ConfigureAwait(false); + + return await HandleResponseAsync(response, cancellationToken: cancellationToken).ConfigureAwait(false); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/Bitbucket.Net.Tests.csproj b/test/Bitbucket.Net.Tests/Bitbucket.Net.Tests.csproj index efb6b0a..7b08685 100644 --- a/test/Bitbucket.Net.Tests/Bitbucket.Net.Tests.csproj +++ b/test/Bitbucket.Net.Tests/Bitbucket.Net.Tests.csproj @@ -2,10 +2,8 @@ Library - net10.0 - latest diff --git a/test/Bitbucket.Net.Tests/Common/Mcp/DiffStreamingExtensionsTests.cs b/test/Bitbucket.Net.Tests/Common/Mcp/DiffStreamingExtensionsTests.cs index 08a88c8..8cfc5d2 100644 --- a/test/Bitbucket.Net.Tests/Common/Mcp/DiffStreamingExtensionsTests.cs +++ b/test/Bitbucket.Net.Tests/Common/Mcp/DiffStreamingExtensionsTests.cs @@ -1,283 +1,278 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; using Bitbucket.Net.Common.Mcp; using Bitbucket.Net.Models.Core.Projects; +using System.Runtime.CompilerServices; using Xunit; +using ProjectPath = Bitbucket.Net.Models.Core.Projects.Path; + +namespace Bitbucket.Net.Tests.Common.Mcp; -namespace Bitbucket.Net.Tests.Common.Mcp +public class DiffStreamingExtensionsTests { - public class DiffStreamingExtensionsTests + #region CountDiffLines Tests + + [Fact] + public void CountDiffLines_WithNullHunks_ReturnsZero() { - #region CountDiffLines Tests + // Arrange + var diff = new Diff { Hunks = null }; - [Fact] - public void CountDiffLines_WithNullHunks_ReturnsZero() - { - // Arrange - var diff = new Diff { Hunks = null }; + // Act + var count = DiffStreamingExtensions.CountDiffLines(diff); - // Act - var count = DiffStreamingExtensions.CountDiffLines(diff); + // Assert + Assert.Equal(0, count); + } - // Assert - Assert.Equal(0, count); - } + [Fact] + public void CountDiffLines_WithEmptyHunks_ReturnsZero() + { + // Arrange + var diff = new Diff { Hunks = [] }; - [Fact] - public void CountDiffLines_WithEmptyHunks_ReturnsZero() - { - // Arrange - var diff = new Diff { Hunks = [] }; + // Act + var count = DiffStreamingExtensions.CountDiffLines(diff); - // Act - var count = DiffStreamingExtensions.CountDiffLines(diff); + // Assert + Assert.Equal(0, count); + } - // Assert - Assert.Equal(0, count); - } + [Fact] + public void CountDiffLines_WithMultipleHunksAndSegments_CountsAllLines() + { + // Arrange + var diff = CreateDiff(hunks: 2, segmentsPerHunk: 3, linesPerSegment: 5); - [Fact] - public void CountDiffLines_WithMultipleHunksAndSegments_CountsAllLines() - { - // Arrange - var diff = CreateDiff(hunks: 2, segmentsPerHunk: 3, linesPerSegment: 5); + // Act + var count = DiffStreamingExtensions.CountDiffLines(diff); - // Act - var count = DiffStreamingExtensions.CountDiffLines(diff); + // Assert + Assert.Equal(30, count); // 2 * 3 * 5 = 30 + } - // Assert - Assert.Equal(30, count); // 2 * 3 * 5 = 30 - } + #endregion - #endregion + #region TakeDiffsWithLimitsAsync Tests - #region TakeDiffsWithLimitsAsync Tests + [Fact] + public async Task TakeDiffsWithLimitsAsync_WithNoLimits_ReturnsAllDiffs() + { + // Arrange + var diffs = CreateAsyncDiffs(5, linesPerDiff: 10); + + // Act + var result = await diffs.TakeDiffsWithLimitsAsync(); + + // Assert + Assert.Equal(5, result.Diffs.Count); + Assert.Equal(50, result.TotalLines); + Assert.Equal(5, result.TotalFiles); + Assert.False(result.WasTruncated); + Assert.Null(result.TruncationReason); + } - [Fact] - public async Task TakeDiffsWithLimitsAsync_WithNoLimits_ReturnsAllDiffs() - { - // Arrange - var diffs = CreateAsyncDiffs(5, linesPerDiff: 10); - - // Act - var result = await diffs.TakeDiffsWithLimitsAsync(); - - // Assert - Assert.Equal(5, result.Diffs.Count); - Assert.Equal(50, result.TotalLines); - Assert.Equal(5, result.TotalFiles); - Assert.False(result.WasTruncated); - Assert.Null(result.TruncationReason); - } + [Fact] + public async Task TakeDiffsWithLimitsAsync_WithMaxFiles_TruncatesAtFileLimit() + { + // Arrange + var diffs = CreateAsyncDiffs(10, linesPerDiff: 5); + + // Act + var result = await diffs.TakeDiffsWithLimitsAsync(maxFiles: 3); + + // Assert + Assert.Equal(3, result.Diffs.Count); + Assert.Equal(3, result.TotalFiles); + Assert.True(result.WasTruncated); + Assert.Equal("max_files_reached", result.TruncationReason); + Assert.True(result.HasMore); + } - [Fact] - public async Task TakeDiffsWithLimitsAsync_WithMaxFiles_TruncatesAtFileLimit() - { - // Arrange - var diffs = CreateAsyncDiffs(10, linesPerDiff: 5); - - // Act - var result = await diffs.TakeDiffsWithLimitsAsync(maxFiles: 3); - - // Assert - Assert.Equal(3, result.Diffs.Count); - Assert.Equal(3, result.TotalFiles); - Assert.True(result.WasTruncated); - Assert.Equal("max_files_reached", result.TruncationReason); - Assert.True(result.HasMore); - } + [Fact] + public async Task TakeDiffsWithLimitsAsync_WithMaxLines_TruncatesAtLineLimit() + { + // Arrange + var diffs = CreateAsyncDiffs(5, linesPerDiff: 20); - [Fact] - public async Task TakeDiffsWithLimitsAsync_WithMaxLines_TruncatesAtLineLimit() - { - // Arrange - var diffs = CreateAsyncDiffs(5, linesPerDiff: 20); + // Act + var result = await diffs.TakeDiffsWithLimitsAsync(maxLines: 35); - // Act - var result = await diffs.TakeDiffsWithLimitsAsync(maxLines: 35); + // Assert + Assert.True(result.TotalLines <= 35); + Assert.True(result.WasTruncated); + Assert.Equal("max_lines_reached", result.TruncationReason); + } - // Assert - Assert.True(result.TotalLines <= 35); - Assert.True(result.WasTruncated); - Assert.Equal("max_lines_reached", result.TruncationReason); - } + [Fact] + public async Task TakeDiffsWithLimitsAsync_WithBothLimits_RespectsFirstHit() + { + // Arrange - 10 diffs with 20 lines each = 200 total lines + var diffs = CreateAsyncDiffs(10, linesPerDiff: 20); - [Fact] - public async Task TakeDiffsWithLimitsAsync_WithBothLimits_RespectsFirstHit() - { - // Arrange - 10 diffs with 20 lines each = 200 total lines - var diffs = CreateAsyncDiffs(10, linesPerDiff: 20); + // Act - max 3 files OR max 100 lines (file limit should hit first) + var result = await diffs.TakeDiffsWithLimitsAsync(maxLines: 100, maxFiles: 3); - // Act - max 3 files OR max 100 lines (file limit should hit first) - var result = await diffs.TakeDiffsWithLimitsAsync(maxLines: 100, maxFiles: 3); + // Assert + Assert.Equal(3, result.TotalFiles); + Assert.Equal("max_files_reached", result.TruncationReason); + } - // Assert - Assert.Equal(3, result.TotalFiles); - Assert.Equal("max_files_reached", result.TruncationReason); - } + [Fact] + public async Task TakeDiffsWithLimitsAsync_SupportsDeconstruction() + { + // Arrange + var diffs = CreateAsyncDiffs(3, linesPerDiff: 10); - [Fact] - public async Task TakeDiffsWithLimitsAsync_SupportsDeconstruction() - { - // Arrange - var diffs = CreateAsyncDiffs(3, linesPerDiff: 10); + // Act + var (diffList, hasMore, totalLines, totalFiles) = await diffs.TakeDiffsWithLimitsAsync(maxFiles: 2); - // Act - var (diffList, hasMore, totalLines, totalFiles) = await diffs.TakeDiffsWithLimitsAsync(maxFiles: 2); + // Assert + Assert.Equal(2, diffList.Count); + Assert.True(hasMore); + Assert.Equal(20, totalLines); + Assert.Equal(2, totalFiles); + } - // Assert - Assert.Equal(2, diffList.Count); - Assert.True(hasMore); - Assert.Equal(20, totalLines); - Assert.Equal(2, totalFiles); - } + #endregion - #endregion + #region StreamDiffsWithLimitsAsync Tests - #region StreamDiffsWithLimitsAsync Tests + [Fact] + public async Task StreamDiffsWithLimitsAsync_WithNoLimits_YieldsAllDiffs() + { + // Arrange + var diffs = CreateAsyncDiffs(5, linesPerDiff: 10); - [Fact] - public async Task StreamDiffsWithLimitsAsync_WithNoLimits_YieldsAllDiffs() + // Act + var results = new List(); + await foreach (var result in diffs.StreamDiffsWithLimitsAsync()) { - // Arrange - var diffs = CreateAsyncDiffs(5, linesPerDiff: 10); + results.Add(result); + } - // Act - var results = new List(); - await foreach (var result in diffs.StreamDiffsWithLimitsAsync()) - { - results.Add(result); - } + // Assert + Assert.Equal(5, results.Count); + Assert.All(results, r => Assert.NotNull(r.Diff)); + Assert.All(results, r => Assert.False(r.IsTruncated)); + } - // Assert - Assert.Equal(5, results.Count); - Assert.All(results, r => Assert.NotNull(r.Diff)); - Assert.All(results, r => Assert.False(r.IsTruncated)); - } + [Fact] + public async Task StreamDiffsWithLimitsAsync_WithMaxFiles_YieldsTruncationMarker() + { + // Arrange + var diffs = CreateAsyncDiffs(10, linesPerDiff: 5); - [Fact] - public async Task StreamDiffsWithLimitsAsync_WithMaxFiles_YieldsTruncationMarker() + // Act + var results = new List(); + await foreach (var result in diffs.StreamDiffsWithLimitsAsync(maxFiles: 3)) { - // Arrange - var diffs = CreateAsyncDiffs(10, linesPerDiff: 5); + results.Add(result); + } - // Act - var results = new List(); - await foreach (var result in diffs.StreamDiffsWithLimitsAsync(maxFiles: 3)) - { - results.Add(result); - } + // Assert + Assert.Equal(4, results.Count); // 3 diffs + 1 truncation marker + Assert.True(results.Last().IsTruncated); + Assert.Equal("max_files_reached", results.Last().TruncationReason); + } - // Assert - Assert.Equal(4, results.Count); // 3 diffs + 1 truncation marker - Assert.True(results.Last().IsTruncated); - Assert.Equal("max_files_reached", results.Last().TruncationReason); - } + [Fact] + public async Task StreamDiffsWithLimitsAsync_TracksRunningTotals() + { + // Arrange - 3 diffs with 10 lines each + var diffs = CreateAsyncDiffs(3, linesPerDiff: 10); - [Fact] - public async Task StreamDiffsWithLimitsAsync_TracksRunningTotals() + // Act + var results = new List(); + await foreach (var result in diffs.StreamDiffsWithLimitsAsync()) { - // Arrange - 3 diffs with 10 lines each - var diffs = CreateAsyncDiffs(3, linesPerDiff: 10); + results.Add(result); + } - // Act - var results = new List(); - await foreach (var result in diffs.StreamDiffsWithLimitsAsync()) - { - results.Add(result); - } + // Assert + Assert.Equal(10, results[0].TotalLines); + Assert.Equal(1, results[0].TotalFiles); - // Assert - Assert.Equal(10, results[0].TotalLines); - Assert.Equal(1, results[0].TotalFiles); + Assert.Equal(20, results[1].TotalLines); + Assert.Equal(2, results[1].TotalFiles); - Assert.Equal(20, results[1].TotalLines); - Assert.Equal(2, results[1].TotalFiles); + Assert.Equal(30, results[2].TotalLines); + Assert.Equal(3, results[2].TotalFiles); + } - Assert.Equal(30, results[2].TotalLines); - Assert.Equal(3, results[2].TotalFiles); - } + #endregion - #endregion + #region Cancellation Tests - #region Cancellation Tests + [Fact] + public async Task TakeDiffsWithLimitsAsync_RespectsCancellation() + { + // Arrange + using var cts = new CancellationTokenSource(); + var diffs = CreateCancellableAsyncDiffs(100, linesPerDiff: 10, cts.Token); - [Fact] - public async Task TakeDiffsWithLimitsAsync_RespectsCancellation() + // Cancel after a short delay + _ = Task.Run(async () => { - // Arrange - using var cts = new CancellationTokenSource(); - var diffs = CreateCancellableAsyncDiffs(100, linesPerDiff: 10, cts.Token); - - // Cancel after a short delay - _ = Task.Run(async () => - { - await Task.Delay(10); - cts.Cancel(); - }); + await Task.Delay(10); + cts.Cancel(); + }); - // Act & Assert - TaskCanceledException inherits from OperationCanceledException - await Assert.ThrowsAnyAsync( - () => diffs.TakeDiffsWithLimitsAsync(cancellationToken: cts.Token)); - } + // Act & Assert - TaskCanceledException inherits from OperationCanceledException + await Assert.ThrowsAnyAsync( + () => diffs.TakeDiffsWithLimitsAsync(cancellationToken: cts.Token)); + } - #endregion + #endregion - #region Helper Methods + #region Helper Methods - private static Diff CreateDiff(int hunks = 1, int segmentsPerHunk = 1, int linesPerSegment = 10) + private static Diff CreateDiff(int hunks = 1, int segmentsPerHunk = 1, int linesPerSegment = 10) + { + return new Diff { - return new Diff + Source = new ProjectPath { toString = "source.cs" }, + Destination = new ProjectPath { toString = "dest.cs" }, + Hunks = [.. Enumerable.Range(0, hunks).Select(_ => new DiffHunk { - Source = new Path { toString = "source.cs" }, - Destination = new Path { toString = "dest.cs" }, - Hunks = Enumerable.Range(0, hunks).Select(_ => new DiffHunk + SourceLine = 1, + SourceSpan = linesPerSegment * segmentsPerHunk, + DestinationLine = 1, + DestinationSpan = linesPerSegment * segmentsPerHunk, + Segments = [.. Enumerable.Range(0, segmentsPerHunk).Select(_ => new Segment { - SourceLine = 1, - SourceSpan = linesPerSegment * segmentsPerHunk, - DestinationLine = 1, - DestinationSpan = linesPerSegment * segmentsPerHunk, - Segments = Enumerable.Range(0, segmentsPerHunk).Select(_ => new Segment + Type = "CONTEXT", + Lines = [.. Enumerable.Range(0, linesPerSegment).Select(i => new LineRef { - Type = "CONTEXT", - Lines = Enumerable.Range(0, linesPerSegment).Select(i => new LineRef - { - Source = i + 1, - Destination = i + 1, - Line = $"Line {i + 1}" - }).ToList() - }).ToList() - }).ToList() - }; - } + Source = i + 1, + Destination = i + 1, + Line = $"Line {i + 1}" + })] + })] + })] + }; + } - private static async IAsyncEnumerable CreateAsyncDiffs(int count, int linesPerDiff) + private static async IAsyncEnumerable CreateAsyncDiffs(int count, int linesPerDiff) + { + // Calculate how to distribute lines: single hunk, single segment + for (int i = 0; i < count; i++) { - // Calculate how to distribute lines: single hunk, single segment - for (int i = 0; i < count; i++) - { - await Task.Yield(); - yield return CreateDiff(hunks: 1, segmentsPerHunk: 1, linesPerSegment: linesPerDiff); - } + await Task.Yield(); + yield return CreateDiff(hunks: 1, segmentsPerHunk: 1, linesPerSegment: linesPerDiff); } + } - private static async IAsyncEnumerable CreateCancellableAsyncDiffs( - int count, - int linesPerDiff, - [EnumeratorCancellation] CancellationToken cancellationToken = default) + private static async IAsyncEnumerable CreateCancellableAsyncDiffs( + int count, + int linesPerDiff, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + for (int i = 0; i < count; i++) { - for (int i = 0; i < count; i++) - { - cancellationToken.ThrowIfCancellationRequested(); - await Task.Delay(5, cancellationToken); - yield return CreateDiff(hunks: 1, segmentsPerHunk: 1, linesPerSegment: linesPerDiff); - } + cancellationToken.ThrowIfCancellationRequested(); + await Task.Delay(5, cancellationToken); + yield return CreateDiff(hunks: 1, segmentsPerHunk: 1, linesPerSegment: linesPerDiff); } - - #endregion } -} + + #endregion +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/Common/Mcp/McpExtensionsTests.cs b/test/Bitbucket.Net.Tests/Common/Mcp/McpExtensionsTests.cs index 2e26821..1c1ee97 100644 --- a/test/Bitbucket.Net.Tests/Common/Mcp/McpExtensionsTests.cs +++ b/test/Bitbucket.Net.Tests/Common/Mcp/McpExtensionsTests.cs @@ -1,339 +1,332 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; using Bitbucket.Net.Common.Mcp; +using System.Runtime.CompilerServices; using Xunit; -namespace Bitbucket.Net.Tests.Common.Mcp +namespace Bitbucket.Net.Tests.Common.Mcp; + +public class McpExtensionsTests { - public class McpExtensionsTests + #region TakeWithPaginationAsync Tests + + [Fact] + public async Task TakeWithPaginationAsync_WithFewerItemsThanLimit_ReturnsAllItems() { - #region TakeWithPaginationAsync Tests + // Arrange + var source = CreateAsyncEnumerable(5); - [Fact] - public async Task TakeWithPaginationAsync_WithFewerItemsThanLimit_ReturnsAllItems() - { - // Arrange - var source = CreateAsyncEnumerable(5); + // Act + var result = await source.TakeWithPaginationAsync(10); - // Act - var result = await source.TakeWithPaginationAsync(10); + // Assert + Assert.Equal(5, result.Items.Count); + Assert.False(result.HasMore); + Assert.Null(result.NextOffset); + } - // Assert - Assert.Equal(5, result.Items.Count); - Assert.False(result.HasMore); - Assert.Null(result.NextOffset); - } + [Fact] + public async Task TakeWithPaginationAsync_WithExactlyLimitItems_ReturnsAllItemsNoMore() + { + // Arrange + var source = CreateAsyncEnumerable(10); - [Fact] - public async Task TakeWithPaginationAsync_WithExactlyLimitItems_ReturnsAllItemsNoMore() - { - // Arrange - var source = CreateAsyncEnumerable(10); + // Act + var result = await source.TakeWithPaginationAsync(10); - // Act - var result = await source.TakeWithPaginationAsync(10); + // Assert + Assert.Equal(10, result.Items.Count); + Assert.False(result.HasMore); + Assert.Null(result.NextOffset); + } - // Assert - Assert.Equal(10, result.Items.Count); - Assert.False(result.HasMore); - Assert.Null(result.NextOffset); - } + [Fact] + public async Task TakeWithPaginationAsync_WithMoreItemsThanLimit_ReturnsLimitWithHasMore() + { + // Arrange + var source = CreateAsyncEnumerable(20); - [Fact] - public async Task TakeWithPaginationAsync_WithMoreItemsThanLimit_ReturnsLimitWithHasMore() - { - // Arrange - var source = CreateAsyncEnumerable(20); + // Act + var result = await source.TakeWithPaginationAsync(10); - // Act - var result = await source.TakeWithPaginationAsync(10); + // Assert + Assert.Equal(10, result.Items.Count); + Assert.True(result.HasMore); + Assert.Equal(10, result.NextOffset); + } - // Assert - Assert.Equal(10, result.Items.Count); - Assert.True(result.HasMore); - Assert.Equal(10, result.NextOffset); - } + [Fact] + public async Task TakeWithPaginationAsync_WithEmptySource_ReturnsEmptyResult() + { + // Arrange + var source = CreateAsyncEnumerable(0); - [Fact] - public async Task TakeWithPaginationAsync_WithEmptySource_ReturnsEmptyResult() - { - // Arrange - var source = CreateAsyncEnumerable(0); + // Act + var result = await source.TakeWithPaginationAsync(10); - // Act - var result = await source.TakeWithPaginationAsync(10); + // Assert + Assert.Empty(result.Items); + Assert.False(result.HasMore); + Assert.Null(result.NextOffset); + } - // Assert - Assert.Empty(result.Items); - Assert.False(result.HasMore); - Assert.Null(result.NextOffset); - } + [Fact] + public async Task TakeWithPaginationAsync_SupportsDeconstruction() + { + // Arrange + var source = CreateAsyncEnumerable(15); - [Fact] - public async Task TakeWithPaginationAsync_SupportsDeconstruction() - { - // Arrange - var source = CreateAsyncEnumerable(15); + // Act + var (items, hasMore, nextOffset) = await source.TakeWithPaginationAsync(10); - // Act - var (items, hasMore, nextOffset) = await source.TakeWithPaginationAsync(10); + // Assert + Assert.Equal(10, items.Count); + Assert.True(hasMore); + Assert.Equal(10, nextOffset); + } - // Assert - Assert.Equal(10, items.Count); - Assert.True(hasMore); - Assert.Equal(10, nextOffset); - } + [Fact] + public async Task TakeWithPaginationAsync_RespectsItemOrder() + { + // Arrange + var source = CreateAsyncEnumerable(5); - [Fact] - public async Task TakeWithPaginationAsync_RespectsItemOrder() - { - // Arrange - var source = CreateAsyncEnumerable(5); + // Act + var result = await source.TakeWithPaginationAsync(5); - // Act - var result = await source.TakeWithPaginationAsync(5); + // Assert + Assert.Equal(new[] { 0, 1, 2, 3, 4 }, result.Items); + } - // Assert - Assert.Equal(new[] { 0, 1, 2, 3, 4 }, result.Items); - } + #endregion - #endregion + #region TakeAsync Tests - #region TakeAsync Tests + [Fact] + public async Task TakeAsync_WithFewerItemsThanLimit_ReturnsAllItems() + { + // Arrange + var source = CreateAsyncEnumerable(5); - [Fact] - public async Task TakeAsync_WithFewerItemsThanLimit_ReturnsAllItems() - { - // Arrange - var source = CreateAsyncEnumerable(5); + // Act + var result = await source.TakeAsync(10).ToListAsync(); - // Act - var result = await source.TakeAsync(10).ToListAsync(); + // Assert + Assert.Equal(5, result.Count); + } - // Assert - Assert.Equal(5, result.Count); - } + [Fact] + public async Task TakeAsync_WithMoreItemsThanLimit_ReturnsExactlyLimit() + { + // Arrange + var source = CreateAsyncEnumerable(100); - [Fact] - public async Task TakeAsync_WithMoreItemsThanLimit_ReturnsExactlyLimit() - { - // Arrange - var source = CreateAsyncEnumerable(100); + // Act + var result = await source.TakeAsync(10).ToListAsync(); - // Act - var result = await source.TakeAsync(10).ToListAsync(); + // Assert + Assert.Equal(10, result.Count); + Assert.Equal([.. Enumerable.Range(0, 10)], result); + } - // Assert - Assert.Equal(10, result.Count); - Assert.Equal(Enumerable.Range(0, 10).ToList(), result); - } + [Fact] + public async Task TakeAsync_StopsEnumerationAfterLimit() + { + // Arrange + int itemsGenerated = 0; + var source = CreateCountingAsyncEnumerable(100, () => itemsGenerated++); + + // Act + var result = await source.TakeAsync(10).ToListAsync(); + + // Assert + Assert.Equal(10, result.Count); + // Iterator may request one more item to check if enumeration should continue + // The important thing is we don't enumerate all 100 items + Assert.True(itemsGenerated <= 11, $"Expected at most 11 items generated, but got {itemsGenerated}"); + } - [Fact] - public async Task TakeAsync_StopsEnumerationAfterLimit() - { - // Arrange - int itemsGenerated = 0; - var source = CreateCountingAsyncEnumerable(100, () => itemsGenerated++); - - // Act - var result = await source.TakeAsync(10).ToListAsync(); - - // Assert - Assert.Equal(10, result.Count); - // Iterator may request one more item to check if enumeration should continue - // The important thing is we don't enumerate all 100 items - Assert.True(itemsGenerated <= 11, $"Expected at most 11 items generated, but got {itemsGenerated}"); - } + #endregion - #endregion + #region SkipAsync Tests - #region SkipAsync Tests + [Fact] + public async Task SkipAsync_WithValidOffset_SkipsCorrectItems() + { + // Arrange + var source = CreateAsyncEnumerable(10); - [Fact] - public async Task SkipAsync_WithValidOffset_SkipsCorrectItems() - { - // Arrange - var source = CreateAsyncEnumerable(10); + // Act + var result = await source.SkipAsync(5).ToListAsync(); - // Act - var result = await source.SkipAsync(5).ToListAsync(); + // Assert + Assert.Equal(5, result.Count); + Assert.Equal(new[] { 5, 6, 7, 8, 9 }, result); + } - // Assert - Assert.Equal(5, result.Count); - Assert.Equal(new[] { 5, 6, 7, 8, 9 }, result); - } + [Fact] + public async Task SkipAsync_WithOffsetGreaterThanCount_ReturnsEmpty() + { + // Arrange + var source = CreateAsyncEnumerable(5); - [Fact] - public async Task SkipAsync_WithOffsetGreaterThanCount_ReturnsEmpty() - { - // Arrange - var source = CreateAsyncEnumerable(5); + // Act + var result = await source.SkipAsync(10).ToListAsync(); - // Act - var result = await source.SkipAsync(10).ToListAsync(); + // Assert + Assert.Empty(result); + } - // Assert - Assert.Empty(result); - } + [Fact] + public async Task SkipAsync_WithZeroOffset_ReturnsAllItems() + { + // Arrange + var source = CreateAsyncEnumerable(5); - [Fact] - public async Task SkipAsync_WithZeroOffset_ReturnsAllItems() - { - // Arrange - var source = CreateAsyncEnumerable(5); + // Act + var result = await source.SkipAsync(0).ToListAsync(); - // Act - var result = await source.SkipAsync(0).ToListAsync(); + // Assert + Assert.Equal(5, result.Count); + } - // Assert - Assert.Equal(5, result.Count); - } + #endregion - #endregion + #region PageAsync Tests - #region PageAsync Tests + [Fact] + public async Task PageAsync_ReturnsCorrectPage() + { + // Arrange + var source = CreateAsyncEnumerable(100); - [Fact] - public async Task PageAsync_ReturnsCorrectPage() - { - // Arrange - var source = CreateAsyncEnumerable(100); + // Act - Get page 3 (offset 20, limit 10) + var result = await source.PageAsync(offset: 20, limit: 10); - // Act - Get page 3 (offset 20, limit 10) - var result = await source.PageAsync(offset: 20, limit: 10); + // Assert + Assert.Equal(10, result.Items.Count); + Assert.Equal(Enumerable.Range(20, 10).ToList(), result.Items); + Assert.True(result.HasMore); + Assert.Equal(10, result.NextOffset); // Relative to the page, not absolute + } - // Assert - Assert.Equal(10, result.Items.Count); - Assert.Equal(Enumerable.Range(20, 10).ToList(), result.Items); - Assert.True(result.HasMore); - Assert.Equal(10, result.NextOffset); // Relative to the page, not absolute - } + [Fact] + public async Task PageAsync_LastPage_HasMoreIsFalse() + { + // Arrange + var source = CreateAsyncEnumerable(25); - [Fact] - public async Task PageAsync_LastPage_HasMoreIsFalse() - { - // Arrange - var source = CreateAsyncEnumerable(25); + // Act - Get last page (offset 20, limit 10 but only 5 items left) + var result = await source.PageAsync(offset: 20, limit: 10); - // Act - Get last page (offset 20, limit 10 but only 5 items left) - var result = await source.PageAsync(offset: 20, limit: 10); + // Assert + Assert.Equal(5, result.Items.Count); + Assert.False(result.HasMore); + Assert.Null(result.NextOffset); + } - // Assert - Assert.Equal(5, result.Items.Count); - Assert.False(result.HasMore); - Assert.Null(result.NextOffset); - } + [Fact] + public async Task PageAsync_BeyondData_ReturnsEmpty() + { + // Arrange + var source = CreateAsyncEnumerable(10); - [Fact] - public async Task PageAsync_BeyondData_ReturnsEmpty() - { - // Arrange - var source = CreateAsyncEnumerable(10); + // Act + var result = await source.PageAsync(offset: 20, limit: 10); - // Act - var result = await source.PageAsync(offset: 20, limit: 10); + // Assert + Assert.Empty(result.Items); + Assert.False(result.HasMore); + } - // Assert - Assert.Empty(result.Items); - Assert.False(result.HasMore); - } + #endregion - #endregion + #region Cancellation Tests - #region Cancellation Tests + [Fact] + public async Task TakeWithPaginationAsync_RespecsCancellation() + { + // Arrange - create source that checks cancellation + using var cts = new CancellationTokenSource(); + var source = CreateCancellableAsyncEnumerable(100, cts.Token); - [Fact] - public async Task TakeWithPaginationAsync_RespecsCancellation() + // Cancel after a short delay + _ = Task.Run(async () => { - // Arrange - create source that checks cancellation - using var cts = new CancellationTokenSource(); - var source = CreateCancellableAsyncEnumerable(100, cts); - - // Cancel after a short delay - _ = Task.Run(async () => - { - await Task.Delay(10); - cts.Cancel(); - }); - - // Act & Assert - await Assert.ThrowsAnyAsync( - () => source.TakeWithPaginationAsync(100, cts.Token)); - } + await Task.Delay(10); + await cts.CancelAsync(); + }); + + // Act & Assert + await Assert.ThrowsAnyAsync( + () => source.TakeWithPaginationAsync(100, cts.Token)); + } - [Fact] - public async Task TakeAsync_RespectsCancellation() + [Fact] + public async Task TakeAsync_RespectsCancellation() + { + // Arrange - create source that checks cancellation + using var cts = new CancellationTokenSource(); + var source = CreateCancellableAsyncEnumerable(100, cts.Token); + + // Cancel after a short delay + _ = Task.Run(async () => { - // Arrange - create source that checks cancellation - using var cts = new CancellationTokenSource(); - var source = CreateCancellableAsyncEnumerable(100, cts); - - // Cancel after a short delay - _ = Task.Run(async () => - { - await Task.Delay(10); - cts.Cancel(); - }); - - // Act & Assert - await Assert.ThrowsAnyAsync( - async () => await source.TakeAsync(100, cts.Token).ToListAsync()); - } + await Task.Delay(10); + await cts.CancelAsync(); + }); + + // Act & Assert + await Assert.ThrowsAnyAsync( + async () => await source.TakeAsync(100, cts.Token).ToListAsync()); + } - #endregion + #endregion - #region Helper Methods + #region Helper Methods - private static async IAsyncEnumerable CreateAsyncEnumerable(int count) + private static async IAsyncEnumerable CreateAsyncEnumerable(int count) + { + for (int i = 0; i < count; i++) { - for (int i = 0; i < count; i++) - { - await Task.Yield(); // Simulate async operation - yield return i; - } + await Task.Yield(); // Simulate async operation + yield return i; } + } - private static async IAsyncEnumerable CreateCountingAsyncEnumerable(int count, System.Action onGenerate) + private static async IAsyncEnumerable CreateCountingAsyncEnumerable(int count, System.Action onGenerate) + { + for (int i = 0; i < count; i++) { - for (int i = 0; i < count; i++) - { - onGenerate(); - await Task.Yield(); - yield return i; - } + onGenerate(); + await Task.Yield(); + yield return i; } + } - private static async IAsyncEnumerable CreateCancellableAsyncEnumerable( - int count, - CancellationTokenSource cts, - [EnumeratorCancellation] CancellationToken cancellationToken = default) + private static async IAsyncEnumerable CreateCancellableAsyncEnumerable( + int count, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + for (int i = 0; i < count; i++) { - for (int i = 0; i < count; i++) - { - cancellationToken.ThrowIfCancellationRequested(); - await Task.Delay(5, cancellationToken); // Small delay to allow cancellation to propagate - yield return i; - } + cancellationToken.ThrowIfCancellationRequested(); + await Task.Delay(5, cancellationToken); // Small delay to allow cancellation to propagate + yield return i; } - - #endregion } - // Helper extension for tests - internal static class AsyncEnumerableExtensions + #endregion +} + +// Helper extension for tests +internal static class AsyncEnumerableExtensions +{ + public static async Task> ToListAsync(this IAsyncEnumerable source) { - public static async Task> ToListAsync(this IAsyncEnumerable source) + var list = new List(); + await foreach (var item in source) { - var list = new List(); - await foreach (var item in source) - { - list.Add(item); - } - return list; + list.Add(item); } + return list; } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/Fixtures/Core/branches-page1.json b/test/Bitbucket.Net.Tests/Fixtures/Core/branches-page1.json new file mode 100644 index 0000000..3c159e0 --- /dev/null +++ b/test/Bitbucket.Net.Tests/Fixtures/Core/branches-page1.json @@ -0,0 +1,23 @@ +{ + "size": 2, + "limit": 2, + "isLastPage": false, + "nextPageStart": 2, + "start": 0, + "values": [ + { + "id": "refs/heads/main", + "displayId": "main", + "type": "BRANCH", + "latestCommit": "aaa111bbb222ccc333ddd444eee555fff6667778", + "isDefault": true + }, + { + "id": "refs/heads/develop", + "displayId": "develop", + "type": "BRANCH", + "latestCommit": "bbb222ccc333ddd444eee555fff666777888999aa", + "isDefault": false + } + ] +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/Fixtures/Core/branches-page2.json b/test/Bitbucket.Net.Tests/Fixtures/Core/branches-page2.json new file mode 100644 index 0000000..2ef7b47 --- /dev/null +++ b/test/Bitbucket.Net.Tests/Fixtures/Core/branches-page2.json @@ -0,0 +1,15 @@ +{ + "size": 1, + "limit": 2, + "isLastPage": true, + "start": 2, + "values": [ + { + "id": "refs/heads/feature-x", + "displayId": "feature-x", + "type": "BRANCH", + "latestCommit": "ccc333ddd444eee555fff666777888999aaabbb00", + "isDefault": false + } + ] +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/Fixtures/Core/commits-page1.json b/test/Bitbucket.Net.Tests/Fixtures/Core/commits-page1.json new file mode 100644 index 0000000..54f3ddf --- /dev/null +++ b/test/Bitbucket.Net.Tests/Fixtures/Core/commits-page1.json @@ -0,0 +1,46 @@ +{ + "size": 2, + "limit": 2, + "isLastPage": false, + "nextPageStart": 2, + "start": 0, + "values": [ + { + "id": "aaa111bbb222ccc333ddd444eee555fff6667778", + "displayId": "aaa111b", + "author": { + "name": "dev1", + "emailAddress": "dev1@example.com" + }, + "authorTimestamp": 1706918400000, + "committer": { + "name": "dev1", + "emailAddress": "dev1@example.com" + }, + "committerTimestamp": 1706918400000, + "message": "First commit", + "parents": [] + }, + { + "id": "bbb222ccc333ddd444eee555fff666777888999aa", + "displayId": "bbb222c", + "author": { + "name": "dev2", + "emailAddress": "dev2@example.com" + }, + "authorTimestamp": 1706922000000, + "committer": { + "name": "dev2", + "emailAddress": "dev2@example.com" + }, + "committerTimestamp": 1706922000000, + "message": "Second commit", + "parents": [ + { + "id": "aaa111bbb222ccc333ddd444eee555fff6667778", + "displayId": "aaa111b" + } + ] + } + ] +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/Fixtures/Core/commits-page2.json b/test/Bitbucket.Net.Tests/Fixtures/Core/commits-page2.json new file mode 100644 index 0000000..ac38f0f --- /dev/null +++ b/test/Bitbucket.Net.Tests/Fixtures/Core/commits-page2.json @@ -0,0 +1,29 @@ +{ + "size": 1, + "limit": 2, + "isLastPage": true, + "start": 2, + "values": [ + { + "id": "ccc333ddd444eee555fff666777888999aaabbb00", + "displayId": "ccc333d", + "author": { + "name": "dev3", + "emailAddress": "dev3@example.com" + }, + "authorTimestamp": 1706925600000, + "committer": { + "name": "dev3", + "emailAddress": "dev3@example.com" + }, + "committerTimestamp": 1706925600000, + "message": "Third commit", + "parents": [ + { + "id": "bbb222ccc333ddd444eee555fff666777888999aa", + "displayId": "bbb222c" + } + ] + } + ] +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/Fixtures/Core/diff-empty.json b/test/Bitbucket.Net.Tests/Fixtures/Core/diff-empty.json new file mode 100644 index 0000000..658f9d6 --- /dev/null +++ b/test/Bitbucket.Net.Tests/Fixtures/Core/diff-empty.json @@ -0,0 +1,8 @@ +{ + "diffs": [], + "truncated": false, + "contextLines": "10", + "fromHash": "abc123", + "toHash": "def456", + "whitespace": "IGNORE_ALL" +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/Fixtures/Core/diff-multiple.json b/test/Bitbucket.Net.Tests/Fixtures/Core/diff-multiple.json new file mode 100644 index 0000000..baca701 --- /dev/null +++ b/test/Bitbucket.Net.Tests/Fixtures/Core/diff-multiple.json @@ -0,0 +1,123 @@ +{ + "diffs": [ + { + "source": { + "parent": "src", + "name": "file1.cs", + "toString": "src/file1.cs" + }, + "destination": { + "parent": "src", + "name": "file1.cs", + "toString": "src/file1.cs" + }, + "hunks": [ + { + "sourceLine": 1, + "sourceSpan": 3, + "destinationLine": 1, + "destinationSpan": 4, + "segments": [ + { + "type": "ADDED", + "lines": [ + { + "source": 0, + "destination": 1, + "line": "// New line in file1" + } + ] + } + ] + } + ], + "truncated": false + }, + { + "source": { + "parent": "src", + "name": "file2.cs", + "toString": "src/file2.cs" + }, + "destination": { + "parent": "src", + "name": "file2.cs", + "toString": "src/file2.cs" + }, + "hunks": [ + { + "sourceLine": 10, + "sourceSpan": 5, + "destinationLine": 10, + "destinationSpan": 6, + "segments": [ + { + "type": "REMOVED", + "lines": [ + { + "source": 10, + "destination": 0, + "line": "// Removed line in file2" + } + ] + }, + { + "type": "ADDED", + "lines": [ + { + "source": 0, + "destination": 10, + "line": "// Added line in file2" + }, + { + "source": 0, + "destination": 11, + "line": "// Another added line" + } + ] + } + ] + } + ], + "truncated": false + }, + { + "source": { + "parent": "tests", + "name": "test.cs", + "toString": "tests/test.cs" + }, + "destination": { + "parent": "tests", + "name": "test.cs", + "toString": "tests/test.cs" + }, + "hunks": [ + { + "sourceLine": 1, + "sourceSpan": 1, + "destinationLine": 1, + "destinationSpan": 2, + "segments": [ + { + "type": "CONTEXT", + "lines": [ + { + "source": 1, + "destination": 1, + "line": "using Xunit;" + } + ] + } + ] + } + ], + "truncated": false + } + ], + "truncated": false, + "contextLines": "10", + "fromHash": "abc123", + "toHash": "def456", + "whitespace": "IGNORE_ALL" +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/Fixtures/Core/empty-paged.json b/test/Bitbucket.Net.Tests/Fixtures/Core/empty-paged.json new file mode 100644 index 0000000..379cb09 --- /dev/null +++ b/test/Bitbucket.Net.Tests/Fixtures/Core/empty-paged.json @@ -0,0 +1,7 @@ +{ + "size": 0, + "limit": 25, + "isLastPage": true, + "start": 0, + "values": [] +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/Fixtures/Core/projects-page1.json b/test/Bitbucket.Net.Tests/Fixtures/Core/projects-page1.json new file mode 100644 index 0000000..1cd3c1a --- /dev/null +++ b/test/Bitbucket.Net.Tests/Fixtures/Core/projects-page1.json @@ -0,0 +1,39 @@ +{ + "size": 2, + "limit": 2, + "isLastPage": false, + "nextPageStart": 2, + "start": 0, + "values": [ + { + "key": "PROJ1", + "id": 1, + "name": "Project One", + "description": "First project", + "public": false, + "type": "NORMAL", + "links": { + "self": [ + { + "href": "http://localhost/projects/PROJ1" + } + ] + } + }, + { + "key": "PROJ2", + "id": 2, + "name": "Project Two", + "description": "Second project", + "public": true, + "type": "NORMAL", + "links": { + "self": [ + { + "href": "http://localhost/projects/PROJ2" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/Fixtures/Core/projects-page2.json b/test/Bitbucket.Net.Tests/Fixtures/Core/projects-page2.json new file mode 100644 index 0000000..0ed1c78 --- /dev/null +++ b/test/Bitbucket.Net.Tests/Fixtures/Core/projects-page2.json @@ -0,0 +1,23 @@ +{ + "size": 1, + "limit": 2, + "isLastPage": true, + "start": 2, + "values": [ + { + "key": "PROJ3", + "id": 3, + "name": "Project Three", + "description": "Third project", + "public": false, + "type": "NORMAL", + "links": { + "self": [ + { + "href": "http://localhost/projects/PROJ3" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/Fixtures/Core/repositories-page1.json b/test/Bitbucket.Net.Tests/Fixtures/Core/repositories-page1.json new file mode 100644 index 0000000..f0abf68 --- /dev/null +++ b/test/Bitbucket.Net.Tests/Fixtures/Core/repositories-page1.json @@ -0,0 +1,47 @@ +{ + "size": 2, + "limit": 2, + "isLastPage": false, + "nextPageStart": 2, + "start": 0, + "values": [ + { + "slug": "repo-alpha", + "id": 1, + "name": "Repo Alpha", + "scmId": "git", + "state": "AVAILABLE", + "forkable": true, + "public": false, + "project": { + "key": "TEST" + }, + "links": { + "self": [ + { + "href": "http://localhost/projects/TEST/repos/repo-alpha" + } + ] + } + }, + { + "slug": "repo-beta", + "id": 2, + "name": "Repo Beta", + "scmId": "git", + "state": "AVAILABLE", + "forkable": true, + "public": false, + "project": { + "key": "TEST" + }, + "links": { + "self": [ + { + "href": "http://localhost/projects/TEST/repos/repo-beta" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/Fixtures/Core/repositories-page2.json b/test/Bitbucket.Net.Tests/Fixtures/Core/repositories-page2.json new file mode 100644 index 0000000..5fda2a8 --- /dev/null +++ b/test/Bitbucket.Net.Tests/Fixtures/Core/repositories-page2.json @@ -0,0 +1,27 @@ +{ + "size": 1, + "limit": 2, + "isLastPage": true, + "start": 2, + "values": [ + { + "slug": "repo-gamma", + "id": 3, + "name": "Repo Gamma", + "scmId": "git", + "state": "AVAILABLE", + "forkable": true, + "public": false, + "project": { + "key": "TEST" + }, + "links": { + "self": [ + { + "href": "http://localhost/projects/TEST/repos/repo-gamma" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/Infrastructure/BitbucketMockFixture.cs b/test/Bitbucket.Net.Tests/Infrastructure/BitbucketMockFixture.cs index 2ce568d..b520bdb 100644 --- a/test/Bitbucket.Net.Tests/Infrastructure/BitbucketMockFixture.cs +++ b/test/Bitbucket.Net.Tests/Infrastructure/BitbucketMockFixture.cs @@ -1,4 +1,4 @@ -using System.Threading.Tasks; +using Flurl.Http; using WireMock.Server; using Xunit; @@ -26,8 +26,20 @@ public BitbucketClient CreateClient() return new BitbucketClient(BaseUrl, TestConstants.TestUsername, TestConstants.TestPassword); } + public BitbucketClient CreateClientWithHttpClient() + { + var httpClient = new HttpClient { BaseAddress = new Uri(BaseUrl) }; + return new BitbucketClient(httpClient, BaseUrl, () => "test-token"); + } + + public BitbucketClient CreateClientWithFlurlClient() + { + var flurlClient = new FlurlClient(BaseUrl); + return new BitbucketClient(flurlClient, () => "test-token"); + } + public void Reset() { Server.Reset(); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/Infrastructure/MockSetupExtensions.cs b/test/Bitbucket.Net.Tests/Infrastructure/MockSetupExtensions.cs index 2e88ecc..06cd610 100644 --- a/test/Bitbucket.Net.Tests/Infrastructure/MockSetupExtensions.cs +++ b/test/Bitbucket.Net.Tests/Infrastructure/MockSetupExtensions.cs @@ -1,5 +1,6 @@ -using System.IO; +using Bitbucket.Net.Common.Models; using System.Net; +using System.Text.Json; using WireMock.Matchers; using WireMock.RequestBuilders; using WireMock.ResponseBuilders; @@ -12,6 +13,78 @@ public static class MockSetupExtensions private const string ApiBasePath = "/rest/api/1.0"; private const string FixturesBasePath = "Fixtures"; + public static WireMockServer SetupPagedEndpoint(this WireMockServer server, string path, string fixtureCategory, string page1File, string page2File) + { + server.Given(Request.Create() + .WithPath(path) + .WithParam("start", "0") + .UsingGet()) + .RespondWith(Response.Create() + .WithStatusCode(HttpStatusCode.OK) + .WithHeader("Content-Type", "application/json") + .WithBodyFromFile(GetFixturePath(fixtureCategory, page1File))); + + server.Given(Request.Create() + .WithPath(path) + .WithParam("start", "2") + .UsingGet()) + .RespondWith(Response.Create() + .WithStatusCode(HttpStatusCode.OK) + .WithHeader("Content-Type", "application/json") + .WithBodyFromFile(GetFixturePath(fixtureCategory, page2File))); + + return server; + } + + public static WireMockServer SetupPagedEndpointNoStartParam(this WireMockServer server, string path, string fixtureCategory, string page1File, string page2File) + { + server.Given(Request.Create() + .WithPath(path) + .WithParam("start", false) + .UsingGet()) + .RespondWith(Response.Create() + .WithStatusCode(HttpStatusCode.OK) + .WithHeader("Content-Type", "application/json") + .WithBodyFromFile(GetFixturePath(fixtureCategory, page1File))); + + server.Given(Request.Create() + .WithPath(path) + .WithParam("start", "2") + .UsingGet()) + .RespondWith(Response.Create() + .WithStatusCode(HttpStatusCode.OK) + .WithHeader("Content-Type", "application/json") + .WithBodyFromFile(GetFixturePath(fixtureCategory, page2File))); + + return server; + } + + public static WireMockServer SetupEmptyPagedEndpoint(this WireMockServer server, string path) + { + server.Given(Request.Create() + .WithPath(path) + .UsingGet()) + .RespondWith(Response.Create() + .WithStatusCode(HttpStatusCode.OK) + .WithHeader("Content-Type", "application/json") + .WithBodyFromFile(GetFixturePath("Core", "empty-paged.json"))); + + return server; + } + + public static WireMockServer SetupDiffEndpoint(this WireMockServer server, string path, string fixtureFile) + { + server.Given(Request.Create() + .WithPath(path) + .UsingGet()) + .RespondWith(Response.Create() + .WithStatusCode(HttpStatusCode.OK) + .WithHeader("Content-Type", "application/json") + .WithBodyFromFile(GetFixturePath("Core", fixtureFile))); + + return server; + } + public static WireMockServer SetupGetProjects(this WireMockServer server, int? start = null) { var request = Request.Create() @@ -149,6 +222,72 @@ public static WireMockServer SetupInternalServerError(this WireMockServer server return server; } + public static WireMockServer SetupBadRequest(this WireMockServer server, string path) + { + return server.SetupErrorWithJsonBody(path, HttpStatusCode.BadRequest, + new Error { Message = "Bad request" }); + } + + public static WireMockServer SetupForbidden(this WireMockServer server, string path) + { + return server.SetupErrorWithJsonBody(path, HttpStatusCode.Forbidden, + new Error { Message = "Forbidden" }); + } + + public static WireMockServer SetupConflict(this WireMockServer server, string path) + { + return server.SetupErrorWithJsonBody(path, HttpStatusCode.Conflict, + new Error { Message = "Conflict" }); + } + + public static WireMockServer SetupRateLimited(this WireMockServer server, string path) + { + return server.SetupErrorWithJsonBody(path, HttpStatusCode.TooManyRequests, + new Error { Message = "Rate limit exceeded" }); + } + + public static WireMockServer SetupErrorWithJsonBody(this WireMockServer server, string path, HttpStatusCode statusCode, params Error[] errors) + { + var json = JsonSerializer.Serialize(new { errors }, new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }); + + server.Given(Request.Create() + .WithPath(path) + .UsingGet()) + .RespondWith(Response.Create() + .WithStatusCode(statusCode) + .WithHeader("Content-Type", "application/json") + .WithBody(json)); + + return server; + } + + public static WireMockServer SetupErrorWithHtmlBody(this WireMockServer server, string path, HttpStatusCode statusCode, string htmlContent) + { + server.Given(Request.Create() + .WithPath(path) + .UsingGet()) + .RespondWith(Response.Create() + .WithStatusCode(statusCode) + .WithHeader("Content-Type", "text/html") + .WithBody(htmlContent)); + + return server; + } + + public static WireMockServer SetupErrorWithEmptyBody(this WireMockServer server, string path, HttpStatusCode statusCode) + { + server.Given(Request.Create() + .WithPath(path) + .UsingGet()) + .RespondWith(Response.Create() + .WithStatusCode(statusCode)); + + return server; + } + public static WireMockServer SetupCustomResponse( this WireMockServer server, string path, @@ -2994,7 +3133,7 @@ public static WireMockServer SetupGetProjectHooksAvatar(this WireMockServer serv .RespondWith(Response.Create() .WithStatusCode(HttpStatusCode.OK) .WithHeader("Content-Type", "image/png") - .WithBody(new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A })); + .WithBody([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])); return server; } @@ -3037,7 +3176,7 @@ public static WireMockServer SetupGetProjectRepositoryArchive(this WireMockServe .RespondWith(Response.Create() .WithStatusCode(HttpStatusCode.OK) .WithHeader("Content-Type", "application/zip") - .WithBody(new byte[] { 0x50, 0x4B, 0x03, 0x04, 0x14, 0x00, 0x00, 0x00 })); + .WithBody([0x50, 0x4B, 0x03, 0x04, 0x14, 0x00, 0x00, 0x00])); return server; } @@ -3220,7 +3359,4 @@ private static string GetFixturePath(string category, string fileName) { return Path.Combine(FixturesBasePath, category, fileName); } -} - - - +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/Infrastructure/TestConstants.cs b/test/Bitbucket.Net.Tests/Infrastructure/TestConstants.cs index 75434b5..e37c8c0 100644 --- a/test/Bitbucket.Net.Tests/Infrastructure/TestConstants.cs +++ b/test/Bitbucket.Net.Tests/Infrastructure/TestConstants.cs @@ -24,4 +24,4 @@ public static class TestConstants public const string TestFileName = "hello.txt"; public const string TestWebHookId = "1"; public const long TestCommentId = 1; -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/AdminExtendedMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/AdminExtendedMockTests.cs index 4379e9e..8f21b9b 100644 --- a/test/Bitbucket.Net.Tests/MockTests/AdminExtendedMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/AdminExtendedMockTests.cs @@ -1,374 +1,367 @@ -using System.Threading.Tasks; using Bitbucket.Net.Models.Core.Admin; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests +namespace Bitbucket.Net.Tests.MockTests; + +public class AdminExtendedMockTests(BitbucketMockFixture fixture) : IClassFixture { - public class AdminExtendedMockTests : IClassFixture - { - private readonly BitbucketMockFixture _fixture; + private readonly BitbucketMockFixture _fixture = fixture; - public AdminExtendedMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } + [Fact] + public async Task CreateAdminUserAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupCreateAdminUser(); + var client = _fixture.CreateClient(); - [Fact] - public async Task CreateAdminUserAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupCreateAdminUser(); - var client = _fixture.CreateClient(); + var result = await client.CreateAdminUserAsync( + "newuser", + "password123", + "New User", + "newuser@example.com"); - var result = await client.CreateAdminUserAsync( - "newuser", - "password123", - "New User", - "newuser@example.com"); + Assert.True(result); + } - Assert.True(result); - } + [Fact] + public async Task UpdateAdminUserAsync_ReturnsUserInfo() + { + _fixture.Reset(); + _fixture.Server.SetupUpdateAdminUser(); + var client = _fixture.CreateClient(); - [Fact] - public async Task UpdateAdminUserAsync_ReturnsUserInfo() - { - _fixture.Reset(); - _fixture.Server.SetupUpdateAdminUser(); - var client = _fixture.CreateClient(); + var user = await client.UpdateAdminUserAsync( + name: "updateduser", + displayName: "Updated User", + emailAddress: "updated@example.com"); - var user = await client.UpdateAdminUserAsync( - name: "updateduser", - displayName: "Updated User", - emailAddress: "updated@example.com"); + Assert.NotNull(user); + } - Assert.NotNull(user); - } + [Fact] + public async Task AddAdminGroupUsersAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupAddAdminGroupUsers(); + var client = _fixture.CreateClient(); - [Fact] - public async Task AddAdminGroupUsersAsync_ReturnsTrue() + var groupUsers = new GroupUsers { - _fixture.Reset(); - _fixture.Server.SetupAddAdminGroupUsers(); - var client = _fixture.CreateClient(); + Group = "developers", + Users = ["user1", "user2"] + }; - var groupUsers = new GroupUsers - { - Group = "developers", - Users = ["user1", "user2"] - }; + var result = await client.AddAdminGroupUsersAsync(groupUsers); - var result = await client.AddAdminGroupUsersAsync(groupUsers); + Assert.True(result); + } - Assert.True(result); - } + [Fact] + public async Task AddAdminUserGroupsAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupAddAdminUserGroups(); + var client = _fixture.CreateClient(); - [Fact] - public async Task AddAdminUserGroupsAsync_ReturnsTrue() + var userGroups = new UserGroups { - _fixture.Reset(); - _fixture.Server.SetupAddAdminUserGroups(); - var client = _fixture.CreateClient(); + User = "testuser", + Groups = ["developers", "reviewers"] + }; - var userGroups = new UserGroups - { - User = "testuser", - Groups = ["developers", "reviewers"] - }; + var result = await client.AddAdminUserGroupsAsync(userGroups); - var result = await client.AddAdminUserGroupsAsync(userGroups); + Assert.True(result); + } - Assert.True(result); - } + [Fact] + public async Task RemoveAdminUserFromGroupAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupRemoveAdminUserFromGroup(); + var client = _fixture.CreateClient(); - [Fact] - public async Task RemoveAdminUserFromGroupAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupRemoveAdminUserFromGroup(); - var client = _fixture.CreateClient(); + var result = await client.RemoveAdminUserFromGroupAsync("testuser", "old-group"); - var result = await client.RemoveAdminUserFromGroupAsync("testuser", "old-group"); + Assert.True(result); + } - Assert.True(result); - } + [Fact] + public async Task DeleteAdminUserCaptchaAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupDeleteAdminUserCaptcha(); + var client = _fixture.CreateClient(); - [Fact] - public async Task DeleteAdminUserCaptchaAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupDeleteAdminUserCaptcha(); - var client = _fixture.CreateClient(); + var result = await client.DeleteAdminUserCaptcha("testuser"); - var result = await client.DeleteAdminUserCaptcha("testuser"); + Assert.True(result); + } - Assert.True(result); - } + [Fact] + public async Task UpdateAdminUserCredentialsAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupUpdateAdminUserCredentials(); + var client = _fixture.CreateClient(); - [Fact] - public async Task UpdateAdminUserCredentialsAsync_ReturnsTrue() + var passwordChange = new PasswordChange { - _fixture.Reset(); - _fixture.Server.SetupUpdateAdminUserCredentials(); - var client = _fixture.CreateClient(); + Name = "testuser", + Password = "oldpass", + PasswordConfirm = "newpass" + }; - var passwordChange = new PasswordChange - { - Name = "testuser", - Password = "oldpass", - PasswordConfirm = "newpass" - }; + var result = await client.UpdateAdminUserCredentialsAsync(passwordChange); - var result = await client.UpdateAdminUserCredentialsAsync(passwordChange); + Assert.True(result); + } - Assert.True(result); - } + [Fact] + public async Task GetAdminMailServerAsync_ReturnsConfiguration() + { + _fixture.Reset(); + _fixture.Server.SetupGetAdminMailServer(); + var client = _fixture.CreateClient(); - [Fact] - public async Task GetAdminMailServerAsync_ReturnsConfiguration() - { - _fixture.Reset(); - _fixture.Server.SetupGetAdminMailServer(); - var client = _fixture.CreateClient(); + var config = await client.GetAdminMailServerAsync(); - var config = await client.GetAdminMailServerAsync(); + Assert.NotNull(config); + Assert.Equal("mail.example.com", config.HostName); + Assert.Equal(587, config.Port); + } - Assert.NotNull(config); - Assert.Equal("mail.example.com", config.HostName); - Assert.Equal(587, config.Port); - } + [Fact] + public async Task UpdateAdminMailServerAsync_ReturnsConfiguration() + { + _fixture.Reset(); + _fixture.Server.SetupUpdateAdminMailServer(); + var client = _fixture.CreateClient(); - [Fact] - public async Task UpdateAdminMailServerAsync_ReturnsConfiguration() + var config = new MailServerConfiguration { - _fixture.Reset(); - _fixture.Server.SetupUpdateAdminMailServer(); - var client = _fixture.CreateClient(); + HostName = "newmail.example.com", + Port = 465, + Protocol = "SMTP" + }; - var config = new MailServerConfiguration - { - HostName = "newmail.example.com", - Port = 465, - Protocol = "SMTP" - }; + var result = await client.UpdateAdminMailServerAsync(config); - var result = await client.UpdateAdminMailServerAsync(config); + Assert.NotNull(result); + } - Assert.NotNull(result); - } + [Fact] + public async Task DeleteAdminMailServerAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupDeleteAdminMailServer(); + var client = _fixture.CreateClient(); - [Fact] - public async Task DeleteAdminMailServerAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupDeleteAdminMailServer(); - var client = _fixture.CreateClient(); + var result = await client.DeleteAdminMailServerAsync(); - var result = await client.DeleteAdminMailServerAsync(); + Assert.True(result); + } - Assert.True(result); - } + [Fact] + public async Task GetAdminMailServerSenderAddressAsync_ReturnsAddress() + { + _fixture.Reset(); + _fixture.Server.SetupGetAdminMailServerSenderAddress(); + var client = _fixture.CreateClient(); - [Fact] - public async Task GetAdminMailServerSenderAddressAsync_ReturnsAddress() - { - _fixture.Reset(); - _fixture.Server.SetupGetAdminMailServerSenderAddress(); - var client = _fixture.CreateClient(); + var address = await client.GetAdminMailServerSenderAddressAsync(); - var address = await client.GetAdminMailServerSenderAddressAsync(); + Assert.Equal("bitbucket@example.com", address); + } - Assert.Equal("bitbucket@example.com", address); - } + [Fact] + public async Task UpdateAdminMailServerSenderAddressAsync_ReturnsAddress() + { + _fixture.Reset(); + _fixture.Server.SetupUpdateAdminMailServerSenderAddress(); + var client = _fixture.CreateClient(); - [Fact] - public async Task UpdateAdminMailServerSenderAddressAsync_ReturnsAddress() - { - _fixture.Reset(); - _fixture.Server.SetupUpdateAdminMailServerSenderAddress(); - var client = _fixture.CreateClient(); + var address = await client.UpdateAdminMailServerSenderAddressAsync("new-sender@example.com"); - var address = await client.UpdateAdminMailServerSenderAddressAsync("new-sender@example.com"); + Assert.Equal("new-sender@example.com", address); + } - Assert.Equal("new-sender@example.com", address); - } + [Fact] + public async Task DeleteAdminMailServerSenderAddressAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupDeleteAdminMailServerSenderAddress(); + var client = _fixture.CreateClient(); - [Fact] - public async Task DeleteAdminMailServerSenderAddressAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupDeleteAdminMailServerSenderAddress(); - var client = _fixture.CreateClient(); + var result = await client.DeleteAdminMailServerSenderAddressAsync(); - var result = await client.DeleteAdminMailServerSenderAddressAsync(); + Assert.True(result); + } - Assert.True(result); - } + [Fact] + public async Task UpdateAdminGroupPermissionsAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupUpdateAdminGroupPermissions(); + var client = _fixture.CreateClient(); - [Fact] - public async Task UpdateAdminGroupPermissionsAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupUpdateAdminGroupPermissions(); - var client = _fixture.CreateClient(); + var result = await client.UpdateAdminGroupPermissionsAsync( + Permissions.Admin, + "developers"); - var result = await client.UpdateAdminGroupPermissionsAsync( - Permissions.Admin, - "developers"); + Assert.True(result); + } - Assert.True(result); - } + [Fact] + public async Task DeleteAdminGroupPermissionsAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupDeleteAdminGroupPermissions(); + var client = _fixture.CreateClient(); - [Fact] - public async Task DeleteAdminGroupPermissionsAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupDeleteAdminGroupPermissions(); - var client = _fixture.CreateClient(); + var result = await client.DeleteAdminGroupPermissionsAsync("developers"); - var result = await client.DeleteAdminGroupPermissionsAsync("developers"); + Assert.True(result); + } - Assert.True(result); - } + [Fact] + public async Task UpdateAdminUserPermissionsAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupUpdateAdminUserPermissions(); + var client = _fixture.CreateClient(); - [Fact] - public async Task UpdateAdminUserPermissionsAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupUpdateAdminUserPermissions(); - var client = _fixture.CreateClient(); + var result = await client.UpdateAdminUserPermissionsAsync( + Permissions.Admin, + "testuser"); - var result = await client.UpdateAdminUserPermissionsAsync( - Permissions.Admin, - "testuser"); + Assert.True(result); + } - Assert.True(result); - } + [Fact] + public async Task DeleteAdminUserPermissionsAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupDeleteAdminUserPermissions(); + var client = _fixture.CreateClient(); - [Fact] - public async Task DeleteAdminUserPermissionsAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupDeleteAdminUserPermissions(); - var client = _fixture.CreateClient(); + var result = await client.DeleteAdminUserPermissionsAsync("testuser"); - var result = await client.DeleteAdminUserPermissionsAsync("testuser"); + Assert.True(result); + } - Assert.True(result); - } + [Fact] + public async Task GetAdminPullRequestsMergeStrategiesAsync_ReturnsStrategies() + { + _fixture.Reset(); + _fixture.Server.SetupGetAdminMergeStrategies("git"); + var client = _fixture.CreateClient(); - [Fact] - public async Task GetAdminPullRequestsMergeStrategiesAsync_ReturnsStrategies() - { - _fixture.Reset(); - _fixture.Server.SetupGetAdminMergeStrategies("git"); - var client = _fixture.CreateClient(); + var strategies = await client.GetAdminPullRequestsMergeStrategiesAsync("git"); - var strategies = await client.GetAdminPullRequestsMergeStrategiesAsync("git"); + Assert.NotNull(strategies); + Assert.NotNull(strategies.DefaultStrategy); + } - Assert.NotNull(strategies); - Assert.NotNull(strategies.DefaultStrategy); - } + [Fact] + public async Task UpdateAdminPullRequestsMergeStrategiesAsync_ReturnsStrategies() + { + _fixture.Reset(); + _fixture.Server.SetupUpdateAdminMergeStrategies("git"); + var client = _fixture.CreateClient(); - [Fact] - public async Task UpdateAdminPullRequestsMergeStrategiesAsync_ReturnsStrategies() + var strategies = new MergeStrategies { - _fixture.Reset(); - _fixture.Server.SetupUpdateAdminMergeStrategies("git"); - var client = _fixture.CreateClient(); + DefaultStrategy = new MergeStrategy { Id = "ff", Name = "Fast-forward", Enabled = true } + }; - var strategies = new MergeStrategies - { - DefaultStrategy = new MergeStrategy { Id = "ff", Name = "Fast-forward", Enabled = true } - }; + var result = await client.UpdateAdminPullRequestsMergeStrategiesAsync("git", strategies); - var result = await client.UpdateAdminPullRequestsMergeStrategiesAsync("git", strategies); + Assert.NotNull(result); + } - Assert.NotNull(result); - } + [Fact] + public async Task UpdateAdminLicenseAsync_ReturnsLicenseDetails() + { + _fixture.Reset(); + _fixture.Server.SetupUpdateAdminLicense(); + var client = _fixture.CreateClient(); - [Fact] - public async Task UpdateAdminLicenseAsync_ReturnsLicenseDetails() - { - _fixture.Reset(); - _fixture.Server.SetupUpdateAdminLicense(); - var client = _fixture.CreateClient(); + var licenseInfo = new LicenseInfo { License = "NEW-LICENSE-KEY" }; - var licenseInfo = new LicenseInfo { License = "NEW-LICENSE-KEY" }; + var result = await client.UpdateAdminLicenseAsync(licenseInfo); - var result = await client.UpdateAdminLicenseAsync(licenseInfo); + Assert.NotNull(result); + } - Assert.NotNull(result); - } + [Fact] + public async Task RenameAdminUserAsync_ReturnsUserInfo() + { + _fixture.Reset(); + _fixture.Server.SetupRenameAdminUser(); + var client = _fixture.CreateClient(); - [Fact] - public async Task RenameAdminUserAsync_ReturnsUserInfo() + var userRename = new UserRename { - _fixture.Reset(); - _fixture.Server.SetupRenameAdminUser(); - var client = _fixture.CreateClient(); + Name = "olduser", + NewName = "newuser" + }; - var userRename = new UserRename - { - Name = "olduser", - NewName = "newuser" - }; + var result = await client.RenameAdminUserAsync(userRename); - var result = await client.RenameAdminUserAsync(userRename); - - Assert.NotNull(result); - } + Assert.NotNull(result); + } - [Fact] - public async Task GetAdminGroupMoreMembersAsync_ReturnsUsers() - { - _fixture.Reset(); - _fixture.Server.SetupGetAdminGroupMoreMembers(); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetAdminGroupMoreMembersAsync_ReturnsUsers() + { + _fixture.Reset(); + _fixture.Server.SetupGetAdminGroupMoreMembers(); + var client = _fixture.CreateClient(); - var result = await client.GetAdminGroupMoreMembersAsync("test-group"); + var result = await client.GetAdminGroupMoreMembersAsync("test-group"); - Assert.NotNull(result); - Assert.NotEmpty(result); - } + Assert.NotNull(result); + Assert.NotEmpty(result); + } - [Fact] - public async Task GetAdminGroupMoreNonMembersAsync_ReturnsUsers() - { - _fixture.Reset(); - _fixture.Server.SetupGetAdminGroupMoreNonMembers(); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetAdminGroupMoreNonMembersAsync_ReturnsUsers() + { + _fixture.Reset(); + _fixture.Server.SetupGetAdminGroupMoreNonMembers(); + var client = _fixture.CreateClient(); - var result = await client.GetAdminGroupMoreNonMembersAsync("test-group"); + var result = await client.GetAdminGroupMoreNonMembersAsync("test-group"); - Assert.NotNull(result); - Assert.NotEmpty(result); - } + Assert.NotNull(result); + Assert.NotEmpty(result); + } - [Fact] - public async Task GetAdminUserMoreMembersAsync_ReturnsGroupsOrUsers() - { - _fixture.Reset(); - _fixture.Server.SetupGetAdminUserMoreMembers(); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetAdminUserMoreMembersAsync_ReturnsGroupsOrUsers() + { + _fixture.Reset(); + _fixture.Server.SetupGetAdminUserMoreMembers(); + var client = _fixture.CreateClient(); - var result = await client.GetAdminUserMoreMembersAsync("testuser"); + var result = await client.GetAdminUserMoreMembersAsync("testuser"); - Assert.NotNull(result); - Assert.NotEmpty(result); - } + Assert.NotNull(result); + Assert.NotEmpty(result); + } - [Fact] - public async Task GetAdminUserMoreNonMembersAsync_ReturnsGroupsOrUsers() - { - _fixture.Reset(); - _fixture.Server.SetupGetAdminUserMoreNonMembers(); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetAdminUserMoreNonMembersAsync_ReturnsGroupsOrUsers() + { + _fixture.Reset(); + _fixture.Server.SetupGetAdminUserMoreNonMembers(); + var client = _fixture.CreateClient(); - var result = await client.GetAdminUserMoreNonMembersAsync("testuser"); + var result = await client.GetAdminUserMoreNonMembersAsync("testuser"); - Assert.NotNull(result); - Assert.NotEmpty(result); - } + Assert.NotNull(result); + Assert.NotEmpty(result); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/AdminMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/AdminMockTests.cs index 5b2a155..5d428be 100644 --- a/test/Bitbucket.Net.Tests/MockTests/AdminMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/AdminMockTests.cs @@ -1,160 +1,153 @@ -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests +namespace Bitbucket.Net.Tests.MockTests; + +public class AdminMockTests(BitbucketMockFixture fixture) : IClassFixture { - public class AdminMockTests : IClassFixture + private readonly BitbucketMockFixture _fixture = fixture; + + [Fact] + public async Task GetAdminGroupsAsync_ReturnsGroups() + { + _fixture.Reset(); + _fixture.Server.SetupGetAdminGroups(); + var client = _fixture.CreateClient(); + + var groups = await client.GetAdminGroupsAsync(); + + var groupList = groups.ToList(); + Assert.NotEmpty(groupList); + Assert.Equal(2, groupList.Count); + Assert.Equal("developers", groupList[0].Name); + Assert.True(groupList[0].Deletable); + Assert.Equal("administrators", groupList[1].Name); + Assert.False(groupList[1].Deletable); + } + + [Fact] + public async Task CreateAdminGroupAsync_ReturnsCreatedGroup() + { + _fixture.Reset(); + _fixture.Server.SetupCreateAdminGroup("new-group"); + var client = _fixture.CreateClient(); + + var group = await client.CreateAdminGroupAsync("new-group"); + + Assert.NotNull(group); + Assert.Equal("new-group", group.Name); + Assert.True(group.Deletable); + } + + [Fact] + public async Task DeleteAdminGroupAsync_ReturnsDeletedGroup() + { + _fixture.Reset(); + _fixture.Server.SetupDeleteAdminGroup("old-group"); + var client = _fixture.CreateClient(); + + var group = await client.DeleteAdminGroupAsync("old-group"); + + Assert.NotNull(group); + Assert.Equal("new-group", group.Name); + } + + [Fact] + public async Task GetAdminUsersAsync_ReturnsUsers() + { + _fixture.Reset(); + _fixture.Server.SetupGetAdminUsers(); + var client = _fixture.CreateClient(); + + var users = await client.GetAdminUsersAsync(); + + var userList = users.ToList(); + Assert.NotEmpty(userList); + Assert.Equal(2, userList.Count); + Assert.Equal("admin", userList[0].Name); + Assert.Equal("admin@example.com", userList[0].EmailAddress); + Assert.Equal("jsmith", userList[1].Name); + } + + [Fact] + public async Task DeleteAdminUserAsync_ReturnsDeletedUser() { - private readonly BitbucketMockFixture _fixture; - - public AdminMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public async Task GetAdminGroupsAsync_ReturnsGroups() - { - _fixture.Reset(); - _fixture.Server.SetupGetAdminGroups(); - var client = _fixture.CreateClient(); - - var groups = await client.GetAdminGroupsAsync(); - - var groupList = groups.ToList(); - Assert.NotEmpty(groupList); - Assert.Equal(2, groupList.Count); - Assert.Equal("developers", groupList[0].Name); - Assert.True(groupList[0].Deletable); - Assert.Equal("administrators", groupList[1].Name); - Assert.False(groupList[1].Deletable); - } - - [Fact] - public async Task CreateAdminGroupAsync_ReturnsCreatedGroup() - { - _fixture.Reset(); - _fixture.Server.SetupCreateAdminGroup("new-group"); - var client = _fixture.CreateClient(); - - var group = await client.CreateAdminGroupAsync("new-group"); - - Assert.NotNull(group); - Assert.Equal("new-group", group.Name); - Assert.True(group.Deletable); - } - - [Fact] - public async Task DeleteAdminGroupAsync_ReturnsDeletedGroup() - { - _fixture.Reset(); - _fixture.Server.SetupDeleteAdminGroup("old-group"); - var client = _fixture.CreateClient(); - - var group = await client.DeleteAdminGroupAsync("old-group"); - - Assert.NotNull(group); - Assert.Equal("new-group", group.Name); - } - - [Fact] - public async Task GetAdminUsersAsync_ReturnsUsers() - { - _fixture.Reset(); - _fixture.Server.SetupGetAdminUsers(); - var client = _fixture.CreateClient(); - - var users = await client.GetAdminUsersAsync(); - - var userList = users.ToList(); - Assert.NotEmpty(userList); - Assert.Equal(2, userList.Count); - Assert.Equal("admin", userList[0].Name); - Assert.Equal("admin@example.com", userList[0].EmailAddress); - Assert.Equal("jsmith", userList[1].Name); - } - - [Fact] - public async Task DeleteAdminUserAsync_ReturnsDeletedUser() - { - _fixture.Reset(); - _fixture.Server.SetupDeleteAdminUser("olduser"); - var client = _fixture.CreateClient(); - - var user = await client.DeleteAdminUserAsync("olduser"); - - Assert.NotNull(user); - Assert.Equal("newuser", user.Name); - Assert.Equal("newuser@example.com", user.EmailAddress); - } - - [Fact] - public async Task GetAdminClusterAsync_ReturnsClusterInfo() - { - _fixture.Reset(); - _fixture.Server.SetupGetAdminCluster(); - var client = _fixture.CreateClient(); - - var cluster = await client.GetAdminClusterAsync(); - - Assert.NotNull(cluster); - Assert.True(cluster.Running); - Assert.NotNull(cluster.LocalNode); - Assert.Equal("node-1", cluster.LocalNode.Id); - Assert.Equal("bitbucket-node-1", cluster.LocalNode.Name); - Assert.True(cluster.LocalNode.Local); - Assert.Equal(2, cluster.Nodes.Count); - } - - [Fact] - public async Task GetAdminLicenseAsync_ReturnsLicenseDetails() - { - _fixture.Reset(); - _fixture.Server.SetupGetAdminLicense(); - var client = _fixture.CreateClient(); - - var license = await client.GetAdminLicenseAsync(); - - Assert.NotNull(license); - Assert.Equal("SERV-1234-5678", license.ServerId); - Assert.Equal("SEN-12345", license.SupportEntitlementNumber); - Assert.Equal(500, license.MaximumNumberOfUsers); - Assert.False(license.UnlimitedNumberOfUsers); - Assert.Equal(365, license.NumberOfDaysBeforeExpiry); - } - - [Fact] - public async Task GetAdminGroupPermissionsAsync_ReturnsPermissions() - { - _fixture.Reset(); - _fixture.Server.SetupGetAdminGroupPermissions(); - var client = _fixture.CreateClient(); - - var permissions = await client.GetAdminGroupPermissionsAsync(); - - var permList = permissions.ToList(); - Assert.NotEmpty(permList); - Assert.Equal(2, permList.Count); - Assert.Equal("administrators", permList[0].Group.Name); - Assert.Equal("developers", permList[1].Group.Name); - } - - [Fact] - public async Task GetAdminUserPermissionsAsync_ReturnsPermissions() - { - _fixture.Reset(); - _fixture.Server.SetupGetAdminUserPermissions(); - var client = _fixture.CreateClient(); - - var permissions = await client.GetAdminUserPermissionsAsync(); - - var permList = permissions.ToList(); - Assert.NotEmpty(permList); - Assert.Equal(2, permList.Count); - Assert.Equal("admin", permList[0].User.Name); - Assert.Equal("jsmith", permList[1].User.Name); - } + _fixture.Reset(); + _fixture.Server.SetupDeleteAdminUser("olduser"); + var client = _fixture.CreateClient(); + + var user = await client.DeleteAdminUserAsync("olduser"); + + Assert.NotNull(user); + Assert.Equal("newuser", user.Name); + Assert.Equal("newuser@example.com", user.EmailAddress); + } + + [Fact] + public async Task GetAdminClusterAsync_ReturnsClusterInfo() + { + _fixture.Reset(); + _fixture.Server.SetupGetAdminCluster(); + var client = _fixture.CreateClient(); + + var cluster = await client.GetAdminClusterAsync(); + + Assert.NotNull(cluster); + Assert.True(cluster.Running); + Assert.NotNull(cluster.LocalNode); + Assert.Equal("node-1", cluster.LocalNode!.Id); + Assert.Equal("bitbucket-node-1", cluster.LocalNode!.Name); + Assert.True(cluster.LocalNode!.Local); + Assert.NotNull(cluster.Nodes); + Assert.Equal(2, cluster.Nodes!.Count); + } + + [Fact] + public async Task GetAdminLicenseAsync_ReturnsLicenseDetails() + { + _fixture.Reset(); + _fixture.Server.SetupGetAdminLicense(); + var client = _fixture.CreateClient(); + + var license = await client.GetAdminLicenseAsync(); + + Assert.NotNull(license); + Assert.Equal("SERV-1234-5678", license.ServerId); + Assert.Equal("SEN-12345", license.SupportEntitlementNumber); + Assert.Equal(500, license.MaximumNumberOfUsers); + Assert.False(license.UnlimitedNumberOfUsers); + Assert.Equal(365, license.NumberOfDaysBeforeExpiry); + } + + [Fact] + public async Task GetAdminGroupPermissionsAsync_ReturnsPermissions() + { + _fixture.Reset(); + _fixture.Server.SetupGetAdminGroupPermissions(); + var client = _fixture.CreateClient(); + + var permissions = await client.GetAdminGroupPermissionsAsync(); + + var permList = permissions.ToList(); + Assert.NotEmpty(permList); + Assert.Equal(2, permList.Count); + Assert.Equal("administrators", permList[0].Group!.Name); + Assert.Equal("developers", permList[1].Group!.Name); + } + + [Fact] + public async Task GetAdminUserPermissionsAsync_ReturnsPermissions() + { + _fixture.Reset(); + _fixture.Server.SetupGetAdminUserPermissions(); + var client = _fixture.CreateClient(); + + var permissions = await client.GetAdminUserPermissionsAsync(); + + var permList = permissions.ToList(); + Assert.NotEmpty(permList); + Assert.Equal(2, permList.Count); + Assert.Equal("admin", permList[0].User!.Name); + Assert.Equal("jsmith", permList[1].User!.Name); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/AdminPermissionsMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/AdminPermissionsMockTests.cs index 4968b7c..9d3953f 100644 --- a/test/Bitbucket.Net.Tests/MockTests/AdminPermissionsMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/AdminPermissionsMockTests.cs @@ -1,70 +1,62 @@ -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests -{ - public class AdminPermissionsMockTests : IClassFixture - { - private readonly BitbucketMockFixture _fixture; +namespace Bitbucket.Net.Tests.MockTests; - public AdminPermissionsMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } +public class AdminPermissionsMockTests(BitbucketMockFixture fixture) : IClassFixture +{ + private readonly BitbucketMockFixture _fixture = fixture; - [Fact] - public async Task GetAdminGroupPermissionsNoneAsync_ReturnsGroups() - { - _fixture.Reset(); - _fixture.Server.SetupGetAdminGroupPermissionsNone(); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetAdminGroupPermissionsNoneAsync_ReturnsGroups() + { + _fixture.Reset(); + _fixture.Server.SetupGetAdminGroupPermissionsNone(); + var client = _fixture.CreateClient(); - var groups = await client.GetAdminGroupPermissionsNoneAsync(); + var groups = await client.GetAdminGroupPermissionsNoneAsync(); - Assert.NotNull(groups); - var groupList = groups.ToList(); - Assert.NotEmpty(groupList); - } + Assert.NotNull(groups); + var groupList = groups.ToList(); + Assert.NotEmpty(groupList); + } - [Fact] - public async Task GetAdminUserPermissionsNoneAsync_ReturnsUsers() - { - _fixture.Reset(); - _fixture.Server.SetupGetAdminUserPermissionsNone(); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetAdminUserPermissionsNoneAsync_ReturnsUsers() + { + _fixture.Reset(); + _fixture.Server.SetupGetAdminUserPermissionsNone(); + var client = _fixture.CreateClient(); - var users = await client.GetAdminUserPermissionsNoneAsync(); + var users = await client.GetAdminUserPermissionsNoneAsync(); - Assert.NotNull(users); - var userList = users.ToList(); - Assert.NotEmpty(userList); - } + Assert.NotNull(users); + var userList = users.ToList(); + Assert.NotEmpty(userList); + } - [Fact] - public async Task GetAdminClusterAsync_ReturnsClusterInfo() - { - _fixture.Reset(); - _fixture.Server.SetupGetAdminCluster(); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetAdminClusterAsync_ReturnsClusterInfo() + { + _fixture.Reset(); + _fixture.Server.SetupGetAdminCluster(); + var client = _fixture.CreateClient(); - var cluster = await client.GetAdminClusterAsync(); + var cluster = await client.GetAdminClusterAsync(); - Assert.NotNull(cluster); - Assert.True(cluster.Running); - } + Assert.NotNull(cluster); + Assert.True(cluster.Running); + } - [Fact] - public async Task GetAdminLicenseAsync_ReturnsLicenseDetails() - { - _fixture.Reset(); - _fixture.Server.SetupGetAdminLicense(); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetAdminLicenseAsync_ReturnsLicenseDetails() + { + _fixture.Reset(); + _fixture.Server.SetupGetAdminLicense(); + var client = _fixture.CreateClient(); - var license = await client.GetAdminLicenseAsync(); + var license = await client.GetAdminLicenseAsync(); - Assert.NotNull(license); - } + Assert.NotNull(license); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/ApplicationPropertiesMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/ApplicationPropertiesMockTests.cs index 9794af3..9e93e58 100644 --- a/test/Bitbucket.Net.Tests/MockTests/ApplicationPropertiesMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/ApplicationPropertiesMockTests.cs @@ -1,32 +1,25 @@ -using System.Threading.Tasks; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests -{ - public class ApplicationPropertiesMockTests : IClassFixture - { - private readonly BitbucketMockFixture _fixture; +namespace Bitbucket.Net.Tests.MockTests; - public ApplicationPropertiesMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } +public class ApplicationPropertiesMockTests(BitbucketMockFixture fixture) : IClassFixture +{ + private readonly BitbucketMockFixture _fixture = fixture; - [Fact] - public async Task GetApplicationPropertiesAsync_ReturnsProperties() - { - _fixture.Reset(); - _fixture.Server.SetupGetApplicationProperties(); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetApplicationPropertiesAsync_ReturnsProperties() + { + _fixture.Reset(); + _fixture.Server.SetupGetApplicationProperties(); + var client = _fixture.CreateClient(); - var result = await client.GetApplicationPropertiesAsync(); + var result = await client.GetApplicationPropertiesAsync(); - Assert.NotNull(result); - Assert.True(result.ContainsKey("version")); - Assert.True(result.ContainsKey("buildNumber")); - Assert.True(result.ContainsKey("displayName")); - Assert.Equal("8.14.0", result["version"]?.ToString()); - } + Assert.NotNull(result); + Assert.True(result.ContainsKey("version")); + Assert.True(result.ContainsKey("buildNumber")); + Assert.True(result.ContainsKey("displayName")); + Assert.Equal("8.14.0", result["version"]?.ToString()); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/AuditMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/AuditMockTests.cs index 7466613..dd95269 100644 --- a/test/Bitbucket.Net.Tests/MockTests/AuditMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/AuditMockTests.cs @@ -1,49 +1,41 @@ -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests +namespace Bitbucket.Net.Tests.MockTests; + +public class AuditMockTests(BitbucketMockFixture fixture) : IClassFixture { - public class AuditMockTests : IClassFixture + private readonly BitbucketMockFixture _fixture = fixture; + private const string ProjectKey = "PROJ"; + private const string RepoSlug = "repo"; + + [Fact] + public async Task GetProjectAuditEventsAsync_ReturnsEvents() { - private readonly BitbucketMockFixture _fixture; - private const string ProjectKey = "PROJ"; - private const string RepoSlug = "repo"; - - public AuditMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public async Task GetProjectAuditEventsAsync_ReturnsEvents() - { - _fixture.Reset(); - _fixture.Server.SetupGetProjectAuditEvents(ProjectKey); - var client = _fixture.CreateClient(); - - var result = await client.GetProjectAuditEventsAsync(ProjectKey); - - Assert.NotNull(result); - var events = result.ToList(); - Assert.Equal(2, events.Count); - Assert.Equal("PROJECT_CREATED", events[0].Action); - } - - [Fact] - public async Task GetProjectRepoAuditEventsAsync_ReturnsEvents() - { - _fixture.Reset(); - _fixture.Server.SetupGetProjectRepoAuditEvents(ProjectKey, RepoSlug); - var client = _fixture.CreateClient(); - - var result = await client.GetProjectRepoAuditEventsAsync(ProjectKey, RepoSlug); - - Assert.NotNull(result); - var events = result.ToList(); - Assert.Equal(2, events.Count); - Assert.Equal("PROJECT_CREATED", events[0].Action); - } + _fixture.Reset(); + _fixture.Server.SetupGetProjectAuditEvents(ProjectKey); + var client = _fixture.CreateClient(); + + var result = await client.GetProjectAuditEventsAsync(ProjectKey); + + Assert.NotNull(result); + var events = result.ToList(); + Assert.Equal(2, events.Count); + Assert.Equal("PROJECT_CREATED", events[0].Action); + } + + [Fact] + public async Task GetProjectRepoAuditEventsAsync_ReturnsEvents() + { + _fixture.Reset(); + _fixture.Server.SetupGetProjectRepoAuditEvents(ProjectKey, RepoSlug); + var client = _fixture.CreateClient(); + + var result = await client.GetProjectRepoAuditEventsAsync(ProjectKey, RepoSlug); + + Assert.NotNull(result); + var events = result.ToList(); + Assert.Equal(2, events.Count); + Assert.Equal("PROJECT_CREATED", events[0].Action); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/BranchExtendedMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/BranchExtendedMockTests.cs index 684c88d..4a8d8e6 100644 --- a/test/Bitbucket.Net.Tests/MockTests/BranchExtendedMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/BranchExtendedMockTests.cs @@ -1,89 +1,81 @@ -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests -{ - public class BranchExtendedMockTests : IClassFixture - { - private readonly BitbucketMockFixture _fixture; +namespace Bitbucket.Net.Tests.MockTests; - public BranchExtendedMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } +public class BranchExtendedMockTests(BitbucketMockFixture fixture) : IClassFixture +{ + private readonly BitbucketMockFixture _fixture = fixture; - [Fact] - public async Task GetCommitBranchInfoAsync_ReturnsBranches() - { - _fixture.Reset(); - _fixture.Server.SetupGetCommitBranchInfo( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestCommitId); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetCommitBranchInfoAsync_ReturnsBranches() + { + _fixture.Reset(); + _fixture.Server.SetupGetCommitBranchInfo( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestCommitId); + var client = _fixture.CreateClient(); - var branches = await client.GetCommitBranchInfoAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestCommitId); + var branches = await client.GetCommitBranchInfoAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestCommitId); - Assert.NotNull(branches); - var branchList = branches.ToList(); - Assert.Equal(2, branchList.Count); - } + Assert.NotNull(branches); + var branchList = branches.ToList(); + Assert.Equal(2, branchList.Count); + } - [Fact] - public async Task GetRepoBranchModelAsync_ReturnsBranchModel() - { - _fixture.Reset(); - _fixture.Server.SetupGetBranchModel( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetRepoBranchModelAsync_ReturnsBranchModel() + { + _fixture.Reset(); + _fixture.Server.SetupGetBranchModel( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); - var model = await client.GetRepoBranchModelAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); + var model = await client.GetRepoBranchModelAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); - Assert.NotNull(model); - } + Assert.NotNull(model); + } - [Fact] - public async Task CreateRepoBranchAsync_ReturnsBranch() - { - _fixture.Reset(); - _fixture.Server.SetupCreateRepoBranch( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); + [Fact] + public async Task CreateRepoBranchAsync_ReturnsBranch() + { + _fixture.Reset(); + _fixture.Server.SetupCreateRepoBranch( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); - var branch = await client.CreateRepoBranchAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - "feature/new-branch", - "refs/heads/master"); + var branch = await client.CreateRepoBranchAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + "feature/new-branch", + "refs/heads/master"); - Assert.NotNull(branch); - } + Assert.NotNull(branch); + } - [Fact] - public async Task DeleteRepoBranchAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupDeleteRepoBranch( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); + [Fact] + public async Task DeleteRepoBranchAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupDeleteRepoBranch( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); - var result = await client.DeleteRepoBranchAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - "feature/old-branch", - dryRun: false); + var result = await client.DeleteRepoBranchAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + "feature/old-branch", + dryRun: false); - Assert.True(result); - } + Assert.True(result); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/BranchMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/BranchMockTests.cs index 9fc578a..6a92523 100644 --- a/test/Bitbucket.Net.Tests/MockTests/BranchMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/BranchMockTests.cs @@ -1,96 +1,88 @@ -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Models.Core.Projects; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests +namespace Bitbucket.Net.Tests.MockTests; + +public class BranchMockTests(BitbucketMockFixture fixture) : IClassFixture { - public class BranchMockTests : IClassFixture + private readonly BitbucketMockFixture _fixture = fixture; + + [Fact] + public async Task GetBranchesAsync_ReturnsBranches() { - private readonly BitbucketMockFixture _fixture; + _fixture.Reset(); + _fixture.Server.SetupGetBranches(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var branches = await client.GetBranchesAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + + Assert.NotNull(branches); + var branchList = branches.ToList(); + Assert.Equal(2, branchList.Count); + Assert.Contains(branchList, b => b.DisplayId == "master"); + Assert.Contains(branchList, b => b.DisplayId == "feature-test"); + } - public BranchMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } + [Fact] + public async Task GetDefaultBranchAsync_ReturnsMasterBranch() + { + _fixture.Reset(); + _fixture.Server.SetupGetDefaultBranch(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); - [Fact] - public async Task GetBranchesAsync_ReturnsBranches() - { - _fixture.Reset(); - _fixture.Server.SetupGetBranches(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var branches = await client.GetBranchesAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - - Assert.NotNull(branches); - var branchList = branches.ToList(); - Assert.Equal(2, branchList.Count); - Assert.Contains(branchList, b => b.DisplayId == "master"); - Assert.Contains(branchList, b => b.DisplayId == "feature-test"); - } - - [Fact] - public async Task GetDefaultBranchAsync_ReturnsMasterBranch() - { - _fixture.Reset(); - _fixture.Server.SetupGetDefaultBranch(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); + var branch = await client.GetDefaultBranchAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); - var branch = await client.GetDefaultBranchAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); + Assert.NotNull(branch); + Assert.Equal("master", branch.DisplayId); + Assert.True(branch.IsDefault); + } - Assert.NotNull(branch); - Assert.Equal("master", branch.DisplayId); - Assert.True(branch.IsDefault); - } + [Fact] + public async Task CreateBranchAsync_ReturnsBranch() + { + _fixture.Reset(); + _fixture.Server.SetupCreateBranch(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); - [Fact] - public async Task CreateBranchAsync_ReturnsBranch() + var branchInfo = new BranchInfo { - _fixture.Reset(); - _fixture.Server.SetupCreateBranch(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var branchInfo = new BranchInfo - { - Name = "feature-test", - StartPoint = "refs/heads/master" - }; - - var branch = await client.CreateBranchAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - branchInfo); - - Assert.NotNull(branch); - Assert.Equal("refs/heads/feature-test", branch.Id); - Assert.Equal("feature-test", branch.DisplayId); - Assert.False(branch.IsDefault); - } - - [Fact] - public async Task SetDefaultBranchAsync_ReturnsTrue() + Name = "feature-test", + StartPoint = "refs/heads/master" + }; + + var branch = await client.CreateBranchAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + branchInfo); + + Assert.NotNull(branch); + Assert.Equal("refs/heads/feature-test", branch.Id); + Assert.Equal("feature-test", branch.DisplayId); + Assert.False(branch.IsDefault); + } + + [Fact] + public async Task SetDefaultBranchAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupSetDefaultBranch(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var branchRef = new BranchRef { - _fixture.Reset(); - _fixture.Server.SetupSetDefaultBranch(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var branchRef = new BranchRef - { - Id = "refs/heads/develop" - }; - - var result = await client.SetDefaultBranchAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - branchRef); - - Assert.True(result); - } + Id = "refs/heads/develop" + }; + + var result = await client.SetDefaultBranchAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + branchRef); + + Assert.True(result); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/BuildExtendedMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/BuildExtendedMockTests.cs index fa69e04..ed0df34 100644 --- a/test/Bitbucket.Net.Tests/MockTests/BuildExtendedMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/BuildExtendedMockTests.cs @@ -1,51 +1,41 @@ -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Bitbucket.Net.Models.Builds; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests +namespace Bitbucket.Net.Tests.MockTests; + +public class BuildExtendedMockTests(BitbucketMockFixture fixture) : IClassFixture { - public class BuildExtendedMockTests : IClassFixture + private readonly BitbucketMockFixture _fixture = fixture; + + [Fact] + public async Task GetBuildStatsForCommitsAsync_WithCancellationToken_ReturnsDictionaryWithStats() { - private readonly BitbucketMockFixture _fixture; - - public BuildExtendedMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public async Task GetBuildStatsForCommitsAsync_WithCancellationToken_ReturnsDictionaryWithStats() - { - _fixture.Reset(); - _fixture.Server.SetupGetBuildStatsForMultipleCommits(); - var client = _fixture.CreateClient(); - - var stats = await client.GetBuildStatsForCommitsAsync( - cancellationToken: CancellationToken.None, - commitIds: ["abc123def456", "def456ghi789"]); - - Assert.NotNull(stats); - Assert.Equal(2, stats.Count); - Assert.True(stats.ContainsKey("abc123def456")); - Assert.True(stats.ContainsKey("def456ghi789")); - Assert.Equal(2, stats["abc123def456"].Successful); - Assert.Equal(1, stats["def456ghi789"].Failed); - } - - [Fact] - public async Task GetBuildStatsForCommitsAsync_WithoutCancellationToken_ReturnsDictionaryWithStats() - { - _fixture.Reset(); - _fixture.Server.SetupGetBuildStatsForMultipleCommits(); - var client = _fixture.CreateClient(); - - var stats = await client.GetBuildStatsForCommitsAsync(commitIds: ["abc123def456", "def456ghi789"]); - - Assert.NotNull(stats); - Assert.Equal(2, stats.Count); - } + _fixture.Reset(); + _fixture.Server.SetupGetBuildStatsForMultipleCommits(); + var client = _fixture.CreateClient(); + + var stats = await client.GetBuildStatsForCommitsAsync( + cancellationToken: CancellationToken.None, + commitIds: ["abc123def456", "def456ghi789"]); + + Assert.NotNull(stats); + Assert.Equal(2, stats.Count); + Assert.True(stats.ContainsKey("abc123def456")); + Assert.True(stats.ContainsKey("def456ghi789")); + Assert.Equal(2, stats["abc123def456"].Successful); + Assert.Equal(1, stats["def456ghi789"].Failed); + } + + [Fact] + public async Task GetBuildStatsForCommitsAsync_WithoutCancellationToken_ReturnsDictionaryWithStats() + { + _fixture.Reset(); + _fixture.Server.SetupGetBuildStatsForMultipleCommits(); + var client = _fixture.CreateClient(); + + var stats = await client.GetBuildStatsForCommitsAsync(commitIds: ["abc123def456", "def456ghi789"]); + + Assert.NotNull(stats); + Assert.Equal(2, stats.Count); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/BuildMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/BuildMockTests.cs index 015d3ed..59b6ba5 100644 --- a/test/Bitbucket.Net.Tests/MockTests/BuildMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/BuildMockTests.cs @@ -1,72 +1,64 @@ -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Models.Builds; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests +namespace Bitbucket.Net.Tests.MockTests; + +public class BuildMockTests(BitbucketMockFixture fixture) : IClassFixture { - public class BuildMockTests : IClassFixture - { - private readonly BitbucketMockFixture _fixture; + private readonly BitbucketMockFixture _fixture = fixture; - public BuildMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } + [Fact] + public async Task GetBuildStatsForCommitAsync_ReturnsStats() + { + _fixture.Reset(); + _fixture.Server.SetupGetBuildStatsForCommit(TestConstants.TestCommitId); + var client = _fixture.CreateClient(); - [Fact] - public async Task GetBuildStatsForCommitAsync_ReturnsStats() - { - _fixture.Reset(); - _fixture.Server.SetupGetBuildStatsForCommit(TestConstants.TestCommitId); - var client = _fixture.CreateClient(); + var stats = await client.GetBuildStatsForCommitAsync(TestConstants.TestCommitId); - var stats = await client.GetBuildStatsForCommitAsync(TestConstants.TestCommitId); + Assert.NotNull(stats); + Assert.Equal(3, stats.Successful); + Assert.Equal(1, stats.InProgress); + Assert.Equal(0, stats.Failed); + } - Assert.NotNull(stats); - Assert.Equal(3, stats.Successful); - Assert.Equal(1, stats.InProgress); - Assert.Equal(0, stats.Failed); - } + [Fact] + public async Task GetBuildStatusForCommitAsync_ReturnsStatuses() + { + _fixture.Reset(); + _fixture.Server.SetupGetBuildStatusForCommit(TestConstants.TestCommitId); + var client = _fixture.CreateClient(); - [Fact] - public async Task GetBuildStatusForCommitAsync_ReturnsStatuses() - { - _fixture.Reset(); - _fixture.Server.SetupGetBuildStatusForCommit(TestConstants.TestCommitId); - var client = _fixture.CreateClient(); + var statuses = await client.GetBuildStatusForCommitAsync(TestConstants.TestCommitId); - var statuses = await client.GetBuildStatusForCommitAsync(TestConstants.TestCommitId); + Assert.NotNull(statuses); + var statusList = statuses.ToList(); + Assert.Equal(2, statusList.Count); + Assert.Equal("build-123", statusList[0].Key); + Assert.Equal("SUCCESSFUL", statusList[0].State); + } - Assert.NotNull(statuses); - var statusList = statuses.ToList(); - Assert.Equal(2, statusList.Count); - Assert.Equal("build-123", statusList[0].Key); - Assert.Equal("SUCCESSFUL", statusList[0].State); - } + [Fact] + public async Task AssociateBuildStatusWithCommitAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupAssociateBuildStatus(TestConstants.TestCommitId); + var client = _fixture.CreateClient(); - [Fact] - public async Task AssociateBuildStatusWithCommitAsync_ReturnsTrue() + var buildStatus = new BuildStatus { - _fixture.Reset(); - _fixture.Server.SetupAssociateBuildStatus(TestConstants.TestCommitId); - var client = _fixture.CreateClient(); - - var buildStatus = new BuildStatus - { - Key = "build-125", - State = "SUCCESSFUL", - Name = "Test Build", - Description = "Build completed", - Url = "https://build-server/builds/125" - }; + Key = "build-125", + State = "SUCCESSFUL", + Name = "Test Build", + Description = "Build completed", + Url = "https://build-server/builds/125" + }; - var result = await client.AssociateBuildStatusWithCommitAsync( - TestConstants.TestCommitId, - buildStatus); + var result = await client.AssociateBuildStatusWithCommitAsync( + TestConstants.TestCommitId, + buildStatus); - Assert.True(result); - } + Assert.True(result); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/CancellationMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/CancellationMockTests.cs new file mode 100644 index 0000000..599b2de --- /dev/null +++ b/test/Bitbucket.Net.Tests/MockTests/CancellationMockTests.cs @@ -0,0 +1,241 @@ +using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Tests.Infrastructure; +using Flurl.Http; +using Xunit; + +namespace Bitbucket.Net.Tests.MockTests; + +public class CancellationMockTests(BitbucketMockFixture fixture) : IClassFixture +{ + private const string ApiBasePath = "/rest/api/1.0"; + private readonly BitbucketMockFixture _fixture = fixture; + + #region Buffered Methods — Pre-cancelled Token + + [Fact] + public async Task GetProjectsAsync_PreCancelledToken_ThrowsOperationCanceled() + { + _fixture.Reset(); + _fixture.Server.SetupGetProjects(); + var client = _fixture.CreateClient(); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + await Assert.ThrowsAnyAsync( + () => client.GetProjectsAsync(cancellationToken: cts.Token)); + } + + [Fact] + public async Task GetProjectAsync_PreCancelledToken_ThrowsOperationCanceled() + { + _fixture.Reset(); + _fixture.Server.SetupGetProject(TestConstants.TestProjectKey); + var client = _fixture.CreateClient(); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + await AssertCancellationPropagatedAsync( + () => client.GetProjectAsync(TestConstants.TestProjectKey, cts.Token)); + } + + [Fact] + public async Task CreateProjectAsync_PreCancelledToken_ThrowsOperationCanceled() + { + _fixture.Reset(); + var client = _fixture.CreateClient(); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + var definition = new ProjectDefinition { Key = TestConstants.TestProjectKey, Name = TestConstants.TestProjectName }; + + await AssertCancellationPropagatedAsync( + () => client.CreateProjectAsync(definition, cts.Token)); + } + + [Fact] + public async Task DeleteProjectAsync_PreCancelledToken_ThrowsOperationCanceled() + { + _fixture.Reset(); + var client = _fixture.CreateClient(); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + await AssertCancellationPropagatedAsync( + () => client.DeleteProjectAsync(TestConstants.TestProjectKey, cts.Token)); + } + + [Fact] + public async Task GetPullRequestsAsync_PreCancelledToken_ThrowsOperationCanceled() + { + _fixture.Reset(); + _fixture.Server.SetupGetPullRequests(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + await Assert.ThrowsAnyAsync( + () => client.GetPullRequestsAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, cancellationToken: cts.Token)); + } + + #endregion + + #region Streaming Methods — Pre-cancelled Token + + [Fact] + public async Task GetProjectsStreamAsync_PreCancelledToken_ThrowsOnMoveNext() + { + _fixture.Reset(); + _fixture.Server.SetupGetProjects(); + var client = _fixture.CreateClient(); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + await Assert.ThrowsAnyAsync(async () => + { + await foreach (var _ in client.GetProjectsStreamAsync(cancellationToken: cts.Token)) + { + } + }); + } + + [Fact] + public async Task GetPullRequestsStreamAsync_PreCancelledToken_ThrowsOnMoveNext() + { + _fixture.Reset(); + _fixture.Server.SetupGetPullRequests(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + await Assert.ThrowsAnyAsync(async () => + { + await foreach (var _ in client.GetPullRequestsStreamAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, cancellationToken: cts.Token)) + { + } + }); + } + + [Fact] + public async Task GetBranchesStreamAsync_PreCancelledToken_ThrowsOnMoveNext() + { + _fixture.Reset(); + _fixture.Server.SetupGetBranches(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + await Assert.ThrowsAnyAsync(async () => + { + await foreach (var _ in client.GetBranchesStreamAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, cancellationToken: cts.Token)) + { + } + }); + } + + #endregion + + #region Streaming Methods — Cancel During Multi-page + + [Fact] + public async Task GetProjectsStreamAsync_CancelAfterFirstPage_StopsEnumeration() + { + _fixture.Reset(); + _fixture.Server.SetupPagedEndpoint( + $"{ApiBasePath}/projects", "Core", "projects-page1.json", "projects-page2.json"); + var client = _fixture.CreateClient(); + using var cts = new CancellationTokenSource(); + var results = new List(); + + await Assert.ThrowsAnyAsync(async () => + { + await foreach (var project in client.GetProjectsStreamAsync(start: 0, cancellationToken: cts.Token)) + { + results.Add(project); + if (results.Count >= 2) + cts.Cancel(); + } + }); + + Assert.Equal(2, results.Count); + } + + #endregion + + #region Diff Streaming — Pre-cancelled Token + + [Fact] + public async Task GetPullRequestDiffStreamAsync_PreCancelledToken_ThrowsOperationCanceled() + { + _fixture.Reset(); + _fixture.Server.SetupGetPullRequestDiff(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + await AssertCancellationPropagatedAsync(async () => + { + await foreach (var _ in client.GetPullRequestDiffStreamAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, TestConstants.TestPullRequestId, cancellationToken: cts.Token)) + { + } + }); + } + + [Fact] + public async Task GetCommitDiffStreamAsync_PreCancelledToken_ThrowsOperationCanceled() + { + _fixture.Reset(); + _fixture.Server.SetupGetCommitDiff(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, TestConstants.TestCommitId); + var client = _fixture.CreateClient(); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + await AssertCancellationPropagatedAsync(async () => + { + await foreach (var _ in client.GetCommitDiffStreamAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, TestConstants.TestCommitId, cancellationToken: cts.Token)) + { + } + }); + } + + #endregion + + #region Pre-cancelled Token — No HTTP Call Observed + + [Fact] + public async Task GetProjectsAsync_PreCancelledToken_NoHttpCallMade() + { + _fixture.Reset(); + var client = _fixture.CreateClient(); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + await Assert.ThrowsAnyAsync( + () => client.GetProjectsAsync(cancellationToken: cts.Token)); + + Assert.Empty(_fixture.Server.LogEntries); + } + + #endregion + + /// + /// Asserts that a cancelled token propagates correctly, whether the runtime throws + /// directly or Flurl wraps it in a + /// . + /// + private static async Task AssertCancellationPropagatedAsync(Func action) + { + try + { + await action(); + Assert.Fail("Expected cancellation to be propagated, but the operation completed normally."); + } + catch (OperationCanceledException) + { + // Direct cancellation — expected (paged methods check token first) + } + catch (FlurlHttpException ex) when (ex.InnerException is OperationCanceledException) + { + // Flurl-wrapped cancellation — also expected (non-paged methods hit Flurl first) + } + } +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/ChangesAndFilesMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/ChangesAndFilesMockTests.cs index 7a7ee64..3e5378d 100644 --- a/test/Bitbucket.Net.Tests/MockTests/ChangesAndFilesMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/ChangesAndFilesMockTests.cs @@ -1,112 +1,104 @@ -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests +namespace Bitbucket.Net.Tests.MockTests; + +public class ChangesAndFilesMockTests(BitbucketMockFixture fixture) : IClassFixture { - public class ChangesAndFilesMockTests : IClassFixture + private readonly BitbucketMockFixture _fixture = fixture; + + [Fact] + public async Task GetChangesAsync_ReturnsChanges() + { + _fixture.Reset(); + _fixture.Server.SetupGetChanges(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var changes = await client.GetChangesAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + until: "HEAD"); + + Assert.NotNull(changes); + var changeList = changes.ToList(); + Assert.Single(changeList); + Assert.Equal("MODIFY", changeList[0].Type); + } + + [Fact] + public async Task GetCommitChangesAsync_ReturnsChanges() + { + _fixture.Reset(); + _fixture.Server.SetupGetCommitChanges( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestCommitId); + var client = _fixture.CreateClient(); + + var changes = await client.GetCommitChangesAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestCommitId); + + Assert.NotNull(changes); + var changeList = changes.ToList(); + Assert.Single(changeList); + } + + [Fact] + public async Task GetRepositoryFilesAsync_ReturnsFiles() + { + _fixture.Reset(); + _fixture.Server.SetupGetFiles(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var files = await client.GetRepositoryFilesAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + + Assert.NotNull(files); + var fileList = files.ToList(); + Assert.Equal(3, fileList.Count); + Assert.Contains("README.md", fileList); + } + + [Fact] + public async Task GetPullRequestChangesAsync_ReturnsChanges() + { + _fixture.Reset(); + _fixture.Server.SetupGetPullRequestChanges( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); + + var changes = await client.GetPullRequestChangesAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + + Assert.NotNull(changes); + var changeList = changes.ToList(); + Assert.Single(changeList); + } + + [Fact] + public async Task GetPullRequestCommitsAsync_ReturnsCommits() { - private readonly BitbucketMockFixture _fixture; - - public ChangesAndFilesMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public async Task GetChangesAsync_ReturnsChanges() - { - _fixture.Reset(); - _fixture.Server.SetupGetChanges(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var changes = await client.GetChangesAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - until: "HEAD"); - - Assert.NotNull(changes); - var changeList = changes.ToList(); - Assert.Single(changeList); - Assert.Equal("MODIFY", changeList[0].Type); - } - - [Fact] - public async Task GetCommitChangesAsync_ReturnsChanges() - { - _fixture.Reset(); - _fixture.Server.SetupGetCommitChanges( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestCommitId); - var client = _fixture.CreateClient(); - - var changes = await client.GetCommitChangesAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestCommitId); - - Assert.NotNull(changes); - var changeList = changes.ToList(); - Assert.Single(changeList); - } - - [Fact] - public async Task GetRepositoryFilesAsync_ReturnsFiles() - { - _fixture.Reset(); - _fixture.Server.SetupGetFiles(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var files = await client.GetRepositoryFilesAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - - Assert.NotNull(files); - var fileList = files.ToList(); - Assert.Equal(3, fileList.Count); - Assert.Contains("README.md", fileList); - } - - [Fact] - public async Task GetPullRequestChangesAsync_ReturnsChanges() - { - _fixture.Reset(); - _fixture.Server.SetupGetPullRequestChanges( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); - - var changes = await client.GetPullRequestChangesAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - - Assert.NotNull(changes); - var changeList = changes.ToList(); - Assert.Single(changeList); - } - - [Fact] - public async Task GetPullRequestCommitsAsync_ReturnsCommits() - { - _fixture.Reset(); - _fixture.Server.SetupGetPullRequestCommits( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); - - var commits = await client.GetPullRequestCommitsAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - - Assert.NotNull(commits); - var commitList = commits.ToList(); - Assert.Equal(2, commitList.Count); - } + _fixture.Reset(); + _fixture.Server.SetupGetPullRequestCommits( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); + + var commits = await client.GetPullRequestCommitsAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + + Assert.NotNull(commits); + var commitList = commits.ToList(); + Assert.Equal(2, commitList.Count); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/CommentLikesMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/CommentLikesMockTests.cs index ac48818..e9c5d60 100644 --- a/test/Bitbucket.Net.Tests/MockTests/CommentLikesMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/CommentLikesMockTests.cs @@ -1,100 +1,92 @@ -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests +namespace Bitbucket.Net.Tests.MockTests; + +public class CommentLikesMockTests(BitbucketMockFixture fixture) : IClassFixture { - public class CommentLikesMockTests : IClassFixture + private readonly BitbucketMockFixture _fixture = fixture; + private const string ProjectKey = "PROJ"; + private const string RepoSlug = "repo"; + private const string CommitId = "abc123"; + private const string CommentId = "100"; + private const string PullRequestId = "1"; + + [Fact] + public async Task GetCommitCommentLikesAsync_ReturnsUsers() + { + _fixture.Reset(); + _fixture.Server.SetupGetCommitCommentLikes(ProjectKey, RepoSlug, CommitId, CommentId); + var client = _fixture.CreateClient(); + + var result = await client.GetCommitCommentLikesAsync(ProjectKey, RepoSlug, CommitId, CommentId); + + Assert.NotNull(result); + var users = result.ToList(); + Assert.Equal(2, users.Count); + Assert.Equal("jsmith", users[0].Name); + Assert.Equal("jdoe", users[1].Name); + } + + [Fact] + public async Task LikeCommitCommentAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupLikeCommitComment(ProjectKey, RepoSlug, CommitId, CommentId); + var client = _fixture.CreateClient(); + + var result = await client.LikeCommitCommentAsync(ProjectKey, RepoSlug, CommitId, CommentId); + + Assert.True(result); + } + + [Fact] + public async Task UnlikeCommitCommentAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupUnlikeCommitComment(ProjectKey, RepoSlug, CommitId, CommentId); + var client = _fixture.CreateClient(); + + var result = await client.UnlikeCommitCommentAsync(ProjectKey, RepoSlug, CommitId, CommentId); + + Assert.True(result); + } + + [Fact] + public async Task GetPullRequestCommentLikesAsync_ReturnsUsers() + { + _fixture.Reset(); + _fixture.Server.SetupGetPullRequestCommentLikes(ProjectKey, RepoSlug, PullRequestId, CommentId); + var client = _fixture.CreateClient(); + + var result = await client.GetPullRequestCommentLikesAsync(ProjectKey, RepoSlug, PullRequestId, CommentId); + + Assert.NotNull(result); + var users = result.ToList(); + Assert.Equal(2, users.Count); + } + + [Fact] + public async Task LikePullRequestCommentAsync_ReturnsTrue() { - private readonly BitbucketMockFixture _fixture; - private const string ProjectKey = "PROJ"; - private const string RepoSlug = "repo"; - private const string CommitId = "abc123"; - private const string CommentId = "100"; - private const string PullRequestId = "1"; - - public CommentLikesMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public async Task GetCommitCommentLikesAsync_ReturnsUsers() - { - _fixture.Reset(); - _fixture.Server.SetupGetCommitCommentLikes(ProjectKey, RepoSlug, CommitId, CommentId); - var client = _fixture.CreateClient(); - - var result = await client.GetCommitCommentLikesAsync(ProjectKey, RepoSlug, CommitId, CommentId); - - Assert.NotNull(result); - var users = result.ToList(); - Assert.Equal(2, users.Count); - Assert.Equal("jsmith", users[0].Name); - Assert.Equal("jdoe", users[1].Name); - } - - [Fact] - public async Task LikeCommitCommentAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupLikeCommitComment(ProjectKey, RepoSlug, CommitId, CommentId); - var client = _fixture.CreateClient(); - - var result = await client.LikeCommitCommentAsync(ProjectKey, RepoSlug, CommitId, CommentId); - - Assert.True(result); - } - - [Fact] - public async Task UnlikeCommitCommentAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupUnlikeCommitComment(ProjectKey, RepoSlug, CommitId, CommentId); - var client = _fixture.CreateClient(); - - var result = await client.UnlikeCommitCommentAsync(ProjectKey, RepoSlug, CommitId, CommentId); - - Assert.True(result); - } - - [Fact] - public async Task GetPullRequestCommentLikesAsync_ReturnsUsers() - { - _fixture.Reset(); - _fixture.Server.SetupGetPullRequestCommentLikes(ProjectKey, RepoSlug, PullRequestId, CommentId); - var client = _fixture.CreateClient(); - - var result = await client.GetPullRequestCommentLikesAsync(ProjectKey, RepoSlug, PullRequestId, CommentId); - - Assert.NotNull(result); - var users = result.ToList(); - Assert.Equal(2, users.Count); - } - - [Fact] - public async Task LikePullRequestCommentAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupLikePullRequestComment(ProjectKey, RepoSlug, PullRequestId, CommentId); - var client = _fixture.CreateClient(); - - var result = await client.LikePullRequestCommentAsync(ProjectKey, RepoSlug, PullRequestId, CommentId); - - Assert.True(result); - } - - [Fact] - public async Task UnlikePullRequestCommentAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupUnlikePullRequestComment(ProjectKey, RepoSlug, PullRequestId, CommentId); - var client = _fixture.CreateClient(); - - var result = await client.UnlikePullRequestCommentAsync(ProjectKey, RepoSlug, PullRequestId, CommentId); - - Assert.True(result); - } + _fixture.Reset(); + _fixture.Server.SetupLikePullRequestComment(ProjectKey, RepoSlug, PullRequestId, CommentId); + var client = _fixture.CreateClient(); + + var result = await client.LikePullRequestCommentAsync(ProjectKey, RepoSlug, PullRequestId, CommentId); + + Assert.True(result); + } + + [Fact] + public async Task UnlikePullRequestCommentAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupUnlikePullRequestComment(ProjectKey, RepoSlug, PullRequestId, CommentId); + var client = _fixture.CreateClient(); + + var result = await client.UnlikePullRequestCommentAsync(ProjectKey, RepoSlug, PullRequestId, CommentId); + + Assert.True(result); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/CommitMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/CommitMockTests.cs index a123fc8..8522a0f 100644 --- a/test/Bitbucket.Net.Tests/MockTests/CommitMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/CommitMockTests.cs @@ -1,56 +1,48 @@ -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests +namespace Bitbucket.Net.Tests.MockTests; + +public class CommitMockTests(BitbucketMockFixture fixture) : IClassFixture { - public class CommitMockTests : IClassFixture + private readonly BitbucketMockFixture _fixture = fixture; + + [Fact] + public async Task GetCommitsAsync_ReturnsCommits() + { + _fixture.Reset(); + _fixture.Server.SetupGetCommits(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var commits = await client.GetCommitsAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + until: "HEAD"); + + Assert.NotNull(commits); + var commitList = commits.ToList(); + Assert.Equal(2, commitList.Count); + Assert.Contains(commitList, c => c.Message == "Initial commit"); + Assert.Contains(commitList, c => c.Message == "Add feature"); + } + + [Fact] + public async Task GetCommitAsync_ReturnsCommit() { - private readonly BitbucketMockFixture _fixture; - - public CommitMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public async Task GetCommitsAsync_ReturnsCommits() - { - _fixture.Reset(); - _fixture.Server.SetupGetCommits(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var commits = await client.GetCommitsAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - until: "HEAD"); - - Assert.NotNull(commits); - var commitList = commits.ToList(); - Assert.Equal(2, commitList.Count); - Assert.Contains(commitList, c => c.Message == "Initial commit"); - Assert.Contains(commitList, c => c.Message == "Add feature"); - } - - [Fact] - public async Task GetCommitAsync_ReturnsCommit() - { - _fixture.Reset(); - _fixture.Server.SetupGetCommit( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestCommitId); - var client = _fixture.CreateClient(); - - var commit = await client.GetCommitAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestCommitId); - - Assert.NotNull(commit); - Assert.Equal(TestConstants.TestCommitId, commit.Id); - Assert.Equal("Initial commit", commit.Message); - } + _fixture.Reset(); + _fixture.Server.SetupGetCommit( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestCommitId); + var client = _fixture.CreateClient(); + + var commit = await client.GetCommitAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestCommitId); + + Assert.NotNull(commit); + Assert.Equal(TestConstants.TestCommitId, commit.Id); + Assert.Equal("Initial commit", commit.Message); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/CoreExtendedMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/CoreExtendedMockTests.cs index c0b126f..65c25c4 100644 --- a/test/Bitbucket.Net.Tests/MockTests/CoreExtendedMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/CoreExtendedMockTests.cs @@ -1,231 +1,223 @@ -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Models.Core.Projects; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests +namespace Bitbucket.Net.Tests.MockTests; + +public class CoreExtendedMockTests(BitbucketMockFixture fixture) : IClassFixture { - public class CoreExtendedMockTests : IClassFixture + private readonly BitbucketMockFixture _fixture = fixture; + + [Fact] + public async Task BrowseProjectRepositoryAsync_ReturnsBrowseItem() + { + _fixture.Reset(); + _fixture.Server.SetupBrowseRepository( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var browseItem = await client.BrowseProjectRepositoryAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + at: "refs/heads/master"); + + Assert.NotNull(browseItem); + } + + [Fact] + public async Task GetProjectRepositoryLastModifiedAsync_ReturnsLastModified() + { + _fixture.Reset(); + _fixture.Server.SetupGetLastModified( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var lastModified = await client.GetProjectRepositoryLastModifiedAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + at: "refs/heads/master"); + + Assert.NotNull(lastModified); + } + + [Fact] + public async Task GetRepositoryCompareChangesAsync_ReturnsChanges() + { + _fixture.Reset(); + _fixture.Server.SetupGetCompareChanges( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var changes = await client.GetRepositoryCompareChangesAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + from: "refs/heads/feature", + to: "refs/heads/master"); + + Assert.NotNull(changes); + var changeList = changes.ToList(); + Assert.Single(changeList); + } + + [Fact] + public async Task GetCommitDiffAsync_ReturnsDifferences() + { + _fixture.Reset(); + _fixture.Server.SetupGetCommitDiff( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestCommitId); + var client = _fixture.CreateClient(); + + var diff = await client.GetCommitDiffAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestCommitId); + + Assert.NotNull(diff); + Assert.NotNull(diff.Diffs); + } + + [Fact] + public async Task GetPullRequestMergeBaseAsync_ReturnsCommit() + { + _fixture.Reset(); + _fixture.Server.SetupGetPullRequestMergeBase( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); + + var commit = await client.GetPullRequestMergeBaseAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + + Assert.NotNull(commit); + Assert.Equal(TestConstants.TestCommitId, commit.Id); + } + + [Fact] + public async Task CreateCommitWatchAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupCreateCommitWatch( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestCommitId); + var client = _fixture.CreateClient(); + + var result = await client.CreateCommitWatchAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestCommitId); + + Assert.True(result); + } + + [Fact] + public async Task DeleteCommitWatchAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupDeleteCommitWatch( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestCommitId); + var client = _fixture.CreateClient(); + + var result = await client.DeleteCommitWatchAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestCommitId); + + Assert.True(result); + } + + [Fact] + public async Task CreateCommitCommentAsync_ReturnsComment() + { + _fixture.Reset(); + _fixture.Server.SetupCreateCommitComment( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestCommitId); + var client = _fixture.CreateClient(); + + var commentInfo = new CommentInfo { Text = "Test comment" }; + + var comment = await client.CreateCommitCommentAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestCommitId, + commentInfo); + + Assert.NotNull(comment); + } + + [Fact] + public async Task GetCommitCommentAsync_ReturnsComment() + { + _fixture.Reset(); + _fixture.Server.SetupGetCommitComment( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestCommitId, + 1); + var client = _fixture.CreateClient(); + + var comment = await client.GetCommitCommentAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestCommitId, + 1); + + Assert.NotNull(comment); + } + + [Fact] + public async Task UpdateCommitCommentAsync_ReturnsComment() + { + _fixture.Reset(); + _fixture.Server.SetupUpdateCommitComment( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestCommitId, + 1); + var client = _fixture.CreateClient(); + + var commentText = new CommentText { Text = "Updated comment", Version = 0 }; + + var comment = await client.UpdateCommitCommentAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestCommitId, + 1, + commentText); + + Assert.NotNull(comment); + } + + [Fact] + public async Task DeleteCommitCommentAsync_ReturnsTrue() { - private readonly BitbucketMockFixture _fixture; - - public CoreExtendedMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public async Task BrowseProjectRepositoryAsync_ReturnsBrowseItem() - { - _fixture.Reset(); - _fixture.Server.SetupBrowseRepository( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var browseItem = await client.BrowseProjectRepositoryAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - at: "refs/heads/master"); - - Assert.NotNull(browseItem); - } - - [Fact] - public async Task GetProjectRepositoryLastModifiedAsync_ReturnsLastModified() - { - _fixture.Reset(); - _fixture.Server.SetupGetLastModified( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var lastModified = await client.GetProjectRepositoryLastModifiedAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - at: "refs/heads/master"); - - Assert.NotNull(lastModified); - } - - [Fact] - public async Task GetRepositoryCompareChangesAsync_ReturnsChanges() - { - _fixture.Reset(); - _fixture.Server.SetupGetCompareChanges( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var changes = await client.GetRepositoryCompareChangesAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - from: "refs/heads/feature", - to: "refs/heads/master"); - - Assert.NotNull(changes); - var changeList = changes.ToList(); - Assert.Single(changeList); - } - - [Fact] - public async Task GetCommitDiffAsync_ReturnsDifferences() - { - _fixture.Reset(); - _fixture.Server.SetupGetCommitDiff( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestCommitId); - var client = _fixture.CreateClient(); - - var diff = await client.GetCommitDiffAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestCommitId); - - Assert.NotNull(diff); - Assert.NotNull(diff.Diffs); - } - - [Fact] - public async Task GetPullRequestMergeBaseAsync_ReturnsCommit() - { - _fixture.Reset(); - _fixture.Server.SetupGetPullRequestMergeBase( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); - - var commit = await client.GetPullRequestMergeBaseAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - - Assert.NotNull(commit); - Assert.Equal(TestConstants.TestCommitId, commit.Id); - } - - [Fact] - public async Task CreateCommitWatchAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupCreateCommitWatch( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestCommitId); - var client = _fixture.CreateClient(); - - var result = await client.CreateCommitWatchAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestCommitId); - - Assert.True(result); - } - - [Fact] - public async Task DeleteCommitWatchAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupDeleteCommitWatch( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestCommitId); - var client = _fixture.CreateClient(); - - var result = await client.DeleteCommitWatchAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestCommitId); - - Assert.True(result); - } - - [Fact] - public async Task CreateCommitCommentAsync_ReturnsComment() - { - _fixture.Reset(); - _fixture.Server.SetupCreateCommitComment( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestCommitId); - var client = _fixture.CreateClient(); - - var commentInfo = new CommentInfo { Text = "Test comment" }; - - var comment = await client.CreateCommitCommentAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestCommitId, - commentInfo); - - Assert.NotNull(comment); - } - - [Fact] - public async Task GetCommitCommentAsync_ReturnsComment() - { - _fixture.Reset(); - _fixture.Server.SetupGetCommitComment( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestCommitId, - 1); - var client = _fixture.CreateClient(); - - var comment = await client.GetCommitCommentAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestCommitId, - 1); - - Assert.NotNull(comment); - } - - [Fact] - public async Task UpdateCommitCommentAsync_ReturnsComment() - { - _fixture.Reset(); - _fixture.Server.SetupUpdateCommitComment( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestCommitId, - 1); - var client = _fixture.CreateClient(); - - var commentText = new CommentText { Text = "Updated comment", Version = 0 }; - - var comment = await client.UpdateCommitCommentAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestCommitId, - 1, - commentText); - - Assert.NotNull(comment); - } - - [Fact] - public async Task DeleteCommitCommentAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupDeleteCommitComment( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestCommitId, - 1); - var client = _fixture.CreateClient(); - - var result = await client.DeleteCommitCommentAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestCommitId, - 1, - version: 0); - - Assert.True(result); - } + _fixture.Reset(); + _fixture.Server.SetupDeleteCommitComment( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestCommitId, + 1); + var client = _fixture.CreateClient(); + + var result = await client.DeleteCommitCommentAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestCommitId, + 1, + version: 0); + + Assert.True(result); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/DashboardMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/DashboardMockTests.cs index d40484e..d6fe92c 100644 --- a/test/Bitbucket.Net.Tests/MockTests/DashboardMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/DashboardMockTests.cs @@ -1,50 +1,42 @@ -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Models.Core.Projects; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests +namespace Bitbucket.Net.Tests.MockTests; + +public class DashboardMockTests(BitbucketMockFixture fixture) : IClassFixture { - public class DashboardMockTests : IClassFixture + private readonly BitbucketMockFixture _fixture = fixture; + + [Fact] + public async Task GetDashboardPullRequestsAsync_ReturnsPullRequests() { - private readonly BitbucketMockFixture _fixture; - - public DashboardMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public async Task GetDashboardPullRequestsAsync_ReturnsPullRequests() - { - _fixture.Reset(); - _fixture.Server.SetupGetDashboardPullRequests(); - var client = _fixture.CreateClient(); - - var result = await client.GetDashboardPullRequestsAsync(); - - Assert.NotNull(result); - var pullRequests = result.ToList(); - Assert.Single(pullRequests); - Assert.Equal("PR Title", pullRequests[0].Title); - Assert.Equal(PullRequestStates.Open, pullRequests[0].State); - } - - [Fact] - public async Task GetDashboardPullRequestSuggestionsAsync_ReturnsSuggestions() - { - _fixture.Reset(); - _fixture.Server.SetupGetDashboardPullRequestSuggestions(); - var client = _fixture.CreateClient(); - - var result = await client.GetDashboardPullRequestSuggestionsAsync(); - - Assert.NotNull(result); - var suggestions = result.ToList(); - Assert.Single(suggestions); - Assert.NotNull(suggestions[0].FromRef); - Assert.Equal("feature/branch", suggestions[0].FromRef.DisplayId); - } + _fixture.Reset(); + _fixture.Server.SetupGetDashboardPullRequests(); + var client = _fixture.CreateClient(); + + var result = await client.GetDashboardPullRequestsAsync(); + + Assert.NotNull(result); + var pullRequests = result.ToList(); + Assert.Single(pullRequests); + Assert.Equal("PR Title", pullRequests[0].Title); + Assert.Equal(PullRequestStates.Open, pullRequests[0].State); + } + + [Fact] + public async Task GetDashboardPullRequestSuggestionsAsync_ReturnsSuggestions() + { + _fixture.Reset(); + _fixture.Server.SetupGetDashboardPullRequestSuggestions(); + var client = _fixture.CreateClient(); + + var result = await client.GetDashboardPullRequestSuggestionsAsync(); + + Assert.NotNull(result); + var suggestions = result.ToList(); + Assert.Single(suggestions); + Assert.NotNull(suggestions[0].FromRef); + Assert.Equal("feature/branch", suggestions[0].FromRef!.DisplayId); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/DefaultReviewersExtendedMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/DefaultReviewersExtendedMockTests.cs index 5d30d3d..8558750 100644 --- a/test/Bitbucket.Net.Tests/MockTests/DefaultReviewersExtendedMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/DefaultReviewersExtendedMockTests.cs @@ -1,157 +1,150 @@ -using System.Threading.Tasks; using Bitbucket.Net.Models.DefaultReviewers; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests +namespace Bitbucket.Net.Tests.MockTests; + +public class DefaultReviewersExtendedMockTests(BitbucketMockFixture fixture) : IClassFixture { - public class DefaultReviewersExtendedMockTests : IClassFixture + private readonly BitbucketMockFixture _fixture = fixture; + + [Fact] + public async Task CreateDefaultReviewerConditionAsync_ByProject_ReturnsCondition() { - private readonly BitbucketMockFixture _fixture; + _fixture.Reset(); + _fixture.Server.SetupCreateDefaultReviewerCondition(TestConstants.TestProjectKey); + var client = _fixture.CreateClient(); - public DefaultReviewersExtendedMockTests(BitbucketMockFixture fixture) + var condition = new DefaultReviewerPullRequestCondition { - _fixture = fixture; - } + SourceRefMatcher = new RefMatcher + { + Id = "refs/heads/feature/**", + Type = new DefaultReviewerPullRequestConditionType { Id = "PATTERN", Name = "Pattern" } + }, + TargetRefMatcher = new RefMatcher + { + Id = "refs/heads/main", + Type = new DefaultReviewerPullRequestConditionType { Id = "BRANCH", Name = "Branch" } + }, + RequiredApprovals = 1 + }; + + var result = await client.CreateDefaultReviewerConditionAsync( + TestConstants.TestProjectKey, + condition); + + Assert.NotNull(result); + Assert.Equal(1, result.Id); + } - [Fact] - public async Task CreateDefaultReviewerConditionAsync_ByProject_ReturnsCondition() - { - _fixture.Reset(); - _fixture.Server.SetupCreateDefaultReviewerCondition(TestConstants.TestProjectKey); - var client = _fixture.CreateClient(); + [Fact] + public async Task UpdateDefaultReviewerConditionAsync_ByProject_ReturnsCondition() + { + _fixture.Reset(); + _fixture.Server.SetupUpdateDefaultReviewerCondition(TestConstants.TestProjectKey, "1"); + var client = _fixture.CreateClient(); - var condition = new DefaultReviewerPullRequestCondition - { - SourceRefMatcher = new RefMatcher - { - Id = "refs/heads/feature/**", - Type = new DefaultReviewerPullRequestConditionType { Id = "PATTERN", Name = "Pattern" } - }, - TargetRefMatcher = new RefMatcher - { - Id = "refs/heads/main", - Type = new DefaultReviewerPullRequestConditionType { Id = "BRANCH", Name = "Branch" } - }, - RequiredApprovals = 1 - }; - - var result = await client.CreateDefaultReviewerConditionAsync( - TestConstants.TestProjectKey, - condition); - - Assert.NotNull(result); - Assert.Equal(1, result.Id); - } - - [Fact] - public async Task UpdateDefaultReviewerConditionAsync_ByProject_ReturnsCondition() + var condition = new DefaultReviewerPullRequestCondition { - _fixture.Reset(); - _fixture.Server.SetupUpdateDefaultReviewerCondition(TestConstants.TestProjectKey, "1"); - var client = _fixture.CreateClient(); + Id = 1, + RequiredApprovals = 2 + }; - var condition = new DefaultReviewerPullRequestCondition - { - Id = 1, - RequiredApprovals = 2 - }; + var result = await client.UpdateDefaultReviewerConditionAsync( + TestConstants.TestProjectKey, + "1", + condition); - var result = await client.UpdateDefaultReviewerConditionAsync( - TestConstants.TestProjectKey, - "1", - condition); + Assert.NotNull(result); + } - Assert.NotNull(result); - } + [Fact] + public async Task DeleteDefaultReviewerConditionAsync_ByProject_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupDeleteDefaultReviewerCondition(TestConstants.TestProjectKey, "1"); + var client = _fixture.CreateClient(); - [Fact] - public async Task DeleteDefaultReviewerConditionAsync_ByProject_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupDeleteDefaultReviewerCondition(TestConstants.TestProjectKey, "1"); - var client = _fixture.CreateClient(); + var result = await client.DeleteDefaultReviewerConditionAsync( + TestConstants.TestProjectKey, + "1"); - var result = await client.DeleteDefaultReviewerConditionAsync( - TestConstants.TestProjectKey, - "1"); + Assert.True(result); + } - Assert.True(result); - } + [Fact] + public async Task CreateDefaultReviewerConditionAsync_ByRepo_ReturnsCondition() + { + _fixture.Reset(); + _fixture.Server.SetupCreateRepoDefaultReviewerCondition( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); - [Fact] - public async Task CreateDefaultReviewerConditionAsync_ByRepo_ReturnsCondition() + var condition = new DefaultReviewerPullRequestCondition { - _fixture.Reset(); - _fixture.Server.SetupCreateRepoDefaultReviewerCondition( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var condition = new DefaultReviewerPullRequestCondition + SourceRefMatcher = new RefMatcher { - SourceRefMatcher = new RefMatcher - { - Id = "refs/heads/feature/**", - Type = new DefaultReviewerPullRequestConditionType { Id = "PATTERN", Name = "Pattern" } - }, - TargetRefMatcher = new RefMatcher - { - Id = "refs/heads/main", - Type = new DefaultReviewerPullRequestConditionType { Id = "BRANCH", Name = "Branch" } - }, - RequiredApprovals = 1 - }; - - var result = await client.CreateDefaultReviewerConditionAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - condition); - - Assert.NotNull(result); - } - - [Fact] - public async Task UpdateDefaultReviewerConditionAsync_ByRepo_ReturnsCondition() - { - _fixture.Reset(); - _fixture.Server.SetupUpdateRepoDefaultReviewerCondition( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - "1"); - var client = _fixture.CreateClient(); - - var condition = new DefaultReviewerPullRequestCondition + Id = "refs/heads/feature/**", + Type = new DefaultReviewerPullRequestConditionType { Id = "PATTERN", Name = "Pattern" } + }, + TargetRefMatcher = new RefMatcher { - Id = 1, - RequiredApprovals = 2 - }; + Id = "refs/heads/main", + Type = new DefaultReviewerPullRequestConditionType { Id = "BRANCH", Name = "Branch" } + }, + RequiredApprovals = 1 + }; + + var result = await client.CreateDefaultReviewerConditionAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + condition); + + Assert.NotNull(result); + } - var result = await client.UpdateDefaultReviewerConditionAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - "1", - condition); + [Fact] + public async Task UpdateDefaultReviewerConditionAsync_ByRepo_ReturnsCondition() + { + _fixture.Reset(); + _fixture.Server.SetupUpdateRepoDefaultReviewerCondition( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + "1"); + var client = _fixture.CreateClient(); + + var condition = new DefaultReviewerPullRequestCondition + { + Id = 1, + RequiredApprovals = 2 + }; - Assert.NotNull(result); - } + var result = await client.UpdateDefaultReviewerConditionAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + "1", + condition); - [Fact] - public async Task DeleteDefaultReviewerConditionAsync_ByRepo_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupDeleteRepoDefaultReviewerCondition( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - "1"); - var client = _fixture.CreateClient(); - - var result = await client.DeleteDefaultReviewerConditionAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - "1"); - - Assert.True(result); - } + Assert.NotNull(result); + } + + [Fact] + public async Task DeleteDefaultReviewerConditionAsync_ByRepo_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupDeleteRepoDefaultReviewerCondition( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + "1"); + var client = _fixture.CreateClient(); + + var result = await client.DeleteDefaultReviewerConditionAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + "1"); + + Assert.True(result); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/DefaultReviewersMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/DefaultReviewersMockTests.cs index 87211f9..8ce9910 100644 --- a/test/Bitbucket.Net.Tests/MockTests/DefaultReviewersMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/DefaultReviewersMockTests.cs @@ -1,65 +1,57 @@ -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests -{ - public class DefaultReviewersMockTests : IClassFixture - { - private readonly BitbucketMockFixture _fixture; - - public DefaultReviewersMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } +namespace Bitbucket.Net.Tests.MockTests; - [Fact] - public async Task GetDefaultReviewerConditionsAsync_ByProjectKey_ReturnsConditions() - { - _fixture.Reset(); - _fixture.Server.SetupGetDefaultReviewerConditions(TestConstants.TestProjectKey); - var client = _fixture.CreateClient(); - - var conditions = await client.GetDefaultReviewerConditionsAsync(TestConstants.TestProjectKey); +public class DefaultReviewersMockTests(BitbucketMockFixture fixture) : IClassFixture +{ + private readonly BitbucketMockFixture _fixture = fixture; - Assert.NotNull(conditions); - } + [Fact] + public async Task GetDefaultReviewerConditionsAsync_ByProjectKey_ReturnsConditions() + { + _fixture.Reset(); + _fixture.Server.SetupGetDefaultReviewerConditions(TestConstants.TestProjectKey); + var client = _fixture.CreateClient(); - [Fact] - public async Task GetDefaultReviewerConditionsAsync_ByProjectAndRepo_ReturnsConditions() - { - _fixture.Reset(); - _fixture.Server.SetupGetRepoDefaultReviewerConditions( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); + var conditions = await client.GetDefaultReviewerConditionsAsync(TestConstants.TestProjectKey); - var conditions = await client.GetDefaultReviewerConditionsAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); + Assert.NotNull(conditions); + } - Assert.NotNull(conditions); - } + [Fact] + public async Task GetDefaultReviewerConditionsAsync_ByProjectAndRepo_ReturnsConditions() + { + _fixture.Reset(); + _fixture.Server.SetupGetRepoDefaultReviewerConditions( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); - [Fact] - public async Task GetDefaultReviewersAsync_ReturnsReviewers() - { - _fixture.Reset(); - _fixture.Server.SetupGetDefaultReviewers( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); + var conditions = await client.GetDefaultReviewerConditionsAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); - var reviewers = await client.GetDefaultReviewersAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - sourceRepoId: 1, - targetRepoId: 1, - sourceRefId: "refs/heads/feature", - targetRefId: "refs/heads/main"); + Assert.NotNull(conditions); + } - Assert.NotNull(reviewers); - } + [Fact] + public async Task GetDefaultReviewersAsync_ReturnsReviewers() + { + _fixture.Reset(); + _fixture.Server.SetupGetDefaultReviewers( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var reviewers = await client.GetDefaultReviewersAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + sourceRepoId: 1, + targetRepoId: 1, + sourceRefId: "refs/heads/feature", + targetRefId: "refs/heads/main"); + + Assert.NotNull(reviewers); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/DiConstructorMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/DiConstructorMockTests.cs new file mode 100644 index 0000000..41eddcd --- /dev/null +++ b/test/Bitbucket.Net.Tests/MockTests/DiConstructorMockTests.cs @@ -0,0 +1,232 @@ +using Bitbucket.Net.Common.Exceptions; +using Bitbucket.Net.Tests.Infrastructure; +using System.Net; +using Xunit; + +namespace Bitbucket.Net.Tests.MockTests; + +public class DiConstructorMockTests(BitbucketMockFixture fixture) : IClassFixture +{ + private const string ApiBasePath = "/rest/api/1.0"; + private readonly BitbucketMockFixture _fixture = fixture; + + #region HttpClient Constructor + + [Fact] + public async Task HttpClientConstructor_GetProjects_Succeeds() + { + _fixture.Reset(); + _fixture.Server.SetupGetProjects(); + var client = _fixture.CreateClientWithHttpClient(); + + var projects = await client.GetProjectsAsync(); + + Assert.NotNull(projects); + var projectList = projects.ToList(); + Assert.Single(projectList); + Assert.Equal(TestConstants.TestProjectKey, projectList[0].Key); + } + + [Fact] + public async Task HttpClientConstructor_GetPullRequests_Succeeds() + { + _fixture.Reset(); + _fixture.Server.SetupGetPullRequests(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClientWithHttpClient(); + + var pullRequests = await client.GetPullRequestsAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + + Assert.NotNull(pullRequests); + Assert.Single(pullRequests); + } + + [Fact] + public async Task HttpClientConstructor_StreamingEndpoint_Succeeds() + { + _fixture.Reset(); + _fixture.Server.SetupGetProjects(); + var client = _fixture.CreateClientWithHttpClient(); + + var results = new List(); + await foreach (var project in client.GetProjectsStreamAsync()) + { + results.Add(project); + } + + Assert.Single(results); + } + + [Fact] + public async Task HttpClientConstructor_ErrorHandling_ThrowsTypedException() + { + _fixture.Reset(); + var projectKey = "NOPE"; + _fixture.Server.SetupNotFound($"{ApiBasePath}/projects/{projectKey}"); + var client = _fixture.CreateClientWithHttpClient(); + + var exception = await Assert.ThrowsAsync( + () => client.GetProjectAsync(projectKey)); + + Assert.Equal(HttpStatusCode.NotFound, exception.StatusCode); + } + + [Fact] + public async Task HttpClientConstructor_Unauthorized_ThrowsAuthenticationException() + { + _fixture.Reset(); + _fixture.Server.SetupUnauthorized($"{ApiBasePath}/projects/{TestConstants.TestProjectKey}"); + var client = _fixture.CreateClientWithHttpClient(); + + var exception = await Assert.ThrowsAsync( + () => client.GetProjectAsync(TestConstants.TestProjectKey)); + + Assert.Equal(HttpStatusCode.Unauthorized, exception.StatusCode); + } + + [Fact] + public async Task HttpClientConstructor_ServerError_ThrowsServerException() + { + _fixture.Reset(); + _fixture.Server.SetupInternalServerError($"{ApiBasePath}/projects/{TestConstants.TestProjectKey}"); + var client = _fixture.CreateClientWithHttpClient(); + + var exception = await Assert.ThrowsAsync( + () => client.GetProjectAsync(TestConstants.TestProjectKey)); + + Assert.Equal(HttpStatusCode.InternalServerError, exception.StatusCode); + } + + #endregion + + #region FlurlClient Constructor + + [Fact] + public async Task FlurlClientConstructor_GetProjects_Succeeds() + { + _fixture.Reset(); + _fixture.Server.SetupGetProjects(); + var client = _fixture.CreateClientWithFlurlClient(); + + var projects = await client.GetProjectsAsync(); + + Assert.NotNull(projects); + var projectList = projects.ToList(); + Assert.Single(projectList); + Assert.Equal(TestConstants.TestProjectKey, projectList[0].Key); + } + + [Fact] + public async Task FlurlClientConstructor_GetPullRequests_Succeeds() + { + _fixture.Reset(); + _fixture.Server.SetupGetPullRequests(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClientWithFlurlClient(); + + var pullRequests = await client.GetPullRequestsAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + + Assert.NotNull(pullRequests); + Assert.Single(pullRequests); + } + + [Fact] + public async Task FlurlClientConstructor_StreamingEndpoint_Succeeds() + { + _fixture.Reset(); + _fixture.Server.SetupGetProjects(); + var client = _fixture.CreateClientWithFlurlClient(); + + var results = new List(); + await foreach (var project in client.GetProjectsStreamAsync()) + { + results.Add(project); + } + + Assert.Single(results); + } + + [Fact] + public async Task FlurlClientConstructor_ErrorHandling_ThrowsTypedException() + { + _fixture.Reset(); + var projectKey = "NOPE"; + _fixture.Server.SetupNotFound($"{ApiBasePath}/projects/{projectKey}"); + var client = _fixture.CreateClientWithFlurlClient(); + + var exception = await Assert.ThrowsAsync( + () => client.GetProjectAsync(projectKey)); + + Assert.Equal(HttpStatusCode.NotFound, exception.StatusCode); + } + + [Fact] + public async Task FlurlClientConstructor_Unauthorized_ThrowsAuthenticationException() + { + _fixture.Reset(); + _fixture.Server.SetupUnauthorized($"{ApiBasePath}/projects/{TestConstants.TestProjectKey}"); + var client = _fixture.CreateClientWithFlurlClient(); + + var exception = await Assert.ThrowsAsync( + () => client.GetProjectAsync(TestConstants.TestProjectKey)); + + Assert.Equal(HttpStatusCode.Unauthorized, exception.StatusCode); + } + + #endregion + + #region Both Constructors — Consistent Behavior + + [Fact] + public async Task BothConstructors_SameEndpoint_ReturnEquivalentResults() + { + _fixture.Reset(); + _fixture.Server.SetupGetProjects(); + var httpClientBasedClient = _fixture.CreateClientWithHttpClient(); + var flurlBasedClient = _fixture.CreateClientWithFlurlClient(); + + var httpResults = (await httpClientBasedClient.GetProjectsAsync()).ToList(); + + _fixture.Reset(); + _fixture.Server.SetupGetProjects(); + var flurlResults = (await flurlBasedClient.GetProjectsAsync()).ToList(); + + Assert.Equal(httpResults.Count, flurlResults.Count); + Assert.Equal(httpResults[0].Key, flurlResults[0].Key); + Assert.Equal(httpResults[0].Name, flurlResults[0].Name); + } + + #endregion + + #region Token Authentication Verification + + [Fact] + public async Task HttpClientConstructor_SendsAuthorizationHeader() + { + _fixture.Reset(); + _fixture.Server.SetupGetProjects(); + var client = _fixture.CreateClientWithHttpClient(); + + await client.GetProjectsAsync(); + + var logEntry = Assert.Single(_fixture.Server.LogEntries); + var authHeader = logEntry.RequestMessage.Headers?["Authorization"]; + Assert.NotNull(authHeader); + Assert.Contains("Bearer test-token", authHeader.ToString()); + } + + [Fact] + public async Task FlurlClientConstructor_SendsAuthorizationHeader() + { + _fixture.Reset(); + _fixture.Server.SetupGetProjects(); + var client = _fixture.CreateClientWithFlurlClient(); + + await client.GetProjectsAsync(); + + var logEntry = Assert.Single(_fixture.Server.LogEntries); + var authHeader = logEntry.RequestMessage.Headers?["Authorization"]; + Assert.NotNull(authHeader); + Assert.Contains("Bearer test-token", authHeader.ToString()); + } + + #endregion +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/DiffAndTagMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/DiffAndTagMockTests.cs index 11c3adf..ac33e84 100644 --- a/test/Bitbucket.Net.Tests/MockTests/DiffAndTagMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/DiffAndTagMockTests.cs @@ -1,97 +1,89 @@ -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests -{ - public class DiffAndTagMockTests : IClassFixture - { - private readonly BitbucketMockFixture _fixture; +namespace Bitbucket.Net.Tests.MockTests; - public DiffAndTagMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } +public class DiffAndTagMockTests(BitbucketMockFixture fixture) : IClassFixture +{ + private readonly BitbucketMockFixture _fixture = fixture; - [Fact] - public async Task GetRepositoryDiffAsync_ReturnsDiff() - { - _fixture.Reset(); - _fixture.Server.SetupGetRepositoryDiff( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetRepositoryDiffAsync_ReturnsDiff() + { + _fixture.Reset(); + _fixture.Server.SetupGetRepositoryDiff( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); - var diff = await client.GetRepositoryDiffAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - "HEAD"); + var diff = await client.GetRepositoryDiffAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + "HEAD"); - Assert.NotNull(diff); - Assert.NotNull(diff.Diffs); - Assert.NotEmpty(diff.Diffs); - } + Assert.NotNull(diff); + Assert.NotNull(diff.Diffs); + Assert.NotEmpty(diff.Diffs); + } - [Fact] - public async Task GetPullRequestDiffAsync_ReturnsDiff() - { - _fixture.Reset(); - _fixture.Server.SetupGetPullRequestDiff( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetPullRequestDiffAsync_ReturnsDiff() + { + _fixture.Reset(); + _fixture.Server.SetupGetPullRequestDiff( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); - var diff = await client.GetPullRequestDiffAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); + var diff = await client.GetPullRequestDiffAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); - Assert.NotNull(diff); - Assert.NotNull(diff.Diffs); - } + Assert.NotNull(diff); + Assert.NotNull(diff.Diffs); + } - [Fact] - public async Task GetCommitCommentsAsync_ReturnsComments() - { - _fixture.Reset(); - _fixture.Server.SetupGetCommentsOnFile( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestCommitId); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetCommitCommentsAsync_ReturnsComments() + { + _fixture.Reset(); + _fixture.Server.SetupGetCommentsOnFile( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestCommitId); + var client = _fixture.CreateClient(); - var comments = await client.GetCommitCommentsAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestCommitId, - "src/main.cs"); + var comments = await client.GetCommitCommentsAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestCommitId, + "src/main.cs"); - Assert.NotNull(comments); - var commentList = comments.ToList(); - Assert.Single(commentList); - Assert.Equal("This is a test comment", commentList[0].Text); - } + Assert.NotNull(comments); + var commentList = comments.ToList(); + Assert.Single(commentList); + Assert.Equal("This is a test comment", commentList[0].Text); + } - [Fact] - public async Task GetPullRequestActivitiesAsync_ReturnsActivities() - { - _fixture.Reset(); - _fixture.Server.SetupGetPullRequestActivities( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetPullRequestActivitiesAsync_ReturnsActivities() + { + _fixture.Reset(); + _fixture.Server.SetupGetPullRequestActivities( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); - var activities = await client.GetPullRequestActivitiesAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); + var activities = await client.GetPullRequestActivitiesAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); - Assert.NotNull(activities); - var activityList = activities.ToList(); - Assert.Equal(2, activityList.Count); - } + Assert.NotNull(activities); + var activityList = activities.ToList(); + Assert.Equal(2, activityList.Count); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/DiffStreamingMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/DiffStreamingMockTests.cs new file mode 100644 index 0000000..88c198c --- /dev/null +++ b/test/Bitbucket.Net.Tests/MockTests/DiffStreamingMockTests.cs @@ -0,0 +1,209 @@ +using Bitbucket.Net.Tests.Infrastructure; +using Xunit; + +namespace Bitbucket.Net.Tests.MockTests; + +public class DiffStreamingMockTests(BitbucketMockFixture fixture) : IClassFixture +{ + private const string ApiBasePath = "/rest/api/1.0"; + private readonly BitbucketMockFixture _fixture = fixture; + + #region GetCommitDiffStreamAsync + + [Fact] + public async Task GetCommitDiffStreamAsync_SingleDiff_YieldsDiffEntry() + { + _fixture.Reset(); + _fixture.Server.SetupGetCommitDiff(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, TestConstants.TestCommitId); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetCommitDiffStreamAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, TestConstants.TestCommitId)); + + Assert.Single(results); + Assert.NotNull(results[0].Source); + Assert.NotNull(results[0].Destination); + Assert.NotNull(results[0].Hunks); + Assert.NotEmpty(results[0].Hunks!); + } + + [Fact] + public async Task GetCommitDiffStreamAsync_MultipleDiffs_YieldsAllEntries() + { + _fixture.Reset(); + _fixture.Server.SetupDiffEndpoint( + $"{ApiBasePath}/projects/{TestConstants.TestProjectKey}/repos/{TestConstants.TestRepositorySlug}/commits/{TestConstants.TestCommitId}/diff", + "diff-multiple.json"); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetCommitDiffStreamAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, TestConstants.TestCommitId)); + + Assert.Equal(3, results.Count); + } + + [Fact] + public async Task GetCommitDiffStreamAsync_EmptyDiffs_YieldsZeroItems() + { + _fixture.Reset(); + _fixture.Server.SetupDiffEndpoint( + $"{ApiBasePath}/projects/{TestConstants.TestProjectKey}/repos/{TestConstants.TestRepositorySlug}/commits/{TestConstants.TestCommitId}/diff", + "diff-empty.json"); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetCommitDiffStreamAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, TestConstants.TestCommitId)); + + Assert.Empty(results); + } + + #endregion + + #region GetRepositoryDiffStreamAsync + + [Fact] + public async Task GetRepositoryDiffStreamAsync_SingleDiff_YieldsDiffEntry() + { + _fixture.Reset(); + _fixture.Server.SetupGetRepositoryDiff(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetRepositoryDiffStreamAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, until: "HEAD")); + + Assert.Single(results); + Assert.NotNull(results[0].Hunks); + } + + [Fact] + public async Task GetRepositoryDiffStreamAsync_MultipleDiffs_YieldsAllEntries() + { + _fixture.Reset(); + _fixture.Server.SetupDiffEndpoint( + $"{ApiBasePath}/projects/{TestConstants.TestProjectKey}/repos/{TestConstants.TestRepositorySlug}/diff", + "diff-multiple.json"); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetRepositoryDiffStreamAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, until: "HEAD")); + + Assert.Equal(3, results.Count); + } + + [Fact] + public async Task GetRepositoryDiffStreamAsync_EmptyDiffs_YieldsZeroItems() + { + _fixture.Reset(); + _fixture.Server.SetupDiffEndpoint( + $"{ApiBasePath}/projects/{TestConstants.TestProjectKey}/repos/{TestConstants.TestRepositorySlug}/diff", + "diff-empty.json"); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetRepositoryDiffStreamAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, until: "HEAD")); + + Assert.Empty(results); + } + + #endregion + + #region GetRepositoryCompareDiffStreamAsync + + [Fact] + public async Task GetRepositoryCompareDiffStreamAsync_SingleDiff_YieldsDiffEntry() + { + _fixture.Reset(); + _fixture.Server.SetupGetCompareDiff(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetRepositoryCompareDiffStreamAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, from: "main", to: "feature")); + + Assert.Single(results); + } + + [Fact] + public async Task GetRepositoryCompareDiffStreamAsync_EmptyDiffs_YieldsZeroItems() + { + _fixture.Reset(); + _fixture.Server.SetupDiffEndpoint( + $"{ApiBasePath}/projects/{TestConstants.TestProjectKey}/repos/{TestConstants.TestRepositorySlug}/compare/diff", + "diff-empty.json"); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetRepositoryCompareDiffStreamAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, from: "main", to: "feature")); + + Assert.Empty(results); + } + + #endregion + + #region GetPullRequestDiffStreamAsync + + [Fact] + public async Task GetPullRequestDiffStreamAsync_SingleDiff_YieldsDiffEntry() + { + _fixture.Reset(); + _fixture.Server.SetupGetPullRequestDiff(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetPullRequestDiffStreamAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, TestConstants.TestPullRequestId)); + + Assert.Single(results); + Assert.NotNull(results[0].Source); + Assert.NotNull(results[0].Destination); + } + + [Fact] + public async Task GetPullRequestDiffStreamAsync_MultipleDiffs_YieldsAllEntries() + { + _fixture.Reset(); + _fixture.Server.SetupDiffEndpoint( + $"{ApiBasePath}/projects/{TestConstants.TestProjectKey}/repos/{TestConstants.TestRepositorySlug}/pull-requests/{TestConstants.TestPullRequestId}/diff", + "diff-multiple.json"); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetPullRequestDiffStreamAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, TestConstants.TestPullRequestId)); + + Assert.Equal(3, results.Count); + } + + [Fact] + public async Task GetPullRequestDiffStreamAsync_EmptyDiffs_YieldsZeroItems() + { + _fixture.Reset(); + _fixture.Server.SetupDiffEndpoint( + $"{ApiBasePath}/projects/{TestConstants.TestProjectKey}/repos/{TestConstants.TestRepositorySlug}/pull-requests/{TestConstants.TestPullRequestId}/diff", + "diff-empty.json"); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetPullRequestDiffStreamAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, TestConstants.TestPullRequestId)); + + Assert.Empty(results); + } + + #endregion + + #region Diff Content Validation + + [Fact] + public async Task GetPullRequestDiffStreamAsync_DiffContainsExpectedSegmentsAndHunks() + { + _fixture.Reset(); + _fixture.Server.SetupGetPullRequestDiff(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetPullRequestDiffStreamAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, TestConstants.TestPullRequestId)); + + var diff = Assert.Single(results); + var hunk = Assert.Single(diff.Hunks!); + var segment = Assert.Single(hunk.Segments!); + Assert.Equal("ADDED", segment.Type); + Assert.NotEmpty(segment.Lines!); + } + + #endregion + + private static async Task> CollectAsync(IAsyncEnumerable source) + { + var list = new List(); + await foreach (var item in source) + { + list.Add(item); + } + return list; + } +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/ErrorHandlingMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/ErrorHandlingMockTests.cs index d6e469b..eb3ff38 100644 --- a/test/Bitbucket.Net.Tests/MockTests/ErrorHandlingMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/ErrorHandlingMockTests.cs @@ -1,112 +1,206 @@ -using System.Net; -using System.Threading.Tasks; +using Bitbucket.Net.Common.Exceptions; +using Bitbucket.Net.Common.Models; using Bitbucket.Net.Tests.Infrastructure; -using Flurl.Http; +using System.Net; using Xunit; -namespace Bitbucket.Net.Tests.MockTests +namespace Bitbucket.Net.Tests.MockTests; + +/// +/// Tests that the typed exception hierarchy fires correctly for HTTP +/// error responses. +/// +public class ErrorHandlingMockTests(BitbucketMockFixture fixture) : IClassFixture { - /// - /// Unit tests for error handling using WireMock. - /// Verifies that appropriate exceptions are thrown for HTTP error responses. - /// - /// - /// NOTE: The current library implementation throws FlurlHttpException directly - /// rather than the documented BitbucketApiException types. This is because - /// Flurl throws before the custom error handling can intercept the response. - /// These tests verify the actual current behavior. - /// - public class ErrorHandlingMockTests : IClassFixture + private const string ApiBasePath = "/rest/api/1.0"; + private readonly BitbucketMockFixture _fixture = fixture; + + [Fact] + public async Task GetProjectAsync_WhenNotFound_ThrowsException() + { + // Arrange + _fixture.Reset(); + var projectKey = "NONEXISTENT"; + _fixture.Server.SetupNotFound($"{ApiBasePath}/projects/{projectKey}"); + var client = _fixture.CreateClient(); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => client.GetProjectAsync(projectKey)); + + Assert.Equal(HttpStatusCode.NotFound, exception.StatusCode); + Assert.Contains(projectKey, exception.RequestUrl ?? string.Empty); + } + + [Fact] + public async Task GetProjectAsync_WhenUnauthorized_ThrowsException() + { + // Arrange + _fixture.Reset(); + var projectKey = "TEST"; + _fixture.Server.SetupUnauthorized($"{ApiBasePath}/projects/{projectKey}"); + var client = _fixture.CreateClient(); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => client.GetProjectAsync(projectKey)); + + Assert.Equal(HttpStatusCode.Unauthorized, exception.StatusCode); + } + + [Fact] + public async Task GetProjectAsync_WhenServerError_ThrowsException() + { + // Arrange + _fixture.Reset(); + var projectKey = "TEST"; + _fixture.Server.SetupInternalServerError($"{ApiBasePath}/projects/{projectKey}"); + var client = _fixture.CreateClient(); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => client.GetProjectAsync(projectKey)); + + Assert.Equal(HttpStatusCode.InternalServerError, exception.StatusCode); + } + + [Fact] + public async Task GetProjectRepositoryAsync_WhenNotFound_ThrowsException() + { + // Arrange + _fixture.Reset(); + var projectKey = "TEST"; + var repoSlug = "nonexistent-repo"; + _fixture.Server.SetupNotFound($"{ApiBasePath}/projects/{projectKey}/repos/{repoSlug}"); + var client = _fixture.CreateClient(); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => client.GetProjectRepositoryAsync(projectKey, repoSlug)); + + Assert.Equal(HttpStatusCode.NotFound, exception.StatusCode); + } + + [Fact] + public async Task GetPullRequestAsync_WhenNotFound_ThrowsException() { - private const string ApiBasePath = "/rest/api/1.0"; - private readonly BitbucketMockFixture _fixture; - - public ErrorHandlingMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public async Task GetProjectAsync_WhenNotFound_ThrowsException() - { - // Arrange - _fixture.Reset(); - var projectKey = "NONEXISTENT"; - _fixture.Server.SetupNotFound($"{ApiBasePath}/projects/{projectKey}"); - var client = _fixture.CreateClient(); - - // Act & Assert - var exception = await Assert.ThrowsAsync( - () => client.GetProjectAsync(projectKey)); - - Assert.Equal((int)HttpStatusCode.NotFound, exception.StatusCode); - } - - [Fact] - public async Task GetProjectAsync_WhenUnauthorized_ThrowsException() - { - // Arrange - _fixture.Reset(); - var projectKey = "TEST"; - _fixture.Server.SetupUnauthorized($"{ApiBasePath}/projects/{projectKey}"); - var client = _fixture.CreateClient(); - - // Act & Assert - var exception = await Assert.ThrowsAsync( - () => client.GetProjectAsync(projectKey)); - - Assert.Equal((int)HttpStatusCode.Unauthorized, exception.StatusCode); - } - - [Fact] - public async Task GetProjectAsync_WhenServerError_ThrowsException() - { - // Arrange - _fixture.Reset(); - var projectKey = "TEST"; - _fixture.Server.SetupInternalServerError($"{ApiBasePath}/projects/{projectKey}"); - var client = _fixture.CreateClient(); - - // Act & Assert - var exception = await Assert.ThrowsAsync( - () => client.GetProjectAsync(projectKey)); - - Assert.Equal((int)HttpStatusCode.InternalServerError, exception.StatusCode); - } - - [Fact] - public async Task GetProjectRepositoryAsync_WhenNotFound_ThrowsException() - { - // Arrange - _fixture.Reset(); - var projectKey = "TEST"; - var repoSlug = "nonexistent-repo"; - _fixture.Server.SetupNotFound($"{ApiBasePath}/projects/{projectKey}/repos/{repoSlug}"); - var client = _fixture.CreateClient(); - - // Act & Assert - var exception = await Assert.ThrowsAsync( - () => client.GetProjectRepositoryAsync(projectKey, repoSlug)); - - Assert.Equal((int)HttpStatusCode.NotFound, exception.StatusCode); - } - - [Fact] - public async Task GetPullRequestAsync_WhenNotFound_ThrowsException() - { - // Arrange - _fixture.Reset(); - var projectKey = "TEST"; - var repoSlug = "test-repo"; - var pullRequestId = 99999L; - _fixture.Server.SetupNotFound($"{ApiBasePath}/projects/{projectKey}/repos/{repoSlug}/pull-requests/{pullRequestId}"); - var client = _fixture.CreateClient(); - - // Act & Assert - var exception = await Assert.ThrowsAsync( - () => client.GetPullRequestAsync(projectKey, repoSlug, pullRequestId)); - - Assert.Equal((int)HttpStatusCode.NotFound, exception.StatusCode); - } + // Arrange + _fixture.Reset(); + var projectKey = "TEST"; + var repoSlug = "test-repo"; + var pullRequestId = 99999L; + _fixture.Server.SetupNotFound($"{ApiBasePath}/projects/{projectKey}/repos/{repoSlug}/pull-requests/{pullRequestId}"); + var client = _fixture.CreateClient(); + + // Act & Assert + var exception = await Assert.ThrowsAsync( + () => client.GetPullRequestAsync(projectKey, repoSlug, pullRequestId)); + + Assert.Equal(HttpStatusCode.NotFound, exception.StatusCode); + } + + [Fact] + public async Task GetProjectAsync_WhenForbidden_ThrowsException() + { + _fixture.Reset(); + var projectKey = "TEST"; + _fixture.Server.SetupForbidden($"{ApiBasePath}/projects/{projectKey}"); + var client = _fixture.CreateClient(); + + var exception = await Assert.ThrowsAsync( + () => client.GetProjectAsync(projectKey)); + + Assert.Equal(HttpStatusCode.Forbidden, exception.StatusCode); + } + + [Fact] + public async Task GetProjectAsync_WhenBadRequest_ThrowsException() + { + _fixture.Reset(); + var projectKey = "TEST"; + _fixture.Server.SetupBadRequest($"{ApiBasePath}/projects/{projectKey}"); + var client = _fixture.CreateClient(); + + var exception = await Assert.ThrowsAsync( + () => client.GetProjectAsync(projectKey)); + + Assert.Equal(HttpStatusCode.BadRequest, exception.StatusCode); + } + + [Fact] + public async Task GetProjectAsync_WhenConflict_ThrowsException() + { + _fixture.Reset(); + var projectKey = "TEST"; + _fixture.Server.SetupConflict($"{ApiBasePath}/projects/{projectKey}"); + var client = _fixture.CreateClient(); + + var exception = await Assert.ThrowsAsync( + () => client.GetProjectAsync(projectKey)); + + Assert.Equal(HttpStatusCode.Conflict, exception.StatusCode); + } + + [Fact] + public async Task GetProjectAsync_WhenRateLimited_ThrowsException() + { + _fixture.Reset(); + var projectKey = "TEST"; + _fixture.Server.SetupRateLimited($"{ApiBasePath}/projects/{projectKey}"); + var client = _fixture.CreateClient(); + + var exception = await Assert.ThrowsAsync( + () => client.GetProjectAsync(projectKey)); + + Assert.Equal(HttpStatusCode.TooManyRequests, exception.StatusCode); + } + + [Fact] + public async Task GetProjectAsync_WhenErrorHasJsonBody_PopulatesErrorsAndContext() + { + _fixture.Reset(); + var projectKey = "CTX"; + var error = new Error { Context = "projectKey", Message = "Invalid project key", ExceptionName = "TestException" }; + _fixture.Server.SetupErrorWithJsonBody($"{ApiBasePath}/projects/{projectKey}", HttpStatusCode.NotFound, error); + var client = _fixture.CreateClient(); + + var exception = await Assert.ThrowsAsync( + () => client.GetProjectAsync(projectKey)); + + Assert.Equal(HttpStatusCode.NotFound, exception.StatusCode); + Assert.NotEmpty(exception.Errors); + Assert.Equal("projectKey", exception.Context); + } + + [Fact] + public async Task GetProjectAsync_WhenErrorHasHtmlBody_PreservesMessage() + { + _fixture.Reset(); + var projectKey = "HTML"; + var htmlBody = "Bad Gateway"; + _fixture.Server.SetupErrorWithHtmlBody($"{ApiBasePath}/projects/{projectKey}", HttpStatusCode.BadGateway, htmlBody); + var client = _fixture.CreateClient(); + + var exception = await Assert.ThrowsAsync( + () => client.GetProjectAsync(projectKey)); + + Assert.Equal(HttpStatusCode.BadGateway, exception.StatusCode); + Assert.Single(exception.Errors); + Assert.Contains("Bad Gateway", exception.Errors[0].Message); + } + + [Fact] + public async Task GetProjectAsync_WhenErrorHasEmptyBody_UsesEmptyErrors() + { + _fixture.Reset(); + var projectKey = "EMPTY"; + _fixture.Server.SetupErrorWithEmptyBody($"{ApiBasePath}/projects/{projectKey}", HttpStatusCode.InternalServerError); + var client = _fixture.CreateClient(); + + var exception = await Assert.ThrowsAsync( + () => client.GetProjectAsync(projectKey)); + + Assert.Equal(HttpStatusCode.InternalServerError, exception.StatusCode); + Assert.Empty(exception.Errors); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/GitAndTagMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/GitAndTagMockTests.cs index 91b7340..e7a8b15 100644 --- a/test/Bitbucket.Net.Tests/MockTests/GitAndTagMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/GitAndTagMockTests.cs @@ -1,90 +1,83 @@ -using System.Threading.Tasks; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests -{ - public class GitAndTagMockTests : IClassFixture - { - private readonly BitbucketMockFixture _fixture; +namespace Bitbucket.Net.Tests.MockTests; - public GitAndTagMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } +public class GitAndTagMockTests(BitbucketMockFixture fixture) : IClassFixture +{ + private readonly BitbucketMockFixture _fixture = fixture; - [Fact] - public async Task GetCanRebasePullRequestAsync_ReturnsCondition() - { - _fixture.Reset(); - _fixture.Server.SetupGetCanRebasePullRequest( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetCanRebasePullRequestAsync_ReturnsCondition() + { + _fixture.Reset(); + _fixture.Server.SetupGetCanRebasePullRequest( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); - var condition = await client.GetCanRebasePullRequestAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); + var condition = await client.GetCanRebasePullRequestAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); - Assert.NotNull(condition); - } + Assert.NotNull(condition); + } - [Fact] - public async Task RebasePullRequestAsync_RebasesSucessfully() - { - _fixture.Reset(); - _fixture.Server.SetupRebasePullRequest( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); + [Fact] + public async Task RebasePullRequestAsync_RebasesSucessfully() + { + _fixture.Reset(); + _fixture.Server.SetupRebasePullRequest( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); - var result = await client.RebasePullRequestAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId, - version: 1); + var result = await client.RebasePullRequestAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId, + version: 1); - Assert.NotNull(result); - } + Assert.NotNull(result); + } - [Fact] - public async Task CreateTagAsync_CreatesTag() - { - _fixture.Reset(); - _fixture.Server.SetupCreateTag( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); + [Fact] + public async Task CreateTagAsync_CreatesTag() + { + _fixture.Reset(); + _fixture.Server.SetupCreateTag( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); - var tag = await client.CreateTagAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - Bitbucket.Net.Models.Git.TagTypes.LightWeight, - "v1.0.0", - "abc123"); + var tag = await client.CreateTagAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + Models.Git.TagTypes.LightWeight, + "v1.0.0", + "abc123"); - Assert.NotNull(tag); - } + Assert.NotNull(tag); + } - [Fact] - public async Task DeleteTagAsync_DeletesTag() - { - _fixture.Reset(); - _fixture.Server.SetupDeleteTag( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - "v1.0.0"); - var client = _fixture.CreateClient(); + [Fact] + public async Task DeleteTagAsync_DeletesTag() + { + _fixture.Reset(); + _fixture.Server.SetupDeleteTag( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + "v1.0.0"); + var client = _fixture.CreateClient(); - var result = await client.DeleteTagAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - "v1.0.0"); + var result = await client.DeleteTagAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + "v1.0.0"); - Assert.True(result); - } + Assert.True(result); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/GitMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/GitMockTests.cs index 4c2e10d..b45f2de 100644 --- a/test/Bitbucket.Net.Tests/MockTests/GitMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/GitMockTests.cs @@ -1,74 +1,67 @@ -using System.Threading.Tasks; using Bitbucket.Net.Models.Git; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests -{ - public class GitMockTests : IClassFixture - { - private readonly BitbucketMockFixture _fixture; - private const string ProjectKey = "TEST"; - private const string RepoSlug = "test-repo"; - private const long PullRequestId = 1; +namespace Bitbucket.Net.Tests.MockTests; - public GitMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } +public class GitMockTests(BitbucketMockFixture fixture) : IClassFixture +{ + private readonly BitbucketMockFixture _fixture = fixture; + private const string ProjectKey = "TEST"; + private const string RepoSlug = "test-repo"; + private const long PullRequestId = 1; - [Fact] - public async Task GetCanRebasePullRequestAsync_ReturnsCondition() - { - _fixture.Reset(); - _fixture.Server.SetupGetCanRebasePullRequest(ProjectKey, RepoSlug, PullRequestId); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetCanRebasePullRequestAsync_ReturnsCondition() + { + _fixture.Reset(); + _fixture.Server.SetupGetCanRebasePullRequest(ProjectKey, RepoSlug, PullRequestId); + var client = _fixture.CreateClient(); - var result = await client.GetCanRebasePullRequestAsync(ProjectKey, RepoSlug, PullRequestId); + var result = await client.GetCanRebasePullRequestAsync(ProjectKey, RepoSlug, PullRequestId); - Assert.NotNull(result); - Assert.True(result.CanRebase); - Assert.NotNull(result.Vetoes); - Assert.Empty(result.Vetoes); - } + Assert.NotNull(result); + Assert.True(result.CanRebase); + Assert.NotNull(result.Vetoes); + Assert.Empty(result.Vetoes); + } - [Fact] - public async Task RebasePullRequestAsync_ReturnsUpdatedPullRequest() - { - _fixture.Reset(); - _fixture.Server.SetupRebasePullRequest(ProjectKey, RepoSlug, PullRequestId); - var client = _fixture.CreateClient(); + [Fact] + public async Task RebasePullRequestAsync_ReturnsUpdatedPullRequest() + { + _fixture.Reset(); + _fixture.Server.SetupRebasePullRequest(ProjectKey, RepoSlug, PullRequestId); + var client = _fixture.CreateClient(); - var result = await client.RebasePullRequestAsync(ProjectKey, RepoSlug, PullRequestId, version: 1); + var result = await client.RebasePullRequestAsync(ProjectKey, RepoSlug, PullRequestId, version: 1); - Assert.NotNull(result); - Assert.Equal(1, result.Id); - } + Assert.NotNull(result); + Assert.Equal(1, result.Id); + } - [Fact] - public async Task CreateTagAsync_ReturnsCreatedTag() - { - _fixture.Reset(); - _fixture.Server.SetupCreateTag(ProjectKey, RepoSlug); - var client = _fixture.CreateClient(); + [Fact] + public async Task CreateTagAsync_ReturnsCreatedTag() + { + _fixture.Reset(); + _fixture.Server.SetupCreateTag(ProjectKey, RepoSlug); + var client = _fixture.CreateClient(); - var result = await client.CreateTagAsync(ProjectKey, RepoSlug, TagTypes.Annotated, "v1.0.0", "abc123"); + var result = await client.CreateTagAsync(ProjectKey, RepoSlug, TagTypes.Annotated, "v1.0.0", "abc123"); - Assert.NotNull(result); - Assert.Equal("v1.0.0", result.DisplayId); - Assert.Equal("refs/tags/v1.0.0", result.Id); - } + Assert.NotNull(result); + Assert.Equal("v1.0.0", result.DisplayId); + Assert.Equal("refs/tags/v1.0.0", result.Id); + } - [Fact] - public async Task DeleteTagAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupDeleteTag(ProjectKey, RepoSlug, "v1.0.0"); - var client = _fixture.CreateClient(); + [Fact] + public async Task DeleteTagAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupDeleteTag(ProjectKey, RepoSlug, "v1.0.0"); + var client = _fixture.CreateClient(); - var result = await client.DeleteTagAsync(ProjectKey, RepoSlug, "v1.0.0"); + var result = await client.DeleteTagAsync(ProjectKey, RepoSlug, "v1.0.0"); - Assert.True(result); - } + Assert.True(result); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/GroupsMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/GroupsMockTests.cs index 76f327a..8ec8b2b 100644 --- a/test/Bitbucket.Net.Tests/MockTests/GroupsMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/GroupsMockTests.cs @@ -1,34 +1,26 @@ -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests -{ - public class GroupsMockTests : IClassFixture - { - private readonly BitbucketMockFixture _fixture; +namespace Bitbucket.Net.Tests.MockTests; - public GroupsMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } +public class GroupsMockTests(BitbucketMockFixture fixture) : IClassFixture +{ + private readonly BitbucketMockFixture _fixture = fixture; - [Fact] - public async Task GetGroupNamesAsync_ReturnsGroupNames() - { - _fixture.Reset(); - _fixture.Server.SetupGetGroupNames(); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetGroupNamesAsync_ReturnsGroupNames() + { + _fixture.Reset(); + _fixture.Server.SetupGetGroupNames(); + var client = _fixture.CreateClient(); - var groups = await client.GetGroupNamesAsync(); + var groups = await client.GetGroupNamesAsync(); - var groupList = groups.ToList(); - Assert.NotEmpty(groupList); - Assert.Equal(3, groupList.Count); - Assert.Equal("developers", groupList[0]); - Assert.Equal("administrators", groupList[1]); - Assert.Equal("testers", groupList[2]); - } + var groupList = groups.ToList(); + Assert.NotEmpty(groupList); + Assert.Equal(3, groupList.Count); + Assert.Equal("developers", groupList[0]); + Assert.Equal("administrators", groupList[1]); + Assert.Equal("testers", groupList[2]); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/HooksMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/HooksMockTests.cs index 9e28ada..a07135a 100644 --- a/test/Bitbucket.Net.Tests/MockTests/HooksMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/HooksMockTests.cs @@ -1,42 +1,35 @@ -using System.Threading.Tasks; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests +namespace Bitbucket.Net.Tests.MockTests; + +public class HooksMockTests(BitbucketMockFixture fixture) : IClassFixture { - public class HooksMockTests : IClassFixture + private readonly BitbucketMockFixture _fixture = fixture; + + [Fact] + public async Task GetProjectHooksAvatarAsync_ReturnsBytes() { - private readonly BitbucketMockFixture _fixture; - - public HooksMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public async Task GetProjectHooksAvatarAsync_ReturnsBytes() - { - _fixture.Reset(); - _fixture.Server.SetupGetProjectHooksAvatar("com.example.myhook"); - var client = _fixture.CreateClient(); - - var avatar = await client.GetProjectHooksAvatarAsync("com.example.myhook"); - - Assert.NotNull(avatar); - Assert.True(avatar.Length > 0); - } - - [Fact] - public async Task GetProjectHooksAvatarAsync_WithVersion_ReturnsBytes() - { - _fixture.Reset(); - _fixture.Server.SetupGetProjectHooksAvatar("com.example.myhook"); - var client = _fixture.CreateClient(); - - var avatar = await client.GetProjectHooksAvatarAsync("com.example.myhook", version: "1.0.0"); - - Assert.NotNull(avatar); - Assert.True(avatar.Length > 0); - } + _fixture.Reset(); + _fixture.Server.SetupGetProjectHooksAvatar("com.example.myhook"); + var client = _fixture.CreateClient(); + + var avatar = await client.GetProjectHooksAvatarAsync("com.example.myhook"); + + Assert.NotNull(avatar); + Assert.True(avatar.Length > 0); + } + + [Fact] + public async Task GetProjectHooksAvatarAsync_WithVersion_ReturnsBytes() + { + _fixture.Reset(); + _fixture.Server.SetupGetProjectHooksAvatar("com.example.myhook"); + var client = _fixture.CreateClient(); + + var avatar = await client.GetProjectHooksAvatarAsync("com.example.myhook", version: "1.0.0"); + + Assert.NotNull(avatar); + Assert.True(avatar.Length > 0); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/InboxMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/InboxMockTests.cs index 74caf65..786e203 100644 --- a/test/Bitbucket.Net.Tests/MockTests/InboxMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/InboxMockTests.cs @@ -1,44 +1,36 @@ -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests +namespace Bitbucket.Net.Tests.MockTests; + +public class InboxMockTests(BitbucketMockFixture fixture) : IClassFixture { - public class InboxMockTests : IClassFixture + private readonly BitbucketMockFixture _fixture = fixture; + + [Fact] + public async Task GetInboxPullRequestsAsync_ReturnsPullRequests() { - private readonly BitbucketMockFixture _fixture; - - public InboxMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public async Task GetInboxPullRequestsAsync_ReturnsPullRequests() - { - _fixture.Reset(); - _fixture.Server.SetupGetInboxPullRequests(); - var client = _fixture.CreateClient(); - - var result = await client.GetInboxPullRequestsAsync(); - - Assert.NotNull(result); - var pullRequests = result.ToList(); - Assert.Single(pullRequests); - Assert.Equal("Inbox PR Title", pullRequests[0].Title); - } - - [Fact] - public async Task GetInboxPullRequestsCountAsync_ReturnsCount() - { - _fixture.Reset(); - _fixture.Server.SetupGetInboxPullRequestsCount(); - var client = _fixture.CreateClient(); - - var result = await client.GetInboxPullRequestsCountAsync(); - - Assert.Equal(5, result); - } + _fixture.Reset(); + _fixture.Server.SetupGetInboxPullRequests(); + var client = _fixture.CreateClient(); + + var result = await client.GetInboxPullRequestsAsync(); + + Assert.NotNull(result); + var pullRequests = result.ToList(); + Assert.Single(pullRequests); + Assert.Equal("Inbox PR Title", pullRequests[0].Title); + } + + [Fact] + public async Task GetInboxPullRequestsCountAsync_ReturnsCount() + { + _fixture.Reset(); + _fixture.Server.SetupGetInboxPullRequestsCount(); + var client = _fixture.CreateClient(); + + var result = await client.GetInboxPullRequestsCountAsync(); + + Assert.Equal(5, result); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/JiraMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/JiraMockTests.cs index ccbd2cb..853cfe5 100644 --- a/test/Bitbucket.Net.Tests/MockTests/JiraMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/JiraMockTests.cs @@ -1,68 +1,60 @@ -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests -{ - public class JiraMockTests : IClassFixture - { - private readonly BitbucketMockFixture _fixture; - private const string ProjectKey = "PROJ"; - private const string RepoSlug = "repo"; - - public JiraMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } +namespace Bitbucket.Net.Tests.MockTests; - [Fact] - public async Task GetJiraIssuesAsync_ReturnsIssues() - { - _fixture.Reset(); - _fixture.Server.SetupGetJiraIssues(ProjectKey, RepoSlug, 1); - var client = _fixture.CreateClient(); - - var result = await client.GetJiraIssuesAsync(ProjectKey, RepoSlug, 1); - - Assert.NotNull(result); - var issues = result.ToList(); - Assert.Equal(2, issues.Count); - Assert.Equal("PROJ-123", issues[0].Key); - Assert.Equal("https://jira.example.com/browse/PROJ-123", issues[0].Url); - Assert.Equal("PROJ-456", issues[1].Key); - } +public class JiraMockTests(BitbucketMockFixture fixture) : IClassFixture +{ + private readonly BitbucketMockFixture _fixture = fixture; + private const string ProjectKey = "PROJ"; + private const string RepoSlug = "repo"; - [Fact] - public async Task CreateJiraIssueAsync_ReturnsCreatedIssue() - { - _fixture.Reset(); - _fixture.Server.SetupCreateJiraIssue(CommentId); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetJiraIssuesAsync_ReturnsIssues() + { + _fixture.Reset(); + _fixture.Server.SetupGetJiraIssues(ProjectKey, RepoSlug, 1); + var client = _fixture.CreateClient(); + + var result = await client.GetJiraIssuesAsync(ProjectKey, RepoSlug, 1); + + Assert.NotNull(result); + var issues = result.ToList(); + Assert.Equal(2, issues.Count); + Assert.Equal("PROJ-123", issues[0].Key); + Assert.Equal("https://jira.example.com/browse/PROJ-123", issues[0].Url); + Assert.Equal("PROJ-456", issues[1].Key); + } - var result = await client.CreateJiraIssueAsync(CommentId, "app-id", "Test Issue", "Bug"); + [Fact] + public async Task CreateJiraIssueAsync_ReturnsCreatedIssue() + { + _fixture.Reset(); + _fixture.Server.SetupCreateJiraIssue(CommentId); + var client = _fixture.CreateClient(); - Assert.NotNull(result); - Assert.Equal(100, result.CommentId); - Assert.Equal("PROJ-789", result.IssueKey); - } + var result = await client.CreateJiraIssueAsync(CommentId, "app-id", "Test Issue", "Bug"); - [Fact] - public async Task GetChangeSetsAsync_ReturnsChangeSets() - { - _fixture.Reset(); - _fixture.Server.SetupGetChangeSets("PROJ-123"); - var client = _fixture.CreateClient(); + Assert.NotNull(result); + Assert.Equal(100, result.CommentId); + Assert.Equal("PROJ-789", result.IssueKey); + } - var result = await client.GetChangeSetsAsync("PROJ-123"); + [Fact] + public async Task GetChangeSetsAsync_ReturnsChangeSets() + { + _fixture.Reset(); + _fixture.Server.SetupGetChangeSets("PROJ-123"); + var client = _fixture.CreateClient(); - Assert.NotNull(result); - var changeSets = result.ToList(); - Assert.Single(changeSets); - Assert.NotNull(changeSets[0].ToCommit); - Assert.Equal("def456abc789", changeSets[0].ToCommit.Id); - } + var result = await client.GetChangeSetsAsync("PROJ-123"); - private const string CommentId = "100"; + Assert.NotNull(result); + var changeSets = result.ToList(); + Assert.Single(changeSets); + Assert.NotNull(changeSets[0].ToCommit); + Assert.Equal("def456abc789", changeSets[0].ToCommit!.Id); } -} + + private const string CommentId = "100"; +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/LogsMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/LogsMockTests.cs index 27e2b21..cfe5f15 100644 --- a/test/Bitbucket.Net.Tests/MockTests/LogsMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/LogsMockTests.cs @@ -1,65 +1,58 @@ -using System.Threading.Tasks; using Bitbucket.Net.Models.Core.Logs; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests -{ - public class LogsMockTests : IClassFixture - { - private readonly BitbucketMockFixture _fixture; +namespace Bitbucket.Net.Tests.MockTests; - public LogsMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } +public class LogsMockTests(BitbucketMockFixture fixture) : IClassFixture +{ + private readonly BitbucketMockFixture _fixture = fixture; - [Fact] - public async Task GetLogLevelAsync_ReturnsLogLevel() - { - _fixture.Reset(); - _fixture.Server.SetupGetLogLevel("com.atlassian.bitbucket"); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetLogLevelAsync_ReturnsLogLevel() + { + _fixture.Reset(); + _fixture.Server.SetupGetLogLevel("com.atlassian.bitbucket"); + var client = _fixture.CreateClient(); - var logLevel = await client.GetLogLevelAsync("com.atlassian.bitbucket"); + var logLevel = await client.GetLogLevelAsync("com.atlassian.bitbucket"); - Assert.Equal(LogLevels.Debug, logLevel); - } + Assert.Equal(LogLevels.Debug, logLevel); + } - [Fact] - public async Task SetLogLevelAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupSetLogLevel("com.atlassian.bitbucket", "INFO"); - var client = _fixture.CreateClient(); + [Fact] + public async Task SetLogLevelAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupSetLogLevel("com.atlassian.bitbucket", "INFO"); + var client = _fixture.CreateClient(); - var result = await client.SetLogLevelAsync("com.atlassian.bitbucket", LogLevels.Info); + var result = await client.SetLogLevelAsync("com.atlassian.bitbucket", LogLevels.Info); - Assert.True(result); - } + Assert.True(result); + } - [Fact] - public async Task GetRootLogLevelAsync_ReturnsLogLevel() - { - _fixture.Reset(); - _fixture.Server.SetupGetRootLogLevel(); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetRootLogLevelAsync_ReturnsLogLevel() + { + _fixture.Reset(); + _fixture.Server.SetupGetRootLogLevel(); + var client = _fixture.CreateClient(); - var logLevel = await client.GetRootLogLevelAsync(); + var logLevel = await client.GetRootLogLevelAsync(); - Assert.Equal(LogLevels.Debug, logLevel); - } + Assert.Equal(LogLevels.Debug, logLevel); + } - [Fact] - public async Task SetRootLogLevelAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupSetRootLogLevel("WARN"); - var client = _fixture.CreateClient(); + [Fact] + public async Task SetRootLogLevelAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupSetRootLogLevel("WARN"); + var client = _fixture.CreateClient(); - var result = await client.SetRootLogLevelAsync(LogLevels.Warn); + var result = await client.SetRootLogLevelAsync(LogLevels.Warn); - Assert.True(result); - } + Assert.True(result); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/MarkupMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/MarkupMockTests.cs index d2bd7e2..bd3ba2f 100644 --- a/test/Bitbucket.Net.Tests/MockTests/MarkupMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/MarkupMockTests.cs @@ -1,29 +1,22 @@ -using System.Threading.Tasks; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests -{ - public class MarkupMockTests : IClassFixture - { - private readonly BitbucketMockFixture _fixture; +namespace Bitbucket.Net.Tests.MockTests; - public MarkupMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } +public class MarkupMockTests(BitbucketMockFixture fixture) : IClassFixture +{ + private readonly BitbucketMockFixture _fixture = fixture; - [Fact] - public async Task PreviewMarkupAsync_ReturnsHtml() - { - _fixture.Reset(); - _fixture.Server.SetupPreviewMarkup(); - var client = _fixture.CreateClient(); + [Fact] + public async Task PreviewMarkupAsync_ReturnsHtml() + { + _fixture.Reset(); + _fixture.Server.SetupPreviewMarkup(); + var client = _fixture.CreateClient(); - var result = await client.PreviewMarkupAsync("**Bold** text"); + var result = await client.PreviewMarkupAsync("**Bold** text"); - Assert.NotNull(result); - Assert.Contains("markdown", result); - } + Assert.NotNull(result); + Assert.Contains("markdown", result); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/PersonalAccessTokensMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/PersonalAccessTokensMockTests.cs index cd68d3b..2078788 100644 --- a/test/Bitbucket.Net.Tests/MockTests/PersonalAccessTokensMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/PersonalAccessTokensMockTests.cs @@ -1,103 +1,94 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Models.Core.Admin; using Bitbucket.Net.Models.PersonalAccessTokens; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests +namespace Bitbucket.Net.Tests.MockTests; + +public class PersonalAccessTokensMockTests(BitbucketMockFixture fixture) : IClassFixture { - public class PersonalAccessTokensMockTests : IClassFixture - { - private readonly BitbucketMockFixture _fixture; - private const string UserSlug = "admin"; - private const string TokenId = "token1"; + private readonly BitbucketMockFixture _fixture = fixture; + private const string UserSlug = "admin"; + private const string TokenId = "token1"; - public PersonalAccessTokensMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } + [Fact] + public async Task GetUserAccessTokensAsync_ReturnsTokens() + { + _fixture.Reset(); + _fixture.Server.SetupGetUserAccessTokens(UserSlug); + var client = _fixture.CreateClient(); - [Fact(Skip = "Permissions List converter mismatch - uses JsonEnumConverter instead of JsonEnumListConverter")] - public async Task GetUserAccessTokensAsync_ReturnsTokens() - { - _fixture.Reset(); - _fixture.Server.SetupGetUserAccessTokens(UserSlug); - var client = _fixture.CreateClient(); + var result = await client.GetUserAccessTokensAsync(UserSlug); - var result = await client.GetUserAccessTokensAsync(UserSlug); + Assert.NotNull(result); + var tokens = result.ToList(); + Assert.Equal(2, tokens.Count); + Assert.Equal("token1", tokens[0].Id); + Assert.Equal("API Token", tokens[0].Name); + } - Assert.NotNull(result); - var tokens = result.ToList(); - Assert.Equal(2, tokens.Count); - Assert.Equal("token1", tokens[0].Id); - Assert.Equal("API Token", tokens[0].Name); - } + [Fact] + public async Task GetUserAccessTokenAsync_ReturnsToken() + { + _fixture.Reset(); + _fixture.Server.SetupGetUserAccessToken(UserSlug, TokenId); + var client = _fixture.CreateClient(); - [Fact(Skip = "Permissions List converter mismatch - uses JsonEnumConverter instead of JsonEnumListConverter")] - public async Task GetUserAccessTokenAsync_ReturnsToken() - { - _fixture.Reset(); - _fixture.Server.SetupGetUserAccessToken(UserSlug, TokenId); - var client = _fixture.CreateClient(); + var result = await client.GetUserAccessTokenAsync(UserSlug, TokenId); - var result = await client.GetUserAccessTokenAsync(UserSlug, TokenId); + Assert.NotNull(result); + Assert.Equal("token1", result.Id); + Assert.Equal("API Token", result.Name); + } - Assert.NotNull(result); - Assert.Equal("token1", result.Id); - Assert.Equal("API Token", result.Name); - } + [Fact] + public async Task CreateAccessTokenAsync_ReturnsCreatedToken() + { + _fixture.Reset(); + _fixture.Server.SetupCreateAccessToken(UserSlug); + var client = _fixture.CreateClient(); - [Fact(Skip = "Permissions enum serialization issue with source generators")] - public async Task CreateAccessTokenAsync_ReturnsCreatedToken() + var tokenCreate = new AccessTokenCreate { - _fixture.Reset(); - _fixture.Server.SetupCreateAccessToken(UserSlug); - var client = _fixture.CreateClient(); + Name = "New API Token", + Permissions = [Permissions.ProjectRead, Permissions.RepoRead] + }; - var tokenCreate = new AccessTokenCreate - { - Name = "New API Token", - Permissions = new List { Permissions.ProjectRead, Permissions.RepoRead } - }; + var result = await client.CreateAccessTokenAsync(UserSlug, tokenCreate); - var result = await client.CreateAccessTokenAsync(UserSlug, tokenCreate); + Assert.NotNull(result); + Assert.Equal("token1", result.Id); + Assert.NotNull(result.Token); + } - Assert.NotNull(result); - Assert.Equal("token1", result.Id); - Assert.NotNull(result.Token); - } + [Fact] + public async Task ChangeUserAccessTokenAsync_ReturnsUpdatedToken() + { + _fixture.Reset(); + _fixture.Server.SetupChangeUserAccessToken(UserSlug, TokenId); + var client = _fixture.CreateClient(); - [Fact(Skip = "Permissions enum serialization issue with source generators")] - public async Task ChangeUserAccessTokenAsync_ReturnsUpdatedToken() + var tokenUpdate = new AccessTokenCreate { - _fixture.Reset(); - _fixture.Server.SetupChangeUserAccessToken(UserSlug, TokenId); - var client = _fixture.CreateClient(); + Name = "Updated API Token", + Permissions = [Permissions.ProjectAdmin, Permissions.RepoAdmin] + }; - var tokenUpdate = new AccessTokenCreate - { - Name = "Updated API Token", - Permissions = new List { Permissions.ProjectAdmin, Permissions.RepoAdmin } - }; + var result = await client.ChangeUserAccessTokenAsync(UserSlug, TokenId, tokenUpdate); - var result = await client.ChangeUserAccessTokenAsync(UserSlug, TokenId, tokenUpdate); - - Assert.NotNull(result); - Assert.Equal("token1", result.Id); - } + Assert.NotNull(result); + Assert.Equal("token1", result.Id); + } - [Fact] - public async Task DeleteUserAccessTokenAsync_ReturnsSuccess() - { - _fixture.Reset(); - _fixture.Server.SetupDeleteUserAccessToken(UserSlug, TokenId); - var client = _fixture.CreateClient(); + [Fact] + public async Task DeleteUserAccessTokenAsync_ReturnsSuccess() + { + _fixture.Reset(); + _fixture.Server.SetupDeleteUserAccessToken(UserSlug, TokenId); + var client = _fixture.CreateClient(); - var result = await client.DeleteUserAccessTokenAsync(UserSlug, TokenId); + var result = await client.DeleteUserAccessTokenAsync(UserSlug, TokenId); - Assert.True(result); - } + Assert.True(result); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/ProfileMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/ProfileMockTests.cs index b71eee3..0091453 100644 --- a/test/Bitbucket.Net.Tests/MockTests/ProfileMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/ProfileMockTests.cs @@ -1,33 +1,25 @@ -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests -{ - public class ProfileMockTests : IClassFixture - { - private readonly BitbucketMockFixture _fixture; +namespace Bitbucket.Net.Tests.MockTests; - public ProfileMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } +public class ProfileMockTests(BitbucketMockFixture fixture) : IClassFixture +{ + private readonly BitbucketMockFixture _fixture = fixture; - [Fact] - public async Task GetRecentReposAsync_ReturnsRepositories() - { - _fixture.Reset(); - _fixture.Server.SetupGetRecentRepos(); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetRecentReposAsync_ReturnsRepositories() + { + _fixture.Reset(); + _fixture.Server.SetupGetRecentRepos(); + var client = _fixture.CreateClient(); - var result = await client.GetRecentReposAsync(); + var result = await client.GetRecentReposAsync(); - Assert.NotNull(result); - var repos = result.ToList(); - Assert.Single(repos); - Assert.Equal("recent-repo", repos[0].Slug); - Assert.Equal("Recent Repository", repos[0].Name); - } + Assert.NotNull(result); + var repos = result.ToList(); + Assert.Single(repos); + Assert.Equal("recent-repo", repos[0].Slug); + Assert.Equal("Recent Repository", repos[0].Name); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/ProjectCrudMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/ProjectCrudMockTests.cs index d198987..3a2ac0b 100644 --- a/test/Bitbucket.Net.Tests/MockTests/ProjectCrudMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/ProjectCrudMockTests.cs @@ -1,69 +1,62 @@ -using System.Threading.Tasks; using Bitbucket.Net.Models.Core.Projects; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests +namespace Bitbucket.Net.Tests.MockTests; + +public class ProjectCrudMockTests(BitbucketMockFixture fixture) : IClassFixture { - public class ProjectCrudMockTests : IClassFixture - { - private readonly BitbucketMockFixture _fixture; + private readonly BitbucketMockFixture _fixture = fixture; - public ProjectCrudMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } + [Fact] + public async Task CreateProjectAsync_ReturnsCreatedProject() + { + _fixture.Reset(); + _fixture.Server.SetupCreateProject(); + var client = _fixture.CreateClient(); - [Fact] - public async Task CreateProjectAsync_ReturnsCreatedProject() + var projectDef = new ProjectDefinition { - _fixture.Reset(); - _fixture.Server.SetupCreateProject(); - var client = _fixture.CreateClient(); + Key = TestConstants.TestProjectKey, + Name = TestConstants.TestProjectName, + Description = "Created by unit test" + }; - var projectDef = new ProjectDefinition - { - Key = TestConstants.TestProjectKey, - Name = TestConstants.TestProjectName, - Description = "Created by unit test" - }; + var project = await client.CreateProjectAsync(projectDef); - var project = await client.CreateProjectAsync(projectDef); + Assert.NotNull(project); + Assert.Equal(TestConstants.TestProjectKey, project.Key); + Assert.Equal(TestConstants.TestProjectName, project.Name); + } - Assert.NotNull(project); - Assert.Equal(TestConstants.TestProjectKey, project.Key); - Assert.Equal(TestConstants.TestProjectName, project.Name); - } + [Fact] + public async Task UpdateProjectAsync_ReturnsUpdatedProject() + { + _fixture.Reset(); + _fixture.Server.SetupUpdateProject(TestConstants.TestProjectKey); + var client = _fixture.CreateClient(); - [Fact] - public async Task UpdateProjectAsync_ReturnsUpdatedProject() + var projectDef = new ProjectDefinition { - _fixture.Reset(); - _fixture.Server.SetupUpdateProject(TestConstants.TestProjectKey); - var client = _fixture.CreateClient(); - - var projectDef = new ProjectDefinition - { - Name = "Updated Name", - Description = "Updated by unit test" - }; + Name = "Updated Name", + Description = "Updated by unit test" + }; - var project = await client.UpdateProjectAsync(TestConstants.TestProjectKey, projectDef); + var project = await client.UpdateProjectAsync(TestConstants.TestProjectKey, projectDef); - Assert.NotNull(project); - Assert.Equal(TestConstants.TestProjectKey, project.Key); - } + Assert.NotNull(project); + Assert.Equal(TestConstants.TestProjectKey, project.Key); + } - [Fact] - public async Task DeleteProjectAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupDeleteProject(TestConstants.TestProjectKey); - var client = _fixture.CreateClient(); + [Fact] + public async Task DeleteProjectAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupDeleteProject(TestConstants.TestProjectKey); + var client = _fixture.CreateClient(); - var result = await client.DeleteProjectAsync(TestConstants.TestProjectKey); + var result = await client.DeleteProjectAsync(TestConstants.TestProjectKey); - Assert.True(result); - } + Assert.True(result); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/ProjectMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/ProjectMockTests.cs index cf7de5d..01fae1f 100644 --- a/test/Bitbucket.Net.Tests/MockTests/ProjectMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/ProjectMockTests.cs @@ -1,78 +1,70 @@ -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests -{ - /// - /// Unit tests for project-related operations using WireMock. - /// - public class ProjectMockTests : IClassFixture - { - private readonly BitbucketMockFixture _fixture; +namespace Bitbucket.Net.Tests.MockTests; - public ProjectMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } +/// +/// Unit tests for project-related operations using WireMock. +/// +public class ProjectMockTests(BitbucketMockFixture fixture) : IClassFixture +{ + private readonly BitbucketMockFixture _fixture = fixture; - [Fact] - public async Task GetProjectsAsync_ReturnsProjects() - { - // Arrange - _fixture.Reset(); - _fixture.Server.SetupGetProjects(); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetProjectsAsync_ReturnsProjects() + { + // Arrange + _fixture.Reset(); + _fixture.Server.SetupGetProjects(); + var client = _fixture.CreateClient(); - // Act - var projects = await client.GetProjectsAsync(); + // Act + var projects = await client.GetProjectsAsync(); - // Assert - Assert.NotNull(projects); - var projectList = projects.ToList(); - Assert.Single(projectList); - var project = projectList[0]; - Assert.Equal(TestConstants.TestProjectKey, project.Key); - Assert.Equal(TestConstants.TestProjectName, project.Name); - } + // Assert + Assert.NotNull(projects); + var projectList = projects.ToList(); + Assert.Single(projectList); + var project = projectList[0]; + Assert.Equal(TestConstants.TestProjectKey, project.Key); + Assert.Equal(TestConstants.TestProjectName, project.Name); + } - [Fact] - public async Task GetProjectAsync_WithValidKey_ReturnsProject() - { - // Arrange - _fixture.Reset(); - _fixture.Server.SetupGetProject(TestConstants.TestProjectKey); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetProjectAsync_WithValidKey_ReturnsProject() + { + // Arrange + _fixture.Reset(); + _fixture.Server.SetupGetProject(TestConstants.TestProjectKey); + var client = _fixture.CreateClient(); - // Act - var project = await client.GetProjectAsync(TestConstants.TestProjectKey); + // Act + var project = await client.GetProjectAsync(TestConstants.TestProjectKey); - // Assert - Assert.NotNull(project); - Assert.Equal(TestConstants.TestProjectKey, project.Key); - Assert.Equal(TestConstants.TestProjectName, project.Name); - Assert.NotNull(project.Description); - } + // Assert + Assert.NotNull(project); + Assert.Equal(TestConstants.TestProjectKey, project.Key); + Assert.Equal(TestConstants.TestProjectName, project.Name); + Assert.NotNull(project.Description); + } - [Fact] - public async Task GetProjectRepositoriesAsync_ReturnsRepositories() - { - // Arrange - _fixture.Reset(); - _fixture.Server.SetupGetRepositories(TestConstants.TestProjectKey); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetProjectRepositoriesAsync_ReturnsRepositories() + { + // Arrange + _fixture.Reset(); + _fixture.Server.SetupGetRepositories(TestConstants.TestProjectKey); + var client = _fixture.CreateClient(); - // Act - var repositories = await client.GetProjectRepositoriesAsync(TestConstants.TestProjectKey); + // Act + var repositories = await client.GetProjectRepositoriesAsync(TestConstants.TestProjectKey); - // Assert - Assert.NotNull(repositories); - var repoList = repositories.ToList(); - Assert.Single(repoList); - var repo = repoList[0]; - Assert.Equal(TestConstants.TestRepositorySlug, repo.Slug); - Assert.Equal(TestConstants.TestRepositoryName, repo.Name); - } + // Assert + Assert.NotNull(repositories); + var repoList = repositories.ToList(); + Assert.Single(repoList); + var repo = repoList[0]; + Assert.Equal(TestConstants.TestRepositorySlug, repo.Slug); + Assert.Equal(TestConstants.TestRepositoryName, repo.Name); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/ProjectPermissionsMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/ProjectPermissionsMockTests.cs index 2d62531..a29a22a 100644 --- a/test/Bitbucket.Net.Tests/MockTests/ProjectPermissionsMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/ProjectPermissionsMockTests.cs @@ -1,171 +1,162 @@ -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Models.Core.Admin; -using Bitbucket.Net.Models.Core.Projects; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests +namespace Bitbucket.Net.Tests.MockTests; + +public class ProjectPermissionsMockTests(BitbucketMockFixture fixture) : IClassFixture { - public class ProjectPermissionsMockTests : IClassFixture + private readonly BitbucketMockFixture _fixture = fixture; + + [Fact] + public async Task GetProjectUserPermissionsAsync_ReturnsUserPermissions() + { + _fixture.Reset(); + _fixture.Server.SetupGetProjectUserPermissions(TestConstants.TestProjectKey); + var client = _fixture.CreateClient(); + + var permissions = await client.GetProjectUserPermissionsAsync(TestConstants.TestProjectKey); + + Assert.NotNull(permissions); + var permissionList = permissions.ToList(); + Assert.Single(permissionList); + Assert.NotNull(permissionList[0].User); + Assert.Equal("testuser", permissionList[0].User!.Name); + Assert.Equal(Permissions.ProjectAdmin, permissionList[0].Permission); + } + + [Fact] + public async Task DeleteProjectUserPermissionsAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupDeleteProjectUserPermissions(TestConstants.TestProjectKey); + var client = _fixture.CreateClient(); + + var result = await client.DeleteProjectUserPermissionsAsync(TestConstants.TestProjectKey, "testuser"); + + Assert.True(result); + } + + [Fact] + public async Task UpdateProjectUserPermissionsAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupUpdateProjectUserPermissions(TestConstants.TestProjectKey); + var client = _fixture.CreateClient(); + + var result = await client.UpdateProjectUserPermissionsAsync( + TestConstants.TestProjectKey, + "testuser", + Permissions.ProjectAdmin); + + Assert.True(result); + } + + [Fact] + public async Task GetProjectUserPermissionsNoneAsync_ReturnsLicensedUsers() + { + _fixture.Reset(); + _fixture.Server.SetupGetProjectUserPermissionsNone(TestConstants.TestProjectKey); + var client = _fixture.CreateClient(); + + var users = await client.GetProjectUserPermissionsNoneAsync(TestConstants.TestProjectKey); + + Assert.NotNull(users); + var userList = users.ToList(); + Assert.Single(userList); + } + + [Fact] + public async Task GetProjectGroupPermissionsAsync_ReturnsGroupPermissions() + { + _fixture.Reset(); + _fixture.Server.SetupGetProjectGroupPermissions(TestConstants.TestProjectKey); + var client = _fixture.CreateClient(); + + var permissions = await client.GetProjectGroupPermissionsAsync(TestConstants.TestProjectKey); + + Assert.NotNull(permissions); + var permissionList = permissions.ToList(); + Assert.Single(permissionList); + Assert.NotNull(permissionList[0].Group); + Assert.Equal("developers", permissionList[0].Group!.Name); + Assert.Equal(Permissions.ProjectWrite, permissionList[0].Permission); + } + + [Fact] + public async Task DeleteProjectGroupPermissionsAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupDeleteProjectGroupPermissions(TestConstants.TestProjectKey); + var client = _fixture.CreateClient(); + + var result = await client.DeleteProjectGroupPermissionsAsync(TestConstants.TestProjectKey, "developers"); + + Assert.True(result); + } + + [Fact] + public async Task UpdateProjectGroupPermissionsAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupUpdateProjectGroupPermissions(TestConstants.TestProjectKey); + var client = _fixture.CreateClient(); + + var result = await client.UpdateProjectGroupPermissionsAsync( + TestConstants.TestProjectKey, + "developers", + Permissions.ProjectWrite); + + Assert.True(result); + } + + [Fact] + public async Task GetProjectGroupPermissionsNoneAsync_ReturnsLicensedUsers() + { + _fixture.Reset(); + _fixture.Server.SetupGetProjectGroupPermissionsNone(TestConstants.TestProjectKey); + var client = _fixture.CreateClient(); + + var users = await client.GetProjectGroupPermissionsNoneAsync(TestConstants.TestProjectKey); + + Assert.NotNull(users); + var userList = users.ToList(); + Assert.Single(userList); + } + + [Fact] + public async Task IsProjectDefaultPermissionAsync_ReturnsTrue() { - private readonly BitbucketMockFixture _fixture; - - public ProjectPermissionsMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public async Task GetProjectUserPermissionsAsync_ReturnsUserPermissions() - { - _fixture.Reset(); - _fixture.Server.SetupGetProjectUserPermissions(TestConstants.TestProjectKey); - var client = _fixture.CreateClient(); - - var permissions = await client.GetProjectUserPermissionsAsync(TestConstants.TestProjectKey); - - Assert.NotNull(permissions); - var permissionList = permissions.ToList(); - Assert.Single(permissionList); - Assert.NotNull(permissionList[0].User); - Assert.Equal("testuser", permissionList[0].User!.Name); - Assert.Equal(Permissions.ProjectAdmin, permissionList[0].Permission); - } - - [Fact] - public async Task DeleteProjectUserPermissionsAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupDeleteProjectUserPermissions(TestConstants.TestProjectKey); - var client = _fixture.CreateClient(); - - var result = await client.DeleteProjectUserPermissionsAsync(TestConstants.TestProjectKey, "testuser"); - - Assert.True(result); - } - - [Fact] - public async Task UpdateProjectUserPermissionsAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupUpdateProjectUserPermissions(TestConstants.TestProjectKey); - var client = _fixture.CreateClient(); - - var result = await client.UpdateProjectUserPermissionsAsync( - TestConstants.TestProjectKey, - "testuser", - Permissions.ProjectAdmin); - - Assert.True(result); - } - - [Fact] - public async Task GetProjectUserPermissionsNoneAsync_ReturnsLicensedUsers() - { - _fixture.Reset(); - _fixture.Server.SetupGetProjectUserPermissionsNone(TestConstants.TestProjectKey); - var client = _fixture.CreateClient(); - - var users = await client.GetProjectUserPermissionsNoneAsync(TestConstants.TestProjectKey); - - Assert.NotNull(users); - var userList = users.ToList(); - Assert.Single(userList); - } - - [Fact] - public async Task GetProjectGroupPermissionsAsync_ReturnsGroupPermissions() - { - _fixture.Reset(); - _fixture.Server.SetupGetProjectGroupPermissions(TestConstants.TestProjectKey); - var client = _fixture.CreateClient(); - - var permissions = await client.GetProjectGroupPermissionsAsync(TestConstants.TestProjectKey); - - Assert.NotNull(permissions); - var permissionList = permissions.ToList(); - Assert.Single(permissionList); - Assert.NotNull(permissionList[0].Group); - Assert.Equal("developers", permissionList[0].Group!.Name); - Assert.Equal(Permissions.ProjectWrite, permissionList[0].Permission); - } - - [Fact] - public async Task DeleteProjectGroupPermissionsAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupDeleteProjectGroupPermissions(TestConstants.TestProjectKey); - var client = _fixture.CreateClient(); - - var result = await client.DeleteProjectGroupPermissionsAsync(TestConstants.TestProjectKey, "developers"); - - Assert.True(result); - } - - [Fact] - public async Task UpdateProjectGroupPermissionsAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupUpdateProjectGroupPermissions(TestConstants.TestProjectKey); - var client = _fixture.CreateClient(); - - var result = await client.UpdateProjectGroupPermissionsAsync( - TestConstants.TestProjectKey, - "developers", - Permissions.ProjectWrite); - - Assert.True(result); - } - - [Fact] - public async Task GetProjectGroupPermissionsNoneAsync_ReturnsLicensedUsers() - { - _fixture.Reset(); - _fixture.Server.SetupGetProjectGroupPermissionsNone(TestConstants.TestProjectKey); - var client = _fixture.CreateClient(); - - var users = await client.GetProjectGroupPermissionsNoneAsync(TestConstants.TestProjectKey); - - Assert.NotNull(users); - var userList = users.ToList(); - Assert.Single(userList); - } - - [Fact] - public async Task IsProjectDefaultPermissionAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupGetProjectDefaultPermission(TestConstants.TestProjectKey, "PROJECT_READ"); - var client = _fixture.CreateClient(); - - var result = await client.IsProjectDefaultPermissionAsync(TestConstants.TestProjectKey, Permissions.ProjectRead); - - Assert.True(result); - } - - [Fact] - public async Task GrantProjectPermissionToAllAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupSetProjectDefaultPermission(TestConstants.TestProjectKey, "PROJECT_READ"); - var client = _fixture.CreateClient(); - - var result = await client.GrantProjectPermissionToAllAsync(TestConstants.TestProjectKey, Permissions.ProjectRead); - - Assert.True(result); - } - - [Fact] - public async Task RevokeProjectPermissionFromAllAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupSetProjectDefaultPermission(TestConstants.TestProjectKey, "PROJECT_READ"); - var client = _fixture.CreateClient(); - - var result = await client.RevokeProjectPermissionFromAllAsync(TestConstants.TestProjectKey, Permissions.ProjectRead); - - Assert.True(result); - } + _fixture.Reset(); + _fixture.Server.SetupGetProjectDefaultPermission(TestConstants.TestProjectKey, "PROJECT_READ"); + var client = _fixture.CreateClient(); + + var result = await client.IsProjectDefaultPermissionAsync(TestConstants.TestProjectKey, Permissions.ProjectRead); + + Assert.True(result); + } + + [Fact] + public async Task GrantProjectPermissionToAllAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupSetProjectDefaultPermission(TestConstants.TestProjectKey, "PROJECT_READ"); + var client = _fixture.CreateClient(); + + var result = await client.GrantProjectPermissionToAllAsync(TestConstants.TestProjectKey, Permissions.ProjectRead); + + Assert.True(result); + } + + [Fact] + public async Task RevokeProjectPermissionFromAllAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupSetProjectDefaultPermission(TestConstants.TestProjectKey, "PROJECT_READ"); + var client = _fixture.CreateClient(); + + var result = await client.RevokeProjectPermissionFromAllAsync(TestConstants.TestProjectKey, Permissions.ProjectRead); + + Assert.True(result); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/ProjectSettingsMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/ProjectSettingsMockTests.cs index cb0c83c..5801cbf 100644 --- a/test/Bitbucket.Net.Tests/MockTests/ProjectSettingsMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/ProjectSettingsMockTests.cs @@ -1,127 +1,118 @@ -using System.IO; -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Models.Core.Admin; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests +namespace Bitbucket.Net.Tests.MockTests; + +public class ProjectSettingsMockTests(BitbucketMockFixture fixture) : IClassFixture { - public class ProjectSettingsMockTests : IClassFixture + private readonly BitbucketMockFixture _fixture = fixture; + + [Fact] + public async Task GetProjectPullRequestsMergeStrategiesAsync_ReturnsSettings() + { + _fixture.Reset(); + _fixture.Server.SetupGetProjectPullRequestsMergeStrategies(TestConstants.TestProjectKey); + var client = _fixture.CreateClient(); + + var result = await client.GetProjectPullRequestsMergeStrategiesAsync( + TestConstants.TestProjectKey, + "git"); + + Assert.NotNull(result); + } + + [Fact] + public async Task UpdateProjectPullRequestsMergeStrategiesAsync_ReturnsStrategies() + { + _fixture.Reset(); + _fixture.Server.SetupUpdateProjectPullRequestsMergeStrategies(TestConstants.TestProjectKey); + var client = _fixture.CreateClient(); + + var strategies = new MergeStrategies(); + var result = await client.UpdateProjectPullRequestsMergeStrategiesAsync( + TestConstants.TestProjectKey, + "git", + strategies); + + Assert.NotNull(result); + } + + [Fact] + public async Task BrowseProjectRepositoryPathAsync_ReturnsBrowseResult() + { + _fixture.Reset(); + _fixture.Server.SetupBrowseProjectRepositoryPath( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var result = await client.BrowseProjectRepositoryPathAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + path: "src", + at: "refs/heads/main"); + + Assert.NotNull(result); + } + + [Fact] + public async Task GetRawFileContentStreamAsync_ReturnsStream() + { + _fixture.Reset(); + _fixture.Server.SetupGetRawFileContentStream( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + using var stream = await client.GetRawFileContentStreamAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + path: "README.md", + at: "refs/heads/main"); + + Assert.NotNull(stream); + using var reader = new StreamReader(stream); + var content = await reader.ReadToEndAsync(); + Assert.NotEmpty(content); + } + + [Fact] + public async Task GetProjectRepositoryTagsAsync_ReturnsTags() + { + _fixture.Reset(); + _fixture.Server.SetupGetProjectRepositoryTags( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var result = await client.GetProjectRepositoryTagsAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + filterText: "", + orderBy: Models.Core.Projects.BranchOrderBy.Alphabetical); + + Assert.NotNull(result); + var tags = result.ToList(); + Assert.NotEmpty(tags); + } + + [Fact] + public async Task CreateProjectRepositoryTagAsync_CreatesTag() { - private readonly BitbucketMockFixture _fixture; - - public ProjectSettingsMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public async Task GetProjectPullRequestsMergeStrategiesAsync_ReturnsSettings() - { - _fixture.Reset(); - _fixture.Server.SetupGetProjectPullRequestsMergeStrategies(TestConstants.TestProjectKey); - var client = _fixture.CreateClient(); - - var result = await client.GetProjectPullRequestsMergeStrategiesAsync( - TestConstants.TestProjectKey, - "git"); - - Assert.NotNull(result); - } - - [Fact] - public async Task UpdateProjectPullRequestsMergeStrategiesAsync_ReturnsStrategies() - { - _fixture.Reset(); - _fixture.Server.SetupUpdateProjectPullRequestsMergeStrategies(TestConstants.TestProjectKey); - var client = _fixture.CreateClient(); - - var strategies = new MergeStrategies(); - var result = await client.UpdateProjectPullRequestsMergeStrategiesAsync( - TestConstants.TestProjectKey, - "git", - strategies); - - Assert.NotNull(result); - } - - [Fact] - public async Task BrowseProjectRepositoryPathAsync_ReturnsBrowseResult() - { - _fixture.Reset(); - _fixture.Server.SetupBrowseProjectRepositoryPath( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var result = await client.BrowseProjectRepositoryPathAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - path: "src", - at: "refs/heads/main"); - - Assert.NotNull(result); - } - - [Fact] - public async Task GetRawFileContentStreamAsync_ReturnsStream() - { - _fixture.Reset(); - _fixture.Server.SetupGetRawFileContentStream( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - using var stream = await client.GetRawFileContentStreamAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - path: "README.md", - at: "refs/heads/main"); - - Assert.NotNull(stream); - using var reader = new StreamReader(stream); - var content = await reader.ReadToEndAsync(); - Assert.NotEmpty(content); - } - - [Fact] - public async Task GetProjectRepositoryTagsAsync_ReturnsTags() - { - _fixture.Reset(); - _fixture.Server.SetupGetProjectRepositoryTags( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var result = await client.GetProjectRepositoryTagsAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - filterText: "", - orderBy: Bitbucket.Net.Models.Core.Projects.BranchOrderBy.Alphabetical); - - Assert.NotNull(result); - var tags = result.ToList(); - Assert.NotEmpty(tags); - } - - [Fact] - public async Task CreateProjectRepositoryTagAsync_CreatesTag() - { - _fixture.Reset(); - _fixture.Server.SetupCreateProjectRepositoryTag( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var result = await client.CreateProjectRepositoryTagAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - name: "v1.0.0", - startPoint: "abc123", - message: "Release v1.0.0"); - - Assert.NotNull(result); - } + _fixture.Reset(); + _fixture.Server.SetupCreateProjectRepositoryTag( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var result = await client.CreateProjectRepositoryTagAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + name: "v1.0.0", + startPoint: "abc123", + message: "Release v1.0.0"); + + Assert.NotNull(result); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/PullRequestActionsMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/PullRequestActionsMockTests.cs index 1dd104f..a81c561 100644 --- a/test/Bitbucket.Net.Tests/MockTests/PullRequestActionsMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/PullRequestActionsMockTests.cs @@ -1,94 +1,87 @@ -using System.Threading.Tasks; using Bitbucket.Net.Models.Core.Projects; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests -{ - public class PullRequestActionsMockTests : IClassFixture - { - private readonly BitbucketMockFixture _fixture; +namespace Bitbucket.Net.Tests.MockTests; - public PullRequestActionsMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } +public class PullRequestActionsMockTests(BitbucketMockFixture fixture) : IClassFixture +{ + private readonly BitbucketMockFixture _fixture = fixture; - [Fact] - public async Task ApprovePullRequestAsync_ReturnsReviewer() - { - _fixture.Reset(); - _fixture.Server.SetupApprovePullRequest( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); + [Fact] + public async Task ApprovePullRequestAsync_ReturnsReviewer() + { + _fixture.Reset(); + _fixture.Server.SetupApprovePullRequest( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); - var reviewer = await client.ApprovePullRequestAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); + var reviewer = await client.ApprovePullRequestAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); - Assert.NotNull(reviewer); - Assert.True(reviewer.Approved); - Assert.Equal(ParticipantStatus.Approved, reviewer.Status); - } + Assert.NotNull(reviewer); + Assert.True(reviewer.Approved); + Assert.Equal(ParticipantStatus.Approved, reviewer.Status); + } - [Fact] - public async Task MergePullRequestAsync_ReturnsMergedPullRequest() - { - _fixture.Reset(); - _fixture.Server.SetupMergePullRequest( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); + [Fact] + public async Task MergePullRequestAsync_ReturnsMergedPullRequest() + { + _fixture.Reset(); + _fixture.Server.SetupMergePullRequest( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); - var pullRequest = await client.MergePullRequestAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); + var pullRequest = await client.MergePullRequestAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); - Assert.NotNull(pullRequest); - Assert.Equal(TestConstants.TestPullRequestId, pullRequest.Id); - } + Assert.NotNull(pullRequest); + Assert.Equal(TestConstants.TestPullRequestId, pullRequest.Id); + } - [Fact] - public async Task DeclinePullRequestAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupDeclinePullRequest( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); + [Fact] + public async Task DeclinePullRequestAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupDeclinePullRequest( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); - var result = await client.DeclinePullRequestAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); + var result = await client.DeclinePullRequestAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); - Assert.True(result); - } + Assert.True(result); + } - [Fact] - public async Task GetPullRequestMergeStateAsync_ReturnsMergeState() - { - _fixture.Reset(); - _fixture.Server.SetupGetPullRequestMergeState( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetPullRequestMergeStateAsync_ReturnsMergeState() + { + _fixture.Reset(); + _fixture.Server.SetupGetPullRequestMergeState( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); - var mergeState = await client.GetPullRequestMergeStateAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); + var mergeState = await client.GetPullRequestMergeStateAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); - Assert.NotNull(mergeState); - Assert.True(mergeState.CanMerge); - Assert.False(mergeState.Conflicted); - } + Assert.NotNull(mergeState); + Assert.True(mergeState.CanMerge); + Assert.False(mergeState.Conflicted); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/PullRequestBlockerMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/PullRequestBlockerMockTests.cs index 0ab7cfb..8aa5221 100644 --- a/test/Bitbucket.Net.Tests/MockTests/PullRequestBlockerMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/PullRequestBlockerMockTests.cs @@ -1,144 +1,136 @@ -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Models.Core.Projects; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests -{ - public class PullRequestBlockerMockTests : IClassFixture - { - private readonly BitbucketMockFixture _fixture; +namespace Bitbucket.Net.Tests.MockTests; - public PullRequestBlockerMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } +public class PullRequestBlockerMockTests(BitbucketMockFixture fixture) : IClassFixture +{ + private readonly BitbucketMockFixture _fixture = fixture; #pragma warning disable CS0618 // Type or member is obsolete - [Fact] - public async Task GetPullRequestTaskCountAsync_ReturnsTaskCount() - { - _fixture.Reset(); - _fixture.Server.SetupGetPullRequestTaskCount( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); - - var taskCount = await client.GetPullRequestTaskCountAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - - Assert.NotNull(taskCount); - Assert.Equal(2, taskCount.Open); - Assert.Equal(1, taskCount.Resolved); - } + [Fact] + public async Task GetPullRequestTaskCountAsync_ReturnsTaskCount() + { + _fixture.Reset(); + _fixture.Server.SetupGetPullRequestTaskCount( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); + + var taskCount = await client.GetPullRequestTaskCountAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + + Assert.NotNull(taskCount); + Assert.Equal(2, taskCount.Open); + Assert.Equal(1, taskCount.Resolved); + } #pragma warning restore CS0618 - [Fact] - public async Task GetPullRequestBlockerCommentsAsync_ReturnsBlockerComments() - { - _fixture.Reset(); - _fixture.Server.SetupGetPullRequestBlockerComments( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); - - var blockerComments = await client.GetPullRequestBlockerCommentsAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - - Assert.NotNull(blockerComments); - var commentList = blockerComments.ToList(); - Assert.Single(commentList); - Assert.Equal(1, commentList[0].Id); - Assert.Equal(BlockerCommentState.Open, commentList[0].State); - } - - [Fact] - public async Task GetPullRequestBlockerCommentAsync_ReturnsBlockerComment() - { - _fixture.Reset(); - _fixture.Server.SetupGetPullRequestBlockerComment( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId, - 1); - var client = _fixture.CreateClient(); - - var blockerComment = await client.GetPullRequestBlockerCommentAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId, - 1); - - Assert.NotNull(blockerComment); - Assert.Equal(1, blockerComment.Id); - Assert.Equal("Please fix this issue before merging", blockerComment.Text); - } - - [Fact] - public async Task CreatePullRequestBlockerCommentAsync_ReturnsBlockerComment() - { - _fixture.Reset(); - _fixture.Server.SetupCreatePullRequestBlockerComment( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); - - var blockerComment = await client.CreatePullRequestBlockerCommentAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId, - "Please fix this issue before merging"); - - Assert.NotNull(blockerComment); - Assert.Equal(1, blockerComment.Id); - } - - [Fact] - public async Task DeletePullRequestBlockerCommentAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupDeletePullRequestBlockerComment( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId, - 1); - var client = _fixture.CreateClient(); - - var result = await client.DeletePullRequestBlockerCommentAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId, - blockerCommentId: 1, - version: 0); - - Assert.True(result); - } - - [Fact] - public async Task GetPullRequestMergeBaseAsync_ReturnsCommit() - { - _fixture.Reset(); - _fixture.Server.SetupGetPullRequestMergeBase( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); - - var commit = await client.GetPullRequestMergeBaseAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - - Assert.NotNull(commit); - Assert.NotNull(commit.Id); - } + [Fact] + public async Task GetPullRequestBlockerCommentsAsync_ReturnsBlockerComments() + { + _fixture.Reset(); + _fixture.Server.SetupGetPullRequestBlockerComments( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); + + var blockerComments = await client.GetPullRequestBlockerCommentsAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + + Assert.NotNull(blockerComments); + var commentList = blockerComments.ToList(); + Assert.Single(commentList); + Assert.Equal(1, commentList[0].Id); + Assert.Equal(BlockerCommentState.Open, commentList[0].State); + } + + [Fact] + public async Task GetPullRequestBlockerCommentAsync_ReturnsBlockerComment() + { + _fixture.Reset(); + _fixture.Server.SetupGetPullRequestBlockerComment( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId, + 1); + var client = _fixture.CreateClient(); + + var blockerComment = await client.GetPullRequestBlockerCommentAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId, + 1); + + Assert.NotNull(blockerComment); + Assert.Equal(1, blockerComment.Id); + Assert.Equal("Please fix this issue before merging", blockerComment.Text); + } + + [Fact] + public async Task CreatePullRequestBlockerCommentAsync_ReturnsBlockerComment() + { + _fixture.Reset(); + _fixture.Server.SetupCreatePullRequestBlockerComment( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); + + var blockerComment = await client.CreatePullRequestBlockerCommentAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId, + "Please fix this issue before merging"); + + Assert.NotNull(blockerComment); + Assert.Equal(1, blockerComment.Id); + } + + [Fact] + public async Task DeletePullRequestBlockerCommentAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupDeletePullRequestBlockerComment( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId, + 1); + var client = _fixture.CreateClient(); + + var result = await client.DeletePullRequestBlockerCommentAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId, + blockerCommentId: 1, + version: 0); + + Assert.True(result); + } + + [Fact] + public async Task GetPullRequestMergeBaseAsync_ReturnsCommit() + { + _fixture.Reset(); + _fixture.Server.SetupGetPullRequestMergeBase( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); + + var commit = await client.GetPullRequestMergeBaseAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + + Assert.NotNull(commit); + Assert.NotNull(commit.Id); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/PullRequestCommentMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/PullRequestCommentMockTests.cs index 48f1618..6d0d2fa 100644 --- a/test/Bitbucket.Net.Tests/MockTests/PullRequestCommentMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/PullRequestCommentMockTests.cs @@ -1,102 +1,95 @@ -using System.Threading.Tasks; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests -{ - public class PullRequestCommentMockTests : IClassFixture - { - private readonly BitbucketMockFixture _fixture; +namespace Bitbucket.Net.Tests.MockTests; - public PullRequestCommentMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } +public class PullRequestCommentMockTests(BitbucketMockFixture fixture) : IClassFixture +{ + private readonly BitbucketMockFixture _fixture = fixture; - [Fact] - public async Task CreatePullRequestCommentAsync_ReturnsComment() - { - _fixture.Reset(); - _fixture.Server.SetupCreatePullRequestComment( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); + [Fact] + public async Task CreatePullRequestCommentAsync_ReturnsComment() + { + _fixture.Reset(); + _fixture.Server.SetupCreatePullRequestComment( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); - var result = await client.CreatePullRequestCommentAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId, - "This is a new comment"); + var result = await client.CreatePullRequestCommentAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId, + "This is a new comment"); - Assert.NotNull(result); - Assert.Equal(101, result.Id); - Assert.Equal("This is a new comment", result.Text); - } + Assert.NotNull(result); + Assert.Equal(101, result.Id); + Assert.Equal("This is a new comment", result.Text); + } - [Fact] - public async Task GetPullRequestCommentAsync_ReturnsComment() - { - _fixture.Reset(); - _fixture.Server.SetupGetPullRequestComment( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId, - 101); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetPullRequestCommentAsync_ReturnsComment() + { + _fixture.Reset(); + _fixture.Server.SetupGetPullRequestComment( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId, + 101); + var client = _fixture.CreateClient(); - var result = await client.GetPullRequestCommentAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId, - 101); + var result = await client.GetPullRequestCommentAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId, + 101); - Assert.NotNull(result); - Assert.Equal(101, result.Id); - } + Assert.NotNull(result); + Assert.Equal(101, result.Id); + } - [Fact] - public async Task UpdatePullRequestCommentAsync_ReturnsUpdatedComment() - { - _fixture.Reset(); - _fixture.Server.SetupUpdatePullRequestComment( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId, - 101); - var client = _fixture.CreateClient(); + [Fact] + public async Task UpdatePullRequestCommentAsync_ReturnsUpdatedComment() + { + _fixture.Reset(); + _fixture.Server.SetupUpdatePullRequestComment( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId, + 101); + var client = _fixture.CreateClient(); - var result = await client.UpdatePullRequestCommentAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId, - 101, - 0, - "Updated comment text"); + var result = await client.UpdatePullRequestCommentAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId, + 101, + 0, + "Updated comment text"); - Assert.NotNull(result); - Assert.Equal(101, result.Id); - } + Assert.NotNull(result); + Assert.Equal(101, result.Id); + } - [Fact] - public async Task DeletePullRequestCommentAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupDeletePullRequestComment( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId, - 101); - var client = _fixture.CreateClient(); + [Fact] + public async Task DeletePullRequestCommentAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupDeletePullRequestComment( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId, + 101); + var client = _fixture.CreateClient(); - var result = await client.DeletePullRequestCommentAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId, - 101, - 0); + var result = await client.DeletePullRequestCommentAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId, + 101, + 0); - Assert.True(result); - } + Assert.True(result); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/PullRequestCrudMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/PullRequestCrudMockTests.cs index 381d70f..421a183 100644 --- a/test/Bitbucket.Net.Tests/MockTests/PullRequestCrudMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/PullRequestCrudMockTests.cs @@ -1,83 +1,76 @@ -using System.Threading.Tasks; using Bitbucket.Net.Models.Core.Projects; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests +namespace Bitbucket.Net.Tests.MockTests; + +public class PullRequestCrudMockTests(BitbucketMockFixture fixture) : IClassFixture { - public class PullRequestCrudMockTests : IClassFixture - { - private readonly BitbucketMockFixture _fixture; + private readonly BitbucketMockFixture _fixture = fixture; - public PullRequestCrudMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } + [Fact] + public async Task CreatePullRequestAsync_ReturnsCreatedPullRequest() + { + _fixture.Reset(); + _fixture.Server.SetupCreatePullRequest(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); - [Fact] - public async Task CreatePullRequestAsync_ReturnsCreatedPullRequest() + var prInfo = new PullRequestInfo { - _fixture.Reset(); - _fixture.Server.SetupCreatePullRequest(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var prInfo = new PullRequestInfo + Title = "Test PR", + Description = "Test description", + FromRef = new FromToRef { - Title = "Test PR", - Description = "Test description", - FromRef = new FromToRef + Id = "refs/heads/feature-test", + Repository = new RepositoryRef { - Id = "refs/heads/feature-test", - Repository = new RepositoryRef - { - Slug = TestConstants.TestRepositorySlug, - Project = new ProjectRef { Key = TestConstants.TestProjectKey } - } - }, - ToRef = new FromToRef + Slug = TestConstants.TestRepositorySlug, + Project = new ProjectRef { Key = TestConstants.TestProjectKey } + } + }, + ToRef = new FromToRef + { + Id = "refs/heads/master", + Repository = new RepositoryRef { - Id = "refs/heads/master", - Repository = new RepositoryRef - { - Slug = TestConstants.TestRepositorySlug, - Project = new ProjectRef { Key = TestConstants.TestProjectKey } - } + Slug = TestConstants.TestRepositorySlug, + Project = new ProjectRef { Key = TestConstants.TestProjectKey } } - }; + } + }; - var pullRequest = await client.CreatePullRequestAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - prInfo); + var pullRequest = await client.CreatePullRequestAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + prInfo); - Assert.NotNull(pullRequest); - Assert.Equal(TestConstants.TestPullRequestId, pullRequest.Id); - } + Assert.NotNull(pullRequest); + Assert.Equal(TestConstants.TestPullRequestId, pullRequest.Id); + } - [Fact] - public async Task UpdatePullRequestAsync_ReturnsUpdatedPullRequest() - { - _fixture.Reset(); - _fixture.Server.SetupUpdatePullRequest( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); + [Fact] + public async Task UpdatePullRequestAsync_ReturnsUpdatedPullRequest() + { + _fixture.Reset(); + _fixture.Server.SetupUpdatePullRequest( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); - var prUpdate = new PullRequestUpdate - { - Title = "Updated Title", - Version = 0 - }; + var prUpdate = new PullRequestUpdate + { + Title = "Updated Title", + Version = 0 + }; - var pullRequest = await client.UpdatePullRequestAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId, - prUpdate); + var pullRequest = await client.UpdatePullRequestAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId, + prUpdate); - Assert.NotNull(pullRequest); - Assert.Equal(TestConstants.TestPullRequestId, pullRequest.Id); - } + Assert.NotNull(pullRequest); + Assert.Equal(TestConstants.TestPullRequestId, pullRequest.Id); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/PullRequestExtendedMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/PullRequestExtendedMockTests.cs index 8925645..a09f417 100644 --- a/test/Bitbucket.Net.Tests/MockTests/PullRequestExtendedMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/PullRequestExtendedMockTests.cs @@ -1,206 +1,198 @@ -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Models.Core.Projects; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests +namespace Bitbucket.Net.Tests.MockTests; + +public class PullRequestExtendedMockTests(BitbucketMockFixture fixture) : IClassFixture { - public class PullRequestExtendedMockTests : IClassFixture + private readonly BitbucketMockFixture _fixture = fixture; + + [Fact] + public async Task GetPullRequestActivitiesAsync_ReturnsActivities() { - private readonly BitbucketMockFixture _fixture; + _fixture.Reset(); + _fixture.Server.SetupGetPullRequestActivities( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); + + var activities = await client.GetPullRequestActivitiesAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + + Assert.NotNull(activities); + var activityList = activities.ToList(); + Assert.Equal(2, activityList.Count); + } - public PullRequestExtendedMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } + [Fact] + public async Task GetPullRequestChangesAsync_ReturnsChanges() + { + _fixture.Reset(); + _fixture.Server.SetupGetPullRequestChanges( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); + + var changes = await client.GetPullRequestChangesAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + + Assert.NotNull(changes); + var changeList = changes.ToList(); + Assert.Single(changeList); + } - [Fact] - public async Task GetPullRequestActivitiesAsync_ReturnsActivities() - { - _fixture.Reset(); - _fixture.Server.SetupGetPullRequestActivities( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); - - var activities = await client.GetPullRequestActivitiesAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - - Assert.NotNull(activities); - var activityList = activities.ToList(); - Assert.Equal(2, activityList.Count); - } - - [Fact] - public async Task GetPullRequestChangesAsync_ReturnsChanges() - { - _fixture.Reset(); - _fixture.Server.SetupGetPullRequestChanges( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); - - var changes = await client.GetPullRequestChangesAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - - Assert.NotNull(changes); - var changeList = changes.ToList(); - Assert.Single(changeList); - } - - [Fact] - public async Task GetPullRequestCommitsAsync_ReturnsCommits() - { - _fixture.Reset(); - _fixture.Server.SetupGetPullRequestCommits( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); - - var commits = await client.GetPullRequestCommitsAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - - Assert.NotNull(commits); - var commitList = commits.ToList(); - Assert.Equal(2, commitList.Count); - Assert.Contains(commitList, c => c.Message == "Initial commit"); - } - - [Fact] - public async Task GetPullRequestMergeStateAsync_ReturnsMergeState() - { - _fixture.Reset(); - _fixture.Server.SetupGetPullRequestMergeState( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); - - var mergeState = await client.GetPullRequestMergeStateAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - - Assert.NotNull(mergeState); - Assert.True(mergeState.CanMerge); - Assert.False(mergeState.Conflicted); - } - - [Fact] - public async Task DeclinePullRequestAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupDeclinePullRequest( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); - - var result = await client.DeclinePullRequestAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - - Assert.True(result); - } - - [Fact] - public async Task MergePullRequestAsync_ReturnsPullRequest() - { - _fixture.Reset(); - _fixture.Server.SetupMergePullRequest( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); - - var result = await client.MergePullRequestAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - - Assert.NotNull(result); - Assert.Equal(TestConstants.TestPullRequestId, result.Id); - } - - [Fact] - public async Task ApprovePullRequestAsync_ReturnsReviewer() - { - _fixture.Reset(); - _fixture.Server.SetupApprovePullRequest( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); - - var reviewer = await client.ApprovePullRequestAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - - Assert.NotNull(reviewer); - } - - [Fact] - public async Task CreatePullRequestAsync_ReturnsPullRequest() + [Fact] + public async Task GetPullRequestCommitsAsync_ReturnsCommits() + { + _fixture.Reset(); + _fixture.Server.SetupGetPullRequestCommits( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); + + var commits = await client.GetPullRequestCommitsAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + + Assert.NotNull(commits); + var commitList = commits.ToList(); + Assert.Equal(2, commitList.Count); + Assert.Contains(commitList, c => c.Message == "Initial commit"); + } + + [Fact] + public async Task GetPullRequestMergeStateAsync_ReturnsMergeState() + { + _fixture.Reset(); + _fixture.Server.SetupGetPullRequestMergeState( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); + + var mergeState = await client.GetPullRequestMergeStateAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + + Assert.NotNull(mergeState); + Assert.True(mergeState.CanMerge); + Assert.False(mergeState.Conflicted); + } + + [Fact] + public async Task DeclinePullRequestAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupDeclinePullRequest( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); + + var result = await client.DeclinePullRequestAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + + Assert.True(result); + } + + [Fact] + public async Task MergePullRequestAsync_ReturnsPullRequest() + { + _fixture.Reset(); + _fixture.Server.SetupMergePullRequest( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); + + var result = await client.MergePullRequestAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + + Assert.NotNull(result); + Assert.Equal(TestConstants.TestPullRequestId, result.Id); + } + + [Fact] + public async Task ApprovePullRequestAsync_ReturnsReviewer() + { + _fixture.Reset(); + _fixture.Server.SetupApprovePullRequest( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); + + var reviewer = await client.ApprovePullRequestAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + + Assert.NotNull(reviewer); + } + + [Fact] + public async Task CreatePullRequestAsync_ReturnsPullRequest() + { + _fixture.Reset(); + _fixture.Server.SetupCreatePullRequest( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var prInfo = new PullRequestInfo { - _fixture.Reset(); - _fixture.Server.SetupCreatePullRequest( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var prInfo = new PullRequestInfo - { - Title = "New PR", - Description = "Description", - FromRef = new FromToRef { Id = "refs/heads/feature" }, - ToRef = new FromToRef { Id = "refs/heads/main" } - }; - - var result = await client.CreatePullRequestAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - prInfo); - - Assert.NotNull(result); - Assert.Equal(TestConstants.TestPullRequestId, result.Id); - } - - [Fact] - public async Task UpdatePullRequestAsync_ReturnsPullRequest() + Title = "New PR", + Description = "Description", + FromRef = new FromToRef { Id = "refs/heads/feature" }, + ToRef = new FromToRef { Id = "refs/heads/main" } + }; + + var result = await client.CreatePullRequestAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + prInfo); + + Assert.NotNull(result); + Assert.Equal(TestConstants.TestPullRequestId, result.Id); + } + + [Fact] + public async Task UpdatePullRequestAsync_ReturnsPullRequest() + { + _fixture.Reset(); + _fixture.Server.SetupUpdatePullRequest( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); + + var update = new PullRequestUpdate { - _fixture.Reset(); - _fixture.Server.SetupUpdatePullRequest( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); - - var update = new PullRequestUpdate - { - Id = (int)TestConstants.TestPullRequestId, - Version = 0, - Title = "Updated Title" - }; - - var result = await client.UpdatePullRequestAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId, - update); - - Assert.NotNull(result); - } + Id = (int)TestConstants.TestPullRequestId, + Version = 0, + Title = "Updated Title" + }; + + var result = await client.UpdatePullRequestAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId, + update); + + Assert.NotNull(result); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/PullRequestMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/PullRequestMockTests.cs index 475d79e..2407fc1 100644 --- a/test/Bitbucket.Net.Tests/MockTests/PullRequestMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/PullRequestMockTests.cs @@ -1,95 +1,87 @@ -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Models.Core.Projects; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests -{ - /// - /// Unit tests for pull request-related operations using WireMock. - /// - public class PullRequestMockTests : IClassFixture - { - private readonly BitbucketMockFixture _fixture; +namespace Bitbucket.Net.Tests.MockTests; - public PullRequestMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } +/// +/// Unit tests for pull request-related operations using WireMock. +/// +public class PullRequestMockTests(BitbucketMockFixture fixture) : IClassFixture +{ + private readonly BitbucketMockFixture _fixture = fixture; - [Fact] - public async Task GetPullRequestsAsync_ReturnsPullRequests() - { - // Arrange - _fixture.Reset(); - _fixture.Server.SetupGetPullRequests(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetPullRequestsAsync_ReturnsPullRequests() + { + // Arrange + _fixture.Reset(); + _fixture.Server.SetupGetPullRequests(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); - // Act - var pullRequests = await client.GetPullRequestsAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); + // Act + var pullRequests = await client.GetPullRequestsAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); - // Assert - Assert.NotNull(pullRequests); - var prList = pullRequests.ToList(); - Assert.Single(prList); - var pr = prList[0]; - Assert.Equal(TestConstants.TestPullRequestId, pr.Id); - Assert.Equal(TestConstants.TestPullRequestTitle, pr.Title); - Assert.Equal(PullRequestStates.Open, pr.State); - } + // Assert + Assert.NotNull(pullRequests); + var prList = pullRequests.ToList(); + Assert.Single(prList); + var pr = prList[0]; + Assert.Equal(TestConstants.TestPullRequestId, pr.Id); + Assert.Equal(TestConstants.TestPullRequestTitle, pr.Title); + Assert.Equal(PullRequestStates.Open, pr.State); + } - [Fact] - public async Task GetPullRequestAsync_WithValidId_ReturnsPullRequest() - { - // Arrange - _fixture.Reset(); - _fixture.Server.SetupGetPullRequest( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetPullRequestAsync_WithValidId_ReturnsPullRequest() + { + // Arrange + _fixture.Reset(); + _fixture.Server.SetupGetPullRequest( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); - // Act - var pullRequest = await client.GetPullRequestAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); + // Act + var pullRequest = await client.GetPullRequestAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); - // Assert - Assert.NotNull(pullRequest); - Assert.Equal(TestConstants.TestPullRequestId, pullRequest.Id); - Assert.Equal(TestConstants.TestPullRequestTitle, pullRequest.Title); - Assert.NotNull(pullRequest.FromRef); - Assert.NotNull(pullRequest.ToRef); - } + // Assert + Assert.NotNull(pullRequest); + Assert.Equal(TestConstants.TestPullRequestId, pullRequest.Id); + Assert.Equal(TestConstants.TestPullRequestTitle, pullRequest.Title); + Assert.NotNull(pullRequest.FromRef); + Assert.NotNull(pullRequest.ToRef); + } - [Fact] - public async Task GetPullRequestCommentsAsync_ReturnsComments() - { - // Arrange - _fixture.Reset(); - _fixture.Server.SetupGetPullRequestComments( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetPullRequestCommentsAsync_ReturnsComments() + { + // Arrange + _fixture.Reset(); + _fixture.Server.SetupGetPullRequestComments( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); - // Act - var comments = await client.GetPullRequestCommentsAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId, - "/"); + // Act + var comments = await client.GetPullRequestCommentsAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId, + "/"); - // Assert - Assert.NotNull(comments); - var commentList = comments.ToList(); - Assert.Single(commentList); - var comment = commentList[0]; - Assert.Equal(TestConstants.TestCommentId, comment.Id); - } + // Assert + Assert.NotNull(comments); + var commentList = comments.ToList(); + Assert.Single(commentList); + var comment = commentList[0]; + Assert.Equal(TestConstants.TestCommentId, comment.Id); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/PullRequestParticipantsMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/PullRequestParticipantsMockTests.cs index 4855ace..c1a541f 100644 --- a/test/Bitbucket.Net.Tests/MockTests/PullRequestParticipantsMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/PullRequestParticipantsMockTests.cs @@ -1,105 +1,97 @@ -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Models.Core.Projects; using Bitbucket.Net.Models.Core.Users; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests -{ - public class PullRequestParticipantsMockTests : IClassFixture - { - private readonly BitbucketMockFixture _fixture; +namespace Bitbucket.Net.Tests.MockTests; - public PullRequestParticipantsMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } +public class PullRequestParticipantsMockTests(BitbucketMockFixture fixture) : IClassFixture +{ + private readonly BitbucketMockFixture _fixture = fixture; - [Fact] - public async Task GetPullRequestParticipantsAsync_ReturnsParticipants() - { - _fixture.Reset(); - _fixture.Server.SetupGetPullRequestParticipants( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetPullRequestParticipantsAsync_ReturnsParticipants() + { + _fixture.Reset(); + _fixture.Server.SetupGetPullRequestParticipants( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); - var participants = await client.GetPullRequestParticipantsAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); + var participants = await client.GetPullRequestParticipantsAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); - Assert.NotNull(participants); - var participantList = participants.ToList(); - Assert.Single(participantList); - Assert.NotNull(participantList[0].User); - Assert.Equal("testuser", participantList[0].User!.Name); - Assert.Equal(Roles.Author, participantList[0].Role); - } + Assert.NotNull(participants); + var participantList = participants.ToList(); + Assert.Single(participantList); + Assert.NotNull(participantList[0].User); + Assert.Equal("testuser", participantList[0].User!.Name); + Assert.Equal(Roles.Author, participantList[0].Role); + } - [Fact] - public async Task AssignUserRoleToPullRequestAsync_ReturnsParticipant() - { - _fixture.Reset(); - _fixture.Server.SetupAssignUserRoleToPullRequest( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); + [Fact] + public async Task AssignUserRoleToPullRequestAsync_ReturnsParticipant() + { + _fixture.Reset(); + _fixture.Server.SetupAssignUserRoleToPullRequest( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); - var named = new Named { Name = "reviewer" }; + var named = new Named { Name = "reviewer" }; - var participant = await client.AssignUserRoleToPullRequestAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId, - named, - Roles.Reviewer); + var participant = await client.AssignUserRoleToPullRequestAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId, + named, + Roles.Reviewer); - Assert.NotNull(participant); - Assert.NotNull(participant.User); - Assert.Equal(Roles.Reviewer, participant.Role); - } + Assert.NotNull(participant); + Assert.NotNull(participant.User); + Assert.Equal(Roles.Reviewer, participant.Role); + } - [Fact] - public async Task DeletePullRequestParticipantAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupDeletePullRequestParticipant( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); + [Fact] + public async Task DeletePullRequestParticipantAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupDeletePullRequestParticipant( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); - var result = await client.DeletePullRequestParticipantAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId, - "testuser"); + var result = await client.DeletePullRequestParticipantAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId, + "testuser"); - Assert.True(result); - } + Assert.True(result); + } - [Fact] - public async Task UnassignUserFromPullRequestAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupUnassignUserFromPullRequest( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId, - "testuser"); - var client = _fixture.CreateClient(); + [Fact] + public async Task UnassignUserFromPullRequestAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupUnassignUserFromPullRequest( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId, + "testuser"); + var client = _fixture.CreateClient(); - var result = await client.UnassignUserFromPullRequestAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId, - "testuser"); + var result = await client.UnassignUserFromPullRequestAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId, + "testuser"); - Assert.True(result); - } + Assert.True(result); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/PullRequestWatchMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/PullRequestWatchMockTests.cs index dca5999..610769f 100644 --- a/test/Bitbucket.Net.Tests/MockTests/PullRequestWatchMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/PullRequestWatchMockTests.cs @@ -1,114 +1,106 @@ -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Models.Core.Projects; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests +namespace Bitbucket.Net.Tests.MockTests; + +public class PullRequestWatchMockTests(BitbucketMockFixture fixture) : IClassFixture { - public class PullRequestWatchMockTests : IClassFixture + private readonly BitbucketMockFixture _fixture = fixture; + + [Fact] + public async Task WatchPullRequestAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupWatchPullRequest( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); + + var result = await client.WatchPullRequestAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + + Assert.True(result); + } + + [Fact] + public async Task UnwatchPullRequestAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupUnwatchPullRequest( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); + + var result = await client.UnwatchPullRequestAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + + Assert.True(result); + } + + [Fact] + public async Task DeletePullRequestAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupDeletePullRequest( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); + + var versionInfo = new VersionInfo { Version = 0 }; + + var result = await client.DeletePullRequestAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId, + versionInfo); + + Assert.True(result); + } + + [Fact] + public async Task ReopenPullRequestAsync_ReturnsPullRequest() + { + _fixture.Reset(); + _fixture.Server.SetupReopenPullRequest( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); + + var pullRequest = await client.ReopenPullRequestAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + + Assert.NotNull(pullRequest); + Assert.Equal(TestConstants.TestPullRequestId, pullRequest.Id); + } + + [Fact] + public async Task DeletePullRequestApprovalAsync_ReturnsReviewer() { - private readonly BitbucketMockFixture _fixture; - - public PullRequestWatchMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public async Task WatchPullRequestAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupWatchPullRequest( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); - - var result = await client.WatchPullRequestAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - - Assert.True(result); - } - - [Fact] - public async Task UnwatchPullRequestAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupUnwatchPullRequest( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); - - var result = await client.UnwatchPullRequestAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - - Assert.True(result); - } - - [Fact] - public async Task DeletePullRequestAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupDeletePullRequest( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); - - var versionInfo = new VersionInfo { Version = 0 }; - - var result = await client.DeletePullRequestAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId, - versionInfo); - - Assert.True(result); - } - - [Fact] - public async Task ReopenPullRequestAsync_ReturnsPullRequest() - { - _fixture.Reset(); - _fixture.Server.SetupReopenPullRequest( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); - - var pullRequest = await client.ReopenPullRequestAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - - Assert.NotNull(pullRequest); - Assert.Equal(TestConstants.TestPullRequestId, pullRequest.Id); - } - - [Fact] - public async Task DeletePullRequestApprovalAsync_ReturnsReviewer() - { - _fixture.Reset(); - _fixture.Server.SetupDeletePullRequestApproval( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - var client = _fixture.CreateClient(); - - var reviewer = await client.DeletePullRequestApprovalAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - TestConstants.TestPullRequestId); - - Assert.NotNull(reviewer); - Assert.False(reviewer.Approved); - Assert.Equal(ParticipantStatus.Unapproved, reviewer.Status); - } + _fixture.Reset(); + _fixture.Server.SetupDeletePullRequestApproval( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); + + var reviewer = await client.DeletePullRequestApprovalAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + TestConstants.TestPullRequestId); + + Assert.NotNull(reviewer); + Assert.False(reviewer.Approved); + Assert.Equal(ParticipantStatus.Unapproved, reviewer.Status); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/RefRestrictionsMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/RefRestrictionsMockTests.cs index 1566901..b007765 100644 --- a/test/Bitbucket.Net.Tests/MockTests/RefRestrictionsMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/RefRestrictionsMockTests.cs @@ -1,204 +1,196 @@ -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Models.DefaultReviewers; using Bitbucket.Net.Models.RefRestrictions; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests +namespace Bitbucket.Net.Tests.MockTests; + +public class RefRestrictionsMockTests(BitbucketMockFixture fixture) : IClassFixture { - public class RefRestrictionsMockTests : IClassFixture + private readonly BitbucketMockFixture _fixture = fixture; + private const string ProjectKey = "PROJ"; + private const string RepoSlug = "repo"; + + [Fact] + public async Task GetProjectRefRestrictionsAsync_ReturnsRestrictions() { - private readonly BitbucketMockFixture _fixture; - private const string ProjectKey = "PROJ"; - private const string RepoSlug = "repo"; + _fixture.Reset(); + _fixture.Server.SetupGetProjectRefRestrictions(ProjectKey); + var client = _fixture.CreateClient(); - public RefRestrictionsMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } + var result = await client.GetProjectRefRestrictionsAsync(ProjectKey); - [Fact] - public async Task GetProjectRefRestrictionsAsync_ReturnsRestrictions() - { - _fixture.Reset(); - _fixture.Server.SetupGetProjectRefRestrictions(ProjectKey); - var client = _fixture.CreateClient(); + Assert.NotNull(result); + var restrictions = result.ToList(); + Assert.Equal(2, restrictions.Count); + Assert.Equal(1, restrictions[0].Id); + } - var result = await client.GetProjectRefRestrictionsAsync(ProjectKey); + [Fact] + public async Task GetProjectRefRestrictionAsync_ReturnsRestriction() + { + _fixture.Reset(); + _fixture.Server.SetupGetProjectRefRestriction(ProjectKey, 1); + var client = _fixture.CreateClient(); - Assert.NotNull(result); - var restrictions = result.ToList(); - Assert.Equal(2, restrictions.Count); - Assert.Equal(1, restrictions[0].Id); - } + var result = await client.GetProjectRefRestrictionAsync(ProjectKey, 1); - [Fact] - public async Task GetProjectRefRestrictionAsync_ReturnsRestriction() + Assert.NotNull(result); + Assert.Equal(1, result.Id); + Assert.NotNull(result.Matcher); + } + + [Fact] + public async Task CreateProjectRefRestrictionAsync_ReturnsCreatedRestriction() + { + _fixture.Reset(); + _fixture.Server.SetupCreateProjectRefRestriction(ProjectKey); + var client = _fixture.CreateClient(); + + var restriction = new RefRestrictionCreate { - _fixture.Reset(); - _fixture.Server.SetupGetProjectRefRestriction(ProjectKey, 1); - var client = _fixture.CreateClient(); + Type = RefRestrictionTypes.AllChanges, + Matcher = new RefMatcher + { + Id = "refs/heads/main", + DisplayId = "main", + Active = true, + Type = new DefaultReviewerPullRequestConditionType { Id = "BRANCH", Name = "Branch" } + } + }; - var result = await client.GetProjectRefRestrictionAsync(ProjectKey, 1); + var result = await client.CreateProjectRefRestrictionAsync(ProjectKey, restriction); - Assert.NotNull(result); - Assert.Equal(1, result.Id); - Assert.NotNull(result.Matcher); - } + Assert.NotNull(result); + Assert.Equal(1, result.Id); + } - [Fact] - public async Task CreateProjectRefRestrictionAsync_ReturnsCreatedRestriction() - { - _fixture.Reset(); - _fixture.Server.SetupCreateProjectRefRestriction(ProjectKey); - var client = _fixture.CreateClient(); + [Fact] + public async Task CreateProjectRefRestrictionsAsync_ReturnsCreatedRestrictions() + { + _fixture.Reset(); + _fixture.Server.SetupCreateProjectRefRestrictions(ProjectKey); + var client = _fixture.CreateClient(); - var restriction = new RefRestrictionCreate - { - Type = RefRestrictionTypes.AllChanges, - Matcher = new RefMatcher - { - Id = "refs/heads/main", - DisplayId = "main", - Active = true, - Type = new DefaultReviewerPullRequestConditionType { Id = "BRANCH", Name = "Branch" } - } - }; - - var result = await client.CreateProjectRefRestrictionAsync(ProjectKey, restriction); - - Assert.NotNull(result); - Assert.Equal(1, result.Id); - } - - [Fact] - public async Task CreateProjectRefRestrictionsAsync_ReturnsCreatedRestrictions() + var restriction = new RefRestrictionCreate { - _fixture.Reset(); - _fixture.Server.SetupCreateProjectRefRestrictions(ProjectKey); - var client = _fixture.CreateClient(); - - var restriction = new RefRestrictionCreate + Type = RefRestrictionTypes.Deletion, + Matcher = new RefMatcher { - Type = RefRestrictionTypes.Deletion, - Matcher = new RefMatcher - { - Id = "refs/heads/main", - DisplayId = "main", - Active = true, - Type = new DefaultReviewerPullRequestConditionType { Id = "BRANCH", Name = "Branch" } - } - }; - - var result = await client.CreateProjectRefRestrictionsAsync(ProjectKey, restriction); - - Assert.NotNull(result); - var restrictions = result.ToList(); - Assert.Single(restrictions); - Assert.Equal(3, restrictions[0].Id); - } - - [Fact] - public async Task DeleteProjectRefRestrictionAsync_ReturnsSuccess() - { - _fixture.Reset(); - _fixture.Server.SetupDeleteProjectRefRestriction(ProjectKey, 1); - var client = _fixture.CreateClient(); + Id = "refs/heads/main", + DisplayId = "main", + Active = true, + Type = new DefaultReviewerPullRequestConditionType { Id = "BRANCH", Name = "Branch" } + } + }; + + var result = await client.CreateProjectRefRestrictionsAsync(ProjectKey, restriction); + + Assert.NotNull(result); + var restrictions = result.ToList(); + Assert.Single(restrictions); + Assert.Equal(3, restrictions[0].Id); + } - var result = await client.DeleteProjectRefRestrictionAsync(ProjectKey, 1); + [Fact] + public async Task DeleteProjectRefRestrictionAsync_ReturnsSuccess() + { + _fixture.Reset(); + _fixture.Server.SetupDeleteProjectRefRestriction(ProjectKey, 1); + var client = _fixture.CreateClient(); - Assert.True(result); - } + var result = await client.DeleteProjectRefRestrictionAsync(ProjectKey, 1); - [Fact] - public async Task GetRepositoryRefRestrictionsAsync_ReturnsRestrictions() - { - _fixture.Reset(); - _fixture.Server.SetupGetRepositoryRefRestrictions(ProjectKey, RepoSlug); - var client = _fixture.CreateClient(); + Assert.True(result); + } - var result = await client.GetRepositoryRefRestrictionsAsync(ProjectKey, RepoSlug); + [Fact] + public async Task GetRepositoryRefRestrictionsAsync_ReturnsRestrictions() + { + _fixture.Reset(); + _fixture.Server.SetupGetRepositoryRefRestrictions(ProjectKey, RepoSlug); + var client = _fixture.CreateClient(); - Assert.NotNull(result); - var restrictions = result.ToList(); - Assert.Equal(2, restrictions.Count); - } + var result = await client.GetRepositoryRefRestrictionsAsync(ProjectKey, RepoSlug); - [Fact] - public async Task GetRepositoryRefRestrictionAsync_ReturnsRestriction() - { - _fixture.Reset(); - _fixture.Server.SetupGetRepositoryRefRestriction(ProjectKey, RepoSlug, 1); - var client = _fixture.CreateClient(); + Assert.NotNull(result); + var restrictions = result.ToList(); + Assert.Equal(2, restrictions.Count); + } - var result = await client.GetRepositoryRefRestrictionAsync(ProjectKey, RepoSlug, 1); + [Fact] + public async Task GetRepositoryRefRestrictionAsync_ReturnsRestriction() + { + _fixture.Reset(); + _fixture.Server.SetupGetRepositoryRefRestriction(ProjectKey, RepoSlug, 1); + var client = _fixture.CreateClient(); - Assert.NotNull(result); - Assert.Equal(1, result.Id); - } + var result = await client.GetRepositoryRefRestrictionAsync(ProjectKey, RepoSlug, 1); - [Fact] - public async Task CreateRepositoryRefRestrictionAsync_ReturnsCreatedRestriction() - { - _fixture.Reset(); - _fixture.Server.SetupCreateRepositoryRefRestriction(ProjectKey, RepoSlug); - var client = _fixture.CreateClient(); + Assert.NotNull(result); + Assert.Equal(1, result.Id); + } - var restriction = new RefRestrictionCreate - { - Type = RefRestrictionTypes.RewritingHistory, - Matcher = new RefMatcher - { - Id = "refs/heads/main", - DisplayId = "main", - Active = true, - Type = new DefaultReviewerPullRequestConditionType { Id = "BRANCH", Name = "Branch" } - } - }; - - var result = await client.CreateRepositoryRefRestrictionAsync(ProjectKey, RepoSlug, restriction); - - Assert.NotNull(result); - Assert.Equal(1, result.Id); - } - - [Fact] - public async Task CreateRepositoryRefRestrictionsAsync_ReturnsCreatedRestrictions() - { - _fixture.Reset(); - _fixture.Server.SetupCreateRepositoryRefRestrictions(ProjectKey, RepoSlug); - var client = _fixture.CreateClient(); + [Fact] + public async Task CreateRepositoryRefRestrictionAsync_ReturnsCreatedRestriction() + { + _fixture.Reset(); + _fixture.Server.SetupCreateRepositoryRefRestriction(ProjectKey, RepoSlug); + var client = _fixture.CreateClient(); - var restriction = new RefRestrictionCreate + var restriction = new RefRestrictionCreate + { + Type = RefRestrictionTypes.RewritingHistory, + Matcher = new RefMatcher { - Type = RefRestrictionTypes.ChangesWithoutPullRequest, - Matcher = new RefMatcher - { - Id = "refs/heads/main", - DisplayId = "main", - Active = true, - Type = new DefaultReviewerPullRequestConditionType { Id = "BRANCH", Name = "Branch" } - } - }; - - var result = await client.CreateRepositoryRefRestrictionsAsync(ProjectKey, RepoSlug, restriction); - - Assert.NotNull(result); - var restrictions = result.ToList(); - Assert.Single(restrictions); - } - - [Fact] - public async Task DeleteRepositoryRefRestrictionAsync_ReturnsSuccess() + Id = "refs/heads/main", + DisplayId = "main", + Active = true, + Type = new DefaultReviewerPullRequestConditionType { Id = "BRANCH", Name = "Branch" } + } + }; + + var result = await client.CreateRepositoryRefRestrictionAsync(ProjectKey, RepoSlug, restriction); + + Assert.NotNull(result); + Assert.Equal(1, result.Id); + } + + [Fact] + public async Task CreateRepositoryRefRestrictionsAsync_ReturnsCreatedRestrictions() + { + _fixture.Reset(); + _fixture.Server.SetupCreateRepositoryRefRestrictions(ProjectKey, RepoSlug); + var client = _fixture.CreateClient(); + + var restriction = new RefRestrictionCreate { - _fixture.Reset(); - _fixture.Server.SetupDeleteRepositoryRefRestriction(ProjectKey, RepoSlug, 1); - var client = _fixture.CreateClient(); + Type = RefRestrictionTypes.ChangesWithoutPullRequest, + Matcher = new RefMatcher + { + Id = "refs/heads/main", + DisplayId = "main", + Active = true, + Type = new DefaultReviewerPullRequestConditionType { Id = "BRANCH", Name = "Branch" } + } + }; + + var result = await client.CreateRepositoryRefRestrictionsAsync(ProjectKey, RepoSlug, restriction); + + Assert.NotNull(result); + var restrictions = result.ToList(); + Assert.Single(restrictions); + } + + [Fact] + public async Task DeleteRepositoryRefRestrictionAsync_ReturnsSuccess() + { + _fixture.Reset(); + _fixture.Server.SetupDeleteRepositoryRefRestriction(ProjectKey, RepoSlug, 1); + var client = _fixture.CreateClient(); - var result = await client.DeleteRepositoryRefRestrictionAsync(ProjectKey, RepoSlug, 1); + var result = await client.DeleteRepositoryRefRestrictionAsync(ProjectKey, RepoSlug, 1); - Assert.True(result); - } + Assert.True(result); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/RefSyncMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/RefSyncMockTests.cs index 93edef8..31b086e 100644 --- a/test/Bitbucket.Net.Tests/MockTests/RefSyncMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/RefSyncMockTests.cs @@ -1,73 +1,65 @@ -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Models.RefSync; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests +namespace Bitbucket.Net.Tests.MockTests; + +public class RefSyncMockTests(BitbucketMockFixture fixture) : IClassFixture { - public class RefSyncMockTests : IClassFixture - { - private readonly BitbucketMockFixture _fixture; - private const string ProjectKey = "PROJ"; - private const string RepoSlug = "repo"; + private readonly BitbucketMockFixture _fixture = fixture; + private const string ProjectKey = "PROJ"; + private const string RepoSlug = "repo"; - public RefSyncMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } + [Fact] + public async Task GetRepositorySynchronizationStatusAsync_ReturnsStatus() + { + _fixture.Reset(); + _fixture.Server.SetupGetRepositorySynchronizationStatus(ProjectKey, RepoSlug); + var client = _fixture.CreateClient(); - [Fact] - public async Task GetRepositorySynchronizationStatusAsync_ReturnsStatus() - { - _fixture.Reset(); - _fixture.Server.SetupGetRepositorySynchronizationStatus(ProjectKey, RepoSlug); - var client = _fixture.CreateClient(); + var result = await client.GetRepositorySynchronizationStatusAsync(ProjectKey, RepoSlug); - var result = await client.GetRepositorySynchronizationStatusAsync(ProjectKey, RepoSlug); + Assert.NotNull(result); + Assert.True(result.Available); + Assert.True(result.Enabled); + Assert.NotNull(result.AheadRefs); + Assert.Single(result.AheadRefs); + Assert.NotNull(result.DivergedRefs); + Assert.Single(result.DivergedRefs); + Assert.NotNull(result.OrphanedRefs); + Assert.Single(result.OrphanedRefs); + } - Assert.NotNull(result); - Assert.True(result.Available); - Assert.True(result.Enabled); - Assert.NotNull(result.AheadRefs); - Assert.Single(result.AheadRefs); - Assert.NotNull(result.DivergedRefs); - Assert.Single(result.DivergedRefs); - Assert.NotNull(result.OrphanedRefs); - Assert.Single(result.OrphanedRefs); - } + [Fact] + public async Task EnableRepositorySynchronizationAsync_ReturnsStatus() + { + _fixture.Reset(); + _fixture.Server.SetupEnableRepositorySynchronization(ProjectKey, RepoSlug); + var client = _fixture.CreateClient(); - [Fact] - public async Task EnableRepositorySynchronizationAsync_ReturnsStatus() - { - _fixture.Reset(); - _fixture.Server.SetupEnableRepositorySynchronization(ProjectKey, RepoSlug); - var client = _fixture.CreateClient(); + var result = await client.EnableRepositorySynchronizationAsync(ProjectKey, RepoSlug, true); - var result = await client.EnableRepositorySynchronizationAsync(ProjectKey, RepoSlug, true); + Assert.NotNull(result); + Assert.True(result.Enabled); + } - Assert.NotNull(result); - Assert.True(result.Enabled); - } + [Fact] + public async Task SynchronizeRepositoryAsync_ReturnsFullRef() + { + _fixture.Reset(); + _fixture.Server.SetupSynchronizeRepository(ProjectKey, RepoSlug); + var client = _fixture.CreateClient(); - [Fact] - public async Task SynchronizeRepositoryAsync_ReturnsFullRef() + var synchronize = new Synchronize { - _fixture.Reset(); - _fixture.Server.SetupSynchronizeRepository(ProjectKey, RepoSlug); - var client = _fixture.CreateClient(); - - var synchronize = new Synchronize - { - RefId = "refs/heads/feature/synced", - Action = SynchronizeActions.Merge - }; + RefId = "refs/heads/feature/synced", + Action = SynchronizeActions.Merge + }; - var result = await client.SynchronizeRepositoryAsync(ProjectKey, RepoSlug, synchronize); + var result = await client.SynchronizeRepositoryAsync(ProjectKey, RepoSlug, synchronize); - Assert.NotNull(result); - Assert.Equal("refs/heads/feature/synced", result.Id); - Assert.Equal("SYNCED", result.State); - } + Assert.NotNull(result); + Assert.Equal("refs/heads/feature/synced", result.Id); + Assert.Equal("SYNCED", result.State); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/RepositoryCrudMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/RepositoryCrudMockTests.cs index 55d49b7..b976e99 100644 --- a/test/Bitbucket.Net.Tests/MockTests/RepositoryCrudMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/RepositoryCrudMockTests.cs @@ -1,108 +1,100 @@ -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests +namespace Bitbucket.Net.Tests.MockTests; + +public class RepositoryCrudMockTests(BitbucketMockFixture fixture) : IClassFixture { - public class RepositoryCrudMockTests : IClassFixture + private readonly BitbucketMockFixture _fixture = fixture; + + [Fact] + public async Task CreateProjectRepositoryAsync_CreatesAndReturnsRepository() + { + _fixture.Reset(); + _fixture.Server.SetupCreateRepository(TestConstants.TestProjectKey); + var client = _fixture.CreateClient(); + + var result = await client.CreateProjectRepositoryAsync( + TestConstants.TestProjectKey, + "new-repo", + "git"); + + Assert.NotNull(result); + Assert.Equal("test-repo", result.Slug); + } + + [Fact] + public async Task UpdateProjectRepositoryAsync_UpdatesAndReturnsRepository() { - private readonly BitbucketMockFixture _fixture; - - public RepositoryCrudMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public async Task CreateProjectRepositoryAsync_CreatesAndReturnsRepository() - { - _fixture.Reset(); - _fixture.Server.SetupCreateRepository(TestConstants.TestProjectKey); - var client = _fixture.CreateClient(); - - var result = await client.CreateProjectRepositoryAsync( - TestConstants.TestProjectKey, - "new-repo", - "git"); - - Assert.NotNull(result); - Assert.Equal("test-repo", result.Slug); - } - - [Fact] - public async Task UpdateProjectRepositoryAsync_UpdatesAndReturnsRepository() - { - _fixture.Reset(); - _fixture.Server.SetupUpdateRepository(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var result = await client.UpdateProjectRepositoryAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - targetName: "updated-repo", - isForkable: true); - - Assert.NotNull(result); - } - - [Fact] - public async Task ScheduleProjectRepositoryForDeletionAsync_DeletesRepository() - { - _fixture.Reset(); - _fixture.Server.SetupDeleteRepository(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var result = await client.ScheduleProjectRepositoryForDeletionAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - - Assert.True(result); - } - - [Fact] - public async Task CreateProjectRepositoryForkAsync_CreatesAndReturnsFork() - { - _fixture.Reset(); - _fixture.Server.SetupForkRepository(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var result = await client.CreateProjectRepositoryForkAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - targetProjectKey: "FORK", - targetName: "forked-repo"); - - Assert.NotNull(result); - } - - [Fact] - public async Task GetProjectRepositoryForksAsync_ReturnsForks() - { - _fixture.Reset(); - _fixture.Server.SetupGetRepositoryForks(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var result = await client.GetProjectRepositoryForksAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - - Assert.NotNull(result); - } - - [Fact] - public async Task GetProjectRepositoryAsync_ReturnsRepository() - { - _fixture.Reset(); - _fixture.Server.SetupGetRepository(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var result = await client.GetProjectRepositoryAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - - Assert.NotNull(result); - Assert.Equal("test-repo", result.Slug); - } + _fixture.Reset(); + _fixture.Server.SetupUpdateRepository(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var result = await client.UpdateProjectRepositoryAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + targetName: "updated-repo", + isForkable: true); + + Assert.NotNull(result); + } + + [Fact] + public async Task ScheduleProjectRepositoryForDeletionAsync_DeletesRepository() + { + _fixture.Reset(); + _fixture.Server.SetupDeleteRepository(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var result = await client.ScheduleProjectRepositoryForDeletionAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + + Assert.True(result); + } + + [Fact] + public async Task CreateProjectRepositoryForkAsync_CreatesAndReturnsFork() + { + _fixture.Reset(); + _fixture.Server.SetupForkRepository(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var result = await client.CreateProjectRepositoryForkAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + targetProjectKey: "FORK", + targetName: "forked-repo"); + + Assert.NotNull(result); + } + + [Fact] + public async Task GetProjectRepositoryForksAsync_ReturnsForks() + { + _fixture.Reset(); + _fixture.Server.SetupGetRepositoryForks(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var result = await client.GetProjectRepositoryForksAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + + Assert.NotNull(result); + } + + [Fact] + public async Task GetProjectRepositoryAsync_ReturnsRepository() + { + _fixture.Reset(); + _fixture.Server.SetupGetRepository(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var result = await client.GetProjectRepositoryAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + + Assert.NotNull(result); + Assert.Equal("test-repo", result.Slug); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/RepositoryMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/RepositoryMockTests.cs index 2e9bccc..4cb53ef 100644 --- a/test/Bitbucket.Net.Tests/MockTests/RepositoryMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/RepositoryMockTests.cs @@ -1,76 +1,68 @@ -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests -{ - /// - /// Unit tests for repository-related operations using WireMock. - /// - public class RepositoryMockTests : IClassFixture - { - private readonly BitbucketMockFixture _fixture; +namespace Bitbucket.Net.Tests.MockTests; - public RepositoryMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } +/// +/// Unit tests for repository-related operations using WireMock. +/// +public class RepositoryMockTests(BitbucketMockFixture fixture) : IClassFixture +{ + private readonly BitbucketMockFixture _fixture = fixture; - [Fact] - public async Task GetProjectRepositoryAsync_WithValidSlug_ReturnsRepository() - { - // Arrange - _fixture.Reset(); - _fixture.Server.SetupGetRepository(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetProjectRepositoryAsync_WithValidSlug_ReturnsRepository() + { + // Arrange + _fixture.Reset(); + _fixture.Server.SetupGetRepository(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); - // Act - var repository = await client.GetProjectRepositoryAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); + // Act + var repository = await client.GetProjectRepositoryAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); - // Assert - Assert.NotNull(repository); - Assert.Equal(TestConstants.TestRepositorySlug, repository.Slug); - Assert.Equal(TestConstants.TestRepositoryName, repository.Name); - Assert.NotNull(repository.Project); - Assert.Equal(TestConstants.TestProjectKey, repository.Project.Key); - } + // Assert + Assert.NotNull(repository); + Assert.Equal(TestConstants.TestRepositorySlug, repository.Slug); + Assert.Equal(TestConstants.TestRepositoryName, repository.Name); + Assert.NotNull(repository.Project); + Assert.Equal(TestConstants.TestProjectKey, repository.Project.Key); + } - [Fact] - public async Task GetProjectRepositoriesAsync_ReturnsRepositories() - { - // Arrange - _fixture.Reset(); - _fixture.Server.SetupGetRepositories(TestConstants.TestProjectKey); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetProjectRepositoriesAsync_ReturnsRepositories() + { + // Arrange + _fixture.Reset(); + _fixture.Server.SetupGetRepositories(TestConstants.TestProjectKey); + var client = _fixture.CreateClient(); - // Act - var repositories = await client.GetProjectRepositoriesAsync(TestConstants.TestProjectKey); + // Act + var repositories = await client.GetProjectRepositoriesAsync(TestConstants.TestProjectKey); - // Assert - Assert.NotNull(repositories); - var repoList = repositories.ToList(); - Assert.Single(repoList); - var repo = repoList[0]; - Assert.Equal(TestConstants.TestRepositorySlug, repo.Slug); - Assert.Equal(TestConstants.TestRepositoryName, repo.Name); - } + // Assert + Assert.NotNull(repositories); + var repoList = repositories.ToList(); + Assert.Single(repoList); + var repo = repoList[0]; + Assert.Equal(TestConstants.TestRepositorySlug, repo.Slug); + Assert.Equal(TestConstants.TestRepositoryName, repo.Name); + } - [Fact] - public async Task GetRepositoryParticipantsAsync_ReturnsParticipants() - { - _fixture.Reset(); - _fixture.Server.SetupGetRepositoryParticipants(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetRepositoryParticipantsAsync_ReturnsParticipants() + { + _fixture.Reset(); + _fixture.Server.SetupGetRepositoryParticipants(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); - var participants = await client.GetRepositoryParticipantsAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); + var participants = await client.GetRepositoryParticipantsAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); - Assert.NotNull(participants); - Assert.NotEmpty(participants); - } + Assert.NotNull(participants); + Assert.NotEmpty(participants); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/RepositoryOperationsMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/RepositoryOperationsMockTests.cs index bc244ce..8d0d993 100644 --- a/test/Bitbucket.Net.Tests/MockTests/RepositoryOperationsMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/RepositoryOperationsMockTests.cs @@ -1,170 +1,162 @@ -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Models.Core.Projects; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests +namespace Bitbucket.Net.Tests.MockTests; + +public class RepositoryOperationsMockTests(BitbucketMockFixture fixture) : IClassFixture { - public class RepositoryOperationsMockTests : IClassFixture + private readonly BitbucketMockFixture _fixture = fixture; + + [Fact] + public async Task RecreateProjectRepositoryAsync_ReturnsRepository() { - private readonly BitbucketMockFixture _fixture; + _fixture.Reset(); + _fixture.Server.SetupRecreateProjectRepository( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var result = await client.RecreateProjectRepositoryAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + + Assert.NotNull(result); + Assert.Equal(TestConstants.TestRepositorySlug, result.Slug); + } - public RepositoryOperationsMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } + [Fact] + public async Task GetRelatedProjectRepositoriesAsync_ReturnsRelatedRepos() + { + _fixture.Reset(); + _fixture.Server.SetupGetRelatedProjectRepositories( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var result = await client.GetRelatedProjectRepositoriesAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + + Assert.NotNull(result); + var repos = result.ToList(); + Assert.NotEmpty(repos); + } - [Fact] - public async Task RecreateProjectRepositoryAsync_ReturnsRepository() - { - _fixture.Reset(); - _fixture.Server.SetupRecreateProjectRepository( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var result = await client.RecreateProjectRepositoryAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - - Assert.NotNull(result); - Assert.Equal(TestConstants.TestRepositorySlug, result.Slug); - } - - [Fact] - public async Task GetRelatedProjectRepositoriesAsync_ReturnsRelatedRepos() - { - _fixture.Reset(); - _fixture.Server.SetupGetRelatedProjectRepositories( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var result = await client.GetRelatedProjectRepositoriesAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - - Assert.NotNull(result); - var repos = result.ToList(); - Assert.NotEmpty(repos); - } - - [Fact] - public async Task GetProjectRepositoryArchiveAsync_ReturnsBytes() - { - _fixture.Reset(); - _fixture.Server.SetupGetProjectRepositoryArchive( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var result = await client.GetProjectRepositoryArchiveAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - at: "refs/heads/main", - fileName: "archive", - archiveFormat: ArchiveFormats.Zip, - path: "/", - prefix: "repo/"); - - Assert.NotNull(result); - Assert.True(result.Length > 0); - } - - [Fact] - public async Task GetProjectRepositoryPullRequestSettingsAsync_ReturnsSettings() - { - _fixture.Reset(); - _fixture.Server.SetupGetProjectRepositoryPullRequestSettings( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetProjectRepositoryArchiveAsync_ReturnsBytes() + { + _fixture.Reset(); + _fixture.Server.SetupGetProjectRepositoryArchive( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var result = await client.GetProjectRepositoryArchiveAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + at: "refs/heads/main", + fileName: "archive", + archiveFormat: ArchiveFormats.Zip, + path: "/", + prefix: "repo/"); + + Assert.NotNull(result); + Assert.True(result.Length > 0); + } - var result = await client.GetProjectRepositoryPullRequestSettingsAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); + [Fact] + public async Task GetProjectRepositoryPullRequestSettingsAsync_ReturnsSettings() + { + _fixture.Reset(); + _fixture.Server.SetupGetProjectRepositoryPullRequestSettings( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); - Assert.NotNull(result); - } + var result = await client.GetProjectRepositoryPullRequestSettingsAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); - [Fact] - public async Task UpdateProjectRepositoryPullRequestSettingsAsync_ReturnsUpdatedSettings() - { - _fixture.Reset(); - _fixture.Server.SetupUpdateProjectRepositoryPullRequestSettings( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var settings = new PullRequestSettings - { - RequiredApprovers = 2, - RequiredSuccessfulBuilds = 1, - RequiredAllApprovers = false, - RequiredAllTasksComplete = true - }; - - var result = await client.UpdateProjectRepositoryPullRequestSettingsAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - settings); - - Assert.NotNull(result); - } - - [Fact] - public async Task GetProjectRepositoryHooksSettingsAsync_ReturnsHooksSettings() - { - _fixture.Reset(); - _fixture.Server.SetupGetProjectRepositoryHooksSettings( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var result = await client.GetProjectRepositoryHooksSettingsAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - - Assert.NotNull(result); - var hooks = result.ToList(); - Assert.NotEmpty(hooks); - } - - [Fact] - public async Task EnableProjectRepositoryHookAsync_ReturnsHook() - { - _fixture.Reset(); - _fixture.Server.SetupEnableProjectRepositoryHook( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - "com.example.hook"); - var client = _fixture.CreateClient(); - - var result = await client.EnableProjectRepositoryHookAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - "com.example.hook"); - - Assert.NotNull(result); - } - - [Fact] - public async Task DisableProjectRepositoryHookAsync_ReturnsHook() + Assert.NotNull(result); + } + + [Fact] + public async Task UpdateProjectRepositoryPullRequestSettingsAsync_ReturnsUpdatedSettings() + { + _fixture.Reset(); + _fixture.Server.SetupUpdateProjectRepositoryPullRequestSettings( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var settings = new PullRequestSettings { - _fixture.Reset(); - _fixture.Server.SetupDisableProjectRepositoryHook( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - "com.example.hook"); - var client = _fixture.CreateClient(); - - var result = await client.DisableProjectRepositoryHookAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - "com.example.hook"); - - Assert.NotNull(result); - } + RequiredApprovers = 2, + RequiredSuccessfulBuilds = 1, + RequiredAllApprovers = false, + RequiredAllTasksComplete = true + }; + + var result = await client.UpdateProjectRepositoryPullRequestSettingsAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + settings); + + Assert.NotNull(result); + } + + [Fact] + public async Task GetProjectRepositoryHooksSettingsAsync_ReturnsHooksSettings() + { + _fixture.Reset(); + _fixture.Server.SetupGetProjectRepositoryHooksSettings( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var result = await client.GetProjectRepositoryHooksSettingsAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + + Assert.NotNull(result); + var hooks = result.ToList(); + Assert.NotEmpty(hooks); + } + + [Fact] + public async Task EnableProjectRepositoryHookAsync_ReturnsHook() + { + _fixture.Reset(); + _fixture.Server.SetupEnableProjectRepositoryHook( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + "com.example.hook"); + var client = _fixture.CreateClient(); + + var result = await client.EnableProjectRepositoryHookAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + "com.example.hook"); + + Assert.NotNull(result); + } + + [Fact] + public async Task DisableProjectRepositoryHookAsync_ReturnsHook() + { + _fixture.Reset(); + _fixture.Server.SetupDisableProjectRepositoryHook( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + "com.example.hook"); + var client = _fixture.CreateClient(); + + var result = await client.DisableProjectRepositoryHookAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + "com.example.hook"); + + Assert.NotNull(result); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/RepositoryPermissionsMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/RepositoryPermissionsMockTests.cs index 4f95cf3..95dd421 100644 --- a/test/Bitbucket.Net.Tests/MockTests/RepositoryPermissionsMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/RepositoryPermissionsMockTests.cs @@ -1,151 +1,142 @@ -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Models.Core.Admin; -using Bitbucket.Net.Models.Core.Projects; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests +namespace Bitbucket.Net.Tests.MockTests; + +public class RepositoryPermissionsMockTests(BitbucketMockFixture fixture) : IClassFixture { - public class RepositoryPermissionsMockTests : IClassFixture + private readonly BitbucketMockFixture _fixture = fixture; + + [Fact] + public async Task GetProjectRepositoryUserPermissionsAsync_ReturnsUserPermissions() + { + _fixture.Reset(); + _fixture.Server.SetupGetRepositoryUserPermissions(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var permissions = await client.GetProjectRepositoryUserPermissionsAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + + Assert.NotNull(permissions); + var permissionList = permissions.ToList(); + Assert.Single(permissionList); + Assert.NotNull(permissionList[0].User); + Assert.Equal("testuser", permissionList[0].User!.Name); + Assert.Equal(Permissions.RepoAdmin, permissionList[0].Permission); + } + + [Fact] + public async Task UpdateProjectRepositoryUserPermissionsAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupUpdateRepositoryUserPermissions(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var result = await client.UpdateProjectRepositoryUserPermissionsAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + Permissions.RepoAdmin, + "testuser"); + + Assert.True(result); + } + + [Fact] + public async Task DeleteProjectRepositoryUserPermissionsAsync_ReturnsTrue() { - private readonly BitbucketMockFixture _fixture; - - public RepositoryPermissionsMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public async Task GetProjectRepositoryUserPermissionsAsync_ReturnsUserPermissions() - { - _fixture.Reset(); - _fixture.Server.SetupGetRepositoryUserPermissions(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var permissions = await client.GetProjectRepositoryUserPermissionsAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - - Assert.NotNull(permissions); - var permissionList = permissions.ToList(); - Assert.Single(permissionList); - Assert.NotNull(permissionList[0].User); - Assert.Equal("testuser", permissionList[0].User!.Name); - Assert.Equal(Permissions.RepoAdmin, permissionList[0].Permission); - } - - [Fact] - public async Task UpdateProjectRepositoryUserPermissionsAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupUpdateRepositoryUserPermissions(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var result = await client.UpdateProjectRepositoryUserPermissionsAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - Permissions.RepoAdmin, - "testuser"); - - Assert.True(result); - } - - [Fact] - public async Task DeleteProjectRepositoryUserPermissionsAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupDeleteRepositoryUserPermissions(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var result = await client.DeleteProjectRepositoryUserPermissionsAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - "testuser"); - - Assert.True(result); - } - - [Fact] - public async Task GetProjectRepositoryUserPermissionsNoneAsync_ReturnsUsers() - { - _fixture.Reset(); - _fixture.Server.SetupGetRepositoryUserPermissionsNone(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var users = await client.GetProjectRepositoryUserPermissionsNoneAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - - Assert.NotNull(users); - var userList = users.ToList(); - Assert.NotEmpty(userList); - } - - [Fact] - public async Task GetProjectRepositoryGroupPermissionsAsync_ReturnsGroupPermissions() - { - _fixture.Reset(); - _fixture.Server.SetupGetRepositoryGroupPermissions(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var permissions = await client.GetProjectRepositoryGroupPermissionsAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - - Assert.NotNull(permissions); - var permissionList = permissions.ToList(); - Assert.Single(permissionList); - Assert.NotNull(permissionList[0].Group); - Assert.Equal("developers", permissionList[0].Group!.Name); - Assert.Equal(Permissions.RepoWrite, permissionList[0].Permission); - } - - [Fact] - public async Task UpdateProjectRepositoryGroupPermissionsAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupUpdateRepositoryGroupPermissions(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var result = await client.UpdateProjectRepositoryGroupPermissionsAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - Permissions.RepoWrite, - "developers"); - - Assert.True(result); - } - - [Fact] - public async Task DeleteProjectRepositoryGroupPermissionsAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupDeleteRepositoryGroupPermissions(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var result = await client.DeleteProjectRepositoryGroupPermissionsAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - "developers"); - - Assert.True(result); - } - - [Fact] - public async Task GetProjectRepositoryGroupPermissionsNoneAsync_ReturnsDeletableGroupsOrUsers() - { - _fixture.Reset(); - _fixture.Server.SetupGetRepositoryGroupPermissionsNone(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var entities = await client.GetProjectRepositoryGroupPermissionsNoneAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - - Assert.NotNull(entities); - var entityList = entities.ToList(); - Assert.NotEmpty(entityList); - } + _fixture.Reset(); + _fixture.Server.SetupDeleteRepositoryUserPermissions(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var result = await client.DeleteProjectRepositoryUserPermissionsAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + "testuser"); + + Assert.True(result); + } + + [Fact] + public async Task GetProjectRepositoryUserPermissionsNoneAsync_ReturnsUsers() + { + _fixture.Reset(); + _fixture.Server.SetupGetRepositoryUserPermissionsNone(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var users = await client.GetProjectRepositoryUserPermissionsNoneAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + + Assert.NotNull(users); + var userList = users.ToList(); + Assert.NotEmpty(userList); + } + + [Fact] + public async Task GetProjectRepositoryGroupPermissionsAsync_ReturnsGroupPermissions() + { + _fixture.Reset(); + _fixture.Server.SetupGetRepositoryGroupPermissions(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var permissions = await client.GetProjectRepositoryGroupPermissionsAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + + Assert.NotNull(permissions); + var permissionList = permissions.ToList(); + Assert.Single(permissionList); + Assert.NotNull(permissionList[0].Group); + Assert.Equal("developers", permissionList[0].Group!.Name); + Assert.Equal(Permissions.RepoWrite, permissionList[0].Permission); + } + + [Fact] + public async Task UpdateProjectRepositoryGroupPermissionsAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupUpdateRepositoryGroupPermissions(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var result = await client.UpdateProjectRepositoryGroupPermissionsAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + Permissions.RepoWrite, + "developers"); + + Assert.True(result); + } + + [Fact] + public async Task DeleteProjectRepositoryGroupPermissionsAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupDeleteRepositoryGroupPermissions(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var result = await client.DeleteProjectRepositoryGroupPermissionsAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + "developers"); + + Assert.True(result); + } + + [Fact] + public async Task GetProjectRepositoryGroupPermissionsNoneAsync_ReturnsDeletableGroupsOrUsers() + { + _fixture.Reset(); + _fixture.Server.SetupGetRepositoryGroupPermissionsNone(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var entities = await client.GetProjectRepositoryGroupPermissionsNoneAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + + Assert.NotNull(entities); + var entityList = entities.ToList(); + Assert.NotEmpty(entityList); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/SshKeyMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/SshKeyMockTests.cs index 9171e72..e0ca529 100644 --- a/test/Bitbucket.Net.Tests/MockTests/SshKeyMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/SshKeyMockTests.cs @@ -1,271 +1,263 @@ -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Models.Core.Admin; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests +namespace Bitbucket.Net.Tests.MockTests; + +public class SshKeyMockTests(BitbucketMockFixture fixture) : IClassFixture { - public class SshKeyMockTests : IClassFixture + private readonly BitbucketMockFixture _fixture = fixture; + + [Fact] + public async Task GetProjectKeysAsync_ByKeyId_ReturnsKeys() { - private readonly BitbucketMockFixture _fixture; - - public SshKeyMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } - - [Fact] - public async Task GetProjectKeysAsync_ByKeyId_ReturnsKeys() - { - _fixture.Reset(); - _fixture.Server.SetupGetProjectKeysByKeyId(1); - var client = _fixture.CreateClient(); - - var keys = await client.GetProjectKeysAsync(keyId: 1); - - Assert.NotNull(keys); - var keyList = keys.ToList(); - Assert.Equal(2, keyList.Count); - } - - [Fact] - public async Task GetProjectKeysAsync_ByProjectKey_ReturnsKeys() - { - _fixture.Reset(); - _fixture.Server.SetupGetProjectKeysByProject(TestConstants.TestProjectKey); - var client = _fixture.CreateClient(); - - var keys = await client.GetProjectKeysAsync(projectKey: TestConstants.TestProjectKey); - - Assert.NotNull(keys); - } - - [Fact] - public async Task CreateProjectKeyAsync_CreatesKey() - { - _fixture.Reset(); - _fixture.Server.SetupCreateProjectKey(TestConstants.TestProjectKey); - var client = _fixture.CreateClient(); - - var key = await client.CreateProjectKeyAsync( - TestConstants.TestProjectKey, - "ssh-rsa AAAAB3...", - Permissions.RepoRead); - - Assert.NotNull(key); - } - - [Fact] - public async Task GetProjectKeyAsync_ReturnsKey() - { - _fixture.Reset(); - _fixture.Server.SetupGetProjectKey(TestConstants.TestProjectKey, 1); - var client = _fixture.CreateClient(); - - var key = await client.GetProjectKeyAsync(TestConstants.TestProjectKey, 1); - - Assert.NotNull(key); - } - - [Fact] - public async Task DeleteProjectKeyAsync_DeletesKey() - { - _fixture.Reset(); - _fixture.Server.SetupDeleteProjectKey(TestConstants.TestProjectKey, 1); - var client = _fixture.CreateClient(); - - var result = await client.DeleteProjectKeyAsync(TestConstants.TestProjectKey, 1); - - Assert.True(result); - } - - [Fact] - public async Task UpdateProjectKeyPermissionAsync_UpdatesPermission() - { - _fixture.Reset(); - _fixture.Server.SetupUpdateProjectKeyPermission(TestConstants.TestProjectKey, 1); - var client = _fixture.CreateClient(); - - var key = await client.UpdateProjectKeyPermissionAsync( - TestConstants.TestProjectKey, - 1, - Permissions.RepoWrite); - - Assert.NotNull(key); - } - - [Fact] - public async Task GetRepoKeysAsync_ByKeyId_ReturnsKeys() - { - _fixture.Reset(); - _fixture.Server.SetupGetRepoKeysByKeyId(1); - var client = _fixture.CreateClient(); - - var keys = await client.GetRepoKeysAsync(keyId: 1); - - Assert.NotNull(keys); - } - - [Fact] - public async Task GetRepoKeysAsync_ByProjectAndRepo_ReturnsKeys() - { - _fixture.Reset(); - _fixture.Server.SetupGetRepoKeysByProjectAndRepo( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var keys = await client.GetRepoKeysAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - - Assert.NotNull(keys); - } - - [Fact] - public async Task CreateRepoKeyAsync_CreatesKey() - { - _fixture.Reset(); - _fixture.Server.SetupCreateRepoKey( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var key = await client.CreateRepoKeyAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - "ssh-rsa AAAAB3...", - Permissions.RepoRead); - - Assert.NotNull(key); - } - - [Fact] - public async Task GetRepoKeyAsync_ReturnsKey() - { - _fixture.Reset(); - _fixture.Server.SetupGetRepoKey( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - 1); - var client = _fixture.CreateClient(); - - var key = await client.GetRepoKeyAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - 1); - - Assert.NotNull(key); - } - - [Fact] - public async Task DeleteRepoKeyAsync_DeletesKey() - { - _fixture.Reset(); - _fixture.Server.SetupDeleteRepoKey( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - 1); - var client = _fixture.CreateClient(); - - var result = await client.DeleteRepoKeyAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - 1); - - Assert.True(result); - } - - [Fact] - public async Task UpdateRepoKeyPermissionAsync_UpdatesPermission() - { - _fixture.Reset(); - _fixture.Server.SetupUpdateRepoKeyPermission( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - 1); - var client = _fixture.CreateClient(); - - var key = await client.UpdateRepoKeyPermissionAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - 1, - Permissions.RepoWrite); - - Assert.NotNull(key); - } - - [Fact] - public async Task GetUserKeysAsync_ReturnsKeys() - { - _fixture.Reset(); - _fixture.Server.SetupGetUserKeys(); - var client = _fixture.CreateClient(); - - var keys = await client.GetUserKeysAsync(); - - Assert.NotNull(keys); - } - - [Fact] - public async Task CreateUserKeyAsync_CreatesKey() - { - _fixture.Reset(); - _fixture.Server.SetupCreateUserKey(); - var client = _fixture.CreateClient(); - - var key = await client.CreateUserKeyAsync("ssh-rsa AAAAB3..."); - - Assert.NotNull(key); - } - - [Fact] - public async Task DeleteUserKeysAsync_DeletesKeys() - { - _fixture.Reset(); - _fixture.Server.SetupDeleteUserKeys(); - var client = _fixture.CreateClient(); - - var result = await client.DeleteUserKeysAsync(); - - Assert.True(result); - } - - [Fact] - public async Task DeleteUserKeyAsync_DeletesKey() - { - _fixture.Reset(); - _fixture.Server.SetupDeleteUserKey(1); - var client = _fixture.CreateClient(); - - var result = await client.DeleteUserKeyAsync(1); - - Assert.True(result); - } - - [Fact] - public async Task DeleteProjectsReposKeysAsync_DeletesKeys() - { - _fixture.Reset(); - _fixture.Server.SetupDeleteProjectsReposKeys(1); - var client = _fixture.CreateClient(); - - var result = await client.DeleteProjectsReposKeysAsync(1, "PROJECT:TEST", "REPO:test-repo"); - - Assert.True(result); - } - - [Fact] - public async Task GetSshSettingsAsync_ReturnsSettings() - { - _fixture.Reset(); - _fixture.Server.SetupGetSshSettings(); - var client = _fixture.CreateClient(); - - var settings = await client.GetSshSettingsAsync(); - - Assert.NotNull(settings); - } + _fixture.Reset(); + _fixture.Server.SetupGetProjectKeysByKeyId(1); + var client = _fixture.CreateClient(); + + var keys = await client.GetProjectKeysAsync(keyId: 1); + + Assert.NotNull(keys); + var keyList = keys.ToList(); + Assert.Equal(2, keyList.Count); + } + + [Fact] + public async Task GetProjectKeysAsync_ByProjectKey_ReturnsKeys() + { + _fixture.Reset(); + _fixture.Server.SetupGetProjectKeysByProject(TestConstants.TestProjectKey); + var client = _fixture.CreateClient(); + + var keys = await client.GetProjectKeysAsync(projectKey: TestConstants.TestProjectKey); + + Assert.NotNull(keys); + } + + [Fact] + public async Task CreateProjectKeyAsync_CreatesKey() + { + _fixture.Reset(); + _fixture.Server.SetupCreateProjectKey(TestConstants.TestProjectKey); + var client = _fixture.CreateClient(); + + var key = await client.CreateProjectKeyAsync( + TestConstants.TestProjectKey, + "ssh-rsa AAAAB3...", + Permissions.RepoRead); + + Assert.NotNull(key); + } + + [Fact] + public async Task GetProjectKeyAsync_ReturnsKey() + { + _fixture.Reset(); + _fixture.Server.SetupGetProjectKey(TestConstants.TestProjectKey, 1); + var client = _fixture.CreateClient(); + + var key = await client.GetProjectKeyAsync(TestConstants.TestProjectKey, 1); + + Assert.NotNull(key); + } + + [Fact] + public async Task DeleteProjectKeyAsync_DeletesKey() + { + _fixture.Reset(); + _fixture.Server.SetupDeleteProjectKey(TestConstants.TestProjectKey, 1); + var client = _fixture.CreateClient(); + + var result = await client.DeleteProjectKeyAsync(TestConstants.TestProjectKey, 1); + + Assert.True(result); + } + + [Fact] + public async Task UpdateProjectKeyPermissionAsync_UpdatesPermission() + { + _fixture.Reset(); + _fixture.Server.SetupUpdateProjectKeyPermission(TestConstants.TestProjectKey, 1); + var client = _fixture.CreateClient(); + + var key = await client.UpdateProjectKeyPermissionAsync( + TestConstants.TestProjectKey, + 1, + Permissions.RepoWrite); + + Assert.NotNull(key); + } + + [Fact] + public async Task GetRepoKeysAsync_ByKeyId_ReturnsKeys() + { + _fixture.Reset(); + _fixture.Server.SetupGetRepoKeysByKeyId(1); + var client = _fixture.CreateClient(); + + var keys = await client.GetRepoKeysAsync(keyId: 1); + + Assert.NotNull(keys); + } + + [Fact] + public async Task GetRepoKeysAsync_ByProjectAndRepo_ReturnsKeys() + { + _fixture.Reset(); + _fixture.Server.SetupGetRepoKeysByProjectAndRepo( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var keys = await client.GetRepoKeysAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + + Assert.NotNull(keys); + } + + [Fact] + public async Task CreateRepoKeyAsync_CreatesKey() + { + _fixture.Reset(); + _fixture.Server.SetupCreateRepoKey( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var key = await client.CreateRepoKeyAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + "ssh-rsa AAAAB3...", + Permissions.RepoRead); + + Assert.NotNull(key); + } + + [Fact] + public async Task GetRepoKeyAsync_ReturnsKey() + { + _fixture.Reset(); + _fixture.Server.SetupGetRepoKey( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + 1); + var client = _fixture.CreateClient(); + + var key = await client.GetRepoKeyAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + 1); + + Assert.NotNull(key); + } + + [Fact] + public async Task DeleteRepoKeyAsync_DeletesKey() + { + _fixture.Reset(); + _fixture.Server.SetupDeleteRepoKey( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + 1); + var client = _fixture.CreateClient(); + + var result = await client.DeleteRepoKeyAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + 1); + + Assert.True(result); + } + + [Fact] + public async Task UpdateRepoKeyPermissionAsync_UpdatesPermission() + { + _fixture.Reset(); + _fixture.Server.SetupUpdateRepoKeyPermission( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + 1); + var client = _fixture.CreateClient(); + + var key = await client.UpdateRepoKeyPermissionAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + 1, + Permissions.RepoWrite); + + Assert.NotNull(key); + } + + [Fact] + public async Task GetUserKeysAsync_ReturnsKeys() + { + _fixture.Reset(); + _fixture.Server.SetupGetUserKeys(); + var client = _fixture.CreateClient(); + + var keys = await client.GetUserKeysAsync(); + + Assert.NotNull(keys); + } + + [Fact] + public async Task CreateUserKeyAsync_CreatesKey() + { + _fixture.Reset(); + _fixture.Server.SetupCreateUserKey(); + var client = _fixture.CreateClient(); + + var key = await client.CreateUserKeyAsync("ssh-rsa AAAAB3..."); + + Assert.NotNull(key); + } + + [Fact] + public async Task DeleteUserKeysAsync_DeletesKeys() + { + _fixture.Reset(); + _fixture.Server.SetupDeleteUserKeys(); + var client = _fixture.CreateClient(); + + var result = await client.DeleteUserKeysAsync(); + + Assert.True(result); + } + + [Fact] + public async Task DeleteUserKeyAsync_DeletesKey() + { + _fixture.Reset(); + _fixture.Server.SetupDeleteUserKey(1); + var client = _fixture.CreateClient(); + + var result = await client.DeleteUserKeyAsync(1); + + Assert.True(result); + } + + [Fact] + public async Task DeleteProjectsReposKeysAsync_DeletesKeys() + { + _fixture.Reset(); + _fixture.Server.SetupDeleteProjectsReposKeys(1); + var client = _fixture.CreateClient(); + + var result = await client.DeleteProjectsReposKeysAsync(1, "PROJECT:TEST", "REPO:test-repo"); + + Assert.True(result); + } + + [Fact] + public async Task GetSshSettingsAsync_ReturnsSettings() + { + _fixture.Reset(); + _fixture.Server.SetupGetSshSettings(); + var client = _fixture.CreateClient(); + + var settings = await client.GetSshSettingsAsync(); + + Assert.NotNull(settings); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/StreamingMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/StreamingMockTests.cs new file mode 100644 index 0000000..705e991 --- /dev/null +++ b/test/Bitbucket.Net.Tests/MockTests/StreamingMockTests.cs @@ -0,0 +1,384 @@ +using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Tests.Infrastructure; +using Xunit; + +namespace Bitbucket.Net.Tests.MockTests; + +public class StreamingMockTests(BitbucketMockFixture fixture) : IClassFixture +{ + private const string ApiBasePath = "/rest/api/1.0"; + private readonly BitbucketMockFixture _fixture = fixture; + + #region GetProjectsStreamAsync + + [Fact] + public async Task GetProjectsStreamAsync_SinglePage_YieldsAllItems() + { + _fixture.Reset(); + _fixture.Server.SetupGetProjects(); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetProjectsStreamAsync()); + + Assert.Single(results); + Assert.Equal(TestConstants.TestProjectKey, results[0].Key); + } + + [Fact] + public async Task GetProjectsStreamAsync_MultiplePages_YieldsAllItems() + { + _fixture.Reset(); + _fixture.Server.SetupPagedEndpoint( + $"{ApiBasePath}/projects", "Core", "projects-page1.json", "projects-page2.json"); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetProjectsStreamAsync(start: 0)); + + Assert.Equal(3, results.Count); + Assert.Equal("PROJ1", results[0].Key); + Assert.Equal("PROJ2", results[1].Key); + Assert.Equal("PROJ3", results[2].Key); + } + + [Fact] + public async Task GetProjectsStreamAsync_EmptyResult_YieldsZeroItems() + { + _fixture.Reset(); + _fixture.Server.SetupEmptyPagedEndpoint($"{ApiBasePath}/projects"); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetProjectsStreamAsync()); + + Assert.Empty(results); + } + + #endregion + + #region GetProjectRepositoriesStreamAsync + + [Fact] + public async Task GetProjectRepositoriesStreamAsync_SinglePage_YieldsAllItems() + { + _fixture.Reset(); + _fixture.Server.SetupGetRepositories(TestConstants.TestProjectKey); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetProjectRepositoriesStreamAsync(TestConstants.TestProjectKey)); + + Assert.Single(results); + Assert.Equal(TestConstants.TestRepositorySlug, results[0].Slug); + } + + [Fact] + public async Task GetProjectRepositoriesStreamAsync_MultiplePages_YieldsAllItems() + { + _fixture.Reset(); + _fixture.Server.SetupPagedEndpoint( + $"{ApiBasePath}/projects/{TestConstants.TestProjectKey}/repos", + "Core", "repositories-page1.json", "repositories-page2.json"); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetProjectRepositoriesStreamAsync(TestConstants.TestProjectKey, start: 0)); + + Assert.Equal(3, results.Count); + Assert.Equal("repo-alpha", results[0].Slug); + Assert.Equal("repo-beta", results[1].Slug); + Assert.Equal("repo-gamma", results[2].Slug); + } + + #endregion + + #region GetRepositoriesStreamAsync + + [Fact] + public async Task GetRepositoriesStreamAsync_SinglePage_YieldsAllItems() + { + _fixture.Reset(); + _fixture.Server.SetupCustomResponse($"{ApiBasePath}/repos", System.Net.HttpStatusCode.OK, "Core", "repositories-list.json"); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetRepositoriesStreamAsync()); + + Assert.Single(results); + } + + [Fact] + public async Task GetRepositoriesStreamAsync_MultiplePages_YieldsAllItems() + { + _fixture.Reset(); + _fixture.Server.SetupPagedEndpoint( + $"{ApiBasePath}/repos", "Core", "repositories-page1.json", "repositories-page2.json"); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetRepositoriesStreamAsync(start: 0)); + + Assert.Equal(3, results.Count); + } + + #endregion + + #region GetBranchesStreamAsync + + [Fact] + public async Task GetBranchesStreamAsync_SinglePage_YieldsAllItems() + { + _fixture.Reset(); + _fixture.Server.SetupGetBranches(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetBranchesStreamAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug)); + + Assert.Equal(2, results.Count); + Assert.Equal("master", results[0].DisplayId); + } + + [Fact] + public async Task GetBranchesStreamAsync_MultiplePages_YieldsAllItems() + { + _fixture.Reset(); + _fixture.Server.SetupPagedEndpoint( + $"{ApiBasePath}/projects/{TestConstants.TestProjectKey}/repos/{TestConstants.TestRepositorySlug}/branches", + "Core", "branches-page1.json", "branches-page2.json"); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetBranchesStreamAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, start: 0)); + + Assert.Equal(3, results.Count); + Assert.Equal("main", results[0].DisplayId); + Assert.Equal("develop", results[1].DisplayId); + Assert.Equal("feature-x", results[2].DisplayId); + } + + #endregion + + #region GetCommitsStreamAsync + + [Fact] + public async Task GetCommitsStreamAsync_SinglePage_YieldsAllItems() + { + _fixture.Reset(); + _fixture.Server.SetupGetCommits(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetCommitsStreamAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, until: "HEAD")); + + Assert.Equal(2, results.Count); + } + + [Fact] + public async Task GetCommitsStreamAsync_MultiplePages_YieldsAllItems() + { + _fixture.Reset(); + _fixture.Server.SetupPagedEndpoint( + $"{ApiBasePath}/projects/{TestConstants.TestProjectKey}/repos/{TestConstants.TestRepositorySlug}/commits", + "Core", "commits-page1.json", "commits-page2.json"); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetCommitsStreamAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, until: "HEAD", start: 0)); + + Assert.Equal(3, results.Count); + } + + #endregion + + #region GetPullRequestsStreamAsync + + [Fact] + public async Task GetPullRequestsStreamAsync_SinglePage_YieldsAllItems() + { + _fixture.Reset(); + _fixture.Server.SetupGetPullRequests(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetPullRequestsStreamAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug)); + + Assert.Single(results); + Assert.Equal(TestConstants.TestPullRequestTitle, results[0].Title); + } + + [Fact] + public async Task GetPullRequestsStreamAsync_EmptyResult_YieldsZeroItems() + { + _fixture.Reset(); + _fixture.Server.SetupEmptyPagedEndpoint( + $"{ApiBasePath}/projects/{TestConstants.TestProjectKey}/repos/{TestConstants.TestRepositorySlug}/pull-requests"); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetPullRequestsStreamAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug)); + + Assert.Empty(results); + } + + #endregion + + #region GetPullRequestCommitsStreamAsync + + [Fact] + public async Task GetPullRequestCommitsStreamAsync_SinglePage_YieldsAllItems() + { + _fixture.Reset(); + _fixture.Server.SetupGetPullRequestCommits(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetPullRequestCommitsStreamAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, TestConstants.TestPullRequestId)); + + Assert.Equal(2, results.Count); + } + + #endregion + + #region GetRawFileContentLinesStreamAsync + + [Fact] + public async Task GetRawFileContentLinesStreamAsync_ReturnsContentLines() + { + _fixture.Reset(); + _fixture.Server.SetupGetRawFileContentStream(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetRawFileContentLinesStreamAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, "README.md")); + + Assert.NotEmpty(results); + Assert.Contains(results, l => l.Contains("README")); + } + + #endregion + + #region Phase 5 Streaming Methods + + [Fact] + public async Task GetPullRequestActivitiesStreamAsync_SinglePage_YieldsAllItems() + { + _fixture.Reset(); + _fixture.Server.SetupGetPullRequestActivities(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetPullRequestActivitiesStreamAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, TestConstants.TestPullRequestId)); + + Assert.Equal(2, results.Count); + } + + [Fact] + public async Task GetPullRequestChangesStreamAsync_SinglePage_YieldsAllItems() + { + _fixture.Reset(); + _fixture.Server.SetupCustomResponse( + $"{ApiBasePath}/projects/{TestConstants.TestProjectKey}/repos/{TestConstants.TestRepositorySlug}/pull-requests/{TestConstants.TestPullRequestId}/changes", + System.Net.HttpStatusCode.OK, "PullRequests", "changes.json"); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetPullRequestChangesStreamAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, TestConstants.TestPullRequestId)); + + Assert.Equal(2, results.Count); + } + + [Fact] + public async Task GetPullRequestParticipantsStreamAsync_SinglePage_YieldsAllItems() + { + _fixture.Reset(); + _fixture.Server.SetupGetPullRequestParticipants(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetPullRequestParticipantsStreamAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, TestConstants.TestPullRequestId)); + + Assert.Single(results); + } + + [Fact] + public async Task GetPullRequestTasksStreamAsync_SinglePage_YieldsAllItems() + { + _fixture.Reset(); + _fixture.Server.SetupGetPullRequestTasks(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); + +#pragma warning disable CS0618 // Obsolete + var results = await CollectAsync(client.GetPullRequestTasksStreamAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, TestConstants.TestPullRequestId)); +#pragma warning restore CS0618 + + Assert.Single(results); + } + + [Fact] + public async Task GetPullRequestBlockerCommentsStreamAsync_SinglePage_YieldsAllItems() + { + _fixture.Reset(); + _fixture.Server.SetupGetPullRequestBlockerComments(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, TestConstants.TestPullRequestId); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetPullRequestBlockerCommentsStreamAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, TestConstants.TestPullRequestId)); + + Assert.Single(results); + } + + [Fact] + public async Task GetChangesStreamAsync_SinglePage_YieldsAllItems() + { + _fixture.Reset(); + _fixture.Server.SetupGetChanges(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetChangesStreamAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, until: "HEAD")); + + Assert.Single(results); + } + + [Fact] + public async Task GetCommitChangesStreamAsync_SinglePage_YieldsAllItems() + { + _fixture.Reset(); + _fixture.Server.SetupGetCommitChanges(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, TestConstants.TestCommitId); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetCommitChangesStreamAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, TestConstants.TestCommitId)); + + Assert.Single(results); + } + + [Fact] + public async Task GetProjectRepositoryTagsStreamAsync_SinglePage_YieldsAllItems() + { + _fixture.Reset(); + _fixture.Server.SetupGetProjectRepositoryTags(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetProjectRepositoryTagsStreamAsync(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, filterText: "", orderBy: BranchOrderBy.Alphabetical)); + + Assert.Equal(2, results.Count); + } + + [Fact] + public async Task GetDashboardPullRequestsStreamAsync_SinglePage_YieldsAllItems() + { + _fixture.Reset(); + _fixture.Server.SetupGetDashboardPullRequests(); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetDashboardPullRequestsStreamAsync()); + + Assert.Single(results); + } + + [Fact] + public async Task GetInboxPullRequestsStreamAsync_SinglePage_YieldsAllItems() + { + _fixture.Reset(); + _fixture.Server.SetupGetInboxPullRequests(); + var client = _fixture.CreateClient(); + + var results = await CollectAsync(client.GetInboxPullRequestsStreamAsync()); + + Assert.Single(results); + } + + #endregion + + private static async Task> CollectAsync(IAsyncEnumerable source) + { + var list = new List(); + await foreach (var item in source) + { + list.Add(item); + } + return list; + } +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/TasksMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/TasksMockTests.cs index 31ddc4f..672b89a 100644 --- a/test/Bitbucket.Net.Tests/MockTests/TasksMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/TasksMockTests.cs @@ -1,82 +1,75 @@ -using System.Threading.Tasks; using Bitbucket.Net.Models.Core.Tasks; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests +namespace Bitbucket.Net.Tests.MockTests; + +public class TasksMockTests(BitbucketMockFixture fixture) : IClassFixture { - public class TasksMockTests : IClassFixture - { - private readonly BitbucketMockFixture _fixture; + private readonly BitbucketMockFixture _fixture = fixture; - public TasksMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } + [Fact] + public async Task CreateTaskAsync_ReturnsCreatedTask() + { + _fixture.Reset(); + _fixture.Server.SetupCreateTask(); + var client = _fixture.CreateClient(); - [Fact] - public async Task CreateTaskAsync_ReturnsCreatedTask() + var taskInfo = new TaskInfo { - _fixture.Reset(); - _fixture.Server.SetupCreateTask(); - var client = _fixture.CreateClient(); + Anchor = new TaskBasicAnchor { Id = 101, Type = "COMMENT" }, + Text = "Fix the null pointer exception" + }; - var taskInfo = new TaskInfo - { - Anchor = new TaskBasicAnchor { Id = 101, Type = "COMMENT" }, - Text = "Fix the null pointer exception" - }; + var task = await client.CreateTaskAsync(taskInfo); - var task = await client.CreateTaskAsync(taskInfo); - - Assert.NotNull(task); - Assert.Equal(1, task.Id); - Assert.Equal("Fix the null pointer exception", task.Text); - Assert.Equal("OPEN", task.State); - Assert.NotNull(task.Author); - Assert.Equal("jsmith", task.Author.Name); - } + Assert.NotNull(task); + Assert.Equal(1, task.Id); + Assert.Equal("Fix the null pointer exception", task.Text); + Assert.Equal("OPEN", task.State); + Assert.NotNull(task.Author); + Assert.Equal("jsmith", task.Author.Name); + } - [Fact] - public async Task GetTaskAsync_ReturnsTask() - { - _fixture.Reset(); - _fixture.Server.SetupGetTask(1); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetTaskAsync_ReturnsTask() + { + _fixture.Reset(); + _fixture.Server.SetupGetTask(1); + var client = _fixture.CreateClient(); - var task = await client.GetTaskAsync(1); + var task = await client.GetTaskAsync(1); - Assert.NotNull(task); - Assert.Equal(1, task.Id); - Assert.Equal("Fix the null pointer exception", task.Text); - Assert.Equal("OPEN", task.State); - Assert.NotNull(task.Anchor); - } + Assert.NotNull(task); + Assert.Equal(1, task.Id); + Assert.Equal("Fix the null pointer exception", task.Text); + Assert.Equal("OPEN", task.State); + Assert.NotNull(task.Anchor); + } - [Fact] - public async Task UpdateTaskAsync_ReturnsUpdatedTask() - { - _fixture.Reset(); - _fixture.Server.SetupUpdateTask(1); - var client = _fixture.CreateClient(); + [Fact] + public async Task UpdateTaskAsync_ReturnsUpdatedTask() + { + _fixture.Reset(); + _fixture.Server.SetupUpdateTask(1); + var client = _fixture.CreateClient(); - var task = await client.UpdateTaskAsync(1, "Updated task text"); + var task = await client.UpdateTaskAsync(1, "Updated task text"); - Assert.NotNull(task); - Assert.Equal(1, task.Id); - Assert.NotNull(task.Author); - } + Assert.NotNull(task); + Assert.Equal(1, task.Id); + Assert.NotNull(task.Author); + } - [Fact] - public async Task DeleteTaskAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupDeleteTask(1); - var client = _fixture.CreateClient(); + [Fact] + public async Task DeleteTaskAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupDeleteTask(1); + var client = _fixture.CreateClient(); - var result = await client.DeleteTaskAsync(1); + var result = await client.DeleteTaskAsync(1); - Assert.True(result); - } + Assert.True(result); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/UserMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/UserMockTests.cs index 6c12192..f1cc626 100644 --- a/test/Bitbucket.Net.Tests/MockTests/UserMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/UserMockTests.cs @@ -1,109 +1,100 @@ #nullable enable -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests +namespace Bitbucket.Net.Tests.MockTests; + +public class UserMockTests(BitbucketMockFixture fixture) : IClassFixture { - public class UserMockTests : IClassFixture - { - private readonly BitbucketMockFixture _fixture; + private readonly BitbucketMockFixture _fixture = fixture; - public UserMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } + [Fact] + public async Task GetUsersAsync_ReturnsUsers() + { + _fixture.Reset(); + _fixture.Server.SetupGetUsers(); + var client = _fixture.CreateClient(); - [Fact] - public async Task GetUsersAsync_ReturnsUsers() - { - _fixture.Reset(); - _fixture.Server.SetupGetUsers(); - var client = _fixture.CreateClient(); + var users = await client.GetUsersAsync(); - var users = await client.GetUsersAsync(); + Assert.NotNull(users); + var userList = users.ToList(); + Assert.Equal(2, userList.Count); + Assert.Equal("admin", userList[0].Name); + Assert.Equal("admin@example.com", userList[0].EmailAddress); + } - Assert.NotNull(users); - var userList = users.ToList(); - Assert.Equal(2, userList.Count); - Assert.Equal("admin", userList[0].Name); - Assert.Equal("admin@example.com", userList[0].EmailAddress); - } + [Fact] + public async Task GetUserAsync_ReturnsUser() + { + _fixture.Reset(); + _fixture.Server.SetupGetUser("admin"); + var client = _fixture.CreateClient(); - [Fact] - public async Task GetUserAsync_ReturnsUser() - { - _fixture.Reset(); - _fixture.Server.SetupGetUser("admin"); - var client = _fixture.CreateClient(); + var user = await client.GetUserAsync("admin"); - var user = await client.GetUserAsync("admin"); + Assert.NotNull(user); + Assert.Equal("admin", user.Name); + Assert.Equal("Administrator", user.DisplayName); + Assert.True(user.Active); + } - Assert.NotNull(user); - Assert.Equal("admin", user.Name); - Assert.Equal("Administrator", user.DisplayName); - Assert.True(user.Active); - } + [Fact] + public async Task UpdateUserAsync_ReturnsUpdatedUser() + { + _fixture.Reset(); + _fixture.Server.SetupUpdateUser(); + var client = _fixture.CreateClient(); - [Fact] - public async Task UpdateUserAsync_ReturnsUpdatedUser() - { - _fixture.Reset(); - _fixture.Server.SetupUpdateUser(); - var client = _fixture.CreateClient(); + var user = await client.UpdateUserAsync( + email: "newemail@example.com", + displayName: "New Display Name"); - var user = await client.UpdateUserAsync( - email: "newemail@example.com", - displayName: "New Display Name"); + Assert.NotNull(user); + Assert.Equal("admin", user.Name); + } - Assert.NotNull(user); - Assert.Equal("admin", user.Name); - } + [Fact] + public async Task DeleteUserAvatarAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupDeleteUserAvatar("admin"); + var client = _fixture.CreateClient(); - [Fact] - public async Task DeleteUserAvatarAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupDeleteUserAvatar("admin"); - var client = _fixture.CreateClient(); + var result = await client.DeleteUserAvatarAsync("admin"); - var result = await client.DeleteUserAvatarAsync("admin"); + Assert.True(result); + } - Assert.True(result); - } + [Fact] + public async Task GetUserSettingsAsync_ReturnsSettings() + { + _fixture.Reset(); + _fixture.Server.SetupGetUserSettings("admin"); + var client = _fixture.CreateClient(); - [Fact] - public async Task GetUserSettingsAsync_ReturnsSettings() - { - _fixture.Reset(); - _fixture.Server.SetupGetUserSettings("admin"); - var client = _fixture.CreateClient(); + var settings = await client.GetUserSettingsAsync("admin"); - var settings = await client.GetUserSettingsAsync("admin"); + Assert.NotNull(settings); + Assert.Equal("dark", settings["theme"]?.ToString()); + } - Assert.NotNull(settings); - Assert.Equal("dark", settings["theme"]?.ToString()); - } + [Fact] + public async Task UpdateUserSettingsAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupUpdateUserSettings("admin"); + var client = _fixture.CreateClient(); - [Fact] - public async Task UpdateUserSettingsAsync_ReturnsTrue() + var newSettings = new Dictionary { - _fixture.Reset(); - _fixture.Server.SetupUpdateUserSettings("admin"); - var client = _fixture.CreateClient(); - - var newSettings = new Dictionary - { - ["theme"] = "light", - ["notifications"] = false - }; + ["theme"] = "light", + ["notifications"] = false + }; - var result = await client.UpdateUserSettingsAsync("admin", newSettings); + var result = await client.UpdateUserSettingsAsync("admin", newSettings); - Assert.True(result); - } + Assert.True(result); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/UsersMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/UsersMockTests.cs index c8f2d78..4348ac1 100644 --- a/test/Bitbucket.Net.Tests/MockTests/UsersMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/UsersMockTests.cs @@ -1,124 +1,115 @@ #nullable enable -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Models.Core.Users; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests +namespace Bitbucket.Net.Tests.MockTests; + +public class UsersMockTests(BitbucketMockFixture fixture) : IClassFixture { - public class UsersMockTests : IClassFixture + private readonly BitbucketMockFixture _fixture = fixture; + + [Fact] + public async Task GetUsersAsync_ReturnsUsers() { - private readonly BitbucketMockFixture _fixture; + _fixture.Reset(); + _fixture.Server.SetupGetUsers(); + var client = _fixture.CreateClient(); + + var users = await client.GetUsersAsync(); + + var userList = users.ToList(); + Assert.NotEmpty(userList); + Assert.Equal(2, userList.Count); + Assert.Equal("admin", userList[0].Name); + Assert.Equal("admin@example.com", userList[0].EmailAddress); + Assert.Equal("developer", userList[1].Name); + } - public UsersMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } + [Fact] + public async Task GetUserAsync_ReturnsUser() + { + _fixture.Reset(); + _fixture.Server.SetupGetUser("admin"); + var client = _fixture.CreateClient(); - [Fact] - public async Task GetUsersAsync_ReturnsUsers() - { - _fixture.Reset(); - _fixture.Server.SetupGetUsers(); - var client = _fixture.CreateClient(); - - var users = await client.GetUsersAsync(); - - var userList = users.ToList(); - Assert.NotEmpty(userList); - Assert.Equal(2, userList.Count); - Assert.Equal("admin", userList[0].Name); - Assert.Equal("admin@example.com", userList[0].EmailAddress); - Assert.Equal("developer", userList[1].Name); - } - - [Fact] - public async Task GetUserAsync_ReturnsUser() - { - _fixture.Reset(); - _fixture.Server.SetupGetUser("admin"); - var client = _fixture.CreateClient(); + var user = await client.GetUserAsync("admin"); - var user = await client.GetUserAsync("admin"); + Assert.NotNull(user); + Assert.Equal("admin", user.Name); + Assert.Equal("admin@example.com", user.EmailAddress); + Assert.Equal("Administrator", user.DisplayName); + Assert.True(user.Active); + } - Assert.NotNull(user); - Assert.Equal("admin", user.Name); - Assert.Equal("admin@example.com", user.EmailAddress); - Assert.Equal("Administrator", user.DisplayName); - Assert.True(user.Active); - } + [Fact] + public async Task UpdateUserAsync_ReturnsUpdatedUser() + { + _fixture.Reset(); + _fixture.Server.SetupUpdateUser(); + var client = _fixture.CreateClient(); - [Fact] - public async Task UpdateUserAsync_ReturnsUpdatedUser() - { - _fixture.Reset(); - _fixture.Server.SetupUpdateUser(); - var client = _fixture.CreateClient(); + var user = await client.UpdateUserAsync(email: "newemail@example.com", displayName: "New Name"); - var user = await client.UpdateUserAsync(email: "newemail@example.com", displayName: "New Name"); + Assert.NotNull(user); + Assert.Equal("admin", user.Name); + } - Assert.NotNull(user); - Assert.Equal("admin", user.Name); - } + [Fact] + public async Task UpdateUserCredentialsAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupUpdateUserCredentials(); + var client = _fixture.CreateClient(); - [Fact] - public async Task UpdateUserCredentialsAsync_ReturnsTrue() + var passwordChange = new PasswordChange { - _fixture.Reset(); - _fixture.Server.SetupUpdateUserCredentials(); - var client = _fixture.CreateClient(); - - var passwordChange = new PasswordChange - { - OldPassword = "oldPassword", - Password = "newPassword", - PasswordConfirm = "newPassword" - }; + OldPassword = "oldPassword", + Password = "newPassword", + PasswordConfirm = "newPassword" + }; - var result = await client.UpdateUserCredentialsAsync(passwordChange); + var result = await client.UpdateUserCredentialsAsync(passwordChange); - Assert.True(result); - } + Assert.True(result); + } - [Fact] - public async Task DeleteUserAvatarAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupDeleteUserAvatar("admin"); - var client = _fixture.CreateClient(); + [Fact] + public async Task DeleteUserAvatarAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupDeleteUserAvatar("admin"); + var client = _fixture.CreateClient(); - var result = await client.DeleteUserAvatarAsync("admin"); + var result = await client.DeleteUserAvatarAsync("admin"); - Assert.True(result); - } + Assert.True(result); + } - [Fact] - public async Task GetUserSettingsAsync_ReturnsSettings() - { - _fixture.Reset(); - _fixture.Server.SetupGetUserSettings("admin"); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetUserSettingsAsync_ReturnsSettings() + { + _fixture.Reset(); + _fixture.Server.SetupGetUserSettings("admin"); + var client = _fixture.CreateClient(); - var settings = await client.GetUserSettingsAsync("admin"); + var settings = await client.GetUserSettingsAsync("admin"); - Assert.NotNull(settings); - Assert.Equal("dark", settings["theme"]?.ToString()); - } + Assert.NotNull(settings); + Assert.Equal("dark", settings["theme"]?.ToString()); + } - [Fact] - public async Task UpdateUserSettingsAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupUpdateUserSettings("admin"); - var client = _fixture.CreateClient(); + [Fact] + public async Task UpdateUserSettingsAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupUpdateUserSettings("admin"); + var client = _fixture.CreateClient(); - var settings = new Dictionary { ["theme"] = "dark" }; - var result = await client.UpdateUserSettingsAsync("admin", settings); + var settings = new Dictionary { ["theme"] = "dark" }; + var result = await client.UpdateUserSettingsAsync("admin", settings); - Assert.True(result); - } + Assert.True(result); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/WebhookAndCompareMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/WebhookAndCompareMockTests.cs index ec41908..caaf49c 100644 --- a/test/Bitbucket.Net.Tests/MockTests/WebhookAndCompareMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/WebhookAndCompareMockTests.cs @@ -1,77 +1,68 @@ -using System.Linq; -using System.Threading.Tasks; -using Bitbucket.Net.Models.Core.Projects; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests -{ - public class WebhookAndCompareMockTests : IClassFixture - { - private readonly BitbucketMockFixture _fixture; +namespace Bitbucket.Net.Tests.MockTests; - public WebhookAndCompareMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } +public class WebhookAndCompareMockTests(BitbucketMockFixture fixture) : IClassFixture +{ + private readonly BitbucketMockFixture _fixture = fixture; - [Fact] - public async Task GetProjectRepositoryWebHooksAsync_ReturnsWebhooks() - { - _fixture.Reset(); - _fixture.Server.SetupGetWebhooks( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetProjectRepositoryWebHooksAsync_ReturnsWebhooks() + { + _fixture.Reset(); + _fixture.Server.SetupGetWebhooks( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); - var webhooks = await client.GetProjectRepositoryWebHooksAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); + var webhooks = await client.GetProjectRepositoryWebHooksAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); - Assert.NotNull(webhooks); - var webhookList = webhooks.ToList(); - Assert.Equal(2, webhookList.Count); - Assert.Equal("CI/CD Webhook", webhookList[0].Name); - Assert.True(webhookList[0].Active); - } + Assert.NotNull(webhooks); + var webhookList = webhooks.ToList(); + Assert.Equal(2, webhookList.Count); + Assert.Equal("CI/CD Webhook", webhookList[0].Name); + Assert.True(webhookList[0].Active); + } - [Fact] - public async Task GetRepositoryCompareCommitsAsync_ReturnsCommits() - { - _fixture.Reset(); - _fixture.Server.SetupGetCompareCommits( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetRepositoryCompareCommitsAsync_ReturnsCommits() + { + _fixture.Reset(); + _fixture.Server.SetupGetCompareCommits( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); - var commits = await client.GetRepositoryCompareCommitsAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - from: "HEAD~5", - to: "HEAD"); + var commits = await client.GetRepositoryCompareCommitsAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + from: "HEAD~5", + to: "HEAD"); - Assert.NotNull(commits); - var commitList = commits.ToList(); - Assert.Equal(2, commitList.Count); - } + Assert.NotNull(commits); + var commitList = commits.ToList(); + Assert.Equal(2, commitList.Count); + } - [Fact] - public async Task GetRepositoryCompareDiffAsync_ReturnsDiff() - { - _fixture.Reset(); - _fixture.Server.SetupGetCompareDiff( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetRepositoryCompareDiffAsync_ReturnsDiff() + { + _fixture.Reset(); + _fixture.Server.SetupGetCompareDiff( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); - var diff = await client.GetRepositoryCompareDiffAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - from: "HEAD~5", - to: "HEAD"); + var diff = await client.GetRepositoryCompareDiffAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + from: "HEAD~5", + to: "HEAD"); - Assert.NotNull(diff); - Assert.NotNull(diff.Diffs); - } + Assert.NotNull(diff); + Assert.NotNull(diff.Diffs); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/WebhookMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/WebhookMockTests.cs index 754f9ed..df64309 100644 --- a/test/Bitbucket.Net.Tests/MockTests/WebhookMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/WebhookMockTests.cs @@ -1,73 +1,66 @@ -using System.Threading.Tasks; using Bitbucket.Net.Models.Core.Projects; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests +namespace Bitbucket.Net.Tests.MockTests; + +public class WebhookMockTests(BitbucketMockFixture fixture) : IClassFixture { - public class WebhookMockTests : IClassFixture - { - private readonly BitbucketMockFixture _fixture; + private readonly BitbucketMockFixture _fixture = fixture; - public WebhookMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } + [Fact] + public async Task GetProjectRepositoryWebHookAsync_ReturnsWebhook() + { + _fixture.Reset(); + _fixture.Server.SetupGetWebhook(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, "1"); + var client = _fixture.CreateClient(); - [Fact] - public async Task GetProjectRepositoryWebHookAsync_ReturnsWebhook() - { - _fixture.Reset(); - _fixture.Server.SetupGetWebhook(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, "1"); - var client = _fixture.CreateClient(); + var webhook = await client.GetProjectRepositoryWebHookAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + "1"); - var webhook = await client.GetProjectRepositoryWebHookAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - "1"); + Assert.NotNull(webhook); + Assert.Equal(1, webhook.Id); + Assert.Equal("CI/CD Webhook", webhook.Name); + } - Assert.NotNull(webhook); - Assert.Equal(1, webhook.Id); - Assert.Equal("CI/CD Webhook", webhook.Name); - } + [Fact] + public async Task CreateProjectRepositoryWebHookAsync_ReturnsWebhook() + { + _fixture.Reset(); + _fixture.Server.SetupCreateWebhook(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); + var client = _fixture.CreateClient(); - [Fact] - public async Task CreateProjectRepositoryWebHookAsync_ReturnsWebhook() + var newWebhook = new WebHook { - _fixture.Reset(); - _fixture.Server.SetupCreateWebhook(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug); - var client = _fixture.CreateClient(); - - var newWebhook = new WebHook - { - Name = "Test Webhook", - Url = "https://example.com/webhook", - Active = true, - Events = ["repo:refs_changed"] - }; + Name = "Test Webhook", + Url = "https://example.com/webhook", + Active = true, + Events = ["repo:refs_changed"] + }; - var webhook = await client.CreateProjectRepositoryWebHookAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - newWebhook); + var webhook = await client.CreateProjectRepositoryWebHookAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + newWebhook); - Assert.NotNull(webhook); - Assert.Equal(1, webhook.Id); - } + Assert.NotNull(webhook); + Assert.Equal(1, webhook.Id); + } - [Fact] - public async Task DeleteProjectRepositoryWebHookAsync_ReturnsTrue() - { - _fixture.Reset(); - _fixture.Server.SetupDeleteWebhook(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, "1"); - var client = _fixture.CreateClient(); + [Fact] + public async Task DeleteProjectRepositoryWebHookAsync_ReturnsTrue() + { + _fixture.Reset(); + _fixture.Server.SetupDeleteWebhook(TestConstants.TestProjectKey, TestConstants.TestRepositorySlug, "1"); + var client = _fixture.CreateClient(); - var result = await client.DeleteProjectRepositoryWebHookAsync( - TestConstants.TestProjectKey, - TestConstants.TestRepositorySlug, - "1"); + var result = await client.DeleteProjectRepositoryWebHookAsync( + TestConstants.TestProjectKey, + TestConstants.TestRepositorySlug, + "1"); - Assert.True(result); - } + Assert.True(result); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/MockTests/WhoAmIMockTests.cs b/test/Bitbucket.Net.Tests/MockTests/WhoAmIMockTests.cs index b6770be..97890a7 100644 --- a/test/Bitbucket.Net.Tests/MockTests/WhoAmIMockTests.cs +++ b/test/Bitbucket.Net.Tests/MockTests/WhoAmIMockTests.cs @@ -1,40 +1,33 @@ -using System.Threading.Tasks; using Bitbucket.Net.Tests.Infrastructure; using Xunit; -namespace Bitbucket.Net.Tests.MockTests -{ - public class WhoAmIMockTests : IClassFixture - { - private readonly BitbucketMockFixture _fixture; +namespace Bitbucket.Net.Tests.MockTests; - public WhoAmIMockTests(BitbucketMockFixture fixture) - { - _fixture = fixture; - } +public class WhoAmIMockTests(BitbucketMockFixture fixture) : IClassFixture +{ + private readonly BitbucketMockFixture _fixture = fixture; - [Fact] - public async Task GetWhoAmIAsync_ReturnsUsername() - { - _fixture.Reset(); - _fixture.Server.SetupGetWhoAmI("jsmith"); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetWhoAmIAsync_ReturnsUsername() + { + _fixture.Reset(); + _fixture.Server.SetupGetWhoAmI("jsmith"); + var client = _fixture.CreateClient(); - var username = await client.GetWhoAmIAsync(); + var username = await client.GetWhoAmIAsync(); - Assert.Equal("jsmith", username); - } + Assert.Equal("jsmith", username); + } - [Fact] - public async Task GetWhoAmIAsync_WithWhitespace_ReturnsTrimmedUsername() - { - _fixture.Reset(); - _fixture.Server.SetupGetWhoAmI(" jsmith "); - var client = _fixture.CreateClient(); + [Fact] + public async Task GetWhoAmIAsync_WithWhitespace_ReturnsTrimmedUsername() + { + _fixture.Reset(); + _fixture.Server.SetupGetWhoAmI(" jsmith "); + var client = _fixture.CreateClient(); - var username = await client.GetWhoAmIAsync(); + var username = await client.GetWhoAmIAsync(); - Assert.Equal("jsmith", username); - } + Assert.Equal("jsmith", username); } -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/UnitTests/BitbucketClientConstructorTests.cs b/test/Bitbucket.Net.Tests/UnitTests/BitbucketClientConstructorTests.cs index 1868d5e..0009687 100644 --- a/test/Bitbucket.Net.Tests/UnitTests/BitbucketClientConstructorTests.cs +++ b/test/Bitbucket.Net.Tests/UnitTests/BitbucketClientConstructorTests.cs @@ -1,7 +1,5 @@ #nullable enable -using System; -using System.Net.Http; using Flurl.Http; using Xunit; @@ -133,4 +131,4 @@ public void Constructor_FlurlClient_NoBaseUrl_ThrowsArgumentException() } #endregion -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/UnitTests/BitbucketHelpersTests.cs b/test/Bitbucket.Net.Tests/UnitTests/BitbucketHelpersTests.cs index b079508..3b72b29 100644 --- a/test/Bitbucket.Net.Tests/UnitTests/BitbucketHelpersTests.cs +++ b/test/Bitbucket.Net.Tests/UnitTests/BitbucketHelpersTests.cs @@ -1,6 +1,5 @@ #nullable enable -using System; using Bitbucket.Net.Common; using Bitbucket.Net.Models.Core.Admin; using Bitbucket.Net.Models.Core.Projects; @@ -676,4 +675,4 @@ public void CommentSeverityToString_Nullable_ReturnsCorrectValue(CommentSeverity } #endregion -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/UnitTests/CommonModelTests.cs b/test/Bitbucket.Net.Tests/UnitTests/CommonModelTests.cs index 27d782e..fab23eb 100644 --- a/test/Bitbucket.Net.Tests/UnitTests/CommonModelTests.cs +++ b/test/Bitbucket.Net.Tests/UnitTests/CommonModelTests.cs @@ -1,11 +1,9 @@ #nullable enable -using System; -using System.Collections.Generic; -using System.Text.Json; using Bitbucket.Net.Common; using Bitbucket.Net.Common.Models; using Bitbucket.Net.Serialization; +using System.Text.Json; using Xunit; namespace Bitbucket.Net.Tests.UnitTests; @@ -195,7 +193,7 @@ public void ErrorResponse_Serialization_RoundTrips() Assert.NotNull(deserialized); Assert.NotNull(deserialized.Errors); - + var errorList = new List(deserialized.Errors); Assert.Equal(2, errorList.Count); Assert.Equal("First error", errorList[0].Message); @@ -255,46 +253,53 @@ public void IsNullableType_ListOfInt_ReturnsFalse() #region UnixDateTimeExtensions Tests [Fact] - public void FromUnixTimeSeconds_ZeroReturnsEpoch() + public void FromUnixTimeMilliseconds_ZeroReturnsEpoch() { long timestamp = 0; - var result = timestamp.FromUnixTimeSeconds(); + var result = timestamp.FromUnixTimeMilliseconds(); - // The method uses AddMilliseconds, so 0 should give us the epoch converted to local time - var expected = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero).ToLocalTime(); + var expected = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero); Assert.Equal(expected, result); } [Fact] - public void FromUnixTimeSeconds_KnownTimestamp_ReturnsCorrectDate() + public void FromUnixTimeMilliseconds_KnownTimestamp_ReturnsCorrectDate() { // 1609459200000 milliseconds = Jan 1, 2021 00:00:00 UTC long timestamp = 1609459200000; - var result = timestamp.FromUnixTimeSeconds(); + var result = timestamp.FromUnixTimeMilliseconds(); - var expected = new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero).ToLocalTime(); + var expected = new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero); Assert.Equal(expected, result); } [Fact] - public void ToUnixTimeSeconds_Epoch_ReturnsZero() + public void ToUnixTimeMilliseconds_Epoch_ReturnsZero() { var epoch = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero); - var result = epoch.ToUnixTimeSeconds(); + var result = epoch.ToUnixTimeMilliseconds(); Assert.Equal(0, result); } [Fact] - public void ToUnixTimeSeconds_KnownDate_ReturnsCorrectValue() + public void ToUnixTimeMilliseconds_KnownDate_ReturnsCorrectValue() { var dateTime = new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero); - var result = dateTime.ToUnixTimeSeconds(); + var result = dateTime.ToUnixTimeMilliseconds(); + + Assert.Equal(1609459200000, result); + } + + [Fact] + public void UnixTimeMilliseconds_RoundTrip() + { + var original = new DateTimeOffset(2025, 6, 15, 12, 30, 45, 123, TimeSpan.Zero); + var milliseconds = original.ToUnixTimeMilliseconds(); + var restored = milliseconds.FromUnixTimeMilliseconds(); - // Note: The method name says "Seconds" but implementation returns Ticks - // This test verifies the actual behavior - Assert.True(result > 0); + Assert.Equal(original, restored); } #endregion -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/UnitTests/DiffStreamingExtensionsTests.cs b/test/Bitbucket.Net.Tests/UnitTests/DiffStreamingExtensionsTests.cs index 52c0b82..e980557 100644 --- a/test/Bitbucket.Net.Tests/UnitTests/DiffStreamingExtensionsTests.cs +++ b/test/Bitbucket.Net.Tests/UnitTests/DiffStreamingExtensionsTests.cs @@ -1,11 +1,9 @@ #nullable enable -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Bitbucket.Net.Common.Mcp; using Bitbucket.Net.Models.Core.Projects; using Xunit; +using ProjectPath = Bitbucket.Net.Models.Core.Projects.Path; namespace Bitbucket.Net.Tests.UnitTests; @@ -239,8 +237,8 @@ private static Diff CreateDiffWithLines(int lineCount) { return new Diff { - Source = new Path { Name = "test.cs" }, - Destination = new Path { Name = "test.cs" }, + Source = new ProjectPath { Name = "test.cs" }, + Destination = new ProjectPath { Name = "test.cs" }, Hunks = [CreateHunkWithLines(lineCount)] }; } @@ -288,4 +286,4 @@ private static async Task> ToListAsync(IAsyncEnumerable source) } #endregion -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/UnitTests/ExceptionTests.cs b/test/Bitbucket.Net.Tests/UnitTests/ExceptionTests.cs index fa3836e..f093539 100644 --- a/test/Bitbucket.Net.Tests/UnitTests/ExceptionTests.cs +++ b/test/Bitbucket.Net.Tests/UnitTests/ExceptionTests.cs @@ -1,17 +1,16 @@ -using System.Collections.Generic; -using System.Net; using Bitbucket.Net.Common.Exceptions; using Bitbucket.Net.Common.Models; +using System.Net; using Xunit; namespace Bitbucket.Net.Tests.UnitTests; public class ExceptionTests { - private static readonly List SampleErrors = new() - { + private static readonly List SampleErrors = + [ new Error { Message = "Test error", Context = "test-context" } - }; + ]; #region BitbucketApiException Factory Tests @@ -140,7 +139,7 @@ public void Create_WithContextInError_IncludesContextInMessage() { var errors = new List { - new Error { Message = "Field is invalid", Context = "username" } + new() { Message = "Field is invalid", Context = "username" } }; var exception = BitbucketApiException.Create(400, errors); @@ -153,7 +152,7 @@ public void Create_WithoutContextInError_OmitsContextFromMessage() { var errors = new List { - new Error { Message = "Something went wrong", Context = null } + new() { Message = "Something went wrong", Context = null } }; var exception = BitbucketApiException.Create(400, errors); @@ -168,8 +167,8 @@ public void Create_WithMultipleErrors_IncludesAllMessages() { var errors = new List { - new Error { Message = "Error 1", Context = "field1" }, - new Error { Message = "Error 2", Context = "field2" } + new() { Message = "Error 1", Context = "field1" }, + new() { Message = "Error 2", Context = "field2" } }; var exception = BitbucketApiException.Create(400, errors); @@ -188,7 +187,7 @@ public void BitbucketApiException_Properties_AreSetCorrectly() { var errors = new List { - new Error { Message = "Test", Context = "ctx" } + new() { Message = "Test", Context = "ctx" } }; var exception = new BitbucketApiException("Test message", HttpStatusCode.NotFound, errors, "https://api.test"); @@ -237,4 +236,4 @@ public void BitbucketApiException_WithInnerException_PreservesInnerException() } #endregion -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/UnitTests/JsonConverterTests.cs b/test/Bitbucket.Net.Tests/UnitTests/JsonConverterTests.cs index dbdd7df..748a9f5 100644 --- a/test/Bitbucket.Net.Tests/UnitTests/JsonConverterTests.cs +++ b/test/Bitbucket.Net.Tests/UnitTests/JsonConverterTests.cs @@ -1,7 +1,6 @@ -using System; -using System.Text.Json; using Bitbucket.Net.Common.Converters; using Bitbucket.Net.Models.Core.Projects; +using System.Text.Json; using Xunit; namespace Bitbucket.Net.Tests.UnitTests; @@ -32,11 +31,10 @@ public class JsonConverterTests [Fact] public void UnixDateTimeOffsetConverter_Read_FromNumber_ReturnsCorrectValue() { - // The converter uses milliseconds internally (despite the "seconds" naming) + // The converter uses milliseconds internally var json = "1609459200000"; // 2021-01-01 00:00:00 UTC in milliseconds var result = JsonSerializer.Deserialize(json, s_options); - // The result will be in local time - Assert.Equal(new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero).ToLocalTime(), result); + Assert.Equal(new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero), result); } [Fact] @@ -44,7 +42,7 @@ public void UnixDateTimeOffsetConverter_Read_FromString_ReturnsCorrectValue() { var json = "\"1609459200000\""; // 2021-01-01 00:00:00 UTC in milliseconds var result = JsonSerializer.Deserialize(json, s_options); - Assert.Equal(new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero).ToLocalTime(), result); + Assert.Equal(new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero), result); } [Fact] @@ -74,7 +72,7 @@ public void NullableUnixDateTimeOffsetConverter_Read_FromNumber_ReturnsCorrectVa { var json = "1609459200000"; // 2021-01-01 00:00:00 UTC in milliseconds var result = JsonSerializer.Deserialize(json, s_options); - Assert.Equal(new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero).ToLocalTime(), result); + Assert.Equal(new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero), result); } [Fact] @@ -82,7 +80,7 @@ public void NullableUnixDateTimeOffsetConverter_Read_FromString_ReturnsCorrectVa { var json = "\"1609459200000\""; // 2021-01-01 00:00:00 UTC in milliseconds var result = JsonSerializer.Deserialize(json, s_options); - Assert.Equal(new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero).ToLocalTime(), result); + Assert.Equal(new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero), result); } [Fact] @@ -381,4 +379,4 @@ public void RolesConverter_Read_NumberToken_ThrowsJsonException() } #endregion -} +} \ No newline at end of file diff --git a/test/Bitbucket.Net.Tests/UnitTests/McpExtensionsTests.cs b/test/Bitbucket.Net.Tests/UnitTests/McpExtensionsTests.cs deleted file mode 100644 index 87a1520..0000000 --- a/test/Bitbucket.Net.Tests/UnitTests/McpExtensionsTests.cs +++ /dev/null @@ -1,279 +0,0 @@ -#nullable enable - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using Bitbucket.Net.Common.Mcp; -using Xunit; - -namespace Bitbucket.Net.Tests.UnitTests; - -public class McpExtensionsTests -{ - #region TakeWithPaginationAsync Tests - - [Fact] - public async Task TakeWithPaginationAsync_EmptySource_ReturnsEmptyResult() - { - var source = AsyncEnumerable.Empty(); - - var result = await source.TakeWithPaginationAsync(10); - - Assert.Empty(result.Items); - Assert.False(result.HasMore); - Assert.Null(result.NextOffset); - Assert.Equal(0, result.Count); - } - - [Fact] - public async Task TakeWithPaginationAsync_LessThanLimit_ReturnsAllItems() - { - var source = CreateAsyncEnumerable(new[] { 1, 2, 3 }); - - var result = await source.TakeWithPaginationAsync(10); - - Assert.Equal(3, result.Items.Count); - Assert.Equal(new[] { 1, 2, 3 }, result.Items); - Assert.False(result.HasMore); - Assert.Null(result.NextOffset); - } - - [Fact] - public async Task TakeWithPaginationAsync_ExactlyLimit_ReturnsAllItems() - { - var source = CreateAsyncEnumerable(new[] { 1, 2, 3, 4, 5 }); - - var result = await source.TakeWithPaginationAsync(5); - - Assert.Equal(5, result.Items.Count); - Assert.False(result.HasMore); - Assert.Null(result.NextOffset); - } - - [Fact] - public async Task TakeWithPaginationAsync_MoreThanLimit_ReturnsLimitedItems() - { - var source = CreateAsyncEnumerable(new[] { 1, 2, 3, 4, 5, 6, 7 }); - - var result = await source.TakeWithPaginationAsync(5); - - Assert.Equal(5, result.Items.Count); - Assert.Equal(new[] { 1, 2, 3, 4, 5 }, result.Items); - Assert.True(result.HasMore); - Assert.Equal(5, result.NextOffset); - } - - [Fact] - public async Task TakeWithPaginationAsync_AcceptsCancellationToken() - { - // Just verify the method accepts and passes through the cancellation token - var cts = new CancellationTokenSource(); - var source = CreateAsyncEnumerable(new[] { 1, 2, 3 }); - - var result = await source.TakeWithPaginationAsync(10, cts.Token); - - Assert.Equal(3, result.Items.Count); - } - - #endregion - - #region TakeAsync Tests - - [Fact] - public async Task TakeAsync_EmptySource_YieldsNothing() - { - var source = AsyncEnumerable.Empty(); - - var result = await source.TakeAsync(10).ToListAsync(); - - Assert.Empty(result); - } - - [Fact] - public async Task TakeAsync_LessThanLimit_YieldsAllItems() - { - var source = CreateAsyncEnumerable(new[] { 1, 2, 3 }); - - var result = await source.TakeAsync(10).ToListAsync(); - - Assert.Equal(new[] { 1, 2, 3 }, result); - } - - [Fact] - public async Task TakeAsync_MoreThanLimit_YieldsLimitedItems() - { - var source = CreateAsyncEnumerable(new[] { 1, 2, 3, 4, 5, 6, 7 }); - - var result = await source.TakeAsync(3).ToListAsync(); - - Assert.Equal(new[] { 1, 2, 3 }, result); - } - - [Fact] - public async Task TakeAsync_ZeroLimit_YieldsNothing() - { - var source = CreateAsyncEnumerable(new[] { 1, 2, 3 }); - - var result = await source.TakeAsync(0).ToListAsync(); - - Assert.Empty(result); - } - - #endregion - - #region SkipAsync Tests - - [Fact] - public async Task SkipAsync_EmptySource_YieldsNothing() - { - var source = AsyncEnumerable.Empty(); - - var result = await source.SkipAsync(5).ToListAsync(); - - Assert.Empty(result); - } - - [Fact] - public async Task SkipAsync_SkipLessThanCount_YieldsRemaining() - { - var source = CreateAsyncEnumerable(new[] { 1, 2, 3, 4, 5 }); - - var result = await source.SkipAsync(2).ToListAsync(); - - Assert.Equal(new[] { 3, 4, 5 }, result); - } - - [Fact] - public async Task SkipAsync_SkipMoreThanCount_YieldsNothing() - { - var source = CreateAsyncEnumerable(new[] { 1, 2, 3 }); - - var result = await source.SkipAsync(10).ToListAsync(); - - Assert.Empty(result); - } - - [Fact] - public async Task SkipAsync_SkipZero_YieldsAllItems() - { - var source = CreateAsyncEnumerable(new[] { 1, 2, 3 }); - - var result = await source.SkipAsync(0).ToListAsync(); - - Assert.Equal(new[] { 1, 2, 3 }, result); - } - - #endregion - - #region PageAsync Tests - - [Fact] - public async Task PageAsync_FirstPage_ReturnsCorrectItems() - { - var source = CreateAsyncEnumerable(Enumerable.Range(1, 100)); - - var result = await source.PageAsync(offset: 0, limit: 10); - - Assert.Equal(Enumerable.Range(1, 10), result.Items); - Assert.True(result.HasMore); - Assert.Equal(10, result.NextOffset); - } - - [Fact] - public async Task PageAsync_MiddlePage_ReturnsCorrectItems() - { - var source = CreateAsyncEnumerable(Enumerable.Range(1, 100)); - - var result = await source.PageAsync(offset: 20, limit: 10); - - Assert.Equal(Enumerable.Range(21, 10), result.Items); - Assert.True(result.HasMore); - Assert.Equal(10, result.NextOffset); - } - - [Fact] - public async Task PageAsync_LastPage_ReturnsRemainingItems() - { - var source = CreateAsyncEnumerable(Enumerable.Range(1, 25)); - - var result = await source.PageAsync(offset: 20, limit: 10); - - Assert.Equal(Enumerable.Range(21, 5), result.Items); - Assert.False(result.HasMore); - Assert.Null(result.NextOffset); - } - - [Fact] - public async Task PageAsync_BeyondEnd_ReturnsEmpty() - { - var source = CreateAsyncEnumerable(Enumerable.Range(1, 10)); - - var result = await source.PageAsync(offset: 100, limit: 10); - - Assert.Empty(result.Items); - Assert.False(result.HasMore); - Assert.Null(result.NextOffset); - } - - #endregion - - #region PaginatedResult Tests - - [Fact] - public void PaginatedResult_Count_ReturnsItemCount() - { - var result = new PaginatedResult([1, 2, 3], hasMore: true, nextOffset: 3); - - Assert.Equal(3, result.Count); - } - - [Fact] - public void PaginatedResult_Deconstruct_Works() - { - var result = new PaginatedResult([1, 2], hasMore: true, nextOffset: 2); - - var (items, hasMore, nextOffset) = result; - - Assert.Equal(2, items.Count); - Assert.True(hasMore); - Assert.Equal(2, nextOffset); - } - - [Fact] - public void PaginatedResult_Items_IsReadOnly() - { - var result = new PaginatedResult([1, 2, 3], hasMore: false, nextOffset: null); - - Assert.IsAssignableFrom>(result.Items); - } - - #endregion - - #region Helper Methods - - private static async IAsyncEnumerable CreateAsyncEnumerable(IEnumerable source) - { - foreach (var item in source) - { - yield return item; - await Task.Yield(); // Simulate async behavior - } - } - - #endregion -} - -internal static class AsyncEnumerableTestExtensions -{ - public static async Task> ToListAsync(this IAsyncEnumerable source) - { - var list = new List(); - await foreach (var item in source) - { - list.Add(item); - } - return list; - } -} diff --git a/test/Bitbucket.Net.Tests/UnitTests/ModelSerializationTests.cs b/test/Bitbucket.Net.Tests/UnitTests/ModelSerializationTests.cs index eb37ae1..3536e2a 100644 --- a/test/Bitbucket.Net.Tests/UnitTests/ModelSerializationTests.cs +++ b/test/Bitbucket.Net.Tests/UnitTests/ModelSerializationTests.cs @@ -1,13 +1,11 @@ #nullable enable -using System; -using System.Collections.Generic; -using System.Text.Json; -using Bitbucket.Net.Models.Core.Projects; +using Bitbucket.Net.Models.Builds; using Bitbucket.Net.Models.Core.Admin; +using Bitbucket.Net.Models.Core.Projects; using Bitbucket.Net.Models.Core.Users; -using Bitbucket.Net.Models.Builds; using Bitbucket.Net.Serialization; +using System.Text.Json; using Xunit; namespace Bitbucket.Net.Tests.UnitTests; @@ -552,4 +550,4 @@ public void PullRequest_WithFromRef_Serialization_RoundTrips() } #endregion -} +} \ No newline at end of file