From 104b0d1ed812ae5e8090dc7bb6d4e943a4255c90 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Thu, 14 Nov 2019 11:35:43 +0200 Subject: [PATCH 001/549] Fix #236 --- src/redmine-net20-api/Types/User.cs | 23 +++++++++++++++---- .../JSonConverters/UserConverter.cs | 18 ++++++++++++--- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/redmine-net20-api/Types/User.cs b/src/redmine-net20-api/Types/User.cs index 92dacd7c..214b70e0 100644 --- a/src/redmine-net20-api/Types/User.cs +++ b/src/redmine-net20-api/Types/User.cs @@ -215,12 +215,27 @@ public void WriteXml(XmlWriter writer) writer.WriteElementString(RedmineKeys.FIRSTNAME, FirstName); writer.WriteElementString(RedmineKeys.LASTNAME, LastName); writer.WriteElementString(RedmineKeys.MAIL, Email); - writer.WriteElementString(RedmineKeys.MAIL_NOTIFICATION, MailNotification); - writer.WriteElementString(RedmineKeys.PASSWORD, Password); - writer.WriteValueOrEmpty(AuthenticationModeId, RedmineKeys.AUTH_SOURCE_ID); + if(!string.IsNullOrEmpty(MailNotification)) + { + writer.WriteElementString(RedmineKeys.MAIL_NOTIFICATION, MailNotification); + } + + if (!string.IsNullOrEmpty(Password)) + { + writer.WriteElementString(RedmineKeys.PASSWORD, Password); + } + + if(AuthenticationModeId.HasValue) + { + writer.WriteValueOrEmpty(AuthenticationModeId, RedmineKeys.AUTH_SOURCE_ID); + } + writer.WriteElementString(RedmineKeys.MUST_CHANGE_PASSWD, MustChangePassword.ToString().ToLowerInvariant()); writer.WriteElementString(RedmineKeys.STATUS, ((int)Status).ToString(CultureInfo.InvariantCulture)); - writer.WriteArray(CustomFields, RedmineKeys.CUSTOM_FIELDS); + if(CustomFields != null) + { + writer.WriteArray(CustomFields, RedmineKeys.CUSTOM_FIELDS); + } } /// diff --git a/src/redmine-net40-api/JSonConverters/UserConverter.cs b/src/redmine-net40-api/JSonConverters/UserConverter.cs index a08baf4e..9c41557b 100644 --- a/src/redmine-net40-api/JSonConverters/UserConverter.cs +++ b/src/redmine-net40-api/JSonConverters/UserConverter.cs @@ -86,11 +86,23 @@ public override IDictionary Serialize(object obj, JavaScriptSeri result.Add(RedmineKeys.FIRSTNAME, entity.FirstName); result.Add(RedmineKeys.LASTNAME, entity.LastName); result.Add(RedmineKeys.MAIL, entity.Email); - result.Add(RedmineKeys.MAIL_NOTIFICATION, entity.MailNotification); - result.Add(RedmineKeys.PASSWORD, entity.Password); + if(!string.IsNullOrWhiteSpace(entity.MailNotification)) + { + result.Add(RedmineKeys.MAIL_NOTIFICATION, entity.MailNotification); + } + + if(!string.IsNullOrWhiteSpace(entity.Password)) + { + result.Add(RedmineKeys.PASSWORD, entity.Password); + } + result.Add(RedmineKeys.MUST_CHANGE_PASSWD, entity.MustChangePassword.ToString().ToLowerInvariant()); result.Add(RedmineKeys.STATUS, ((int)entity.Status).ToString(CultureInfo.InvariantCulture)); - result.WriteValueOrEmpty(entity.AuthenticationModeId, RedmineKeys.AUTH_SOURCE_ID); + + if(entity.AuthenticationModeId.HasValue) + { + result.WriteValueOrEmpty(entity.AuthenticationModeId, RedmineKeys.AUTH_SOURCE_ID); + } result.WriteArray(RedmineKeys.CUSTOM_FIELDS, entity.CustomFields, new IssueCustomFieldConverter(), serializer); From a14897a1db2371d5dc8d7a59b7f41e12938a0598 Mon Sep 17 00:00:00 2001 From: Padi Date: Mon, 18 Nov 2019 16:59:23 +0200 Subject: [PATCH 002/549] Update README.md --- README.md | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index f62774a3..4cdf8671 100755 --- a/README.md +++ b/README.md @@ -34,17 +34,10 @@ Resource | Read | Create | Update | Delete ## Packages and Status -Package | Build status | Nuget --------- | ------------ | ------- -redmine-net20-api | ![alt text](https://ci.appveyor.com/api/projects/status/github/zapadi/redmine-net-api?branch=master&svg=true) | [![NuGet package](https://img.shields.io/nuget/v/redmine-api.svg)](https://www.nuget.org/packages/redmine-api) -redmine-net40-api | ![alt text](https://ci.appveyor.com/api/projects/status/github/zapadi/redmine-net-api?branch=master&svg=true) | [![NuGet package](https://img.shields.io/nuget/v/redmine-api.svg)](https://www.nuget.org/packages/redmine-api) -redmine-net40-api-signed | ![alt text](https://ci.appveyor.com/api/projects/status/github/zapadi/redmine-net-api?branch=master&svg=true) | [![NuGet package](https://img.shields.io/nuget/v/redmine-api.svg)](https://www.nuget.org/packages/redmine-api) -redmine-net45-api | ![alt text](https://ci.appveyor.com/api/projects/status/github/zapadi/redmine-net-api?branch=master&svg=true) | [![NuGet package](https://img.shields.io/nuget/v/redmine-api.svg)](https://www.nuget.org/packages/redmine-api) -redmine-net45-api-signed | ![alt text](https://ci.appveyor.com/api/projects/status/github/zapadi/redmine-net-api?branch=master&svg=true) | [![NuGet package](https://img.shields.io/nuget/v/redmine-api.svg)](https://www.nuget.org/packages/redmine-api) -redmine-net451-api | ![alt text](https://ci.appveyor.com/api/projects/status/github/zapadi/redmine-net-api?branch=master&svg=true) | [![NuGet package](https://img.shields.io/nuget/v/redmine-api.svg)](https://www.nuget.org/packages/redmine-api) -redmine-net451-api-signed | ![alt text](https://ci.appveyor.com/api/projects/status/github/zapadi/redmine-net-api?branch=master&svg=true) | [![NuGet package](https://img.shields.io/nuget/v/redmine-api.svg)](https://www.nuget.org/packages/redmine-api) -redmine-net452-api | ![alt text](https://ci.appveyor.com/api/projects/status/github/zapadi/redmine-net-api?branch=master&svg=true) | [![NuGet package](https://img.shields.io/nuget/v/redmine-api.svg)](https://www.nuget.org/packages/redmine-api) -redmine-net452-api-signed | ![alt text](https://ci.appveyor.com/api/projects/status/github/zapadi/redmine-net-api?branch=master&svg=true) | [![NuGet package](https://img.shields.io/nuget/v/redmine-api.svg)](https://www.nuget.org/packages/redmine-api) +Build status | Nuget + ------------ | ------- +![alt text](https://ci.appveyor.com/api/projects/status/github/zapadi/redmine-net-api?branch=master&svg=true) | [![NuGet package](https://img.shields.io/nuget/v/redmine-api.svg)](https://www.nuget.org/packages/redmine-api) + ## WIKI From d5eef2931649c2cae6ece5141514458d9b8a8101 Mon Sep 17 00:00:00 2001 From: Padi Date: Mon, 18 Nov 2019 17:03:45 +0200 Subject: [PATCH 003/549] Update README.md --- README.md | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 4cdf8671..0c540592 100755 --- a/README.md +++ b/README.md @@ -1,7 +1,11 @@ -![](https://github.com/zapadi/redmine-net-api/blob/master/logo.png) + ![Nuget](https://img.shields.io/nuget/dt/redmine-net-api) +![alt text](https://ci.appveyor.com/api/projects/status/github/zapadi/redmine-net-api?branch=master&svg=true) +[![NuGet package](https://img.shields.io/nuget/v/redmine-api.svg)](https://www.nuget.org/packages/redmine-api) Buy Me A Coffee -# redmine-net-api + + +# redmine-net-api ![](https://github.com/zapadi/redmine-net-api/blob/master/logo.png) redmine-net-api is a library for communicating with a Redmine project management application. @@ -32,13 +36,6 @@ Resource | Read | Create | Update | Delete Wiki Pages |x|x|x|x Files |x|x|-|- -## Packages and Status - -Build status | Nuget - ------------ | ------- -![alt text](https://ci.appveyor.com/api/projects/status/github/zapadi/redmine-net-api?branch=master&svg=true) | [![NuGet package](https://img.shields.io/nuget/v/redmine-api.svg)](https://www.nuget.org/packages/redmine-api) - - ## WIKI Please review the wiki pages on how to use **redmine-net-api**. From 6c88278c2010674355e62a2943d7754fee99aec1 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Tue, 19 Nov 2019 12:34:30 +0200 Subject: [PATCH 004/549] Change csproj format & small refactoring --- build/docker-compose.yml | 8 +- src/redmine-net-api.Tests/Helper.cs | 10 +- .../Infrastructure/CaseOrder.cs | 4 +- .../Infrastructure/CollectionOrderer.cs | 5 +- .../Infrastructure/RedmineCollection.cs | 6 +- .../Properties/AssemblyInfo.cs | 34 -- src/redmine-net-api.Tests/RedmineFixture.cs | 3 +- .../Tests/Async/AttachmentAsyncTests.cs | 11 +- .../Tests/Async/IssueAsyncTests.cs | 10 +- .../Tests/Async/UserAsyncTests.cs | 8 +- .../Tests/Async/WikiPageAsyncTests.cs | 6 +- .../Tests/RedmineTest.cs | 7 +- .../Tests/Sync/AttachmentTests.cs | 3 +- .../Tests/Sync/CustomFieldTests.cs | 5 +- .../Tests/Sync/GroupTests.cs | 10 +- .../Tests/Sync/IssueCategoryTests.cs | 5 +- .../Tests/Sync/IssuePriorityTests.cs | 5 +- .../Tests/Sync/IssueRelationTests.cs | 4 +- .../Tests/Sync/IssueStatusTests.cs | 5 +- .../Tests/Sync/IssueTests.cs | 13 +- .../Tests/Sync/NewsTests.cs | 6 +- .../Tests/Sync/ProjectMembershipTests.cs | 7 +- .../Tests/Sync/ProjectTests.cs | 4 +- .../Tests/Sync/QueryTests.cs | 5 +- .../Tests/Sync/RoleTests.cs | 6 +- .../Tests/Sync/TimeEntryActivtiyTests.cs | 5 +- .../Tests/Sync/TimeEntryTests.cs | 5 +- .../Tests/Sync/TrackerTests.cs | 5 +- .../Tests/Sync/UserTests.cs | 22 +- .../Tests/Sync/VersionTests.cs | 7 +- .../Tests/Sync/WikiPageTests.cs | 6 +- .../redmine-net-api.Tests.csproj | 189 +++++----- src/redmine-net-api.sln | 115 +----- .../Async/RedmineManagerAsync.cs | 5 +- .../Async/RedmineManagerAsync40.cs} | 20 +- .../Async/RedmineManagerAsync45.cs} | 26 +- .../Exceptions/ConflictException.cs | 5 +- .../Exceptions/ForbiddenException.cs | 5 +- .../InternalServerErrorException.cs | 5 +- .../NameResolutionFailureException.cs | 5 +- .../Exceptions/NotAcceptableException.cs | 5 +- .../Exceptions/NotFoundException.cs | 5 +- .../Exceptions/RedmineException.cs | 15 +- .../Exceptions/RedmineTimeoutException.cs | 5 +- .../Exceptions/UnauthorizedException.cs | 5 +- .../Extensions/CollectionExtensions.cs | 65 +++- .../Extensions/JsonExtensions.cs | 21 +- .../NameValueCollectionExtensions.cs | 2 +- .../Extensions/StringExtensions.cs | 53 +++ .../Extensions/WebExtensions.cs | 112 +++--- .../Extensions/XmlReaderExtensions.cs | 162 +++++++-- .../Extensions/XmlWriterExtensions.cs | 11 +- src/redmine-net20-api/IRedmineManager.cs | 4 +- src/redmine-net20-api/Internals/DataHelper.cs | 4 +- src/redmine-net20-api/Internals/Func.cs | 6 +- .../Internals/HashCodeHelper.cs | 32 +- .../Internals/RedmineSerializer.cs | 152 +++++--- .../Internals/RedmineSerializerJson.cs | 11 +- src/redmine-net20-api/Internals/UrlHelper.cs | 50 +-- .../Internals/WebApiAsyncHelper.cs | 5 +- .../Internals/XmlStreamingDeserializer.cs | 58 ---- .../Internals/XmlStreamingSerializer.cs | 62 ---- .../JSonConverters/AttachmentConverter.cs | 100 ++++++ .../JSonConverters/AttachmentsConverter.cs | 82 +++++ .../JSonConverters/ChangeSetConverter.cs | 82 +++++ .../JSonConverters/CustomFieldConverter.cs | 93 +++++ .../CustomFieldPossibleValueConverter.cs | 77 +++++ .../CustomFieldRoleConverter.cs | 77 +++++ .../JSonConverters/DetailConverter.cs | 83 +++++ .../JSonConverters/ErrorConverter.cs | 77 +++++ .../JSonConverters/FileConverter.cs | 112 ++++++ .../JSonConverters/GroupConverter.cs | 98 ++++++ .../JSonConverters/GroupUserConverter.cs | 78 +++++ .../IdentifiableNameConverter.cs | 92 +++++ .../JSonConverters/IssueCategoryConverter.cs | 98 ++++++ .../JSonConverters/IssueChildConverter.cs | 76 ++++ .../JSonConverters/IssueConverter.cs | 156 +++++++++ .../IssueCustomFieldConverter.cs | 122 +++++++ .../JSonConverters/IssuePriorityConverter.cs | 78 +++++ .../JSonConverters/IssueRelationConverter.cs | 102 ++++++ .../JSonConverters/IssueStatusConverter.cs | 82 +++++ .../JSonConverters/JournalConverter.cs | 85 +++++ .../JSonConverters/MembershipConverter.cs | 82 +++++ .../JSonConverters/MembershipRoleConverter.cs | 82 +++++ .../JSonConverters/NewsConverter.cs | 85 +++++ .../JSonConverters/PermissionConverter.cs | 73 ++++ .../JSonConverters/ProjectConverter.cs | 119 +++++++ .../ProjectEnabledModuleConverter.cs | 78 +++++ .../ProjectIssueCategoryConverter.cs | 90 +++++ .../ProjectMembershipConverter.cs | 96 +++++ .../JSonConverters/ProjectTrackerConverter.cs | 78 +++++ .../JSonConverters/QueryConverter.cs | 83 +++++ .../JSonConverters/RoleConverter.cs | 92 +++++ .../TimeEntryActivityConverter.cs | 78 +++++ .../JSonConverters/TimeEntryConverter.cs | 121 +++++++ .../JSonConverters/TrackerConverter.cs | 81 +++++ .../TrackerCustomFieldConverter.cs | 68 ++++ .../JSonConverters/UploadConverter.cs | 93 +++++ .../JSonConverters/UserConverter.cs | 127 +++++++ .../JSonConverters/UserGroupConverter.cs | 68 ++++ .../JSonConverters/VersionConverter.cs | 107 ++++++ .../JSonConverters/WatcherConverter.cs | 85 +++++ .../JSonConverters/WikiPageConverter.cs | 99 ++++++ src/redmine-net20-api/MimeFormat.cs | 7 +- src/redmine-net20-api/RedmineManager.cs | 26 +- src/redmine-net20-api/Types/Attachment.cs | 8 +- src/redmine-net20-api/Types/CustomField.cs | 10 +- src/redmine-net20-api/Types/Detail.cs | 8 +- src/redmine-net20-api/Types/Error.cs | 2 +- src/redmine-net20-api/Types/Group.cs | 8 +- .../Types/IdentifiableName.cs | 6 +- src/redmine-net20-api/Types/Issue.cs | 18 +- src/redmine-net20-api/Types/IssueChild.cs | 2 +- .../Types/IssueCustomField.cs | 5 +- src/redmine-net20-api/Types/IssuePriority.cs | 3 +- src/redmine-net20-api/Types/IssueRelation.cs | 5 +- src/redmine-net20-api/Types/IssueStatus.cs | 1 + src/redmine-net20-api/Types/Journal.cs | 3 +- src/redmine-net20-api/Types/Membership.cs | 3 +- src/redmine-net20-api/Types/MembershipRole.cs | 5 +- src/redmine-net20-api/Types/News.cs | 7 +- src/redmine-net20-api/Types/Permission.cs | 2 +- src/redmine-net20-api/Types/Project.cs | 25 +- .../Types/ProjectMembership.cs | 1 + src/redmine-net20-api/Types/ProjectTracker.cs | 3 +- src/redmine-net20-api/Types/Role.cs | 1 + src/redmine-net20-api/Types/TimeEntry.cs | 16 +- .../Types/TimeEntryActivity.cs | 2 +- src/redmine-net20-api/Types/Tracker.cs | 2 +- src/redmine-net20-api/Types/Upload.cs | 8 +- src/redmine-net20-api/Types/User.cs | 30 +- src/redmine-net20-api/Types/Version.cs | 4 +- src/redmine-net20-api/Types/Watcher.cs | 3 +- .../redmine-net20-api.csproj | 327 ++++++++++-------- .../redmine-net40-api-signed.csproj | 4 +- .../Extensions/CollectionExtensions.cs | 62 ---- .../Extensions/WebExtensions.cs | 121 ------- .../Extensions/XmlReaderExtensions.cs | 219 ------------ .../Internals/RedmineSerializer.cs | 230 ------------ src/redmine-net40-api/MimeFormat.cs | 33 -- .../redmine-net40-api.csproj | 38 +- .../redmine-net450-api.csproj | 5 +- .../redmine-net452-api.csproj | 2 +- 143 files changed, 4809 insertions(+), 1651 deletions(-) delete mode 100644 src/redmine-net-api.Tests/Properties/AssemblyInfo.cs rename src/{redmine-net40-api/Async/RedmineManagerAsync.cs => redmine-net20-api/Async/RedmineManagerAsync40.cs} (96%) rename src/{redmine-net450-api/Async/RedmineManagerAsync.cs => redmine-net20-api/Async/RedmineManagerAsync45.cs} (95%) rename src/{redmine-net40-api => redmine-net20-api}/Extensions/JsonExtensions.cs (93%) mode change 100755 => 100644 create mode 100644 src/redmine-net20-api/Extensions/StringExtensions.cs mode change 100755 => 100644 src/redmine-net20-api/Internals/Func.cs rename src/{redmine-net40-api => redmine-net20-api}/Internals/RedmineSerializerJson.cs (98%) rename src/{redmine-net450-api => redmine-net20-api}/Internals/WebApiAsyncHelper.cs (99%) delete mode 100755 src/redmine-net20-api/Internals/XmlStreamingDeserializer.cs delete mode 100755 src/redmine-net20-api/Internals/XmlStreamingSerializer.cs create mode 100755 src/redmine-net20-api/JSonConverters/AttachmentConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/AttachmentsConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/ChangeSetConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/CustomFieldConverter.cs create mode 100644 src/redmine-net20-api/JSonConverters/CustomFieldPossibleValueConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/CustomFieldRoleConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/DetailConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/ErrorConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/FileConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/GroupConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/GroupUserConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/IdentifiableNameConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/IssueCategoryConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/IssueChildConverter.cs create mode 100644 src/redmine-net20-api/JSonConverters/IssueConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/IssueCustomFieldConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/IssuePriorityConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/IssueRelationConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/IssueStatusConverter.cs create mode 100644 src/redmine-net20-api/JSonConverters/JournalConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/MembershipConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/MembershipRoleConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/NewsConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/PermissionConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/ProjectConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/ProjectEnabledModuleConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/ProjectIssueCategoryConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/ProjectMembershipConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/ProjectTrackerConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/QueryConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/RoleConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/TimeEntryActivityConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/TimeEntryConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/TrackerConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/TrackerCustomFieldConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/UploadConverter.cs create mode 100644 src/redmine-net20-api/JSonConverters/UserConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/UserGroupConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/VersionConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/WatcherConverter.cs create mode 100755 src/redmine-net20-api/JSonConverters/WikiPageConverter.cs delete mode 100755 src/redmine-net40-api/Extensions/CollectionExtensions.cs delete mode 100755 src/redmine-net40-api/Extensions/WebExtensions.cs delete mode 100755 src/redmine-net40-api/Extensions/XmlReaderExtensions.cs delete mode 100755 src/redmine-net40-api/Internals/RedmineSerializer.cs delete mode 100755 src/redmine-net40-api/MimeFormat.cs diff --git a/build/docker-compose.yml b/build/docker-compose.yml index 2433e66d..1d017917 100755 --- a/build/docker-compose.yml +++ b/build/docker-compose.yml @@ -4,8 +4,8 @@ services: redmine: ports: - '8089:3000' - image: 'redmine:3.4.4' - container_name: 'redmine-web' + image: 'redmine:4.0.4' + container_name: 'redmine-web-404' depends_on: - postgres links: @@ -21,8 +21,8 @@ services: environment: POSTGRES_USER: redmine POSTGRES_PASSWORD: redmine-pswd - container_name: 'redmine-db' - image: 'postgres:9.5' + container_name: 'redmine-db-111' + image: 'postgres:11.1' #restart: always ports: - '5432:5432' diff --git a/src/redmine-net-api.Tests/Helper.cs b/src/redmine-net-api.Tests/Helper.cs index 647b64f8..b54b7cd3 100644 --- a/src/redmine-net-api.Tests/Helper.cs +++ b/src/redmine-net-api.Tests/Helper.cs @@ -14,11 +14,13 @@ internal static class Helper static Helper() { - Uri = ConfigurationManager.AppSettings["uri"]; - ApiKey = ConfigurationManager.AppSettings["apiKey"]; + Uri = "http://192.168.1.53:8089/"; - Username = ConfigurationManager.AppSettings["username"]; - Password = ConfigurationManager.AppSettings["password"]; + ApiKey = "a96e35d02bc6a6dbe655b83a2f6db57b82df2dff"; + + + Username = "zapadi"; + Password = "1qaz2wsx"; } } } diff --git a/src/redmine-net-api.Tests/Infrastructure/CaseOrder.cs b/src/redmine-net-api.Tests/Infrastructure/CaseOrder.cs index f2a0e911..1f36a704 100644 --- a/src/redmine-net-api.Tests/Infrastructure/CaseOrder.cs +++ b/src/redmine-net-api.Tests/Infrastructure/CaseOrder.cs @@ -1,3 +1,4 @@ +#if !(NET20 || NET40) using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -38,4 +39,5 @@ private static int GetOrder(TTestCase testCase) return attr != null ? attr.Index : 0; } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/src/redmine-net-api.Tests/Infrastructure/CollectionOrderer.cs b/src/redmine-net-api.Tests/Infrastructure/CollectionOrderer.cs index 906e2860..ca8575de 100644 --- a/src/redmine-net-api.Tests/Infrastructure/CollectionOrderer.cs +++ b/src/redmine-net-api.Tests/Infrastructure/CollectionOrderer.cs @@ -1,3 +1,5 @@ +#if !(NET20 || NET40) + using System; using System.Collections.Generic; using System.Linq; @@ -39,4 +41,5 @@ private static int GetOrder(ITestCollection testCollection) return attr != null ? attr.Index : 0; } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/src/redmine-net-api.Tests/Infrastructure/RedmineCollection.cs b/src/redmine-net-api.Tests/Infrastructure/RedmineCollection.cs index 3da13f61..f40dc201 100644 --- a/src/redmine-net-api.Tests/Infrastructure/RedmineCollection.cs +++ b/src/redmine-net-api.Tests/Infrastructure/RedmineCollection.cs @@ -1,4 +1,5 @@ -using Xunit; +#if !(NET20 || NET40) +using Xunit; namespace redmine.net.api.Tests.Infrastructure { @@ -7,4 +8,5 @@ public class RedmineCollection : ICollectionFixture { } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/src/redmine-net-api.Tests/Properties/AssemblyInfo.cs b/src/redmine-net-api.Tests/Properties/AssemblyInfo.cs deleted file mode 100644 index 415116a3..00000000 --- a/src/redmine-net-api.Tests/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Reflection; -using redmine.net.api.Tests.Infrastructure; -using Xunit; - -// Information about this assembly is defined by the following attributes. -// Change them to the values specific to your project. - -[assembly: AssemblyTitle("xUnitTest-redmine-net45-api")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("")] -[assembly: AssemblyCopyright("Copyright © 2016")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// The assembly version has the format "{Major}.{Minor}.{Build}.{Revision}". -// The form "{Major}.{Minor}.*" will automatically update the build and revision, -// and "{Major}.{Minor}.{Build}.*" will update just the revision. - -[assembly: AssemblyVersion("1.0.*")] - -// The following attributes are used to specify the signing key for the assembly, -// if desired. See the Mono documentation for more information about signing. - -//[assembly: AssemblyDelaySign(false)] -//[assembly: AssemblyKeyFile("")] - - - -[assembly: TestCaseOrderer(CaseOrderer.TYPE_NAME, CaseOrderer.ASSEMBY_NAME)] -[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly, DisableTestParallelization = true)] -[assembly: TestCollectionOrderer(CollectionOrderer.TYPE_NAME, CollectionOrderer.ASSEMBY_NAME)] - diff --git a/src/redmine-net-api.Tests/RedmineFixture.cs b/src/redmine-net-api.Tests/RedmineFixture.cs index ce3dc36f..7c8a5a39 100644 --- a/src/redmine-net-api.Tests/RedmineFixture.cs +++ b/src/redmine-net-api.Tests/RedmineFixture.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; + +using System.Diagnostics; using Redmine.Net.Api; namespace redmine.net.api.Tests diff --git a/src/redmine-net-api.Tests/Tests/Async/AttachmentAsyncTests.cs b/src/redmine-net-api.Tests/Tests/Async/AttachmentAsyncTests.cs index ec48aba9..ff57814c 100644 --- a/src/redmine-net-api.Tests/Tests/Async/AttachmentAsyncTests.cs +++ b/src/redmine-net-api.Tests/Tests/Async/AttachmentAsyncTests.cs @@ -1,9 +1,11 @@ -using System; +#if !(NET20 || NET40) +using Redmine.Net.Api.Async; +using Redmine.Net.Api.Types; +using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Threading.Tasks; -using Redmine.Net.Api.Async; -using Redmine.Net.Api.Types; + using Xunit; namespace redmine.net.api.Tests.Tests.Async @@ -91,4 +93,5 @@ public async Task Sould_Download_Attachment() Assert.NotNull(document); } } -} \ No newline at end of file +} +#endif diff --git a/src/redmine-net-api.Tests/Tests/Async/IssueAsyncTests.cs b/src/redmine-net-api.Tests/Tests/Async/IssueAsyncTests.cs index cd52bfdf..2d7052ab 100644 --- a/src/redmine-net-api.Tests/Tests/Async/IssueAsyncTests.cs +++ b/src/redmine-net-api.Tests/Tests/Async/IssueAsyncTests.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +#if !(NET20 || NET40) +using System.Collections.Generic; using System.Collections.Specialized; using System.Threading.Tasks; using Redmine.Net.Api.Async; @@ -22,7 +23,7 @@ public IssueAsyncTests(RedmineFixture fixture) [Fact] public async Task Should_Add_Watcher_To_Issue() { - await fixture.RedmineManager.AddWatcherAsync(WATCHER_ISSUE_ID, WATCHER_USER_ID); + await fixture.RedmineManager.AddWatcherToIssueAsync(WATCHER_ISSUE_ID, WATCHER_USER_ID); Issue issue = await fixture.RedmineManager.GetObjectAsync(WATCHER_ISSUE_ID.ToString(), new NameValueCollection { { "include", "watchers" } }); @@ -34,11 +35,12 @@ public async Task Should_Add_Watcher_To_Issue() [Fact] public async Task Should_Remove_Watcher_From_Issue() { - await fixture.RedmineManager.RemoveWatcherAsync(WATCHER_ISSUE_ID, WATCHER_USER_ID); + await fixture.RedmineManager.RemoveWatcherFromIssueAsync(WATCHER_ISSUE_ID, WATCHER_USER_ID); Issue issue = await fixture.RedmineManager.GetObjectAsync(WATCHER_ISSUE_ID.ToString(), new NameValueCollection { { "include", "watchers" } }); Assert.True(issue.Watchers == null || ((List)issue.Watchers).Find(w => w.Id == WATCHER_USER_ID) == null); } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/src/redmine-net-api.Tests/Tests/Async/UserAsyncTests.cs b/src/redmine-net-api.Tests/Tests/Async/UserAsyncTests.cs index e73a6bc4..d37a2f45 100644 --- a/src/redmine-net-api.Tests/Tests/Async/UserAsyncTests.cs +++ b/src/redmine-net-api.Tests/Tests/Async/UserAsyncTests.cs @@ -1,4 +1,5 @@ -using System; +#if !(NET20 || NET40) +using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; @@ -155,7 +156,7 @@ public async Task Should_Add_User_To_Group() [Fact] public async Task Should_Remove_User_From_Group() { - await fixture.RedmineManager.DeleteUserFromGroupAsync(GROUP_ID, int.Parse(USER_ID)); + await fixture.RedmineManager.RemoveUserFromGroupAsync(GROUP_ID, int.Parse(USER_ID)); User user = await fixture.RedmineManager.GetObjectAsync(USER_ID.ToString(CultureInfo.InvariantCulture), new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.GROUPS } }); @@ -204,4 +205,5 @@ public async Task Should_Delete_User() } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/src/redmine-net-api.Tests/Tests/Async/WikiPageAsyncTests.cs b/src/redmine-net-api.Tests/Tests/Async/WikiPageAsyncTests.cs index d119c730..9336513d 100644 --- a/src/redmine-net-api.Tests/Tests/Async/WikiPageAsyncTests.cs +++ b/src/redmine-net-api.Tests/Tests/Async/WikiPageAsyncTests.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +#if !(NET20 || NET40) +using System.Collections.Generic; using System.Collections.Specialized; using System.Threading.Tasks; using Redmine.Net.Api.Async; @@ -71,4 +72,5 @@ public async Task Should_Delete_WikiPage() } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/src/redmine-net-api.Tests/Tests/RedmineTest.cs b/src/redmine-net-api.Tests/Tests/RedmineTest.cs index b15ebad2..98f4acff 100644 --- a/src/redmine-net-api.Tests/Tests/RedmineTest.cs +++ b/src/redmine-net-api.Tests/Tests/RedmineTest.cs @@ -1,4 +1,5 @@ -using System; + +using System; using redmine.net.api.Tests.Infrastructure; using Redmine.Net.Api; using Redmine.Net.Api.Exceptions; @@ -7,7 +8,9 @@ namespace redmine.net.api.Tests.Tests { [Trait("Redmine-api", "Credentials")] +#if !(NET20 || NET40) [Collection("RedmineCollection")] +#endif [Order(1)] public class RedmineTest { @@ -21,7 +24,7 @@ public void Should_Throw_Redmine_Exception_When_Host_Is_Null() [Fact] public void Should_Throw_Redmine_Exception_When_Host_Is_Empty() { - Assert.Throws(() => new RedmineManager(String.Empty, Helper.Username, Helper.Password)); + Assert.Throws(() => new RedmineManager(string.Empty, Helper.Username, Helper.Password)); } [Fact] diff --git a/src/redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs b/src/redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs index 4e1e3f3e..edec151e 100644 --- a/src/redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs +++ b/src/redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs @@ -26,7 +26,9 @@ limitations under the License. namespace redmine.net.api.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "Attachments")] +#if !(NET20 || NET40) [Collection("RedmineCollection")] +#endif public class AttachmentTests { public AttachmentTests(RedmineFixture fixture) @@ -100,7 +102,6 @@ public void Should_Upload_Attachment() Assert.NotNull(issue); Assert.NotNull(issue.Attachments); - Assert.All(issue.Attachments, a => Assert.IsType(a)); Assert.True(issue.Attachments.Count == 1, "Number of attachments ( " + issue.Attachments.Count + " ) != 1"); var firstAttachment = issue.Attachments[0]; diff --git a/src/redmine-net-api.Tests/Tests/Sync/CustomFieldTests.cs b/src/redmine-net-api.Tests/Tests/Sync/CustomFieldTests.cs index e85646a6..267efdbd 100644 --- a/src/redmine-net-api.Tests/Tests/Sync/CustomFieldTests.cs +++ b/src/redmine-net-api.Tests/Tests/Sync/CustomFieldTests.cs @@ -21,7 +21,9 @@ limitations under the License. namespace redmine.net.api.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "CustomFields")] - [Collection("RedmineCollection")] +#if !(NET20 || NET40) + [Collection("RedmineCollection")] +#endif public class CustomFieldTests { public CustomFieldTests(RedmineFixture fixture) @@ -39,7 +41,6 @@ public void Should_Get_All_CustomFields() var customFields = fixture.RedmineManager.GetObjects(); Assert.NotNull(customFields); - Assert.All(customFields, cf => Assert.IsType(cf)); Assert.True(customFields.Count == NUMBER_OF_CUSTOM_FIELDS, "Custom fields count(" + customFields.Count + ") != " + NUMBER_OF_CUSTOM_FIELDS); } diff --git a/src/redmine-net-api.Tests/Tests/Sync/GroupTests.cs b/src/redmine-net-api.Tests/Tests/Sync/GroupTests.cs index c590e4f0..d0f27027 100644 --- a/src/redmine-net-api.Tests/Tests/Sync/GroupTests.cs +++ b/src/redmine-net-api.Tests/Tests/Sync/GroupTests.cs @@ -9,7 +9,9 @@ namespace redmine.net.api.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "Groups")] - [Collection("RedmineCollection")] +#if !(NET20 || NET40) + [Collection("RedmineCollection")] +#endif public class GroupTests { public GroupTests(RedmineFixture fixture) @@ -62,7 +64,6 @@ public void Should_Update_Group() Assert.NotNull(updatedGroup); Assert.True(updatedGroup.Name.Equals(UPDATED_GROUP_NAME), "Group name was not updated."); Assert.NotNull(updatedGroup.Users); - Assert.All(updatedGroup.Users, u => Assert.IsType(u)); Assert.True(updatedGroup.Users.Find(u => u.Id == UPDATED_GROUP_USER_ID) != null, "User was not added to group."); } @@ -75,7 +76,6 @@ public void Should_Get_All_Groups() var groups = fixture.RedmineManager.GetObjects(); Assert.NotNull(groups); - Assert.All(groups, g => Assert.IsType(g)); Assert.True(groups.Count == NUMBER_OF_GROUPS, "Number of groups ( "+groups.Count+" ) != " + NUMBER_OF_GROUPS); } @@ -87,11 +87,9 @@ public void Should_Get_Group_With_All_Associated_Data() Assert.NotNull(group); - Assert.All(group.Memberships, m => Assert.IsType(m)); Assert.True(group.Memberships.Count == NUMBER_OF_MEMBERSHIPS, "Number of memberships != " + NUMBER_OF_MEMBERSHIPS); - Assert.All(group.Users, u => Assert.IsType(u)); Assert.True(group.Users.Count == NUMBER_OF_USERS, "Number of users ( "+ group.Users.Count +" ) != " + NUMBER_OF_USERS); Assert.True(group.Name.Equals("Test"), "Group name is not valid."); } @@ -103,7 +101,6 @@ public void Should_Get_Group_With_Memberships() new NameValueCollection {{RedmineKeys.INCLUDE, RedmineKeys.MEMBERSHIPS}}); Assert.NotNull(group); - Assert.All(group.Memberships, m => Assert.IsType(m)); Assert.True(group.Memberships.Count == NUMBER_OF_MEMBERSHIPS, "Number of memberships ( "+ group.Memberships.Count +" ) != " + NUMBER_OF_MEMBERSHIPS); } @@ -115,7 +112,6 @@ public void Should_Get_Group_With_Users() new NameValueCollection {{RedmineKeys.INCLUDE, RedmineKeys.USERS}}); Assert.NotNull(group); - Assert.All(group.Users, u => Assert.IsType(u)); Assert.True(group.Users.Count == NUMBER_OF_USERS, "Number of users ( "+ group.Users.Count +" ) != " + NUMBER_OF_USERS); } diff --git a/src/redmine-net-api.Tests/Tests/Sync/IssueCategoryTests.cs b/src/redmine-net-api.Tests/Tests/Sync/IssueCategoryTests.cs index aecc9e3b..3c4bd254 100644 --- a/src/redmine-net-api.Tests/Tests/Sync/IssueCategoryTests.cs +++ b/src/redmine-net-api.Tests/Tests/Sync/IssueCategoryTests.cs @@ -8,7 +8,9 @@ namespace redmine.net.api.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "IssueCategories")] - [Collection("RedmineCollection")] +#if !(NET20 || NET40) + [Collection("RedmineCollection")] +#endif public class IssueCategoryTests { public IssueCategoryTests(RedmineFixture fixture) @@ -64,7 +66,6 @@ public void Should_Get_All_IssueCategories_By_ProjectId() }); Assert.NotNull(issueCategories); - Assert.All(issueCategories, ic => Assert.IsType(ic)); Assert.True(issueCategories.Count == NUMBER_OF_ISSUE_CATEGORIES, "Number of issue categories ( "+issueCategories.Count+" ) != " + NUMBER_OF_ISSUE_CATEGORIES); } diff --git a/src/redmine-net-api.Tests/Tests/Sync/IssuePriorityTests.cs b/src/redmine-net-api.Tests/Tests/Sync/IssuePriorityTests.cs index f21c60d6..cefbabe4 100644 --- a/src/redmine-net-api.Tests/Tests/Sync/IssuePriorityTests.cs +++ b/src/redmine-net-api.Tests/Tests/Sync/IssuePriorityTests.cs @@ -20,7 +20,9 @@ limitations under the License. namespace redmine.net.api.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "IssuePriorities")] - [Collection("RedmineCollection")] +#if !(NET20 || NET40) + [Collection("RedmineCollection")] +#endif public class IssuePriorityTests { public IssuePriorityTests(RedmineFixture fixture) @@ -37,7 +39,6 @@ public void Should_Get_All_Issue_Priority() var issuePriorities = fixture.RedmineManager.GetObjects(); Assert.NotNull(issuePriorities); - Assert.All(issuePriorities, ip => Assert.IsType(ip)); Assert.True(issuePriorities.Count == NUMBER_OF_ISSUE_PRIORITIES, "Issue priorities count(" + issuePriorities.Count + ") != " + NUMBER_OF_ISSUE_PRIORITIES); } diff --git a/src/redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs b/src/redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs index 4d555169..3508d9fd 100644 --- a/src/redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs +++ b/src/redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs @@ -24,7 +24,9 @@ limitations under the License. namespace redmine.net.api.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "IssueRelations")] - [Collection("RedmineCollection")] +#if !(NET20 || NET40) + [Collection("RedmineCollection")] +#endif public class IssueRelationTests { public IssueRelationTests(RedmineFixture fixture) diff --git a/src/redmine-net-api.Tests/Tests/Sync/IssueStatusTests.cs b/src/redmine-net-api.Tests/Tests/Sync/IssueStatusTests.cs index 84088270..815423e1 100644 --- a/src/redmine-net-api.Tests/Tests/Sync/IssueStatusTests.cs +++ b/src/redmine-net-api.Tests/Tests/Sync/IssueStatusTests.cs @@ -20,7 +20,9 @@ limitations under the License. namespace redmine.net.api.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "IssueStatuses")] - [Collection("RedmineCollection")] +#if !(NET20 || NET40) + [Collection("RedmineCollection")] +#endif public class IssueStatusTests { public IssueStatusTests(RedmineFixture fixture) @@ -37,7 +39,6 @@ public void Should_Get_All_Issue_Statuses() var issueStatuses = fixture.RedmineManager.GetObjects(); Assert.NotNull(issueStatuses); - Assert.All(issueStatuses, i => Assert.IsType(i)); Assert.True(issueStatuses.Count == NUMBER_OF_ISSUE_STATUSES, "Issue statuses count(" + issueStatuses.Count + ") != " + NUMBER_OF_ISSUE_STATUSES); } diff --git a/src/redmine-net-api.Tests/Tests/Sync/IssueTests.cs b/src/redmine-net-api.Tests/Tests/Sync/IssueTests.cs index d7ae18ce..dc6e4c3f 100644 --- a/src/redmine-net-api.Tests/Tests/Sync/IssueTests.cs +++ b/src/redmine-net-api.Tests/Tests/Sync/IssueTests.cs @@ -10,7 +10,9 @@ namespace redmine.net.api.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "Issues")] - [Collection("RedmineCollection")] +#if !(NET20 || NET40) + [Collection("RedmineCollection")] +#endif public class IssueTests { public IssueTests(RedmineFixture fixture) @@ -33,7 +35,6 @@ public void Should_Get_All_Issues() var issues = fixture.RedmineManager.GetObjects(); Assert.NotNull(issues); - Assert.All (issues, i => Assert.IsType (i)); } [Fact, Order(2)] @@ -45,7 +46,6 @@ public void Should_Get_Paginated_Issues() var issues = fixture.RedmineManager.GetPaginatedObjects(new NameValueCollection { { RedmineKeys.OFFSET, OFFSET.ToString() }, { RedmineKeys.LIMIT, NUMBER_OF_PAGINATED_ISSUES.ToString() }, { "sort", "id:desc" } }); Assert.NotNull(issues.Objects); - Assert.All (issues.Objects, i => Assert.IsType (i)); Assert.True(issues.Objects.Count <= NUMBER_OF_PAGINATED_ISSUES, "number of issues ( "+ issues.Objects.Count +" ) != " + NUMBER_OF_PAGINATED_ISSUES.ToString()); } @@ -55,7 +55,6 @@ public void Should_Get_Issues_By_Project_Id() var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { RedmineKeys.PROJECT_ID, PROJECT_ID } }); Assert.NotNull(issues); - Assert.All (issues, i => Assert.IsType (i)); } [Fact, Order(4)] @@ -66,7 +65,6 @@ public void Should_Get_Issues_By_subproject_Id() var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { RedmineKeys.SUBPROJECT_ID, SUBPROJECT_ID } }); Assert.NotNull(issues); - Assert.All (issues, i => Assert.IsType (i)); } [Fact, Order(5)] @@ -77,7 +75,6 @@ public void Should_Get_Issues_By_Project_Without_Subproject() var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { RedmineKeys.PROJECT_ID, PROJECT_ID }, { RedmineKeys.SUBPROJECT_ID, ALL_SUBPROJECTS } }); Assert.NotNull(issues); - Assert.All (issues, i => Assert.IsType (i)); } [Fact, Order(6)] @@ -87,7 +84,6 @@ public void Should_Get_Issues_By_Tracker() var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { RedmineKeys.TRACKER_ID, TRACKER_ID } }); Assert.NotNull(issues); - Assert.All (issues, i => Assert.IsType (i)); } [Fact, Order(7)] @@ -96,7 +92,6 @@ public void Should_Get_Issues_By_Status() const string STATUS_ID = "*"; var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { RedmineKeys.STATUS_ID, STATUS_ID } }); Assert.NotNull(issues); - Assert.All (issues, i => Assert.IsType (i)); } [Fact, Order(8)] @@ -106,7 +101,6 @@ public void Should_Get_Issues_By_Asignee() var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { RedmineKeys.ASSIGNED_TO_ID, ASSIGNED_TO_ID } }); Assert.NotNull(issues); - Assert.All (issues, i => Assert.IsType (i)); } [Fact, Order(9)] @@ -118,7 +112,6 @@ public void Should_Get_Issues_By_Custom_Field() var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { CUSTOM_FIELD_NAME, CUSTOM_FIELD_VALUE } }); Assert.NotNull(issues); - Assert.All (issues, i => Assert.IsType (i)); } [Fact, Order(10)] diff --git a/src/redmine-net-api.Tests/Tests/Sync/NewsTests.cs b/src/redmine-net-api.Tests/Tests/Sync/NewsTests.cs index b608e02f..1bacda60 100644 --- a/src/redmine-net-api.Tests/Tests/Sync/NewsTests.cs +++ b/src/redmine-net-api.Tests/Tests/Sync/NewsTests.cs @@ -23,7 +23,9 @@ limitations under the License. namespace redmine.net.api.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "News")] - [Collection("RedmineCollection")] +#if !(NET20 || NET40) + [Collection("RedmineCollection")] +#endif public class NewsTests { public NewsTests(RedmineFixture fixture) @@ -40,7 +42,6 @@ public void Should_Get_All_News() var news = fixture.RedmineManager.GetObjects(); Assert.NotNull(news); - Assert.All(news, n => Assert.IsType(n)); Assert.True(news.Count == NUMBER_OF_NEWS, "News count(" + news.Count + ") != " + NUMBER_OF_NEWS); } @@ -53,7 +54,6 @@ public void Should_Get_News_By_Project_Id() fixture.RedmineManager.GetObjects(new NameValueCollection {{RedmineKeys.PROJECT_ID, PROJECT_ID}}); Assert.NotNull(news); - Assert.All(news, n => Assert.IsType(n)); Assert.True(news.Count == NUMBER_OF_NEWS_BY_PROJECT_ID, "News count(" + news.Count + ") != " + NUMBER_OF_NEWS_BY_PROJECT_ID); } diff --git a/src/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs b/src/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs index f8986a89..6a6489e0 100644 --- a/src/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs +++ b/src/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs @@ -25,7 +25,9 @@ limitations under the License. namespace redmine.net.api.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "ProjectMemberships")] - [Collection("RedmineCollection")] +#if !(NET20 || NET40) + [Collection("RedmineCollection")] +#endif public class ProjectMembershipTests { public ProjectMembershipTests(RedmineFixture fixture) @@ -86,7 +88,6 @@ public void Should_Get_Memberships_By_Project_Identifier() Assert.NotNull(projectMemberships); Assert.True(projectMemberships.Count == NUMBER_OF_PROJECT_MEMBERSHIPS, "Project memberships count ( "+ projectMemberships.Count +" ) != " + NUMBER_OF_PROJECT_MEMBERSHIPS); - Assert.All(projectMemberships, pm => Assert.IsType(pm)); } [Fact, Order(3)] @@ -100,7 +101,6 @@ public void Should_Get_Project_Membership_By_Id() Assert.True(projectMembership.User != null || projectMembership.Group != null, "User and group are both null."); Assert.NotNull(projectMembership.Roles); - Assert.All(projectMembership.Roles, r => Assert.IsType(r)); } [Fact, Order(4)] @@ -118,7 +118,6 @@ public void Should_Update_Project_Membership() Assert.NotNull(updatedPm); Assert.NotNull(updatedPm.Roles); - Assert.All(updatedPm.Roles, r => Assert.IsType(r)); Assert.True(updatedPm.Roles.Find(r => r.Id == UPDATED_PROJECT_MEMBERSHIP_ROLE_ID) != null, string.Format("Role with id {0} was not found in roles list.", UPDATED_PROJECT_MEMBERSHIP_ROLE_ID)); } diff --git a/src/redmine-net-api.Tests/Tests/Sync/ProjectTests.cs b/src/redmine-net-api.Tests/Tests/Sync/ProjectTests.cs index ff9f64a0..eb8e3458 100644 --- a/src/redmine-net-api.Tests/Tests/Sync/ProjectTests.cs +++ b/src/redmine-net-api.Tests/Tests/Sync/ProjectTests.cs @@ -25,7 +25,9 @@ limitations under the License. namespace redmine.net.api.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "Projects")] +#if !(NET20 || NET40) [Collection("RedmineCollection")] +#endif [Order(1)] public class ProjectTests { @@ -169,12 +171,10 @@ public void Should_Get_Test_Project_With_All_Properties_Set() Assert.NotNull(project.Trackers); Assert.True(project.Trackers.Count == 2, "Trackers count != " + 2); - Assert.All(project.Trackers, t => Assert.IsType(t)); Assert.NotNull(project.EnabledModules); Assert.True(project.EnabledModules.Count == 2, "Enabled modules count (" + project.EnabledModules.Count + ") != " + 2); - Assert.All(project.EnabledModules, em => Assert.IsType(em)); } [Fact, Order(5)] diff --git a/src/redmine-net-api.Tests/Tests/Sync/QueryTests.cs b/src/redmine-net-api.Tests/Tests/Sync/QueryTests.cs index a09d8dc2..8297d5bb 100644 --- a/src/redmine-net-api.Tests/Tests/Sync/QueryTests.cs +++ b/src/redmine-net-api.Tests/Tests/Sync/QueryTests.cs @@ -21,7 +21,9 @@ limitations under the License. namespace redmine.net.api.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "Queries")] - [Collection("RedmineCollection")] +#if !(NET20 || NET40) + [Collection("RedmineCollection")] +#endif public class QueryTests { public QueryTests(RedmineFixture fixture) @@ -38,7 +40,6 @@ public void Should_Get_All_Queries() var queries = fixture.RedmineManager.GetObjects(); Assert.NotNull(queries); - Assert.All(queries, q => Assert.IsType(q)); Assert.True(queries.Count == NUMBER_OF_QUERIES, "Queries count(" + queries.Count + ") != " + NUMBER_OF_QUERIES); } diff --git a/src/redmine-net-api.Tests/Tests/Sync/RoleTests.cs b/src/redmine-net-api.Tests/Tests/Sync/RoleTests.cs index a7b93801..6495b953 100644 --- a/src/redmine-net-api.Tests/Tests/Sync/RoleTests.cs +++ b/src/redmine-net-api.Tests/Tests/Sync/RoleTests.cs @@ -21,7 +21,9 @@ limitations under the License. namespace redmine.net.api.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "Roles")] - [Collection("RedmineCollection")] +#if !(NET20 || NET40) + [Collection("RedmineCollection")] +#endif public class RoleTests { public RoleTests(RedmineFixture fixture) @@ -38,7 +40,6 @@ public void Should_Get_All_Roles() var roles = fixture.RedmineManager.GetObjects(); Assert.NotNull(roles); - Assert.All(roles, r => Assert.IsType(r)); Assert.True(roles.Count == NUMBER_OF_ROLES, "Roles count(" + roles.Count + ") != " + NUMBER_OF_ROLES); } @@ -55,7 +56,6 @@ public void Should_Get_Role_By_Id() Assert.True(role.Name.Equals(ROLE_NAME), "Role name is invalid."); Assert.NotNull(role.Permissions); - Assert.All(role.Permissions, p => Assert.IsType(p)); Assert.True(role.Permissions.Count == NUMBER_OF_ROLE_PERMISSIONS, "Permissions count(" + role.Permissions.Count + ") != " + NUMBER_OF_ROLE_PERMISSIONS); } diff --git a/src/redmine-net-api.Tests/Tests/Sync/TimeEntryActivtiyTests.cs b/src/redmine-net-api.Tests/Tests/Sync/TimeEntryActivtiyTests.cs index e859a68f..1e25dda4 100644 --- a/src/redmine-net-api.Tests/Tests/Sync/TimeEntryActivtiyTests.cs +++ b/src/redmine-net-api.Tests/Tests/Sync/TimeEntryActivtiyTests.cs @@ -21,7 +21,9 @@ limitations under the License. namespace redmine.net.api.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "TimeEntryActivities")] - [Collection("RedmineCollection")] +#if !(NET20 || NET40) + [Collection("RedmineCollection")] +#endif public class TimeEntryActivityTests { public TimeEntryActivityTests(RedmineFixture fixture) @@ -39,7 +41,6 @@ public void Should_Get_All_TimeEntryActivities() var timeEntryActivities = fixture.RedmineManager.GetObjects(); Assert.NotNull(timeEntryActivities); - Assert.All(timeEntryActivities, t => Assert.IsType(t)); Assert.True(timeEntryActivities.Count == NUMBER_OF_TIME_ENTRY_ACTIVITIES, "Time entry activities count ( "+ timeEntryActivities.Count +" ) != " + NUMBER_OF_TIME_ENTRY_ACTIVITIES); } diff --git a/src/redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs b/src/redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs index dc195bea..f798c7b4 100644 --- a/src/redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs +++ b/src/redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs @@ -23,7 +23,9 @@ limitations under the License. namespace redmine.net.api.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "TimeEntries")] - [Collection("RedmineCollection")] +#if !(NET20 || NET40) + [Collection("RedmineCollection")] +#endif public class TimeEntryTests { public TimeEntryTests(RedmineFixture fixture) @@ -89,7 +91,6 @@ public void Should_Get_All_Time_Entries() Assert.NotNull(timeEntries); Assert.NotEmpty(timeEntries); - Assert.All(timeEntries, t => Assert.IsType(t)); } [Fact, Order(3)] diff --git a/src/redmine-net-api.Tests/Tests/Sync/TrackerTests.cs b/src/redmine-net-api.Tests/Tests/Sync/TrackerTests.cs index 750370a3..d941c902 100644 --- a/src/redmine-net-api.Tests/Tests/Sync/TrackerTests.cs +++ b/src/redmine-net-api.Tests/Tests/Sync/TrackerTests.cs @@ -20,7 +20,9 @@ limitations under the License. namespace redmine.net.api.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "Trackers")] - [Collection("RedmineCollection")] +#if !(NET20 || NET40) + [Collection("RedmineCollection")] +#endif public class TrackerTests { public TrackerTests(RedmineFixture fixture) @@ -38,7 +40,6 @@ public void RedmineTrackers_ShouldGetAllTrackers() var trackers = fixture.RedmineManager.GetObjects(); Assert.NotNull(trackers); - Assert.All(trackers, t => Assert.IsType(t)); Assert.True(trackers.Count == NUMBER_OF_TRACKERS, "Trackers count(" + trackers.Count + ") != " + NUMBER_OF_TRACKERS); } } diff --git a/src/redmine-net-api.Tests/Tests/Sync/UserTests.cs b/src/redmine-net-api.Tests/Tests/Sync/UserTests.cs index 6a431ba6..59fab743 100644 --- a/src/redmine-net-api.Tests/Tests/Sync/UserTests.cs +++ b/src/redmine-net-api.Tests/Tests/Sync/UserTests.cs @@ -25,7 +25,9 @@ limitations under the License. namespace redmine.net.api.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "Users")] - [Collection("RedmineCollection")] +#if !(NET20 || NET40) + [Collection("RedmineCollection")] +#endif [Order(2)] public class UserTests { @@ -42,9 +44,9 @@ public UserTests(RedmineFixture fixture) private const string USER_EMAIL = "testUser@mail.com"; private static string CREATED_USER_ID; - private static string CREATED_USER_WITH_ALL_PROP_ID; + private static string CREATED_USER_WITH_ALL_PROP_ID; - private static User CreateTestUserWithRequiredPropertiesSet() + private static User CreateTestUserWithRequiredPropertiesSet() { var user = new User() { @@ -131,24 +133,26 @@ public void Should_Update_User() const string UPDATED_USER_LAST_NAME = "UpdatedLastName"; const string UPDATED_USER_EMAIL = "updatedEmail@mail.com"; - var user = fixture.RedmineManager.GetObject(CREATED_USER_ID, null); + var user = fixture.RedmineManager.GetObject("8", null); user.FirstName = UPDATED_USER_FIRST_NAME; user.LastName = UPDATED_USER_LAST_NAME; user.Email = UPDATED_USER_EMAIL; var exception = (RedmineException) - Record.Exception(() => fixture.RedmineManager.UpdateObject(CREATED_USER_ID, user)); + Record.Exception(() => fixture.RedmineManager.UpdateObject("8", user)); Assert.Null(exception); - var updatedUser = fixture.RedmineManager.GetObject(CREATED_USER_ID, null); + var updatedUser = fixture.RedmineManager.GetObject("8", null); Assert.True(updatedUser.FirstName.Equals(UPDATED_USER_FIRST_NAME), "User first name was not updated."); Assert.True(updatedUser.LastName.Equals(UPDATED_USER_LAST_NAME), "User last name was not updated."); Assert.True(updatedUser.Email.Equals(UPDATED_USER_EMAIL), "User email was not updated."); - } - [Fact, Order(6)] + // curl -v --user zapadi:1qaz2wsx -H 'Content-Type: application/json' -X PUT -d '{"user":{"login":"testuser","firstname":"UpdatedFirstName","lastname":"UpdatedLastName","mail":"updatedEmail@mail.com","must_change_passwd":"false","status":"1"}}' http://192.168.1.53:8089/users/8.json + } + + [Fact, Order(6)] public void Should_Not_Update_User_With_Invalid_Properties() { var user = fixture.RedmineManager.GetObject(CREATED_USER_ID, null); @@ -199,7 +203,6 @@ public void Should_Get_X_Users_From_Offset_Y() }); Assert.NotNull(result); - Assert.All(result.Objects, u => Assert.IsType(u)); } [Fact, Order(11)] @@ -211,7 +214,6 @@ public void Should_Get_Users_By_State() }); Assert.NotNull(users); - Assert.All(users, u => Assert.IsType(u)); } } } \ No newline at end of file diff --git a/src/redmine-net-api.Tests/Tests/Sync/VersionTests.cs b/src/redmine-net-api.Tests/Tests/Sync/VersionTests.cs index 4ed61cee..cc9abde9 100644 --- a/src/redmine-net-api.Tests/Tests/Sync/VersionTests.cs +++ b/src/redmine-net-api.Tests/Tests/Sync/VersionTests.cs @@ -16,9 +16,9 @@ limitations under the License. using System; using System.Collections.Specialized; -using redmine.net.api.Tests.Infrastructure; using Redmine.Net.Api; using Redmine.Net.Api.Exceptions; +using redmine.net.api.Tests.Infrastructure; using Redmine.Net.Api.Types; using Xunit; using Version = Redmine.Net.Api.Types.Version; @@ -26,7 +26,9 @@ limitations under the License. namespace redmine.net.api.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "Versions")] - [Collection("RedmineCollection")] +#if !(NET20 || NET40) + [Collection("RedmineCollection")] +#endif public class VersionTests { public VersionTests(RedmineFixture fixture) @@ -104,7 +106,6 @@ public void Should_Get_Versions_By_Project_Id() }); Assert.NotNull(versions); - Assert.All(versions, v => Assert.IsType(v)); Assert.True(versions.Count == NUMBER_OF_VERSIONS, "Versions count ( "+versions.Count+" ) != " + NUMBER_OF_VERSIONS); } diff --git a/src/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs b/src/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs index ad598d15..5674056e 100644 --- a/src/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs +++ b/src/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs @@ -26,7 +26,9 @@ limitations under the License. namespace redmine.net.api.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "WikiPages")] - [Collection("RedmineCollection")] +#if !(NET20 || NET40) + [Collection("RedmineCollection")] +#endif public class WikiPageTests { public WikiPageTests(RedmineFixture fixture) @@ -69,7 +71,6 @@ public void Should_Get_All_Wiki_Pages_By_Project_Id() var pages = (List) fixture.RedmineManager.GetAllWikiPages(PROJECT_ID); Assert.NotNull(pages); - Assert.All(pages, p => Assert.IsType(p)); Assert.True(pages.Count == NUMBER_OF_WIKI_PAGES, "Wiki pages count != " + NUMBER_OF_WIKI_PAGES); Assert.True(pages.Exists(p => p.Title == WIKI_PAGE_NAME), string.Format("Wiki page {0} does not exist", WIKI_PAGE_NAME)); @@ -95,7 +96,6 @@ public void Should_Get_Wiki_Page_By_Title_With_Attachments() Assert.NotNull(page); Assert.Equal(page.Title, WIKI_PAGE_NAME); Assert.NotNull(page.Attachments.ToList()); - Assert.All(page.Attachments.ToList(), a => Assert.IsType(a)); } [Fact, Order(5)] diff --git a/src/redmine-net-api.Tests/redmine-net-api.Tests.csproj b/src/redmine-net-api.Tests/redmine-net-api.Tests.csproj index 57b20d99..d7fffce9 100644 --- a/src/redmine-net-api.Tests/redmine-net-api.Tests.csproj +++ b/src/redmine-net-api.Tests/redmine-net-api.Tests.csproj @@ -1,119 +1,94 @@  - - - - - - Debug - AnyCPU - {900EF0B3-0233-45DA-811F-4C59483E8452} - Library + + + + + false + net48 + net45;net451;net452;net46;net461;net462;net47;net471;net472;net48; + true redmine.net.api.Tests redmine-net-api.Tests - v4.5.2 - - - + - - true - full - false - bin\Debug - DEBUG;JSON;XML - prompt - 4 - false + + + NET20;NETFULL - - full - true - bin\Release - prompt - 4 - false + + + NET40;NETFULL - - Program - false + + + NET45;NETFULL - - - - - - ..\packages\xunit.abstractions.2.0.1\lib\net35\xunit.abstractions.dll - - - ..\packages\xunit.assert.2.3.1\lib\netstandard1.1\xunit.assert.dll - - - ..\packages\xunit.extensibility.core.2.3.1\lib\netstandard1.1\xunit.core.dll - - - ..\packages\xunit.extensibility.execution.2.3.1\lib\net452\xunit.execution.desktop.dll - - - ..\packages\xunit.runner.utility.2.3.1\lib\net452\xunit.runner.utility.net452.dll - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + NET451;NETFULL + + + + NET452;NETFULL + + + + NET46;NETFULL + + + + NET461;NETFULL + + + + NET462;NETFULL + + + + + NET47;NETFULL + + + + NET471;NETFULL + + + + NET472;NETFULL + + + + NET48;NETFULL + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + - - - {AEDFD095-F4B0-4630-B41A-9A22169456E9} - redmine-net450-api - + + + + + - - + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - - + + + + - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - - - + + \ No newline at end of file diff --git a/src/redmine-net-api.sln b/src/redmine-net-api.sln index 2124d3f5..40373ace 100644 --- a/src/redmine-net-api.sln +++ b/src/redmine-net-api.sln @@ -1,33 +1,15 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26228.9 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29503.13 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0DFF4758-5C19-4D8F-BA6C-76E618323F6A}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{F3F4278D-6271-4F77-BA88-41555D53CBD1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "redmine-net20-api", "redmine-net20-api\redmine-net20-api.csproj", "{0E6B9B72-445D-4E71-8D29-48C4A009AB03}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "redmine-net20-api", "redmine-net20-api\redmine-net20-api.csproj", "{0E6B9B72-445D-4E71-8D29-48C4A009AB03}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "redmine-net40-api", "redmine-net40-api\redmine-net40-api.csproj", "{22492A69-B890-4D5B-A2FC-E2F6C63935B8}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "redmine-net40-api-signed", "redmine-net40-api-signed\redmine-net40-api-signed.csproj", "{00F410C6-E398-4F58-869B-34CD7275096A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "redmine-net450-api", "redmine-net450-api\redmine-net450-api.csproj", "{AEDFD095-F4B0-4630-B41A-9A22169456E9}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "redmine-net450-api-signed", "redmine-net450-api-signed\redmine-net450-api-signed.csproj", "{028B9120-A7FC-4B23-AA9C-F18087058F76}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "redmine-net451-api", "redmine-net451-api\redmine-net451-api.csproj", "{B67F0035-336C-4CDA-80A8-DE94EEDF5627}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "redmine-net451-api-signed", "redmine-net451-api-signed\redmine-net451-api-signed.csproj", "{7FB65A2A-946B-4ACD-A6A2-85FA6D517CC2}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "redmine-net452-api", "redmine-net452-api\redmine-net452-api.csproj", "{4EE7D8D8-AA65-442B-A928-580B4604B9AF}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "redmine-net452-api-signed", "redmine-net452-api-signed\redmine-net452-api-signed.csproj", "{6CBF5FC3-7783-44E7-90CA-8D12B165B9C3}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "redmine-net-api.Tests", "redmine-net-api.Tests\redmine-net-api.Tests.csproj", "{900EF0B3-0233-45DA-811F-4C59483E8452}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "redmine-net-api.TestConsole", "redmine-net-api.TestConsole\redmine-net-api.TestConsole.csproj", "{5A5D51BC-2800-44B4-9E12-05264BDCEBF7}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "redmine-net-api.Tests", "redmine-net-api.Tests\redmine-net-api.Tests.csproj", "{900EF0B3-0233-45DA-811F-4C59483E8452}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -39,76 +21,12 @@ Global GlobalSection(ProjectConfigurationPlatforms) = postSolution {0E6B9B72-445D-4E71-8D29-48C4A009AB03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0E6B9B72-445D-4E71-8D29-48C4A009AB03}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0E6B9B72-445D-4E71-8D29-48C4A009AB03}.DebugJSON|Any CPU.ActiveCfg = DebugJSON|Any CPU - {0E6B9B72-445D-4E71-8D29-48C4A009AB03}.DebugJSON|Any CPU.Build.0 = DebugJSON|Any CPU - {0E6B9B72-445D-4E71-8D29-48C4A009AB03}.DebugXML|Any CPU.ActiveCfg = DebugXML|Any CPU - {0E6B9B72-445D-4E71-8D29-48C4A009AB03}.DebugXML|Any CPU.Build.0 = DebugXML|Any CPU + {0E6B9B72-445D-4E71-8D29-48C4A009AB03}.DebugJSON|Any CPU.ActiveCfg = Debug|Any CPU + {0E6B9B72-445D-4E71-8D29-48C4A009AB03}.DebugJSON|Any CPU.Build.0 = Debug|Any CPU + {0E6B9B72-445D-4E71-8D29-48C4A009AB03}.DebugXML|Any CPU.ActiveCfg = Debug|Any CPU + {0E6B9B72-445D-4E71-8D29-48C4A009AB03}.DebugXML|Any CPU.Build.0 = Debug|Any CPU {0E6B9B72-445D-4E71-8D29-48C4A009AB03}.Release|Any CPU.ActiveCfg = Release|Any CPU {0E6B9B72-445D-4E71-8D29-48C4A009AB03}.Release|Any CPU.Build.0 = Release|Any CPU - {22492A69-B890-4D5B-A2FC-E2F6C63935B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {22492A69-B890-4D5B-A2FC-E2F6C63935B8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {22492A69-B890-4D5B-A2FC-E2F6C63935B8}.DebugJSON|Any CPU.ActiveCfg = DebugJSON|Any CPU - {22492A69-B890-4D5B-A2FC-E2F6C63935B8}.DebugJSON|Any CPU.Build.0 = DebugJSON|Any CPU - {22492A69-B890-4D5B-A2FC-E2F6C63935B8}.DebugXML|Any CPU.ActiveCfg = DebugXML|Any CPU - {22492A69-B890-4D5B-A2FC-E2F6C63935B8}.DebugXML|Any CPU.Build.0 = DebugXML|Any CPU - {22492A69-B890-4D5B-A2FC-E2F6C63935B8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {22492A69-B890-4D5B-A2FC-E2F6C63935B8}.Release|Any CPU.Build.0 = Release|Any CPU - {00F410C6-E398-4F58-869B-34CD7275096A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {00F410C6-E398-4F58-869B-34CD7275096A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {00F410C6-E398-4F58-869B-34CD7275096A}.DebugJSON|Any CPU.ActiveCfg = DebugJSON|Any CPU - {00F410C6-E398-4F58-869B-34CD7275096A}.DebugJSON|Any CPU.Build.0 = DebugJSON|Any CPU - {00F410C6-E398-4F58-869B-34CD7275096A}.DebugXML|Any CPU.ActiveCfg = DebugXML|Any CPU - {00F410C6-E398-4F58-869B-34CD7275096A}.DebugXML|Any CPU.Build.0 = DebugXML|Any CPU - {00F410C6-E398-4F58-869B-34CD7275096A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {00F410C6-E398-4F58-869B-34CD7275096A}.Release|Any CPU.Build.0 = Release|Any CPU - {AEDFD095-F4B0-4630-B41A-9A22169456E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AEDFD095-F4B0-4630-B41A-9A22169456E9}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AEDFD095-F4B0-4630-B41A-9A22169456E9}.DebugJSON|Any CPU.ActiveCfg = DebugJSON|Any CPU - {AEDFD095-F4B0-4630-B41A-9A22169456E9}.DebugJSON|Any CPU.Build.0 = DebugJSON|Any CPU - {AEDFD095-F4B0-4630-B41A-9A22169456E9}.DebugXML|Any CPU.ActiveCfg = DebugXML|Any CPU - {AEDFD095-F4B0-4630-B41A-9A22169456E9}.DebugXML|Any CPU.Build.0 = DebugXML|Any CPU - {AEDFD095-F4B0-4630-B41A-9A22169456E9}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AEDFD095-F4B0-4630-B41A-9A22169456E9}.Release|Any CPU.Build.0 = Release|Any CPU - {028B9120-A7FC-4B23-AA9C-F18087058F76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {028B9120-A7FC-4B23-AA9C-F18087058F76}.Debug|Any CPU.Build.0 = Debug|Any CPU - {028B9120-A7FC-4B23-AA9C-F18087058F76}.DebugJSON|Any CPU.ActiveCfg = DebugJSON|Any CPU - {028B9120-A7FC-4B23-AA9C-F18087058F76}.DebugJSON|Any CPU.Build.0 = DebugJSON|Any CPU - {028B9120-A7FC-4B23-AA9C-F18087058F76}.DebugXML|Any CPU.ActiveCfg = DebugXML|Any CPU - {028B9120-A7FC-4B23-AA9C-F18087058F76}.DebugXML|Any CPU.Build.0 = DebugXML|Any CPU - {028B9120-A7FC-4B23-AA9C-F18087058F76}.Release|Any CPU.ActiveCfg = Release|Any CPU - {028B9120-A7FC-4B23-AA9C-F18087058F76}.Release|Any CPU.Build.0 = Release|Any CPU - {B67F0035-336C-4CDA-80A8-DE94EEDF5627}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B67F0035-336C-4CDA-80A8-DE94EEDF5627}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B67F0035-336C-4CDA-80A8-DE94EEDF5627}.DebugJSON|Any CPU.ActiveCfg = Debug|Any CPU - {B67F0035-336C-4CDA-80A8-DE94EEDF5627}.DebugJSON|Any CPU.Build.0 = Debug|Any CPU - {B67F0035-336C-4CDA-80A8-DE94EEDF5627}.DebugXML|Any CPU.ActiveCfg = Debug|Any CPU - {B67F0035-336C-4CDA-80A8-DE94EEDF5627}.DebugXML|Any CPU.Build.0 = Debug|Any CPU - {B67F0035-336C-4CDA-80A8-DE94EEDF5627}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B67F0035-336C-4CDA-80A8-DE94EEDF5627}.Release|Any CPU.Build.0 = Release|Any CPU - {7FB65A2A-946B-4ACD-A6A2-85FA6D517CC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7FB65A2A-946B-4ACD-A6A2-85FA6D517CC2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7FB65A2A-946B-4ACD-A6A2-85FA6D517CC2}.DebugJSON|Any CPU.ActiveCfg = Debug|Any CPU - {7FB65A2A-946B-4ACD-A6A2-85FA6D517CC2}.DebugJSON|Any CPU.Build.0 = Debug|Any CPU - {7FB65A2A-946B-4ACD-A6A2-85FA6D517CC2}.DebugXML|Any CPU.ActiveCfg = Debug|Any CPU - {7FB65A2A-946B-4ACD-A6A2-85FA6D517CC2}.DebugXML|Any CPU.Build.0 = Debug|Any CPU - {7FB65A2A-946B-4ACD-A6A2-85FA6D517CC2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7FB65A2A-946B-4ACD-A6A2-85FA6D517CC2}.Release|Any CPU.Build.0 = Release|Any CPU - {4EE7D8D8-AA65-442B-A928-580B4604B9AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {4EE7D8D8-AA65-442B-A928-580B4604B9AF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {4EE7D8D8-AA65-442B-A928-580B4604B9AF}.DebugJSON|Any CPU.ActiveCfg = Debug|Any CPU - {4EE7D8D8-AA65-442B-A928-580B4604B9AF}.DebugJSON|Any CPU.Build.0 = Debug|Any CPU - {4EE7D8D8-AA65-442B-A928-580B4604B9AF}.DebugXML|Any CPU.ActiveCfg = Debug|Any CPU - {4EE7D8D8-AA65-442B-A928-580B4604B9AF}.DebugXML|Any CPU.Build.0 = Debug|Any CPU - {4EE7D8D8-AA65-442B-A928-580B4604B9AF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {4EE7D8D8-AA65-442B-A928-580B4604B9AF}.Release|Any CPU.Build.0 = Release|Any CPU - {6CBF5FC3-7783-44E7-90CA-8D12B165B9C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6CBF5FC3-7783-44E7-90CA-8D12B165B9C3}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6CBF5FC3-7783-44E7-90CA-8D12B165B9C3}.DebugJSON|Any CPU.ActiveCfg = Debug|Any CPU - {6CBF5FC3-7783-44E7-90CA-8D12B165B9C3}.DebugJSON|Any CPU.Build.0 = Debug|Any CPU - {6CBF5FC3-7783-44E7-90CA-8D12B165B9C3}.DebugXML|Any CPU.ActiveCfg = Debug|Any CPU - {6CBF5FC3-7783-44E7-90CA-8D12B165B9C3}.DebugXML|Any CPU.Build.0 = Debug|Any CPU - {6CBF5FC3-7783-44E7-90CA-8D12B165B9C3}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6CBF5FC3-7783-44E7-90CA-8D12B165B9C3}.Release|Any CPU.Build.0 = Release|Any CPU {900EF0B3-0233-45DA-811F-4C59483E8452}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {900EF0B3-0233-45DA-811F-4C59483E8452}.Debug|Any CPU.Build.0 = Debug|Any CPU {900EF0B3-0233-45DA-811F-4C59483E8452}.DebugJSON|Any CPU.ActiveCfg = Debug|Any CPU @@ -117,30 +35,13 @@ Global {900EF0B3-0233-45DA-811F-4C59483E8452}.DebugXML|Any CPU.Build.0 = Debug|Any CPU {900EF0B3-0233-45DA-811F-4C59483E8452}.Release|Any CPU.ActiveCfg = Release|Any CPU {900EF0B3-0233-45DA-811F-4C59483E8452}.Release|Any CPU.Build.0 = Release|Any CPU - {5A5D51BC-2800-44B4-9E12-05264BDCEBF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5A5D51BC-2800-44B4-9E12-05264BDCEBF7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5A5D51BC-2800-44B4-9E12-05264BDCEBF7}.DebugJSON|Any CPU.ActiveCfg = Debug|Any CPU - {5A5D51BC-2800-44B4-9E12-05264BDCEBF7}.DebugJSON|Any CPU.Build.0 = Debug|Any CPU - {5A5D51BC-2800-44B4-9E12-05264BDCEBF7}.DebugXML|Any CPU.ActiveCfg = Debug|Any CPU - {5A5D51BC-2800-44B4-9E12-05264BDCEBF7}.DebugXML|Any CPU.Build.0 = Debug|Any CPU - {5A5D51BC-2800-44B4-9E12-05264BDCEBF7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5A5D51BC-2800-44B4-9E12-05264BDCEBF7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {0E6B9B72-445D-4E71-8D29-48C4A009AB03} = {0DFF4758-5C19-4D8F-BA6C-76E618323F6A} - {22492A69-B890-4D5B-A2FC-E2F6C63935B8} = {0DFF4758-5C19-4D8F-BA6C-76E618323F6A} - {00F410C6-E398-4F58-869B-34CD7275096A} = {0DFF4758-5C19-4D8F-BA6C-76E618323F6A} - {AEDFD095-F4B0-4630-B41A-9A22169456E9} = {0DFF4758-5C19-4D8F-BA6C-76E618323F6A} - {028B9120-A7FC-4B23-AA9C-F18087058F76} = {0DFF4758-5C19-4D8F-BA6C-76E618323F6A} - {B67F0035-336C-4CDA-80A8-DE94EEDF5627} = {0DFF4758-5C19-4D8F-BA6C-76E618323F6A} - {7FB65A2A-946B-4ACD-A6A2-85FA6D517CC2} = {0DFF4758-5C19-4D8F-BA6C-76E618323F6A} - {4EE7D8D8-AA65-442B-A928-580B4604B9AF} = {0DFF4758-5C19-4D8F-BA6C-76E618323F6A} - {6CBF5FC3-7783-44E7-90CA-8D12B165B9C3} = {0DFF4758-5C19-4D8F-BA6C-76E618323F6A} {900EF0B3-0233-45DA-811F-4C59483E8452} = {F3F4278D-6271-4F77-BA88-41555D53CBD1} - {5A5D51BC-2800-44B4-9E12-05264BDCEBF7} = {F3F4278D-6271-4F77-BA88-41555D53CBD1} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4AA87D90-ABD0-4793-BE47-955B35FAE2BB} diff --git a/src/redmine-net20-api/Async/RedmineManagerAsync.cs b/src/redmine-net20-api/Async/RedmineManagerAsync.cs index 40154158..887ef118 100644 --- a/src/redmine-net20-api/Async/RedmineManagerAsync.cs +++ b/src/redmine-net20-api/Async/RedmineManagerAsync.cs @@ -1,3 +1,5 @@ + +#if NET20 using System.Collections.Generic; using System.Collections.Specialized; using Redmine.Net.Api.Types; @@ -252,4 +254,5 @@ public static Task DownloadFileAsync(this RedmineManager redmineManager, return delegate { return redmineManager.DownloadFile(address); }; } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/src/redmine-net40-api/Async/RedmineManagerAsync.cs b/src/redmine-net20-api/Async/RedmineManagerAsync40.cs similarity index 96% rename from src/redmine-net40-api/Async/RedmineManagerAsync.cs rename to src/redmine-net20-api/Async/RedmineManagerAsync40.cs index a93dd571..ab178809 100644 --- a/src/redmine-net40-api/Async/RedmineManagerAsync.cs +++ b/src/redmine-net20-api/Async/RedmineManagerAsync40.cs @@ -14,6 +14,9 @@ You may obtain a copy of the License at limitations under the License. */ + +#if NET40 + using System.Collections.Generic; using System.Collections.Specialized; using System.Threading.Tasks; @@ -161,11 +164,25 @@ public static Task RemoveWatcherFromIssueAsync(this RedmineManager redmineManage } + /// + /// + /// + /// + /// + /// + /// public static Task CountAsync(this RedmineManager redmineManager, params string[] include) where T : class, new() { return Task.Factory.StartNew(()=> redmineManager.Count(include)); } + /// + /// + /// + /// + /// + /// + /// public static Task CountAsync(this RedmineManager redmineManager, NameValueCollection parameters) where T : class, new() { return Task.Factory.StartNew(() => redmineManager.Count(parameters)); @@ -257,4 +274,5 @@ public static Task DownloadFileAsync(this RedmineManager redmineManager, return Task.Factory.StartNew(() => redmineManager.DownloadFile(address)); } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/src/redmine-net450-api/Async/RedmineManagerAsync.cs b/src/redmine-net20-api/Async/RedmineManagerAsync45.cs similarity index 95% rename from src/redmine-net450-api/Async/RedmineManagerAsync.cs rename to src/redmine-net20-api/Async/RedmineManagerAsync45.cs index 474e8a7a..4642e872 100644 --- a/src/redmine-net450-api/Async/RedmineManagerAsync.cs +++ b/src/redmine-net20-api/Async/RedmineManagerAsync45.cs @@ -14,6 +14,8 @@ You may obtain a copy of the License at limitations under the License. */ +#if !(NET20 || NET40) + using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; @@ -152,7 +154,7 @@ public static async Task AddUserToGroupAsync(this RedmineManager redmineManager, /// The group id. /// The user id. /// - public static async Task DeleteUserFromGroupAsync(this RedmineManager redmineManager, int groupId, int userId) + public static async Task RemoveUserFromGroupAsync(this RedmineManager redmineManager, int groupId, int userId) { var uri = UrlHelper.GetRemoveUserFromGroupUrl(redmineManager, groupId, userId); await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.DELETE, string.Empty, "DeleteUserFromGroupAsync").ConfigureAwait(false); @@ -165,7 +167,7 @@ public static async Task DeleteUserFromGroupAsync(this RedmineManager redmineMan /// The issue identifier. /// The user identifier. /// - public static async Task AddWatcherAsync(this RedmineManager redmineManager, int issueId, int userId) + public static async Task AddWatcherToIssueAsync(this RedmineManager redmineManager, int issueId, int userId) { var data = DataHelper.UserData(userId, redmineManager.MimeFormat); var uri = UrlHelper.GetAddWatcherUrl(redmineManager, issueId, userId); @@ -180,13 +182,19 @@ public static async Task AddWatcherAsync(this RedmineManager redmineManager, int /// The issue identifier. /// The user identifier. /// - public static async Task RemoveWatcherAsync(this RedmineManager redmineManager, int issueId, int userId) + public static async Task RemoveWatcherFromIssueAsync(this RedmineManager redmineManager, int issueId, int userId) { var uri = UrlHelper.GetRemoveWatcherUrl(redmineManager, issueId, userId); await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.DELETE, string.Empty, "RemoveWatcherAsync").ConfigureAwait(false); } - + /// + /// + /// + /// + /// + /// + /// public static async Task CountAsync(this RedmineManager redmineManager, params string[] include) where T : class, new() { var parameters = new NameValueCollection(); @@ -199,6 +207,13 @@ public static async Task RemoveWatcherAsync(this RedmineManager redmineManager, return await CountAsync(redmineManager,parameters).ConfigureAwait(false); } + /// + /// + /// + /// + /// + /// + /// public static async Task CountAsync(this RedmineManager redmineManager, NameValueCollection parameters) where T : class, new() { int totalCount = 0, pageSize = 1, offset = 0; @@ -375,4 +390,5 @@ public static async Task DeleteObjectAsync(this RedmineManager redmineManager await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.DELETE, string.Empty, "DeleteObjectAsync").ConfigureAwait(false); } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/Exceptions/ConflictException.cs b/src/redmine-net20-api/Exceptions/ConflictException.cs index b70ddf5b..0acc079f 100644 --- a/src/redmine-net20-api/Exceptions/ConflictException.cs +++ b/src/redmine-net20-api/Exceptions/ConflictException.cs @@ -15,6 +15,7 @@ limitations under the License. */ using System; +using System.Globalization; using System.Runtime.Serialization; namespace Redmine.Net.Api.Exceptions @@ -46,7 +47,7 @@ public ConflictException(string message) /// /// public ConflictException(string format, params object[] args) - : base(string.Format(format, args)) + : base(string.Format(CultureInfo.InvariantCulture,format, args)) { } @@ -67,7 +68,7 @@ public ConflictException(string message, Exception innerException) /// /// public ConflictException(string format, Exception innerException, params object[] args) - : base(string.Format(format, args), innerException) + : base(string.Format(CultureInfo.InvariantCulture,format, args), innerException) { } diff --git a/src/redmine-net20-api/Exceptions/ForbiddenException.cs b/src/redmine-net20-api/Exceptions/ForbiddenException.cs index 6d396815..e9c2fa2c 100644 --- a/src/redmine-net20-api/Exceptions/ForbiddenException.cs +++ b/src/redmine-net20-api/Exceptions/ForbiddenException.cs @@ -15,6 +15,7 @@ limitations under the License. */ using System; +using System.Globalization; using System.Runtime.Serialization; namespace Redmine.Net.Api.Exceptions @@ -46,7 +47,7 @@ public ForbiddenException(string message) /// /// public ForbiddenException(string format, params object[] args) - : base(string.Format(format, args)) + : base(string.Format(CultureInfo.InvariantCulture,format, args)) { } @@ -67,7 +68,7 @@ public ForbiddenException(string message, Exception innerException) /// /// public ForbiddenException(string format, Exception innerException, params object[] args) - : base(string.Format(format, args), innerException) + : base(string.Format(CultureInfo.InvariantCulture,format, args), innerException) { } diff --git a/src/redmine-net20-api/Exceptions/InternalServerErrorException.cs b/src/redmine-net20-api/Exceptions/InternalServerErrorException.cs index 74076acc..be8e3e5c 100644 --- a/src/redmine-net20-api/Exceptions/InternalServerErrorException.cs +++ b/src/redmine-net20-api/Exceptions/InternalServerErrorException.cs @@ -15,6 +15,7 @@ limitations under the License. */ using System; +using System.Globalization; using System.Runtime.Serialization; namespace Redmine.Net.Api.Exceptions @@ -46,7 +47,7 @@ public InternalServerErrorException(string message) /// /// public InternalServerErrorException(string format, params object[] args) - : base(string.Format(format, args)) + : base(string.Format(CultureInfo.InvariantCulture,format, args)) { } @@ -67,7 +68,7 @@ public InternalServerErrorException(string message, Exception innerException) /// /// public InternalServerErrorException(string format, Exception innerException, params object[] args) - : base(string.Format(format, args), innerException) + : base(string.Format(CultureInfo.InvariantCulture,format, args), innerException) { } diff --git a/src/redmine-net20-api/Exceptions/NameResolutionFailureException.cs b/src/redmine-net20-api/Exceptions/NameResolutionFailureException.cs index 220b9392..6a08cb46 100644 --- a/src/redmine-net20-api/Exceptions/NameResolutionFailureException.cs +++ b/src/redmine-net20-api/Exceptions/NameResolutionFailureException.cs @@ -15,6 +15,7 @@ limitations under the License. */ using System; +using System.Globalization; using System.Runtime.Serialization; namespace Redmine.Net.Api.Exceptions @@ -46,7 +47,7 @@ public NameResolutionFailureException(string message) /// /// public NameResolutionFailureException(string format, params object[] args) - : base(string.Format(format, args)) + : base(string.Format(CultureInfo.InvariantCulture,format, args)) { } @@ -67,7 +68,7 @@ public NameResolutionFailureException(string message, Exception innerException) /// /// public NameResolutionFailureException(string format, Exception innerException, params object[] args) - : base(string.Format(format, args), innerException) + : base(string.Format(CultureInfo.InvariantCulture,format, args), innerException) { } diff --git a/src/redmine-net20-api/Exceptions/NotAcceptableException.cs b/src/redmine-net20-api/Exceptions/NotAcceptableException.cs index c1ae2541..281e0cfb 100644 --- a/src/redmine-net20-api/Exceptions/NotAcceptableException.cs +++ b/src/redmine-net20-api/Exceptions/NotAcceptableException.cs @@ -15,6 +15,7 @@ limitations under the License. */ using System; +using System.Globalization; using System.Runtime.Serialization; namespace Redmine.Net.Api.Exceptions @@ -46,7 +47,7 @@ public NotAcceptableException(string message) /// /// public NotAcceptableException(string format, params object[] args) - : base(string.Format(format, args)) + : base(string.Format(CultureInfo.InvariantCulture,format, args)) { } @@ -67,7 +68,7 @@ public NotAcceptableException(string message, Exception innerException) /// /// public NotAcceptableException(string format, Exception innerException, params object[] args) - : base(string.Format(format, args), innerException) + : base(string.Format(CultureInfo.InvariantCulture,format, args), innerException) { } diff --git a/src/redmine-net20-api/Exceptions/NotFoundException.cs b/src/redmine-net20-api/Exceptions/NotFoundException.cs index e50970fd..b28ca9d3 100644 --- a/src/redmine-net20-api/Exceptions/NotFoundException.cs +++ b/src/redmine-net20-api/Exceptions/NotFoundException.cs @@ -15,6 +15,7 @@ limitations under the License. */ using System; +using System.Globalization; using System.Runtime.Serialization; namespace Redmine.Net.Api.Exceptions @@ -47,7 +48,7 @@ public NotFoundException(string message) /// /// public NotFoundException(string format, params object[] args) - : base(string.Format(format, args)) + : base(string.Format(CultureInfo.InvariantCulture,format, args)) { } @@ -68,7 +69,7 @@ public NotFoundException(string message, Exception innerException) /// /// public NotFoundException(string format, Exception innerException, params object[] args) - : base(string.Format(format, args), innerException) + : base(string.Format(CultureInfo.InvariantCulture,format, args), innerException) { } } diff --git a/src/redmine-net20-api/Exceptions/RedmineException.cs b/src/redmine-net20-api/Exceptions/RedmineException.cs index b28a41ae..d917780b 100644 --- a/src/redmine-net20-api/Exceptions/RedmineException.cs +++ b/src/redmine-net20-api/Exceptions/RedmineException.cs @@ -15,6 +15,7 @@ limitations under the License. */ using System; +using System.Globalization; using System.Runtime.Serialization; namespace Redmine.Net.Api.Exceptions @@ -47,7 +48,7 @@ public RedmineException(string message) /// The format. /// The arguments. public RedmineException(string format, params object[] args) - : base(string.Format(format, args)) + : base(string.Format(CultureInfo.InvariantCulture,format, args)) { } @@ -68,18 +69,10 @@ public RedmineException(string message, Exception innerException) /// The inner exception. /// The arguments. public RedmineException(string format, Exception innerException, params object[] args) - : base(string.Format(format, args), innerException) + : base(string.Format(CultureInfo.InvariantCulture,format, args), innerException) { } - /// - /// Initializes a new instance of the class. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - protected RedmineException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } + } } \ No newline at end of file diff --git a/src/redmine-net20-api/Exceptions/RedmineTimeoutException.cs b/src/redmine-net20-api/Exceptions/RedmineTimeoutException.cs index b55503bb..cc4a17f9 100644 --- a/src/redmine-net20-api/Exceptions/RedmineTimeoutException.cs +++ b/src/redmine-net20-api/Exceptions/RedmineTimeoutException.cs @@ -15,6 +15,7 @@ limitations under the License. */ using System; +using System.Globalization; using System.Runtime.Serialization; namespace Redmine.Net.Api.Exceptions @@ -46,7 +47,7 @@ public RedmineTimeoutException(string message) /// The format. /// The arguments. public RedmineTimeoutException(string format, params object[] args) - : base(string.Format(format, args)) + : base(string.Format(CultureInfo.InvariantCulture,format, args)) { } @@ -70,7 +71,7 @@ public RedmineTimeoutException(string message, Exception innerException) /// The inner exception. /// The arguments. public RedmineTimeoutException(string format, Exception innerException, params object[] args) - : base(string.Format(format, args), innerException) + : base(string.Format(CultureInfo.InvariantCulture,format, args), innerException) { } } diff --git a/src/redmine-net20-api/Exceptions/UnauthorizedException.cs b/src/redmine-net20-api/Exceptions/UnauthorizedException.cs index d4a18179..32991005 100644 --- a/src/redmine-net20-api/Exceptions/UnauthorizedException.cs +++ b/src/redmine-net20-api/Exceptions/UnauthorizedException.cs @@ -15,6 +15,7 @@ limitations under the License. */ using System; +using System.Globalization; using System.Runtime.Serialization; namespace Redmine.Net.Api.Exceptions @@ -47,7 +48,7 @@ public UnauthorizedException(string message) /// The format. /// The arguments. public UnauthorizedException(string format, params object[] args) - : base(string.Format(format, args)) + : base(string.Format(CultureInfo.InvariantCulture,format, args)) { } @@ -71,7 +72,7 @@ public UnauthorizedException(string message, Exception innerException) /// The inner exception. /// The arguments. public UnauthorizedException(string format, Exception innerException, params object[] args) - : base(string.Format(format, args), innerException) + : base(string.Format(CultureInfo.InvariantCulture,format, args), innerException) { } } diff --git a/src/redmine-net20-api/Extensions/CollectionExtensions.cs b/src/redmine-net20-api/Extensions/CollectionExtensions.cs index fe17f3d9..187e6f1a 100755 --- a/src/redmine-net20-api/Extensions/CollectionExtensions.cs +++ b/src/redmine-net20-api/Extensions/CollectionExtensions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2016 Adrian Popescu + Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,16 +16,19 @@ limitations under the License. using System; using System.Collections.Generic; +using System.Text; namespace Redmine.Net.Api.Extensions { /// /// /// + + public static class CollectionExtensions { /// - /// Clones the specified list to clone. + /// Clones the specified list to clone. /// /// /// The list to clone. @@ -34,13 +37,14 @@ public static IList Clone(this IList listToClone) where T : ICloneable { if (listToClone == null) return null; IList clonedList = new List(); - foreach (T item in listToClone) - clonedList.Add((T)item.Clone()); + foreach (var item in listToClone) + clonedList.Add((T) item.Clone()); return clonedList; } + /// - /// Equalses the specified list to compare. + /// Equalses the specified list to compare. /// /// /// The list. @@ -48,14 +52,55 @@ public static IList Clone(this IList listToClone) where T : ICloneable /// public static bool Equals(this IList list, IList listToCompare) where T : class { - if (listToCompare == null) return false; - - if (list.Count != listToCompare.Count) return false; + if (list ==null || listToCompare == null) return false; +#if NET20 + if (list.Count != listToCompare.Count) + { + return false; + } var index = 0; - while (index < list.Count && (list[index] as T).Equals(listToCompare[index] as T)) index++; - + while (index < list.Count && (list[index] as T).Equals(listToCompare[index] as T)) + { + index++; + } + return index == list.Count; +#else + var set = new HashSet(list); + var setToCompare = new HashSet(listToCompare); + + return set.SetEquals(setToCompare); +#endif + } + + /// + /// + /// + /// + public static string Dump(this IEnumerable collection) where TIn : class + { + if (collection == null) + { + return null; + } + + StringBuilder sb = new StringBuilder(); + foreach (var item in collection) + { + sb.Append(",").Append(item); + } + + sb[0] = '{'; + sb.Append("}"); + + var str = sb.ToString(); +#if NET20 + sb = null; +#else + sb.Clear(); +#endif + return str; } } } \ No newline at end of file diff --git a/src/redmine-net40-api/Extensions/JsonExtensions.cs b/src/redmine-net20-api/Extensions/JsonExtensions.cs old mode 100755 new mode 100644 similarity index 93% rename from src/redmine-net40-api/Extensions/JsonExtensions.cs rename to src/redmine-net20-api/Extensions/JsonExtensions.cs index cf02206a..91292ae1 --- a/src/redmine-net40-api/Extensions/JsonExtensions.cs +++ b/src/redmine-net20-api/Extensions/JsonExtensions.cs @@ -14,6 +14,7 @@ You may obtain a copy of the License at limitations under the License. */ +#if !NET20 using System; using System.Collections; using System.Collections.Generic; @@ -39,7 +40,7 @@ public static class JsonExtensions /// The key. public static void WriteIdIfNotNull(this Dictionary dictionary, IdentifiableName ident, string key) { - if (ident != null) dictionary.Add(key, ident.Id); + if (ident != null) dictionary.Add(key, ident.Id.ToString(CultureInfo.InvariantCulture)); } /// @@ -51,7 +52,7 @@ public static void WriteIdIfNotNull(this Dictionary dictionary, /// The empty value. public static void WriteIdOrEmpty(this Dictionary dictionary, IdentifiableName ident, string key, string emptyValue = null) { - if (ident != null) dictionary.Add(key, ident.Id); + if (ident != null) dictionary.Add(key, ident.Id.ToString(CultureInfo.InvariantCulture)); else dictionary.Add(key, emptyValue); } @@ -126,7 +127,7 @@ public static void WriteValueOrEmpty(this Dictionary dictiona if (!val.HasValue || EqualityComparer.Default.Equals(val.Value, default(T))) dictionary.Add(tag, string.Empty); else - dictionary.Add(tag, val.Value); + dictionary.Add(tag, val.Value.ToString()); } /// @@ -138,7 +139,7 @@ public static void WriteValueOrEmpty(this Dictionary dictiona /// The tag. public static void WriteValueOrDefault(this Dictionary dictionary, T? val, string tag) where T : struct { - dictionary.Add(tag, val ?? default(T)); + dictionary.Add(tag, val.GetValueOrDefault().ToString()); } /// @@ -150,10 +151,9 @@ public static void WriteValueOrDefault(this Dictionary dictio /// public static T GetValue(this IDictionary dictionary, string key) { - object val; var dict = dictionary; var type = typeof(T); - if (!dict.TryGetValue(key, out val)) return default(T); + if (!dict.TryGetValue(key, out var val)) return default(T); if (val == null) return default(T); @@ -177,8 +177,7 @@ public static T GetValue(this IDictionary dictionary, string /// public static IdentifiableName GetValueAsIdentifiableName(this IDictionary dictionary, string key) { - object val; - if (!dictionary.TryGetValue(key, out val)) return null; + if (!dictionary.TryGetValue(key, out var val)) return null; var ser = new JavaScriptSerializer(); ser.RegisterConverters(new[] { new IdentifiableNameConverter() }); @@ -196,8 +195,7 @@ public static IdentifiableName GetValueAsIdentifiableName(this IDictionary public static List GetValueAsCollection(this IDictionary dictionary, string key) where T : new() { - object val; - if (!dictionary.TryGetValue(key, out val)) return null; + if (!dictionary.TryGetValue(key, out var val)) return null; var ser = new JavaScriptSerializer(); ser.RegisterConverters(new[] { RedmineSerializer.JsonConverters[typeof(T)] }); @@ -220,4 +218,5 @@ public static IdentifiableName GetValueAsIdentifiableName(this IDictionary + /// + /// + public static class StringExtensions + { + /// + /// + /// + /// + /// + public static bool IsNullOrWhiteSpace(this string value) + { +#if NET20 + if (value == null) + { + return true; + } + + for (int index = 0; index < value.Length; ++index) + { + if (!char.IsWhiteSpace(value[index])) + { + return false; + } + } + return true; +#else + return string.IsNullOrWhiteSpace(value); +#endif + } + + /// + /// + /// + /// + /// + /// + public static string Truncate(this string text, int maximumLength) + { + if (!text.IsNullOrWhiteSpace()) + { + if (text.Length > maximumLength) + { + text = text.Substring(0, maximumLength); + } + } + + return text; + } + } +} \ No newline at end of file diff --git a/src/redmine-net20-api/Extensions/WebExtensions.cs b/src/redmine-net20-api/Extensions/WebExtensions.cs index be8df291..30971246 100755 --- a/src/redmine-net20-api/Extensions/WebExtensions.cs +++ b/src/redmine-net20-api/Extensions/WebExtensions.cs @@ -14,18 +14,18 @@ You may obtain a copy of the License at limitations under the License. */ -using System; using System.Collections.Generic; using System.IO; + using System.Net; -using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Logging; using Redmine.Net.Api.Types; +using Redmine.Net.Api.Exceptions; namespace Redmine.Net.Api.Extensions { /// + /// /// public static class WebExtensions { @@ -35,65 +35,66 @@ public static class WebExtensions /// The exception. /// The method. /// The MIME format. - /// Timeout! - /// Bad domain name! - /// - /// - /// - /// - /// The page that you are trying to update is staled! - /// + /// Timeout! + /// Bad domain name! + /// + /// + /// + /// + /// The page that you are trying to update is staled! + /// /// - /// + /// public static void HandleWebException(this WebException exception, string method, MimeFormat mimeFormat) { if (exception == null) return; switch (exception.Status) { - case WebExceptionStatus.Timeout: - throw new RedmineTimeoutException("Timeout!", exception); - case WebExceptionStatus.NameResolutionFailure: - throw new NameResolutionFailureException("Bad domain name!", exception); + case WebExceptionStatus.Timeout: throw new RedmineTimeoutException("Timeout!", exception); + case WebExceptionStatus.NameResolutionFailure: throw new NameResolutionFailureException("Bad domain name!", exception); case WebExceptionStatus.ProtocolError: - { - var response = (HttpWebResponse) exception.Response; - switch ((int) response.StatusCode) { - case (int) HttpStatusCode.NotFound: - throw new NotFoundException(response.StatusDescription, exception); - - case (int) HttpStatusCode.InternalServerError: - throw new InternalServerErrorException(response.StatusDescription, exception); - - case (int) HttpStatusCode.Unauthorized: - throw new UnauthorizedException(response.StatusDescription, exception); - - case (int) HttpStatusCode.Forbidden: - throw new ForbiddenException(response.StatusDescription, exception); - - case (int) HttpStatusCode.Conflict: - throw new ConflictException("The page that you are trying to update is staled!", exception); - - case 422: - var errors = GetRedmineExceptions(exception.Response, mimeFormat); - var message = string.Empty; - if (errors != null) - { - foreach (var error in errors) - message = message + error.Info + "\n"; - } - throw new RedmineException( - method + " has invalid or missing attribute parameters: " + message, exception); - - case (int) HttpStatusCode.NotAcceptable: - throw new NotAcceptableException(response.StatusDescription, exception); + var response = (HttpWebResponse)exception.Response; + switch ((int)response.StatusCode) + { + + case (int)HttpStatusCode.NotFound: + throw new NotFoundException (response.StatusDescription, exception); + + case (int)HttpStatusCode.InternalServerError: + throw new InternalServerErrorException(response.StatusDescription, exception); + + case (int)HttpStatusCode.Unauthorized: + throw new UnauthorizedException(response.StatusDescription, exception); + + case (int)HttpStatusCode.Forbidden: + throw new ForbiddenException(response.StatusDescription, exception); + + case (int)HttpStatusCode.Conflict: + throw new ConflictException("The page that you are trying to update is staled!", exception); + + case 422: + + var errors = GetRedmineExceptions(exception.Response, mimeFormat); + string message = string.Empty; + if (errors != null) + { + for (var index = 0; index < errors.Count; index++) + { + var error = errors[index]; + message = message + (error.Info + "\n"); + } + } + throw new RedmineException( + $"{method} has invalid or missing attribute parameters: {message}", exception); + + case (int)HttpStatusCode.NotAcceptable: throw new NotAcceptableException(response.StatusDescription, exception); + } } - } break; - default: - throw new RedmineException(exception.Message, exception); + default: throw new RedmineException(exception.Message, exception); } } @@ -112,15 +113,10 @@ private static List GetRedmineExceptions(this WebResponse webResponse, Mi { var responseFromServer = reader.ReadToEnd(); - if (string.IsNullOrEmpty(responseFromServer.Trim())) return null; - try - { - var result = RedmineSerializer.DeserializeList(responseFromServer, mimeFormat); - return result.Objects; - } - catch (Exception ex) + if (!responseFromServer.IsNullOrWhiteSpace()) { - Logger.Current.Error(ex.Message); + var errors = RedmineSerializer.DeserializeList(responseFromServer, mimeFormat); + return errors.Objects; } } return null; diff --git a/src/redmine-net20-api/Extensions/XmlReaderExtensions.cs b/src/redmine-net20-api/Extensions/XmlReaderExtensions.cs index f4db31a9..9bc95d7c 100755 --- a/src/redmine-net20-api/Extensions/XmlReaderExtensions.cs +++ b/src/redmine-net20-api/Extensions/XmlReaderExtensions.cs @@ -15,6 +15,7 @@ limitations under the License. */ using System; +using System.Collections; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -24,10 +25,14 @@ limitations under the License. namespace Redmine.Net.Api.Extensions { /// - /// /// - public static partial class XmlExtensions + public static class XmlReaderExtensions { + /// + /// Date time format for journals, attachments etc. + /// + private const string INCLUDE_DATE_TIME_FORMAT = "yyyy'-'MM'-'dd HH':'mm':'ss UTC"; + /// /// Reads the attribute as int. /// @@ -37,8 +42,11 @@ public static partial class XmlExtensions public static int ReadAttributeAsInt(this XmlReader reader, string attributeName) { var attribute = reader.GetAttribute(attributeName); - int result; - if (string.IsNullOrEmpty(attribute) || !int.TryParse(attribute, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out result)) return default(int); + + if (attribute.IsNullOrWhiteSpace() || !int.TryParse(attribute, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out var result)) + { + return default(int); + } return result; } @@ -52,8 +60,11 @@ public static int ReadAttributeAsInt(this XmlReader reader, string attributeName public static int? ReadAttributeAsNullableInt(this XmlReader reader, string attributeName) { var attribute = reader.GetAttribute(attributeName); - int result; - if (string.IsNullOrEmpty(attribute) || !int.TryParse(attribute, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out result)) return default(int?); + + if (attribute.IsNullOrWhiteSpace() || !int.TryParse(attribute, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out var result)) + { + return default(int?); + } return result; } @@ -67,8 +78,11 @@ public static int ReadAttributeAsInt(this XmlReader reader, string attributeName public static bool ReadAttributeAsBoolean(this XmlReader reader, string attributeName) { var attribute = reader.GetAttribute(attributeName); - bool result; - if (string.IsNullOrEmpty(attribute) || !bool.TryParse(attribute, out result)) return false; + + if (attribute.IsNullOrWhiteSpace() || !bool.TryParse(attribute, out var result)) + { + return false; + } return result; } @@ -80,9 +94,15 @@ public static bool ReadAttributeAsBoolean(this XmlReader reader, string attribut /// public static DateTime? ReadElementContentAsNullableDateTime(this XmlReader reader) { - var str = reader.ReadElementContentAsString(); - DateTime result; - if (string.IsNullOrEmpty(str) || !DateTime.TryParse(str, out result)) return null; + var content = reader.ReadElementContentAsString(); + + if (content.IsNullOrWhiteSpace() || !DateTime.TryParse(content, out var result)) + { + if (!DateTime.TryParseExact(content, INCLUDE_DATE_TIME_FORMAT, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) + { + return null; + } + } return result; } @@ -94,9 +114,11 @@ public static bool ReadAttributeAsBoolean(this XmlReader reader, string attribut /// public static float? ReadElementContentAsNullableFloat(this XmlReader reader) { - var str = reader.ReadElementContentAsString(); - float result; - if (string.IsNullOrEmpty(str) || !float.TryParse(str, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out result)) return null; + var content = reader.ReadElementContentAsString(); + if (content.IsNullOrWhiteSpace() || !float.TryParse(content, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out var result)) + { + return null; + } return result; } @@ -108,9 +130,12 @@ public static bool ReadAttributeAsBoolean(this XmlReader reader, string attribut /// public static int? ReadElementContentAsNullableInt(this XmlReader reader) { - var str = reader.ReadElementContentAsString(); - int result; - if (string.IsNullOrEmpty(str) || !int.TryParse(str, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out result)) return null; + var content = reader.ReadElementContentAsString(); + + if (content.IsNullOrWhiteSpace() || !int.TryParse(content, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out var result)) + { + return null; + } return result; } @@ -122,9 +147,12 @@ public static bool ReadAttributeAsBoolean(this XmlReader reader, string attribut /// public static decimal? ReadElementContentAsNullableDecimal(this XmlReader reader) { - var str = reader.ReadElementContentAsString(); - decimal result; - if (string.IsNullOrEmpty(str) || !decimal.TryParse(str, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out result)) return null; + var content = reader.ReadElementContentAsString(); + + if (content.IsNullOrWhiteSpace() || !decimal.TryParse(content, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out var result)) + { + return null; + } return result; } @@ -137,12 +165,13 @@ public static bool ReadAttributeAsBoolean(this XmlReader reader, string attribut /// public static List ReadElementContentAsCollection(this XmlReader reader) where T : class { - var result = new List(); - var serializer = new XmlSerializer(typeof(T)); - var xml = reader.ReadOuterXml(); - using (var sr = new StringReader(xml)) + List result = null; + XmlSerializer serializer = null; + var outerXml = reader.ReadOuterXml(); + + using (var stringReader = new StringReader(outerXml)) { - using (var xmlTextReader = new XmlTextReader(sr)) + using (var xmlTextReader = new XmlTextReader(stringReader)) { xmlTextReader.ReadStartElement(); while (!xmlTextReader.EOF) @@ -153,26 +182,99 @@ public static List ReadElementContentAsCollection(this XmlReader reader) w continue; } - T obj; + T entity; + + if (serializer == null) + { + serializer = new XmlSerializer(typeof(T)); + } if (xmlTextReader.IsEmptyElement && xmlTextReader.HasAttributes) { - obj = serializer.Deserialize(xmlTextReader) as T; + entity = serializer.Deserialize(xmlTextReader) as T; } else { + if (xmlTextReader.NodeType != XmlNodeType.Element) + { + xmlTextReader.Read(); + continue; + } + var subTree = xmlTextReader.ReadSubtree(); - obj = serializer.Deserialize(subTree) as T; + entity = serializer.Deserialize(subTree) as T; + } + + if (entity != null) + { + if (result == null) + { + result = new List(); + } + + result.Add(entity); } - if (obj != null) - result.Add(obj); if (!xmlTextReader.IsEmptyElement) + { xmlTextReader.Read(); + } } } } return result; } + + /// + /// Reads the element content as enumerable. + /// + /// + /// The reader. + /// + public static IEnumerable ReadElementContentAsEnumerable(this XmlReader reader) where T : class + { + XmlSerializer serializer = null; + var outerXml = reader.ReadOuterXml(); + using (var stringReader = new StringReader(outerXml)) + { + using (var xmlTextReader = new XmlTextReader(stringReader)) + { + xmlTextReader.ReadStartElement(); + while (!xmlTextReader.EOF) + { + if (xmlTextReader.NodeType == XmlNodeType.EndElement) + { + xmlTextReader.ReadEndElement(); + continue; + } + + T entity; + if (serializer == null) + { + serializer = new XmlSerializer(typeof(T)); + } + + if (xmlTextReader.IsEmptyElement && xmlTextReader.HasAttributes) + { + entity = serializer.Deserialize(xmlTextReader) as T; + } + else + { + var subTree = xmlTextReader.ReadSubtree(); + entity = serializer.Deserialize(subTree) as T; + } + if (entity != null) + { + yield return entity; + } + + if (!xmlTextReader.IsEmptyElement) + { + xmlTextReader.Read(); + } + } + } + } + } } } \ No newline at end of file diff --git a/src/redmine-net20-api/Extensions/XmlWriterExtensions.cs b/src/redmine-net20-api/Extensions/XmlWriterExtensions.cs index d521101e..4fa3e5c1 100755 --- a/src/redmine-net20-api/Extensions/XmlWriterExtensions.cs +++ b/src/redmine-net20-api/Extensions/XmlWriterExtensions.cs @@ -100,8 +100,13 @@ public static void WriteArray(this XmlWriter writer, IEnumerable collection, str foreach (var item in collection) { - new XmlSerializer(type, new XmlAttributeOverrides(), new Type[] { }, new XmlRootAttribute(root), + #if (NET20 || NET40 || NET45 || NET451 || NET452) + new XmlSerializer(type, new XmlAttributeOverrides(), new Type[]{}, new XmlRootAttribute(root), defaultNamespace).Serialize(writer, item); +#else + new XmlSerializer(type, new XmlAttributeOverrides(), Array.Empty(), new XmlRootAttribute(root), + defaultNamespace).Serialize(writer, item); + #endif } writer.WriteEndElement(); @@ -165,7 +170,7 @@ public static void WriteIfNotDefaultOrNull(this XmlWriter writer, T? val, str { if (!val.HasValue) return; if (!EqualityComparer.Default.Equals(val.Value, default(T))) - writer.WriteElementString(tag, string.Format(NumberFormatInfo.InvariantInfo, "{0}", val.Value)); + writer.WriteElementString(tag, string.Format(NumberFormatInfo.InvariantInfo, "{0}", val.Value.ToString())); } /// @@ -180,7 +185,7 @@ public static void WriteValueOrEmpty(this XmlWriter writer, T? val, string ta if (!val.HasValue || EqualityComparer.Default.Equals(val.Value, default(T))) writer.WriteElementString(tag, string.Empty); else - writer.WriteElementString(tag, string.Format(NumberFormatInfo.InvariantInfo, "{0}", val.Value)); + writer.WriteElementString(tag, string.Format(NumberFormatInfo.InvariantInfo, "{0}", val.Value.ToString())); } /// diff --git a/src/redmine-net20-api/IRedmineManager.cs b/src/redmine-net20-api/IRedmineManager.cs index 743327d6..4a39c132 100644 --- a/src/redmine-net20-api/IRedmineManager.cs +++ b/src/redmine-net20-api/IRedmineManager.cs @@ -254,8 +254,8 @@ public interface IRedmineManager /// /// /// - /// + /// /// - bool RemoteCertValidate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors error); + bool RemoteCertValidate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors sslPolicyErrors); } } \ No newline at end of file diff --git a/src/redmine-net20-api/Internals/DataHelper.cs b/src/redmine-net20-api/Internals/DataHelper.cs index d6f108ab..dce5dd76 100755 --- a/src/redmine-net20-api/Internals/DataHelper.cs +++ b/src/redmine-net20-api/Internals/DataHelper.cs @@ -30,8 +30,8 @@ internal static class DataHelper public static string UserData(int userId, MimeFormat mimeFormat) { return mimeFormat == MimeFormat.Xml - ? "" + userId + "" - : "{\"user_id\":\"" + userId + "\"}"; + ? $"{userId}" + : $"{{\"user_id\":\"{userId}\"}}"; } } } \ No newline at end of file diff --git a/src/redmine-net20-api/Internals/Func.cs b/src/redmine-net20-api/Internals/Func.cs old mode 100755 new mode 100644 index 380ff125..d279f3eb --- a/src/redmine-net20-api/Internals/Func.cs +++ b/src/redmine-net20-api/Internals/Func.cs @@ -14,7 +14,8 @@ You may obtain a copy of the License at limitations under the License. */ -namespace Redmine.Net.Api.Internals +#if NET20 +namespace System { /// /// @@ -66,4 +67,5 @@ namespace Redmine.Net.Api.Internals /// The arg4. /// public delegate TResult Func(T1 arg1, T2 arg2, T3 arg3, T4 arg4); -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/Internals/HashCodeHelper.cs b/src/redmine-net20-api/Internals/HashCodeHelper.cs index 59c80263..c33d5f28 100755 --- a/src/redmine-net20-api/Internals/HashCodeHelper.cs +++ b/src/redmine-net20-api/Internals/HashCodeHelper.cs @@ -14,6 +14,7 @@ You may obtain a copy of the License at limitations under the License. */ +using System; using System.Collections.Generic; namespace Redmine.Net.Api.Internals @@ -32,19 +33,17 @@ internal static class HashCodeHelper /// /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. /// - public static int GetHashCode(IList list, int hash) + public static int GetHashCode(IList list, int hash) where T : class { unchecked { var hashCode = hash; - if (list != null) + if (list == null) return hashCode; + hashCode = (hashCode * 13) + list.Count; + foreach (var t in list) { - hashCode = (hashCode * 13) + list.Count; - foreach (T t in list) - { - hashCode *= 13; - if (t != null) hashCode = hashCode + t.GetHashCode(); - } + hashCode *= 13; + if (t != null) hashCode += t.GetHashCode(); } return hashCode; @@ -66,7 +65,22 @@ public static int GetHashCode(T entity, int hash) { var hashCode = hash; - hashCode = (hashCode * 397) ^ (entity == null ? 0 : entity.GetHashCode()); + var type = typeof(T); + + var isNullable = Nullable.GetUnderlyingType(type) != null; + if (isNullable) + { + type = type.UnderlyingSystemType; + } + + if (type.IsValueType) + { + hashCode = (hashCode * 397) ^ entity.GetHashCode(); + } + else + { + hashCode = (hashCode * 397) ^ (entity?.GetHashCode() ?? 0); + } return hashCode; } diff --git a/src/redmine-net20-api/Internals/RedmineSerializer.cs b/src/redmine-net20-api/Internals/RedmineSerializer.cs index 4a1cbdb4..9cbb0943 100755 --- a/src/redmine-net20-api/Internals/RedmineSerializer.cs +++ b/src/redmine-net20-api/Internals/RedmineSerializer.cs @@ -16,6 +16,9 @@ limitations under the License. using System; using System.IO; +#if !NET20 +using System.Linq; +#endif using System.Xml; using System.Xml.Serialization; using Redmine.Net.Api.Extensions; @@ -24,11 +27,9 @@ limitations under the License. namespace Redmine.Net.Api.Internals { - /// - /// - /// - internal static class RedmineSerializer - { + internal static partial class RedmineSerializer + { + private static readonly XmlWriterSettings xws = new XmlWriterSettings {OmitXmlDeclaration = true}; /// /// Serializes the specified System.Object and writes the XML document to a string. /// @@ -38,16 +39,14 @@ internal static class RedmineSerializer /// The System.String that contains the XML document. /// /// - /// // ReSharper disable once InconsistentNaming private static string ToXML(T obj) where T : class { - var xws = new XmlWriterSettings { OmitXmlDeclaration = true }; using (var stringWriter = new StringWriter()) { using (var xmlWriter = XmlWriter.Create(stringWriter, xws)) { - var sr = new XmlSerializer(typeof(T)); + var sr = new XmlSerializer(typeof (T)); sr.Serialize(xmlWriter, obj); return stringWriter.ToString(); } @@ -67,86 +66,152 @@ private static string ToXML(T obj) where T : class // ReSharper disable once InconsistentNaming private static T FromXML(string xml) where T : class { - using (var text = new StringReader(xml)) + if(xml.IsNullOrWhiteSpace()) throw new ArgumentNullException(nameof(xml)); + + using (var text = new XmlTextReader(xml)) { - var sr = new XmlSerializer(typeof(T)); + var sr = new XmlSerializer(typeof (T)); return sr.Deserialize(text) as T; } } /// - /// Deserializes the XML document contained by the specific System.String. + /// Serializes the specified type T and writes the XML document to a string. /// - /// The System.String that contains the XML document to deserialize. - /// The type of objects to deserialize. - /// - /// The System.Object being deserialized. - /// - /// An error occurred during deserialization. The original exception is available - /// using the System.Exception.InnerException property. - // ReSharper disable once InconsistentNaming - private static object FromXML(string xml, Type type) + /// + /// The object. + /// The MIME format. + /// + /// Serialization error + public static string Serialize(T obj, MimeFormat mimeFormat) where T : class, new() { - using (var text = new StringReader(xml)) + try + { +#if !NET20 + if (mimeFormat == MimeFormat.Json) + { + return JsonSerializer(obj); + } +#endif + return ToXML(obj); + } + catch (Exception ex) { - var sr = new XmlSerializer(type); - return sr.Deserialize(text); + throw new RedmineException("Serialization error", ex); } } /// - /// Serializes the specified object. + /// Deserializes the XML document contained by the specific System.String. /// /// - /// The object. + /// The response. /// The MIME format. /// - public static string Serialize(T obj, MimeFormat mimeFormat) where T : class, new() + /// + /// Could not deserialize null! + /// or + /// Deserialization error + /// + /// + /// + /// + public static T Deserialize(string response, MimeFormat mimeFormat) where T : class, new() { - return ToXML(obj); + if (string.IsNullOrEmpty(response)) throw new RedmineException("Could not deserialize null!"); + try + { +#if !NET20 + if (mimeFormat == MimeFormat.Json) + { + var type = typeof (T); + var jsonRoot = (string) null; + if (type == typeof (IssueCategory)) jsonRoot = RedmineKeys.ISSUE_CATEGORY; + if (type == typeof (IssueRelation)) jsonRoot = RedmineKeys.RELATION; + if (type == typeof (TimeEntry)) jsonRoot = RedmineKeys.TIME_ENTRY; + if (type == typeof (ProjectMembership)) jsonRoot = RedmineKeys.MEMBERSHIP; + if (type == typeof (WikiPage)) jsonRoot = RedmineKeys.WIKI_PAGE; + return JsonDeserialize(response, jsonRoot); + } +#endif + return FromXML(response); + } + catch (Exception ex) + { + throw new RedmineException("Deserialization error",ex); + } } /// - /// Deserializes the specified response. + /// Deserializes the list. /// /// /// The response. /// The MIME format. /// - /// could not deserialize: + response - public static T Deserialize(string response, MimeFormat mimeFormat) where T : class, new() + /// + /// Could not deserialize null! + /// or + /// Deserialization error + /// + public static PaginatedObjects DeserializeList(string response, MimeFormat mimeFormat) + where T : class, new() { - if (string.IsNullOrEmpty(response)) throw new RedmineException("could not deserialize: " + response); + try + { + if (response.IsNullOrWhiteSpace()) throw new RedmineException("Could not deserialize null!"); +#if !NET20 + if (mimeFormat == MimeFormat.Json) + { + return JSonDeserializeList(response); + } +#endif + return XmlDeserializeList(response); + } - return FromXML(response); + catch (Exception ex) + { + throw new RedmineException("Deserialization error", ex); + } } +#if !NET20 /// - /// Deserializes the list. + /// js the son deserialize list. /// /// /// The response. - /// The MIME format. /// - /// web response is null! - public static PaginatedObjects DeserializeList(string response, MimeFormat mimeFormat) where T : class, new() + private static PaginatedObjects JSonDeserializeList(string response) where T : class, new() { - if (string.IsNullOrEmpty(response)) throw new RedmineException("web response is null!"); + var type = typeof(T); + var jsonRoot = (string)null; + if (type == typeof(Error)) jsonRoot = RedmineKeys.ERRORS; + if (type == typeof(WikiPage)) jsonRoot = RedmineKeys.WIKI_PAGES; + if (type == typeof(IssuePriority)) jsonRoot = RedmineKeys.ISSUE_PRIORITIES; + if (type == typeof(TimeEntryActivity)) jsonRoot = RedmineKeys.TIME_ENTRY_ACTIVITIES; - return XmlDeserializeList(response); - } + if (string.IsNullOrEmpty(jsonRoot)) + jsonRoot = RedmineManager.Sufixes[type]; + var result = JsonDeserializeToList(response, jsonRoot, out int totalItems, out int offset); + + return new PaginatedObjects() + { + TotalCount = totalItems, + Offset = offset, + Objects = result.ToList() + }; + } +#endif /// /// XMLs the deserialize list. /// /// /// The response. /// - /// could not deserialize: + response private static PaginatedObjects XmlDeserializeList(string response) where T : class, new() { - if (string.IsNullOrEmpty(response)) throw new RedmineException("could not deserialize: " + response); - using (var stringReader = new StringReader(response)) { using (var xmlReader = new XmlTextReader(stringReader)) @@ -156,11 +221,12 @@ private static object FromXML(string xml, Type type) xmlReader.Read(); var totalItems = xmlReader.ReadAttributeAsInt(RedmineKeys.TOTAL_COUNT); - + var offset = xmlReader.ReadAttributeAsInt(RedmineKeys.OFFSET); var result = xmlReader.ReadElementContentAsCollection(); return new PaginatedObjects() { TotalCount = totalItems, + Offset = offset, Objects = result }; } diff --git a/src/redmine-net40-api/Internals/RedmineSerializerJson.cs b/src/redmine-net20-api/Internals/RedmineSerializerJson.cs similarity index 98% rename from src/redmine-net40-api/Internals/RedmineSerializerJson.cs rename to src/redmine-net20-api/Internals/RedmineSerializerJson.cs index 024d91fb..2395db87 100755 --- a/src/redmine-net40-api/Internals/RedmineSerializerJson.cs +++ b/src/redmine-net20-api/Internals/RedmineSerializerJson.cs @@ -13,7 +13,7 @@ You may obtain a copy of the License at See the License for the specific language governing permissions and limitations under the License. */ - +#if !NET20 using System; using System.Collections.Generic; using System.Web.Script.Serialization; @@ -149,7 +149,7 @@ public static List JsonDeserializeToList(string jsonString, string root, o /// public static object JsonDeserialize(string jsonString, Type type, string root) { - if (string.IsNullOrEmpty(jsonString)) throw new ArgumentNullException("jsonString"); + if (string.IsNullOrEmpty(jsonString)) throw new ArgumentNullException(nameof(jsonString)); var serializer = new JavaScriptSerializer(); serializer.RegisterConverters(new[] { jsonConverters[type] }); @@ -198,7 +198,7 @@ private static object JsonDeserializeToList(string jsonString, string root, Type { totalCount = 0; offset = 0; - if (string.IsNullOrEmpty(jsonString)) throw new ArgumentNullException("jsonString"); + if (string.IsNullOrEmpty(jsonString)) throw new ArgumentNullException(nameof(jsonString)); var serializer = new JavaScriptSerializer(); serializer.RegisterConverters(new[] { jsonConverters[type] }); @@ -227,7 +227,7 @@ private static object JsonDeserializeToList(string jsonString, string root, Type } else { - info += item as string + " "; + info += $"{item as string} "; } } var err = new Error { Info = info }; @@ -240,4 +240,5 @@ private static object JsonDeserializeToList(string jsonString, string root, Type return arrayList; } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/Internals/UrlHelper.cs b/src/redmine-net20-api/Internals/UrlHelper.cs index 5efb6f25..f869daa1 100644 --- a/src/redmine-net20-api/Internals/UrlHelper.cs +++ b/src/redmine-net20-api/Internals/UrlHelper.cs @@ -84,7 +84,7 @@ public static string GetUploadUrl(RedmineManager redmineManager, string id, T if (!RedmineManager.Sufixes.ContainsKey(type)) throw new KeyNotFoundException(type.Name); return string.Format(REQUEST_FORMAT, redmineManager.Host, RedmineManager.Sufixes[type], id, - redmineManager.MimeFormat.ToString().ToLower()); + redmineManager.MimeFormat.ToString().ToLowerInvariant()); } /// @@ -110,13 +110,13 @@ public static string GetUploadUrl(RedmineManager redmineManager, string id, T { if (string.IsNullOrEmpty(ownerId)) throw new RedmineException("The owner id(project id) is mandatory!"); return string.Format(ENTITY_WITH_PARENT_FORMAT, redmineManager.Host, RedmineKeys.PROJECTS, - ownerId, RedmineManager.Sufixes[type], redmineManager.MimeFormat.ToString().ToLower()); + ownerId, RedmineManager.Sufixes[type], redmineManager.MimeFormat.ToString().ToLowerInvariant()); } if (type == typeof(IssueRelation)) { if (string.IsNullOrEmpty(ownerId)) throw new RedmineException("The owner id(issue id) is mandatory!"); return string.Format(ENTITY_WITH_PARENT_FORMAT, redmineManager.Host, RedmineKeys.ISSUES, - ownerId, RedmineManager.Sufixes[type], redmineManager.MimeFormat.ToString().ToLower()); + ownerId, RedmineManager.Sufixes[type], redmineManager.MimeFormat.ToString().ToLowerInvariant()); } if (type == typeof(File)) @@ -125,11 +125,11 @@ public static string GetUploadUrl(RedmineManager redmineManager, string id, T { throw new RedmineException("The owner id(project id) is mandatory!"); } - return string.Format(FILE_URL_FORMAT, redmineManager.Host, ownerId, redmineManager.MimeFormat.ToString().ToLower()); + return string.Format(FILE_URL_FORMAT, redmineManager.Host, ownerId, redmineManager.MimeFormat.ToString().ToLowerInvariant()); } return string.Format(FORMAT, redmineManager.Host, RedmineManager.Sufixes[type], - redmineManager.MimeFormat.ToString().ToLower()); + redmineManager.MimeFormat.ToString().ToLowerInvariant()); } /// @@ -148,7 +148,7 @@ public static string GetUploadUrl(RedmineManager redmineManager, string id, T if (!RedmineManager.Sufixes.ContainsKey(type)) throw new KeyNotFoundException(type.Name); return string.Format(REQUEST_FORMAT, redmineManager.Host, RedmineManager.Sufixes[type], id, - redmineManager.MimeFormat.ToString().ToLower()); + redmineManager.MimeFormat.ToString().ToLowerInvariant()); } /// @@ -166,7 +166,7 @@ public static string GetUploadUrl(RedmineManager redmineManager, string id, T if (!RedmineManager.Sufixes.ContainsKey(type)) throw new KeyNotFoundException(type.Name); return string.Format(REQUEST_FORMAT, redmineManager.Host, RedmineManager.Sufixes[type], id, - redmineManager.MimeFormat.ToString().ToLower()); + redmineManager.MimeFormat.ToString().ToLowerInvariant()); } /// @@ -196,7 +196,7 @@ public static string GetListUrl(RedmineManager redmineManager, NameValueColle throw new RedmineException("The project id is mandatory! \nCheck if you have included the parameter project_id to parameters."); return string.Format(ENTITY_WITH_PARENT_FORMAT, redmineManager.Host, RedmineKeys.PROJECTS, - projectId, RedmineManager.Sufixes[type], redmineManager.MimeFormat.ToString().ToLower()); + projectId, RedmineManager.Sufixes[type], redmineManager.MimeFormat.ToString().ToLowerInvariant()); } if (type == typeof(IssueRelation)) { @@ -205,7 +205,7 @@ public static string GetListUrl(RedmineManager redmineManager, NameValueColle throw new RedmineException("The issue id is mandatory! \nCheck if you have included the parameter issue_id to parameters"); return string.Format(ENTITY_WITH_PARENT_FORMAT, redmineManager.Host, RedmineKeys.ISSUES, - issueId, RedmineManager.Sufixes[type], redmineManager.MimeFormat.ToString().ToLower()); + issueId, RedmineManager.Sufixes[type], redmineManager.MimeFormat.ToString().ToLowerInvariant()); } if (type == typeof(File)) @@ -215,11 +215,11 @@ public static string GetListUrl(RedmineManager redmineManager, NameValueColle { throw new RedmineException("The project id is mandatory! \nCheck if you have included the parameter project_id to parameters."); } - return string.Format(FILE_URL_FORMAT, redmineManager.Host, projectId, redmineManager.MimeFormat.ToString().ToLower()); + return string.Format(FILE_URL_FORMAT, redmineManager.Host, projectId, redmineManager.MimeFormat.ToString().ToLowerInvariant()); } return string.Format(FORMAT, redmineManager.Host, RedmineManager.Sufixes[type], - redmineManager.MimeFormat.ToString().ToLower()); + redmineManager.MimeFormat.ToString().ToLowerInvariant()); } /// @@ -231,7 +231,7 @@ public static string GetListUrl(RedmineManager redmineManager, NameValueColle public static string GetWikisUrl(RedmineManager redmineManager, string projectId) { return string.Format(WIKI_INDEX_FORMAT, redmineManager.Host, projectId, - redmineManager.MimeFormat.ToString().ToLower()); + redmineManager.MimeFormat.ToString().ToLowerInvariant()); } /// @@ -248,9 +248,9 @@ public static string GetWikiPageUrl(RedmineManager redmineManager, string projec { var uri = version == 0 ? string.Format(WIKI_PAGE_FORMAT, redmineManager.Host, projectId, pageName, - redmineManager.MimeFormat.ToString().ToLower()) + redmineManager.MimeFormat.ToString().ToLowerInvariant()) : string.Format(WIKI_VERSION_FORMAT, redmineManager.Host, projectId, pageName, version, - redmineManager.MimeFormat.ToString().ToLower()); + redmineManager.MimeFormat.ToString().ToLowerInvariant()); return uri; } @@ -264,7 +264,7 @@ public static string GetAddUserToGroupUrl(RedmineManager redmineManager, int gro { return string.Format(REQUEST_FORMAT, redmineManager.Host, RedmineManager.Sufixes[typeof(Group)], - groupId + "/users", redmineManager.MimeFormat.ToString().ToLower()); + $"{groupId}/users", redmineManager.MimeFormat.ToString().ToLowerInvariant()); } /// @@ -278,7 +278,7 @@ public static string GetRemoveUserFromGroupUrl(RedmineManager redmineManager, in { return string.Format(REQUEST_FORMAT, redmineManager.Host, RedmineManager.Sufixes[typeof(Group)], - groupId + "/users/" + userId, redmineManager.MimeFormat.ToString().ToLower()); + $"{groupId}/users/{userId}", redmineManager.MimeFormat.ToString().ToLowerInvariant()); } /// @@ -289,7 +289,7 @@ public static string GetRemoveUserFromGroupUrl(RedmineManager redmineManager, in public static string GetUploadFileUrl(RedmineManager redmineManager) { return string.Format(FORMAT, redmineManager.Host, RedmineKeys.UPLOADS, - redmineManager.MimeFormat.ToString().ToLower()); + redmineManager.MimeFormat.ToString().ToLowerInvariant()); } /// @@ -301,7 +301,7 @@ public static string GetCurrentUserUrl(RedmineManager redmineManager) { return string.Format(REQUEST_FORMAT, redmineManager.Host, RedmineManager.Sufixes[typeof(User)], CURRENT_USER_URI, - redmineManager.MimeFormat.ToString().ToLower()); + redmineManager.MimeFormat.ToString().ToLowerInvariant()); } /// @@ -314,7 +314,7 @@ public static string GetCurrentUserUrl(RedmineManager redmineManager) public static string GetWikiCreateOrUpdaterUrl(RedmineManager redmineManager, string projectId, string pageName) { return string.Format(WIKI_PAGE_FORMAT, redmineManager.Host, projectId, pageName, - redmineManager.MimeFormat.ToString().ToLower()); + redmineManager.MimeFormat.ToString().ToLowerInvariant()); } /// @@ -327,7 +327,7 @@ public static string GetWikiCreateOrUpdaterUrl(RedmineManager redmineManager, st public static string GetDeleteWikirUrl(RedmineManager redmineManager, string projectId, string pageName) { return string.Format(WIKI_PAGE_FORMAT, redmineManager.Host, projectId, pageName, - redmineManager.MimeFormat.ToString().ToLower()); + redmineManager.MimeFormat.ToString().ToLowerInvariant()); } /// @@ -340,8 +340,8 @@ public static string GetDeleteWikirUrl(RedmineManager redmineManager, string pro public static string GetAddWatcherUrl(RedmineManager redmineManager, int issueId, int userId) { return string.Format(REQUEST_FORMAT, redmineManager.Host, - RedmineManager.Sufixes[typeof(Issue)], issueId + "/watchers", - redmineManager.MimeFormat.ToString().ToLower()); + RedmineManager.Sufixes[typeof(Issue)], $"{issueId}/watchers", + redmineManager.MimeFormat.ToString().ToLowerInvariant()); } /// @@ -354,8 +354,8 @@ public static string GetAddWatcherUrl(RedmineManager redmineManager, int issueId public static string GetRemoveWatcherUrl(RedmineManager redmineManager, int issueId, int userId) { return string.Format(REQUEST_FORMAT, redmineManager.Host, - RedmineManager.Sufixes[typeof(Issue)], issueId + "/watchers/" + userId, - redmineManager.MimeFormat.ToString().ToLower()); + RedmineManager.Sufixes[typeof(Issue)], $"{issueId}/watchers/{userId}", + redmineManager.MimeFormat.ToString().ToLowerInvariant()); } /// @@ -367,7 +367,7 @@ public static string GetRemoveWatcherUrl(RedmineManager redmineManager, int issu public static string GetAttachmentUpdateUrl(RedmineManager redmineManager, int issueId) { return string.Format(ATTACHMENT_UPDATE_FORMAT, redmineManager.Host, issueId, - redmineManager.MimeFormat.ToString().ToLower()); + redmineManager.MimeFormat.ToString().ToLowerInvariant()); } } } \ No newline at end of file diff --git a/src/redmine-net450-api/Internals/WebApiAsyncHelper.cs b/src/redmine-net20-api/Internals/WebApiAsyncHelper.cs similarity index 99% rename from src/redmine-net450-api/Internals/WebApiAsyncHelper.cs rename to src/redmine-net20-api/Internals/WebApiAsyncHelper.cs index 69b89410..704207fe 100644 --- a/src/redmine-net450-api/Internals/WebApiAsyncHelper.cs +++ b/src/redmine-net20-api/Internals/WebApiAsyncHelper.cs @@ -13,7 +13,7 @@ You may obtain a copy of the License at See the License for the specific language governing permissions and limitations under the License. */ - +#if !(NET20 || NET40) using System.Collections.Generic; using System.Collections.Specialized; using System.Net; @@ -198,4 +198,5 @@ public static async Task ExecuteUploadFile(RedmineManager redmineManager } } } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/Internals/XmlStreamingDeserializer.cs b/src/redmine-net20-api/Internals/XmlStreamingDeserializer.cs deleted file mode 100755 index 6e939e7b..00000000 --- a/src/redmine-net20-api/Internals/XmlStreamingDeserializer.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.IO; -using System.Xml; -using System.Xml.Serialization; - -namespace Redmine.Net.Api -{ - /// - /// - /// - /// - /// http://florianreischl.blogspot.ro/search/label/c%23 - public class XmlStreamingDeserializer - { - static XmlSerializerNamespaces ns; - XmlSerializer serializer; - XmlReader reader; - - static XmlStreamingDeserializer() - { - ns = new XmlSerializerNamespaces(); - ns.Add("", ""); - } - - private XmlStreamingDeserializer() - { - serializer = new XmlSerializer(typeof(T)); - } - - public XmlStreamingDeserializer(TextReader reader) - : this(XmlReader.Create(reader)) - { - } - - public XmlStreamingDeserializer(XmlReader reader) - : this() - { - this.reader = reader; - } - - public void Close() - { - reader.Close(); - } - - public T Deserialize() - { - while (reader.Read()) - { - if (reader.NodeType == XmlNodeType.Element && reader.Depth == 1 && reader.Name == typeof(T).Name) - { - XmlReader xmlReader = reader.ReadSubtree(); - return (T)serializer.Deserialize(xmlReader); - } - } - return default(T); - } - } -} \ No newline at end of file diff --git a/src/redmine-net20-api/Internals/XmlStreamingSerializer.cs b/src/redmine-net20-api/Internals/XmlStreamingSerializer.cs deleted file mode 100755 index 56549eee..00000000 --- a/src/redmine-net20-api/Internals/XmlStreamingSerializer.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System.IO; -using System.Xml; -using System.Xml.Serialization; - -namespace Redmine.Net.Api -{ - /// - /// - /// - /// - /// http://florianreischl.blogspot.ro/search/label/c%23 - public class XmlStreamingSerializer - { - static XmlSerializerNamespaces ns; - XmlSerializer serializer = new XmlSerializer(typeof(T)); - XmlWriter writer; - bool finished; - - static XmlStreamingSerializer() - { - ns = new XmlSerializerNamespaces(); - ns.Add("", ""); - } - - private XmlStreamingSerializer() - { - serializer = new XmlSerializer(typeof(T)); - } - - public XmlStreamingSerializer(TextWriter w) - : this(XmlWriter.Create(w)) - { - } - - public XmlStreamingSerializer(XmlWriter writer) - : this() - { - this.writer = writer; - writer.WriteStartDocument(); - writer.WriteStartElement("ArrayOf" + typeof(T).Name); - } - - public void Finish() - { - writer.WriteEndDocument(); - writer.Flush(); - finished = true; - } - - public void Close() - { - if (!finished) - Finish(); - writer.Close(); - } - - public void Serialize(T item) - { - serializer.Serialize(writer, item, ns); - } - } -} \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/AttachmentConverter.cs b/src/redmine-net20-api/JSonConverters/AttachmentConverter.cs new file mode 100755 index 00000000..7122a92a --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/AttachmentConverter.cs @@ -0,0 +1,100 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 + + +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + + +namespace Redmine.Net.Api.JSonConverters +{ + /// + /// + /// + /// + internal class AttachmentConverter : JavaScriptConverter + { + #region Overrides of JavaScriptConverter + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// An instance of property data stored as name/value pairs. + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var attachment = new Attachment(); + + attachment.Id = dictionary.GetValue(RedmineKeys.ID); + attachment.Description = dictionary.GetValue(RedmineKeys.DESCRIPTION); + attachment.Author = dictionary.GetValueAsIdentifiableName(RedmineKeys.AUTHOR); + attachment.ContentType = dictionary.GetValue(RedmineKeys.CONTENT_TYPE); + attachment.ContentUrl = dictionary.GetValue(RedmineKeys.CONTENT_URL); + attachment.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); + attachment.FileName = dictionary.GetValue(RedmineKeys.FILENAME); + attachment.FileSize = dictionary.GetValue(RedmineKeys.FILESIZE); + + return attachment; + } + + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object�s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + var entity = obj as Attachment; + var result = new Dictionary(); + + if (entity != null) + { + result.Add(RedmineKeys.FILENAME, entity.FileName); + result.Add(RedmineKeys.DESCRIPTION, entity.Description); + } + + var root = new Dictionary(); + root[RedmineKeys.ATTACHMENT] = result; + + return root; + } + + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(Attachment) }); } } + + #endregion + } +} + +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/AttachmentsConverter.cs b/src/redmine-net20-api/JSonConverters/AttachmentsConverter.cs new file mode 100755 index 00000000..9b1b209d --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/AttachmentsConverter.cs @@ -0,0 +1,82 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System.Globalization; +#if !NET20 +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class AttachmentsConverter : JavaScriptConverter + { + #region Overrides of JavaScriptConverter + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// An instance of property data stored as name/value pairs. + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) + { + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object’s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + var entity = obj as Attachments; + var result = new Dictionary(); + + if (entity != null) + { + foreach (var entry in entity) + { + var attachment = new AttachmentConverter().Serialize(entry.Value, serializer); + result.Add(entry.Key.ToString(CultureInfo.InvariantCulture), attachment.First().Value); + } + } + + var root = new Dictionary(); + root[RedmineKeys.ATTACHMENTS] = result; + + return root; + } + + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(Attachments) }); } } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/ChangeSetConverter.cs b/src/redmine-net20-api/JSonConverters/ChangeSetConverter.cs new file mode 100755 index 00000000..f73ee73a --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/ChangeSetConverter.cs @@ -0,0 +1,82 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Types; +using Redmine.Net.Api.Extensions; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class ChangeSetConverter : JavaScriptConverter + { + #region Overrides of JavaScriptConverter + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// An instance of property data stored as name/value pairs. + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var changeSet = new ChangeSet + { + Revision = dictionary.GetValue(RedmineKeys.REVISION), + Comments = dictionary.GetValue(RedmineKeys.COMMENTS), + User = dictionary.GetValueAsIdentifiableName(RedmineKeys.USER), + CommittedOn = dictionary.GetValue(RedmineKeys.COMMITTED_ON) + }; + + + return changeSet; + } + + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object�s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + return null; + } + + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(ChangeSet)}); } + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/CustomFieldConverter.cs b/src/redmine-net20-api/JSonConverters/CustomFieldConverter.cs new file mode 100755 index 00000000..db516280 --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/CustomFieldConverter.cs @@ -0,0 +1,93 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class CustomFieldConverter : IdentifiableNameConverter + { + #region Overrides of JavaScriptConverter + + /// + /// Deserializes the specified dictionary. + /// + /// The dictionary. + /// The type. + /// The serializer. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var customField = new CustomField(); + + customField.Id = dictionary.GetValue(RedmineKeys.ID); + customField.Name = dictionary.GetValue(RedmineKeys.NAME); + customField.CustomizedType = dictionary.GetValue(RedmineKeys.CUSTOMIZED_TYPE); + customField.FieldFormat = dictionary.GetValue(RedmineKeys.FIELD_FORMAT); + customField.Regexp = dictionary.GetValue(RedmineKeys.REGEXP); + customField.MinLength = dictionary.GetValue(RedmineKeys.MIN_LENGTH); + customField.MaxLength = dictionary.GetValue(RedmineKeys.MAX_LENGTH); + customField.IsRequired = dictionary.GetValue(RedmineKeys.IS_REQUIRED); + customField.IsFilter = dictionary.GetValue(RedmineKeys.IS_FILTER); + customField.Searchable = dictionary.GetValue(RedmineKeys.SEARCHABLE); + customField.Multiple = dictionary.GetValue(RedmineKeys.MULTIPLE); + customField.DefaultValue = dictionary.GetValue(RedmineKeys.DEFAULT_VALUE); + customField.Visible = dictionary.GetValue(RedmineKeys.VISIBLE); + customField.PossibleValues = + dictionary.GetValueAsCollection(RedmineKeys.POSSIBLE_VALUES); + customField.Trackers = dictionary.GetValueAsCollection(RedmineKeys.TRACKERS); + customField.Roles = dictionary.GetValueAsCollection(RedmineKeys.ROLES); + + + return customField; + } + + return null; + } + + /// + /// Serializes the specified object. + /// + /// The object. + /// The serializer. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + return null; + } + + /// + /// Gets the supported types. + /// + /// + /// The supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(CustomField)}); } + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/CustomFieldPossibleValueConverter.cs b/src/redmine-net20-api/JSonConverters/CustomFieldPossibleValueConverter.cs new file mode 100644 index 00000000..cc36315a --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/CustomFieldPossibleValueConverter.cs @@ -0,0 +1,77 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class CustomFieldPossibleValueConverter : IdentifiableNameConverter + { + #region Overrides of JavaScriptConverter + + /// + /// Deserializes the specified dictionary. + /// + /// The dictionary. + /// The type. + /// The serializer. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var entity = new CustomFieldPossibleValue(); + + entity.Value = dictionary.GetValue(RedmineKeys.VALUE); + entity.Label = dictionary.GetValue(RedmineKeys.LABEL); + + return entity; + } + + return null; + } + + /// + /// Serializes the specified object. + /// + /// The object. + /// The serializer. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + return null; + } + + /// + /// Gets the supported types. + /// + /// + /// The supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(CustomFieldPossibleValue)}); } + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/CustomFieldRoleConverter.cs b/src/redmine-net20-api/JSonConverters/CustomFieldRoleConverter.cs new file mode 100755 index 00000000..6d93f6fe --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/CustomFieldRoleConverter.cs @@ -0,0 +1,77 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Types; +using Redmine.Net.Api.Extensions; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class CustomFieldRoleConverter : IdentifiableNameConverter + { + #region Overrides of JavaScriptConverter + + /// + /// Deserializes the specified dictionary. + /// + /// The dictionary. + /// The type. + /// The serializer. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var entity = new CustomFieldRole(); + + entity.Id = dictionary.GetValue(RedmineKeys.ID); + entity.Name = dictionary.GetValue(RedmineKeys.NAME); + + return entity; + } + + return null; + } + + /// + /// Serializes the specified object. + /// + /// The object. + /// The serializer. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + return null; + } + + /// + /// Gets the supported types. + /// + /// + /// The supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(CustomFieldRole)}); } + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/DetailConverter.cs b/src/redmine-net20-api/JSonConverters/DetailConverter.cs new file mode 100755 index 00000000..4984a2fd --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/DetailConverter.cs @@ -0,0 +1,83 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Types; +using Redmine.Net.Api.Extensions; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class DetailConverter : JavaScriptConverter + { + #region Overrides of JavaScriptConverter + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// + /// An instance of property data stored + /// as name/value pairs. + /// + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var detail = new Detail(); + + detail.NewValue = dictionary.GetValue(RedmineKeys.NEW_VALUE); + detail.OldValue = dictionary.GetValue(RedmineKeys.OLD_VALUE); + detail.Property = dictionary.GetValue(RedmineKeys.PROPERTY); + detail.Name = dictionary.GetValue(RedmineKeys.NAME); + + return detail; + } + + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object�s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + return null; + } + + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(Detail)}); } + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/ErrorConverter.cs b/src/redmine-net20-api/JSonConverters/ErrorConverter.cs new file mode 100755 index 00000000..e19fdc00 --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/ErrorConverter.cs @@ -0,0 +1,77 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Types; +using Redmine.Net.Api.Extensions; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class ErrorConverter : JavaScriptConverter + { + #region Overrides of JavaScriptConverter + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// + /// An instance of property data stored + /// as name/value pairs. + /// + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var error = new Error {Info = dictionary.GetValue(RedmineKeys.ERROR)}; + return error; + } + + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object�s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + return null; + } + + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(Error)}); } + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/FileConverter.cs b/src/redmine-net20-api/JSonConverters/FileConverter.cs new file mode 100755 index 00000000..8165083e --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/FileConverter.cs @@ -0,0 +1,112 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Types; +using Redmine.Net.Api.Extensions; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class FileConverter : JavaScriptConverter + { + #region Overrides of JavaScriptConverter + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// + /// An instance of property data stored + /// as name/value pairs. + /// + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var file = new File { }; + + file.Author = dictionary.GetValueAsIdentifiableName(RedmineKeys.AUTHOR); + file.ContentType = dictionary.GetValue(RedmineKeys.CONTENT_TYPE); + file.ContentUrl = dictionary.GetValue(RedmineKeys.CONTENT_URL); + file.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); + file.Description = dictionary.GetValue(RedmineKeys.DESCRIPTION); + file.Digest = dictionary.GetValue(RedmineKeys.DIGEST); + file.Downloads = dictionary.GetValue(RedmineKeys.DOWNLOADS); + file.Filename = dictionary.GetValue(RedmineKeys.FILENAME); + file.Filesize = dictionary.GetValue(RedmineKeys.FILESIZE); + file.Id = dictionary.GetValue(RedmineKeys.ID); + file.Token = dictionary.GetValue(RedmineKeys.TOKEN); + var versionId = dictionary.GetValue(RedmineKeys.VERSION_ID); + if (versionId.HasValue) + { + file.Version = new IdentifiableName { Id = versionId.Value }; + } + else + { + file.Version = dictionary.GetValueAsIdentifiableName(RedmineKeys.VERSION); + } + return file; + } + + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object’s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + var entity = obj as File; + var result = new Dictionary(); + + if (entity != null) + { + result.Add(RedmineKeys.TOKEN, entity.Token); + result.WriteIdIfNotNull(entity.Version, RedmineKeys.VERSION_ID); + result.Add(RedmineKeys.FILENAME, entity.Filename); + result.Add(RedmineKeys.DESCRIPTION, entity.Description); + + var root = new Dictionary(); + root[RedmineKeys.FILE] = result; + return root; + } + return result; + } + + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] { typeof(File) }); } + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/GroupConverter.cs b/src/redmine-net20-api/JSonConverters/GroupConverter.cs new file mode 100755 index 00000000..02083800 --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/GroupConverter.cs @@ -0,0 +1,98 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Types; +using Redmine.Net.Api.Extensions; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class GroupConverter : JavaScriptConverter + { + #region Overrides of JavaScriptConverter + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// + /// An instance of property data stored + /// as name/value pairs. + /// + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var group = new Group(); + + group.Id = dictionary.GetValue(RedmineKeys.ID); + group.Name = dictionary.GetValue(RedmineKeys.NAME); + group.Users = dictionary.GetValueAsCollection(RedmineKeys.USERS); + group.CustomFields = dictionary.GetValueAsCollection(RedmineKeys.CUSTOM_FIELDS); + group.Memberships = dictionary.GetValueAsCollection(RedmineKeys.MEMBERSHIPS); + + return group; + } + + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object�s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + var entity = obj as Group; + + var result = new Dictionary(); + + if (entity != null) + { + result.Add(RedmineKeys.NAME, entity.Name); + result.WriteIdsArray(RedmineKeys.USER_IDS, entity.Users); + + var root = new Dictionary(); + root[RedmineKeys.GROUP] = result; + return root; + } + + return result; + } + + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(Group)}); } + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/GroupUserConverter.cs b/src/redmine-net20-api/JSonConverters/GroupUserConverter.cs new file mode 100755 index 00000000..9b65c69a --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/GroupUserConverter.cs @@ -0,0 +1,78 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + + +namespace Redmine.Net.Api.JSonConverters +{ + internal class GroupUserConverter : IdentifiableNameConverter + { + #region Overrides of JavaScriptConverter + + /// + /// Deserializes the specified dictionary. + /// + /// The dictionary. + /// The type. + /// The serializer. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var userGroup = new GroupUser(); + + userGroup.Id = dictionary.GetValue(RedmineKeys.ID); + userGroup.Name = dictionary.GetValue(RedmineKeys.NAME); + + return userGroup; + } + + return null; + } + + /// + /// Serializes the specified object. + /// + /// The object. + /// The serializer. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + return null; + } + + /// + /// Gets the supported types. + /// + /// + /// The supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(GroupUser)}); } + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/IdentifiableNameConverter.cs b/src/redmine-net20-api/JSonConverters/IdentifiableNameConverter.cs new file mode 100755 index 00000000..3b0e94f3 --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/IdentifiableNameConverter.cs @@ -0,0 +1,92 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Types; +using Redmine.Net.Api.Extensions; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class IdentifiableNameConverter : JavaScriptConverter + { + #region Overrides of JavaScriptConverter + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// + /// An instance of property data stored + /// as name/value pairs. + /// + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var entity = new IdentifiableName(); + + entity.Id = dictionary.GetValue(RedmineKeys.ID); + entity.Name = dictionary.GetValue(RedmineKeys.NAME); + + return entity; + } + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object�s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + var entity = obj as IdentifiableName; + var result = new Dictionary(); + + if (entity != null) + { + result.WriteIdIfNotNull(entity, RedmineKeys.ID); + + if (!string.IsNullOrEmpty(entity.Name)) + result.Add(RedmineKeys.NAME, entity.Name); + return result; + } + + return result; + } + + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(IdentifiableName)}); } + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/IssueCategoryConverter.cs b/src/redmine-net20-api/JSonConverters/IssueCategoryConverter.cs new file mode 100755 index 00000000..ae698a96 --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/IssueCategoryConverter.cs @@ -0,0 +1,98 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Types; +using Redmine.Net.Api.Extensions; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class IssueCategoryConverter : JavaScriptConverter + { + #region Overrides of JavaScriptConverter + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// + /// An instance of property data stored + /// as name/value pairs. + /// + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var issueCategory = new IssueCategory(); + + issueCategory.Id = dictionary.GetValue(RedmineKeys.ID); + issueCategory.Project = dictionary.GetValueAsIdentifiableName(RedmineKeys.PROJECT); + issueCategory.AsignTo = dictionary.GetValueAsIdentifiableName(RedmineKeys.ASSIGNED_TO); + issueCategory.Name = dictionary.GetValue(RedmineKeys.NAME); + + return issueCategory; + } + + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object�s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + var entity = obj as IssueCategory; + var result = new Dictionary(); + + if (entity != null) + { + result.Add(RedmineKeys.NAME, entity.Name); + result.WriteIdIfNotNull(entity.Project, RedmineKeys.PROJECT_ID); + result.WriteIdIfNotNull(entity.AsignTo, RedmineKeys.ASSIGNED_TO_ID); + + var root = new Dictionary(); + + root[RedmineKeys.ISSUE_CATEGORY] = result; + return root; + } + + return result; + } + + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(IssueCategory)}); } + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/IssueChildConverter.cs b/src/redmine-net20-api/JSonConverters/IssueChildConverter.cs new file mode 100755 index 00000000..082eeb60 --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/IssueChildConverter.cs @@ -0,0 +1,76 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Types; +using Redmine.Net.Api.Extensions; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class IssueChildConverter : JavaScriptConverter + { + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(IssueChild)}); } + } + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// An instance of property data stored as name/value pairs. + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var issueChild = new IssueChild + { + Id = dictionary.GetValue(RedmineKeys.ID), + Tracker = dictionary.GetValueAsIdentifiableName(RedmineKeys.TRACKER), + Subject = dictionary.GetValue(RedmineKeys.SUBJECT) + }; + + return issueChild; + } + + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object�s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + return null; + } + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/IssueConverter.cs b/src/redmine-net20-api/JSonConverters/IssueConverter.cs new file mode 100644 index 00000000..d3f14331 --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/IssueConverter.cs @@ -0,0 +1,156 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + + +namespace Redmine.Net.Api.JSonConverters +{ + internal class IssueConverter : JavaScriptConverter + { + #region Overrides of JavaScriptConverter + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// + /// An instance of property data stored + /// as name/value pairs. + /// + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var issue = new Issue(); + + issue.Id = dictionary.GetValue(RedmineKeys.ID); + issue.Description = dictionary.GetValue(RedmineKeys.DESCRIPTION); + issue.Project = dictionary.GetValueAsIdentifiableName(RedmineKeys.PROJECT); + issue.Tracker = dictionary.GetValueAsIdentifiableName(RedmineKeys.TRACKER); + issue.Status = dictionary.GetValueAsIdentifiableName(RedmineKeys.STATUS); + issue.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); + issue.UpdatedOn = dictionary.GetValue(RedmineKeys.UPDATED_ON); + issue.ClosedOn = dictionary.GetValue(RedmineKeys.CLOSED_ON); + issue.Priority = dictionary.GetValueAsIdentifiableName(RedmineKeys.PRIORITY); + issue.Author = dictionary.GetValueAsIdentifiableName(RedmineKeys.AUTHOR); + issue.AssignedTo = dictionary.GetValueAsIdentifiableName(RedmineKeys.ASSIGNED_TO); + issue.Category = dictionary.GetValueAsIdentifiableName(RedmineKeys.CATEGORY); + issue.FixedVersion = dictionary.GetValueAsIdentifiableName(RedmineKeys.FIXED_VERSION); + issue.Subject = dictionary.GetValue(RedmineKeys.SUBJECT); + issue.Notes = dictionary.GetValue(RedmineKeys.NOTES); + issue.IsPrivate = dictionary.GetValue(RedmineKeys.IS_PRIVATE); + issue.StartDate = dictionary.GetValue(RedmineKeys.START_DATE); + issue.DueDate = dictionary.GetValue(RedmineKeys.DUE_DATE); + issue.SpentHours = dictionary.GetValue(RedmineKeys.SPENT_HOURS); + issue.TotalSpentHours = dictionary.GetValue(RedmineKeys.TOTAL_SPENT_HOURS); + issue.DoneRatio = dictionary.GetValue(RedmineKeys.DONE_RATIO); + issue.EstimatedHours = dictionary.GetValue(RedmineKeys.ESTIMATED_HOURS); + issue.TotalEstimatedHours = dictionary.GetValue(RedmineKeys.TOTAL_ESTIMATED_HOURS); + issue.ParentIssue = dictionary.GetValueAsIdentifiableName(RedmineKeys.PARENT); + + issue.CustomFields = dictionary.GetValueAsCollection(RedmineKeys.CUSTOM_FIELDS); + issue.Attachments = dictionary.GetValueAsCollection(RedmineKeys.ATTACHMENTS); + issue.Relations = dictionary.GetValueAsCollection(RedmineKeys.RELATIONS); + issue.Journals = dictionary.GetValueAsCollection(RedmineKeys.JOURNALS); + issue.Changesets = dictionary.GetValueAsCollection(RedmineKeys.CHANGESETS); + issue.Watchers = dictionary.GetValueAsCollection(RedmineKeys.WATCHERS); + issue.Children = dictionary.GetValueAsCollection(RedmineKeys.CHILDREN); + return issue; + } + + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object�s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + var entity = obj as Issue; + var result = new Dictionary(); + + if (entity != null) + { + result.Add(RedmineKeys.SUBJECT, entity.Subject); + result.Add(RedmineKeys.DESCRIPTION, entity.Description); + result.Add(RedmineKeys.NOTES, entity.Notes); + if (entity.Id != 0) + { + result.Add(RedmineKeys.PRIVATE_NOTES, entity.PrivateNotes.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + } + result.Add(RedmineKeys.IS_PRIVATE, entity.IsPrivate.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + result.WriteIdIfNotNull(entity.Project, RedmineKeys.PROJECT_ID); + result.WriteIdIfNotNull(entity.Priority, RedmineKeys.PRIORITY_ID); + result.WriteIdIfNotNull(entity.Status, RedmineKeys.STATUS_ID); + result.WriteIdIfNotNull(entity.Category, RedmineKeys.CATEGORY_ID); + result.WriteIdIfNotNull(entity.Tracker, RedmineKeys.TRACKER_ID); + result.WriteIdIfNotNull(entity.AssignedTo, RedmineKeys.ASSIGNED_TO_ID); + result.WriteIdIfNotNull(entity.FixedVersion, RedmineKeys.FIXED_VERSION_ID); + result.WriteValueOrEmpty(entity.EstimatedHours, RedmineKeys.ESTIMATED_HOURS); + + result.WriteIdOrEmpty(entity.ParentIssue, RedmineKeys.PARENT_ISSUE_ID); + result.WriteDateOrEmpty(entity.StartDate, RedmineKeys.START_DATE); + result.WriteDateOrEmpty(entity.DueDate, RedmineKeys.DUE_DATE); + result.WriteDateOrEmpty(entity.UpdatedOn, RedmineKeys.UPDATED_ON); + + if (entity.DoneRatio != null) + result.Add(RedmineKeys.DONE_RATIO, entity.DoneRatio.Value.ToString(CultureInfo.InvariantCulture)); + + if (entity.SpentHours != null) + result.Add(RedmineKeys.SPENT_HOURS, entity.SpentHours.Value.ToString(CultureInfo.InvariantCulture)); + + result.WriteArray(RedmineKeys.UPLOADS, entity.Uploads, new UploadConverter(), serializer); + result.WriteArray(RedmineKeys.CUSTOM_FIELDS, entity.CustomFields, new IssueCustomFieldConverter(), + serializer); + + result.WriteIdsArray(RedmineKeys.WATCHER_USER_IDS, entity.Watchers); + + var root = new Dictionary(); + root[RedmineKeys.ISSUE] = result; + return root; + } + + return result; + } + + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(Issue)}); } + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/IssueCustomFieldConverter.cs b/src/redmine-net20-api/JSonConverters/IssueCustomFieldConverter.cs new file mode 100755 index 00000000..19170bd3 --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/IssueCustomFieldConverter.cs @@ -0,0 +1,122 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System.Globalization; +#if !NET20 +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Types; +using Redmine.Net.Api.Extensions; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class IssueCustomFieldConverter : JavaScriptConverter + { + #region Overrides of JavaScriptConverter + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// + /// An instance of property data stored + /// as name/value pairs. + /// + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var customField = new IssueCustomField(); + + customField.Id = dictionary.GetValue(RedmineKeys.ID); + customField.Name = dictionary.GetValue(RedmineKeys.NAME); + customField.Multiple = dictionary.GetValue(RedmineKeys.MULTIPLE); + + var val = dictionary.GetValue(RedmineKeys.VALUE); + + if (val != null) + { + if (customField.Values == null) customField.Values = new List(); + var list = val as ArrayList; + if (list != null) + { + foreach (var value in list) + { + customField.Values.Add(new CustomFieldValue {Info = Convert.ToString(value, CultureInfo.InvariantCulture)}); + } + } + else + { + customField.Values.Add(new CustomFieldValue {Info = Convert.ToString(val, CultureInfo.InvariantCulture)}); + } + } + return customField; + } + + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object�s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + var entity = obj as IssueCustomField; + + var result = new Dictionary(); + + if (entity == null) return result; + if (entity.Values == null) return null; + var itemsCount = entity.Values.Count; + + result.Add(RedmineKeys.ID, entity.Id.ToString(CultureInfo.InvariantCulture)); + if (itemsCount > 1) + { + result.Add(RedmineKeys.VALUE, entity.Values.Select(x => x.Info).ToArray()); + } + else + { + result.Add(RedmineKeys.VALUE, itemsCount > 0 ? entity.Values[0].Info : null); + } + + return result; + } + + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(IssueCustomField)}); } + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/IssuePriorityConverter.cs b/src/redmine-net20-api/JSonConverters/IssuePriorityConverter.cs new file mode 100755 index 00000000..6205a98e --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/IssuePriorityConverter.cs @@ -0,0 +1,78 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class IssuePriorityConverter : JavaScriptConverter + { + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(IssuePriority)}); } + } + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// + /// An instance of property data stored + /// as name/value pairs. + /// + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var issuePriority = new IssuePriority(); + + issuePriority.Id = dictionary.GetValue(RedmineKeys.ID); + issuePriority.Name = dictionary.GetValue(RedmineKeys.NAME); + issuePriority.IsDefault = dictionary.GetValue(RedmineKeys.IS_DEFAULT); + + return issuePriority; + } + + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object�s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + return null; + } + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/IssueRelationConverter.cs b/src/redmine-net20-api/JSonConverters/IssueRelationConverter.cs new file mode 100755 index 00000000..0c42b0c5 --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/IssueRelationConverter.cs @@ -0,0 +1,102 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System.Globalization; +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class IssueRelationConverter : JavaScriptConverter + { + #region Overrides of JavaScriptConverter + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// + /// An instance of property data stored + /// as name/value pairs. + /// + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var issueRelation = new IssueRelation(); + + issueRelation.Id = dictionary.GetValue(RedmineKeys.ID); + issueRelation.IssueId = dictionary.GetValue(RedmineKeys.ISSUE_ID); + issueRelation.IssueToId = dictionary.GetValue(RedmineKeys.ISSUE_TO_ID); + issueRelation.Type = dictionary.GetValue(RedmineKeys.RELATION_TYPE); + issueRelation.Delay = dictionary.GetValue(RedmineKeys.DELAY); + + return issueRelation; + } + + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object�s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + var entity = obj as IssueRelation; + + var result = new Dictionary(); + + if (entity != null) + { + result.Add(RedmineKeys.ISSUE_TO_ID, entity.IssueToId.ToString(CultureInfo.InvariantCulture)); + result.Add(RedmineKeys.RELATION_TYPE, entity.Type.ToString()); + if (entity.Type == IssueRelationType.precedes || entity.Type == IssueRelationType.follows) + result.WriteValueOrEmpty(entity.Delay, RedmineKeys.DELAY); + + var root = new Dictionary(); + root[RedmineKeys.RELATION] = result; + return root; + } + + return result; + } + + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(IssueRelation)}); } + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/IssueStatusConverter.cs b/src/redmine-net20-api/JSonConverters/IssueStatusConverter.cs new file mode 100755 index 00000000..0965ce90 --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/IssueStatusConverter.cs @@ -0,0 +1,82 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class IssueStatusConverter : JavaScriptConverter + { + #region Overrides of JavaScriptConverter + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// + /// An instance of property data stored + /// as name/value pairs. + /// + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var issueStatus = new IssueStatus(); + + issueStatus.Id = dictionary.GetValue(RedmineKeys.ID); + issueStatus.Name = dictionary.GetValue(RedmineKeys.NAME); + issueStatus.IsClosed = dictionary.GetValue(RedmineKeys.IS_CLOSED); + issueStatus.IsDefault = dictionary.GetValue(RedmineKeys.IS_DEFAULT); + return issueStatus; + } + + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object�s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + return null; + } + + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(IssueStatus)}); } + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/JournalConverter.cs b/src/redmine-net20-api/JSonConverters/JournalConverter.cs new file mode 100644 index 00000000..7336f90d --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/JournalConverter.cs @@ -0,0 +1,85 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class JournalConverter : JavaScriptConverter + { + #region Overrides of JavaScriptConverter + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// + /// An instance of property data stored + /// as name/value pairs. + /// + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var journal = new Journal(); + + journal.Id = dictionary.GetValue(RedmineKeys.ID); + journal.Notes = dictionary.GetValue(RedmineKeys.NOTES); + journal.User = dictionary.GetValueAsIdentifiableName(RedmineKeys.USER); + journal.PrivateNotes = dictionary.GetValue(RedmineKeys.PRIVATE_NOTES); + journal.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); + journal.Details = dictionary.GetValueAsCollection(RedmineKeys.DETAILS); + + return journal; + } + + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object�s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + return null; + } + + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(Journal)}); } + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/MembershipConverter.cs b/src/redmine-net20-api/JSonConverters/MembershipConverter.cs new file mode 100755 index 00000000..784de771 --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/MembershipConverter.cs @@ -0,0 +1,82 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class MembershipConverter : JavaScriptConverter + { + #region Overrides of JavaScriptConverter + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// + /// An instance of property data stored + /// as name/value pairs. + /// + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var membership = new Membership(); + + membership.Id = dictionary.GetValue(RedmineKeys.ID); + membership.Project = dictionary.GetValueAsIdentifiableName(RedmineKeys.PROJECT); + membership.Roles = dictionary.GetValueAsCollection(RedmineKeys.ROLES); + + return membership; + } + + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object�s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + return null; + } + + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(Membership)}); } + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/MembershipRoleConverter.cs b/src/redmine-net20-api/JSonConverters/MembershipRoleConverter.cs new file mode 100755 index 00000000..cd9b57c0 --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/MembershipRoleConverter.cs @@ -0,0 +1,82 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class MembershipRoleConverter : JavaScriptConverter + { + #region Overrides of JavaScriptConverter + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// + /// An instance of property data stored + /// as name/value pairs. + /// + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var membershipRole = new MembershipRole(); + + membershipRole.Id = dictionary.GetValue(RedmineKeys.ID); + membershipRole.Inherited = dictionary.GetValue(RedmineKeys.INHERITED); + membershipRole.Name = dictionary.GetValue(RedmineKeys.NAME); + + return membershipRole; + } + + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object�s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + return null; + } + + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(MembershipRole)}); } + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/NewsConverter.cs b/src/redmine-net20-api/JSonConverters/NewsConverter.cs new file mode 100755 index 00000000..fb1ce0b2 --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/NewsConverter.cs @@ -0,0 +1,85 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class NewsConverter : JavaScriptConverter + { + #region Overrides of JavaScriptConverter + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// + /// An instance of property data stored + /// as name/value pairs. + /// + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var news = new News(); + + news.Id = dictionary.GetValue(RedmineKeys.ID); + news.Author = dictionary.GetValueAsIdentifiableName(RedmineKeys.AUTHOR); + news.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); + news.Description = dictionary.GetValue(RedmineKeys.DESCRIPTION); + news.Project = dictionary.GetValueAsIdentifiableName(RedmineKeys.PROJECT); + news.Summary = dictionary.GetValue(RedmineKeys.SUMMARY); + news.Title = dictionary.GetValue(RedmineKeys.TITLE); + + return news; + } + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object�s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + return null; + } + + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(News)}); } + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/PermissionConverter.cs b/src/redmine-net20-api/JSonConverters/PermissionConverter.cs new file mode 100755 index 00000000..dd4905d2 --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/PermissionConverter.cs @@ -0,0 +1,73 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class PermissionConverter : JavaScriptConverter + { + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(Permission)}); } + } + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// + /// An instance of property data stored + /// as name/value pairs. + /// + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var permission = new Permission {Info = dictionary.GetValue(RedmineKeys.PERMISSION)}; + return permission; + } + + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object�s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + return null; + } + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/ProjectConverter.cs b/src/redmine-net20-api/JSonConverters/ProjectConverter.cs new file mode 100755 index 00000000..9f897864 --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/ProjectConverter.cs @@ -0,0 +1,119 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System.Globalization; +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Types; +using Redmine.Net.Api.Extensions; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class ProjectConverter : JavaScriptConverter + { + #region Overrides of JavaScriptConverter + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// + /// An instance of property data stored + /// as name/value pairs. + /// + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var project = new Project(); + + project.Id = dictionary.GetValue(RedmineKeys.ID); + project.Description = dictionary.GetValue(RedmineKeys.DESCRIPTION); + project.HomePage = dictionary.GetValue(RedmineKeys.HOMEPAGE); + project.Name = dictionary.GetValue(RedmineKeys.NAME); + project.Identifier = dictionary.GetValue(RedmineKeys.IDENTIFIER); + project.Status = dictionary.GetValue(RedmineKeys.STATUS); + project.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); + project.UpdatedOn = dictionary.GetValue(RedmineKeys.UPDATED_ON); + project.Trackers = dictionary.GetValueAsCollection(RedmineKeys.TRACKERS); + project.CustomFields = dictionary.GetValueAsCollection(RedmineKeys.CUSTOM_FIELDS); + project.IsPublic = dictionary.GetValue(RedmineKeys.IS_PUBLIC); + project.Parent = dictionary.GetValueAsIdentifiableName(RedmineKeys.PARENT); + project.IssueCategories = dictionary.GetValueAsCollection(RedmineKeys.ISSUE_CATEGORIES); + project.EnabledModules = dictionary.GetValueAsCollection(RedmineKeys.ENABLED_MODULES); + project.TimeEntryActivities = dictionary.GetValueAsCollection(RedmineKeys.TIME_ENTRY_ACTIVITIES); + return project; + } + + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object’s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + var entity = obj as Project; + var result = new Dictionary(); + + if (entity != null) + { + result.Add(RedmineKeys.NAME, entity.Name); + result.Add(RedmineKeys.IDENTIFIER, entity.Identifier); + result.Add(RedmineKeys.DESCRIPTION, entity.Description); + result.Add(RedmineKeys.HOMEPAGE, entity.HomePage); + //result.Add(RedmineKeys.INHERIT_MEMBERS, entity.InheritMembers.ToString().ToLowerInvariant()); + result.Add(RedmineKeys.IS_PUBLIC, entity.IsPublic.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + result.WriteIdOrEmpty(entity.Parent, RedmineKeys.PARENT_ID, string.Empty); + result.WriteIdsArray(RedmineKeys.TRACKER_IDS, entity.Trackers); + result.WriteNamesArray(RedmineKeys.ENABLED_MODULE_NAMES, entity.EnabledModules); + if (entity.Id > 0) + { + result.WriteArray(RedmineKeys.CUSTOM_FIELDS, entity.CustomFields, new IssueCustomFieldConverter(), + serializer); + } + var root = new Dictionary(); + root[RedmineKeys.PROJECT] = result; + return root; + } + + return result; + } + + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] { typeof(Project) }); } + } + + #endregion + } +} +#endif diff --git a/src/redmine-net20-api/JSonConverters/ProjectEnabledModuleConverter.cs b/src/redmine-net20-api/JSonConverters/ProjectEnabledModuleConverter.cs new file mode 100755 index 00000000..8ec50390 --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/ProjectEnabledModuleConverter.cs @@ -0,0 +1,78 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class ProjectEnabledModuleConverter : JavaScriptConverter + { + #region Overrides of JavaScriptConverter + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// + /// An instance of property data stored + /// as name/value pairs. + /// + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var projectEnableModule = new ProjectEnabledModule(); + projectEnableModule.Id = dictionary.GetValue(RedmineKeys.ID); + projectEnableModule.Name = dictionary.GetValue(RedmineKeys.NAME); + return projectEnableModule; + } + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object�s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + return null; + } + + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(ProjectEnabledModule)}); } + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/ProjectIssueCategoryConverter.cs b/src/redmine-net20-api/JSonConverters/ProjectIssueCategoryConverter.cs new file mode 100755 index 00000000..44d366d7 --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/ProjectIssueCategoryConverter.cs @@ -0,0 +1,90 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class ProjectIssueCategoryConverter : JavaScriptConverter + { + #region Overrides of JavaScriptConverter + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// + /// An instance of property data stored + /// as name/value pairs. + /// + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var projectTracker = new ProjectIssueCategory(); + projectTracker.Id = dictionary.GetValue(RedmineKeys.ID); + projectTracker.Name = dictionary.GetValue(RedmineKeys.NAME); + return projectTracker; + } + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object�s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + var entity = obj as ProjectIssueCategory; + var result = new Dictionary(); + + if (entity != null) + { + result.Add(RedmineKeys.ID, entity.Id); + result.Add(RedmineKeys.NAME, entity.Name); + + var root = new Dictionary(); + root[RedmineKeys.ISSUE_CATEGORY] = result; + return root; + } + return result; + } + + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(ProjectIssueCategory)}); } + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/ProjectMembershipConverter.cs b/src/redmine-net20-api/JSonConverters/ProjectMembershipConverter.cs new file mode 100755 index 00000000..9e22e06b --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/ProjectMembershipConverter.cs @@ -0,0 +1,96 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class ProjectMembershipConverter : JavaScriptConverter + { + #region Overrides of JavaScriptConverter + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// + /// An instance of property data stored + /// as name/value pairs. + /// + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var projectMembership = new ProjectMembership(); + + projectMembership.Id = dictionary.GetValue(RedmineKeys.ID); + projectMembership.Group = dictionary.GetValueAsIdentifiableName(RedmineKeys.GROUP); + projectMembership.Project = dictionary.GetValueAsIdentifiableName(RedmineKeys.PROJECT); + projectMembership.Roles = dictionary.GetValueAsCollection(RedmineKeys.ROLES); + projectMembership.User = dictionary.GetValueAsIdentifiableName(RedmineKeys.USER); + + return projectMembership; + } + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object�s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + var entity = obj as ProjectMembership; + var result = new Dictionary(); + + if (entity != null) + { + result.WriteIdIfNotNull(entity.User, RedmineKeys.USER_ID); + result.WriteIdsArray(RedmineKeys.ROLE_IDS, entity.Roles); + + var root = new Dictionary(); + root[RedmineKeys.MEMBERSHIP] = result; + return root; + } + + return result; + } + + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(ProjectMembership)}); } + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/ProjectTrackerConverter.cs b/src/redmine-net20-api/JSonConverters/ProjectTrackerConverter.cs new file mode 100755 index 00000000..cc41bdf4 --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/ProjectTrackerConverter.cs @@ -0,0 +1,78 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class ProjectTrackerConverter : JavaScriptConverter + { + #region Overrides of JavaScriptConverter + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// + /// An instance of property data stored + /// as name/value pairs. + /// + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var projectTracker = new ProjectTracker(); + projectTracker.Id = dictionary.GetValue(RedmineKeys.ID); + projectTracker.Name = dictionary.GetValue(RedmineKeys.NAME); + return projectTracker; + } + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object�s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + return null; + } + + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(ProjectTracker)}); } + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/QueryConverter.cs b/src/redmine-net20-api/JSonConverters/QueryConverter.cs new file mode 100755 index 00000000..75bb5410 --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/QueryConverter.cs @@ -0,0 +1,83 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class QueryConverter : JavaScriptConverter + { + #region Overrides of JavaScriptConverter + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// + /// An instance of property data stored + /// as name/value pairs. + /// + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var query = new Query(); + + query.Id = dictionary.GetValue(RedmineKeys.ID); + query.IsPublic = dictionary.GetValue(RedmineKeys.IS_PUBLIC); + query.ProjectId = dictionary.GetValue(RedmineKeys.PROJECT_ID); + query.Name = dictionary.GetValue(RedmineKeys.NAME); + + return query; + } + + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object�s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + return null; + } + + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(Query)}); } + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/RoleConverter.cs b/src/redmine-net20-api/JSonConverters/RoleConverter.cs new file mode 100755 index 00000000..754a21f3 --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/RoleConverter.cs @@ -0,0 +1,92 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class RoleConverter : JavaScriptConverter + { + #region Overrides of JavaScriptConverter + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// + /// An instance of property data stored + /// as name/value pairs. + /// + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var role = new Role(); + + role.Id = dictionary.GetValue(RedmineKeys.ID); + role.Name = dictionary.GetValue(RedmineKeys.NAME); + + var permissions = dictionary.GetValue(RedmineKeys.PERMISSIONS); + if (permissions != null) + { + role.Permissions = new List(); + foreach (var permission in permissions) + { + var perms = new Permission {Info = permission.ToString()}; + role.Permissions.Add(perms); + } + } + + return role; + } + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object�s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + return null; + } + + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(Role)}); } + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/TimeEntryActivityConverter.cs b/src/redmine-net20-api/JSonConverters/TimeEntryActivityConverter.cs new file mode 100755 index 00000000..8ee50dfb --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/TimeEntryActivityConverter.cs @@ -0,0 +1,78 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class TimeEntryActivityConverter : JavaScriptConverter + { + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(TimeEntryActivity)}); } + } + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// + /// An instance of property data stored + /// as name/value pairs. + /// + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var timeEntryActivity = new TimeEntryActivity + { + Id = dictionary.GetValue(RedmineKeys.ID), + Name = dictionary.GetValue(RedmineKeys.NAME), + IsDefault = dictionary.GetValue(RedmineKeys.IS_DEFAULT) + }; + return timeEntryActivity; + } + + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object�s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + return null; + } + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/TimeEntryConverter.cs b/src/redmine-net20-api/JSonConverters/TimeEntryConverter.cs new file mode 100755 index 00000000..1da798c6 --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/TimeEntryConverter.cs @@ -0,0 +1,121 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Types; +using Redmine.Net.Api.Extensions; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class TimeEntryConverter : JavaScriptConverter + { + #region Overrides of JavaScriptConverter + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// + /// An instance of property data stored + /// as name/value pairs. + /// + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var timeEntry = new TimeEntry(); + + timeEntry.Id = dictionary.GetValue(RedmineKeys.ID); + timeEntry.Activity = + dictionary.GetValueAsIdentifiableName(dictionary.ContainsKey(RedmineKeys.ACTIVITY) + ? RedmineKeys.ACTIVITY + : RedmineKeys.ACTIVITY_ID); + timeEntry.Comments = dictionary.GetValue(RedmineKeys.COMMENTS); + timeEntry.Hours = dictionary.GetValue(RedmineKeys.HOURS); + timeEntry.Issue = + dictionary.GetValueAsIdentifiableName(dictionary.ContainsKey(RedmineKeys.ISSUE) + ? RedmineKeys.ISSUE + : RedmineKeys.ISSUE_ID); + timeEntry.Project = + dictionary.GetValueAsIdentifiableName(dictionary.ContainsKey(RedmineKeys.PROJECT) + ? RedmineKeys.PROJECT + : RedmineKeys.PROJECT_ID); + timeEntry.SpentOn = dictionary.GetValue(RedmineKeys.SPENT_ON); + timeEntry.User = dictionary.GetValueAsIdentifiableName(RedmineKeys.USER); + timeEntry.CustomFields = dictionary.GetValueAsCollection(RedmineKeys.CUSTOM_FIELDS); + timeEntry.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); + timeEntry.UpdatedOn = dictionary.GetValue(RedmineKeys.UPDATED_ON); + + return timeEntry; + } + + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object�s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + var entity = obj as TimeEntry; + var result = new Dictionary(); + + if (entity != null) + { + result.WriteIdIfNotNull(entity.Issue, RedmineKeys.ISSUE_ID); + result.WriteIdIfNotNull(entity.Project, RedmineKeys.PROJECT_ID); + result.WriteIdIfNotNull(entity.Activity, RedmineKeys.ACTIVITY_ID); + + if (!entity.SpentOn.HasValue) entity.SpentOn = DateTime.Now; + + result.WriteDateOrEmpty(entity.SpentOn, RedmineKeys.SPENT_ON); + result.Add(RedmineKeys.HOURS, entity.Hours); + result.Add(RedmineKeys.COMMENTS, entity.Comments); + result.WriteArray(RedmineKeys.CUSTOM_FIELDS, entity.CustomFields, new IssueCustomFieldConverter(), + serializer); + + var root = new Dictionary(); + root[RedmineKeys.TIME_ENTRY] = result; + return root; + } + + return result; + } + + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(TimeEntry)}); } + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/TrackerConverter.cs b/src/redmine-net20-api/JSonConverters/TrackerConverter.cs new file mode 100755 index 00000000..a402c1a8 --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/TrackerConverter.cs @@ -0,0 +1,81 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class TrackerConverter : JavaScriptConverter + { + #region Overrides of JavaScriptConverter + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// + /// An instance of property data stored + /// as name/value pairs. + /// + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var tracker = new Tracker + { + Id = dictionary.GetValue(RedmineKeys.ID), + Name = dictionary.GetValue(RedmineKeys.NAME) + }; + return tracker; + } + + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object�s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + return null; + } + + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(Tracker)}); } + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/TrackerCustomFieldConverter.cs b/src/redmine-net20-api/JSonConverters/TrackerCustomFieldConverter.cs new file mode 100755 index 00000000..51b699b0 --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/TrackerCustomFieldConverter.cs @@ -0,0 +1,68 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class TrackerCustomFieldConverter : IdentifiableNameConverter + { + #region Overrides of JavaScriptConverter + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// + /// An instance of property data stored + /// as name/value pairs. + /// + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var entity = new TrackerCustomField(); + + entity.Id = dictionary.GetValue(RedmineKeys.ID); + entity.Name = dictionary.GetValue(RedmineKeys.NAME); + + return entity; + } + + return null; + } + + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(TrackerCustomField)}); } + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/UploadConverter.cs b/src/redmine-net20-api/JSonConverters/UploadConverter.cs new file mode 100755 index 00000000..1400509c --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/UploadConverter.cs @@ -0,0 +1,93 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class UploadConverter : JavaScriptConverter + { + #region Overrides of JavaScriptConverter + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// + /// An instance of property data stored + /// as name/value pairs. + /// + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var upload = new Upload(); + + upload.ContentType = dictionary.GetValue(RedmineKeys.CONTENT_TYPE); + upload.FileName = dictionary.GetValue(RedmineKeys.FILENAME); + upload.Token = dictionary.GetValue(RedmineKeys.TOKEN); + upload.Description = dictionary.GetValue(RedmineKeys.DESCRIPTION); + return upload; + } + + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object�s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + var entity = obj as Upload; + var result = new Dictionary(); + + if (entity != null) + { + result.Add(RedmineKeys.CONTENT_TYPE, entity.ContentType); + result.Add(RedmineKeys.FILENAME, entity.FileName); + result.Add(RedmineKeys.TOKEN, entity.Token); + result.Add(RedmineKeys.DESCRIPTION, entity.Description); + } + + return result; + } + + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(Upload)}); } + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/UserConverter.cs b/src/redmine-net20-api/JSonConverters/UserConverter.cs new file mode 100644 index 00000000..6a688835 --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/UserConverter.cs @@ -0,0 +1,127 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class UserConverter : JavaScriptConverter + { + #region Overrides of JavaScriptConverter + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// + /// An instance of property data stored + /// as name/value pairs. + /// + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var user = new User(); + user.Login = dictionary.GetValue(RedmineKeys.LOGIN); + user.Id = dictionary.GetValue(RedmineKeys.ID); + user.FirstName = dictionary.GetValue(RedmineKeys.FIRSTNAME); + user.LastName = dictionary.GetValue(RedmineKeys.LASTNAME); + user.Email = dictionary.GetValue(RedmineKeys.MAIL); + user.MailNotification = dictionary.GetValue(RedmineKeys.MAIL_NOTIFICATION); + user.AuthenticationModeId = dictionary.GetValue(RedmineKeys.AUTH_SOURCE_ID); + user.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); + user.LastLoginOn = dictionary.GetValue(RedmineKeys.LAST_LOGIN_ON); + user.ApiKey = dictionary.GetValue(RedmineKeys.API_KEY); + user.Status = dictionary.GetValue(RedmineKeys.STATUS); + user.MustChangePassword = dictionary.GetValue(RedmineKeys.MUST_CHANGE_PASSWD); + user.CustomFields = dictionary.GetValueAsCollection(RedmineKeys.CUSTOM_FIELDS); + user.Memberships = dictionary.GetValueAsCollection(RedmineKeys.MEMBERSHIPS); + user.Groups = dictionary.GetValueAsCollection(RedmineKeys.GROUPS); + + return user; + } + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object�s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + var entity = obj as User; + + var result = new Dictionary(); + + if (entity != null) + { + result.Add(RedmineKeys.LOGIN, entity.Login); + result.Add(RedmineKeys.FIRSTNAME, entity.FirstName); + result.Add(RedmineKeys.LASTNAME, entity.LastName); + result.Add(RedmineKeys.MAIL, entity.Email); + if(!string.IsNullOrWhiteSpace(entity.MailNotification)) + { + result.Add(RedmineKeys.MAIL_NOTIFICATION, entity.MailNotification); + } + + if(!string.IsNullOrWhiteSpace(entity.Password)) + { + result.Add(RedmineKeys.PASSWORD, entity.Password); + } + + result.Add(RedmineKeys.MUST_CHANGE_PASSWD, entity.MustChangePassword.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + result.Add(RedmineKeys.STATUS, ((int)entity.Status).ToString(CultureInfo.InvariantCulture)); + + if(entity.AuthenticationModeId.HasValue) + { + result.WriteValueOrEmpty(entity.AuthenticationModeId, RedmineKeys.AUTH_SOURCE_ID); + } + result.WriteArray(RedmineKeys.CUSTOM_FIELDS, entity.CustomFields, new IssueCustomFieldConverter(), + serializer); + + var root = new Dictionary(); + root[RedmineKeys.USER] = result; + return root; + } + return result; + } + + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(User)}); } + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/UserGroupConverter.cs b/src/redmine-net20-api/JSonConverters/UserGroupConverter.cs new file mode 100755 index 00000000..e295464a --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/UserGroupConverter.cs @@ -0,0 +1,68 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class UserGroupConverter : IdentifiableNameConverter + { + #region Overrides of JavaScriptConverter + + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(UserGroup)}); } + } + + #endregion + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// + /// An instance of property data stored + /// as name/value pairs. + /// + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var userGroup = new UserGroup(); + + userGroup.Id = dictionary.GetValue(RedmineKeys.ID); + userGroup.Name = dictionary.GetValue(RedmineKeys.NAME); + + return userGroup; + } + + return null; + } + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/VersionConverter.cs b/src/redmine-net20-api/JSonConverters/VersionConverter.cs new file mode 100755 index 00000000..8a081573 --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/VersionConverter.cs @@ -0,0 +1,107 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; +using Version = Redmine.Net.Api.Types.Version; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class VersionConverter : JavaScriptConverter + { + #region Overrides of JavaScriptConverter + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// + /// An instance of property data stored + /// as name/value pairs. + /// + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var version = new Version(); + + version.Id = dictionary.GetValue(RedmineKeys.ID); + version.Description = dictionary.GetValue(RedmineKeys.DESCRIPTION); + version.Name = dictionary.GetValue(RedmineKeys.NAME); + version.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); + version.UpdatedOn = dictionary.GetValue(RedmineKeys.UPDATED_ON); + version.DueDate = dictionary.GetValue(RedmineKeys.DUE_DATE); + version.Project = dictionary.GetValueAsIdentifiableName(RedmineKeys.PROJECT); + version.Sharing = dictionary.GetValue(RedmineKeys.SHARING); + version.Status = dictionary.GetValue(RedmineKeys.STATUS); + version.CustomFields = dictionary.GetValueAsCollection(RedmineKeys.CUSTOM_FIELDS); + + return version; + } + + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object�s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + var entity = obj as Version; + + var result = new Dictionary(); + + if (entity != null) + { + result.Add(RedmineKeys.NAME, entity.Name); + result.Add(RedmineKeys.STATUS, entity.Status.ToString().ToLowerInvariant()); + result.Add(RedmineKeys.SHARING, entity.Sharing.ToString().ToLowerInvariant()); + result.Add(RedmineKeys.DESCRIPTION, entity.Description); + + var root = new Dictionary(); + result.WriteDateOrEmpty(entity.DueDate, RedmineKeys.DUE_DATE); + root[RedmineKeys.VERSION] = result; + return root; + } + + return result; + } + + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(Version)}); } + } + + #endregion + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/WatcherConverter.cs b/src/redmine-net20-api/JSonConverters/WatcherConverter.cs new file mode 100755 index 00000000..6752be08 --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/WatcherConverter.cs @@ -0,0 +1,85 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class WatcherConverter : JavaScriptConverter + { + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] {typeof(Watcher)}); } + } + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// + /// An instance of property data stored + /// as name/value pairs. + /// + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var watcher = new Watcher(); + + watcher.Id = dictionary.GetValue(RedmineKeys.ID); + watcher.Name = dictionary.GetValue(RedmineKeys.NAME); + + return watcher; + } + + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object�s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + var entity = obj as Watcher; + var result = new Dictionary(); + + if (entity != null) + { + result.Add(RedmineKeys.ID, entity.Id); + } + + return result; + } + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/JSonConverters/WikiPageConverter.cs b/src/redmine-net20-api/JSonConverters/WikiPageConverter.cs new file mode 100755 index 00000000..d5111b6d --- /dev/null +++ b/src/redmine-net20-api/JSonConverters/WikiPageConverter.cs @@ -0,0 +1,99 @@ +/* + Copyright 2011 - 2019 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +#if !NET20 +using System; +using System.Collections.Generic; +using System.Web.Script.Serialization; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.JSonConverters +{ + internal class WikiPageConverter : JavaScriptConverter + { + /// + /// When overridden in a derived class, gets a collection of the supported types. + /// + public override IEnumerable SupportedTypes + { + get { return new List(new[] { typeof(WikiPage) }); } + } + + /// + /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. + /// + /// + /// An instance of property data stored + /// as name/value pairs. + /// + /// The type of the resulting object. + /// The instance. + /// + /// The deserialized object. + /// + public override object Deserialize(IDictionary dictionary, Type type, + JavaScriptSerializer serializer) + { + if (dictionary != null) + { + var tracker = new WikiPage(); + + tracker.Id = dictionary.GetValue(RedmineKeys.ID); + tracker.Author = dictionary.GetValueAsIdentifiableName(RedmineKeys.AUTHOR); + tracker.Comments = dictionary.GetValue(RedmineKeys.COMMENTS); + tracker.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); + tracker.Text = dictionary.GetValue(RedmineKeys.TEXT); + tracker.Title = dictionary.GetValue(RedmineKeys.TITLE); + tracker.UpdatedOn = dictionary.GetValue(RedmineKeys.UPDATED_ON); + tracker.Version = dictionary.GetValue(RedmineKeys.VERSION); + tracker.Attachments = dictionary.GetValueAsCollection(RedmineKeys.ATTACHMENTS); + + return tracker; + } + + return null; + } + + /// + /// When overridden in a derived class, builds a dictionary of name/value pairs. + /// + /// The object to serialize. + /// The object that is responsible for the serialization. + /// + /// An object that contains key/value pairs that represent the object�s data. + /// + public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) + { + var entity = obj as WikiPage; + var result = new Dictionary(); + + if (entity != null) + { + result.Add(RedmineKeys.TEXT, entity.Text); + result.Add(RedmineKeys.COMMENTS, entity.Comments); + result.WriteValueOrEmpty(entity.Version, RedmineKeys.VERSION); + result.WriteArray(RedmineKeys.UPLOADS, entity.Uploads, new UploadConverter(), serializer); + + var root = new Dictionary(); + root[RedmineKeys.WIKI_PAGE] = result; + return root; + } + + return result; + } + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net20-api/MimeFormat.cs b/src/redmine-net20-api/MimeFormat.cs index 6959b8a3..b99bf3c6 100755 --- a/src/redmine-net20-api/MimeFormat.cs +++ b/src/redmine-net20-api/MimeFormat.cs @@ -14,6 +14,7 @@ You may obtain a copy of the License at limitations under the License. */ + namespace Redmine.Net.Api { /// @@ -23,6 +24,10 @@ public enum MimeFormat { /// /// - Xml + Xml, + /// + /// The json + /// + Json } } \ No newline at end of file diff --git a/src/redmine-net20-api/RedmineManager.cs b/src/redmine-net20-api/RedmineManager.cs index 195490e8..bc393d61 100644 --- a/src/redmine-net20-api/RedmineManager.cs +++ b/src/redmine-net20-api/RedmineManager.cs @@ -26,7 +26,7 @@ limitations under the License. using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Logging; + using Redmine.Net.Api.Types; using Group = Redmine.Net.Api.Types.Group; using Version = Redmine.Net.Api.Types.Version; @@ -204,7 +204,7 @@ private set if (!Uri.TryCreate(host, UriKind.Absolute, out uriResult) || !(uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps)) { - host = "http://" + host; + host = $"http://{host}/"; } if (!Uri.TryCreate(host, UriKind.Absolute, out uriResult)) @@ -364,7 +364,7 @@ public List GetAllWikiPages(string projectId) { var url = UrlHelper.GetWikisUrl(this, projectId); var result = WebApiHelper.ExecuteDownloadList(this, url, "GetAllWikiPages"); - return result != null ? result.Objects : null; + return result?.Objects; } /// @@ -379,6 +379,12 @@ public void DeleteWikiPage(string projectId, string pageName) WebApiHelper.ExecuteUpload(this, url, HttpVerbs.DELETE, string.Empty, "DeleteWikiPage"); } + /// + /// + /// + /// + /// + /// public int Count(NameValueCollection parameters) where T : class, new() { int totalCount = 0, pageSize = 1, offset = 0; @@ -445,6 +451,12 @@ public void DeleteWikiPage(string projectId, string pageName) return WebApiHelper.ExecuteDownloadList(this, url, "GetObjectList", parameters); } + /// + /// + /// + /// + /// + /// public int Count(params string[] include) where T : class, new() { var parameters = new NameValueCollection(); @@ -799,17 +811,17 @@ public virtual RedmineWebClient CreateWebClient(NameValueCollection parameters, /// The sender. /// The cert. /// The chain. - /// The error. + /// The error. /// /// - public virtual bool RemoteCertValidate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors error) + public virtual bool RemoteCertValidate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors sslPolicyErrors) { - if (error == SslPolicyErrors.None) + if (sslPolicyErrors == SslPolicyErrors.None) { return true; } - Logger.Current.Error("X509Certificate [{0}] Policy Error: '{1}'", cert.Subject, error); + // Logger.Current.Error("X509Certificate [{0}] Policy Error: '{1}'", cert.Subject, error); return false; } diff --git a/src/redmine-net20-api/Types/Attachment.cs b/src/redmine-net20-api/Types/Attachment.cs index ebdf64d9..ac171889 100755 --- a/src/redmine-net20-api/Types/Attachment.cs +++ b/src/redmine-net20-api/Types/Attachment.cs @@ -34,7 +34,7 @@ public class Attachment : Identifiable, IXmlSerializable, IEquatable /// /// The name of the file. [XmlElement(RedmineKeys.FILENAME)] - public String FileName { get; set; } + public string FileName { get; set; } /// /// Gets or sets the size of the file. @@ -48,21 +48,21 @@ public class Attachment : Identifiable, IXmlSerializable, IEquatable /// /// The type of the content. [XmlElement(RedmineKeys.CONTENT_TYPE)] - public String ContentType { get; set; } + public string ContentType { get; set; } /// /// Gets or sets the description. /// /// The description. [XmlElement(RedmineKeys.DESCRIPTION)] - public String Description { get; set; } + public string Description { get; set; } /// /// Gets or sets the content URL. /// /// The content URL. [XmlElement(RedmineKeys.CONTENT_URL)] - public String ContentUrl { get; set; } + public string ContentUrl { get; set; } /// /// Gets or sets the author. diff --git a/src/redmine-net20-api/Types/CustomField.cs b/src/redmine-net20-api/Types/CustomField.cs index 192f582d..a4bc7a69 100755 --- a/src/redmine-net20-api/Types/CustomField.cs +++ b/src/redmine-net20-api/Types/CustomField.cs @@ -191,13 +191,13 @@ public bool Equals(CustomField other) && Multiple == other.Multiple && Searchable == other.Searchable && Visible == other.Visible - && CustomizedType.Equals(other.CustomizedType) - && DefaultValue.Equals(other.DefaultValue) - && FieldFormat.Equals(other.FieldFormat) + && CustomizedType.Equals(other.CustomizedType, StringComparison.OrdinalIgnoreCase) + && DefaultValue.Equals(other.DefaultValue, StringComparison.OrdinalIgnoreCase) + && FieldFormat.Equals(other.FieldFormat, StringComparison.OrdinalIgnoreCase) && MaxLength == other.MaxLength && MinLength == other.MinLength - && Name.Equals(other.Name) - && Regexp.Equals(other.Regexp) + && Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase) + && Regexp.Equals(other.Regexp, StringComparison.OrdinalIgnoreCase) && PossibleValues.Equals(other.PossibleValues) && Roles.Equals(other.Roles) && Trackers.Equals(other.Trackers); diff --git a/src/redmine-net20-api/Types/Detail.cs b/src/redmine-net20-api/Types/Detail.cs index fc946d62..23838086 100755 --- a/src/redmine-net20-api/Types/Detail.cs +++ b/src/redmine-net20-api/Types/Detail.cs @@ -114,10 +114,10 @@ public void WriteXml(XmlWriter writer) { } public bool Equals(Detail other) { if (other == null) return false; - return (Property != null ? Property.Equals(other.Property) : other.Property == null) - && (Name != null ? Name.Equals(other.Name) : other.Name == null) - && (OldValue != null ? OldValue.Equals(other.OldValue) : other.OldValue == null) - && (NewValue != null ? NewValue.Equals(other.NewValue) : other.NewValue == null); + return (Property?.Equals(other.Property, StringComparison.OrdinalIgnoreCase) ?? other.Property == null) + && (Name?.Equals(other.Name, StringComparison.OrdinalIgnoreCase) ?? other.Name == null) + && (OldValue?.Equals(other.OldValue, StringComparison.OrdinalIgnoreCase) ?? other.OldValue == null) + && (NewValue?.Equals(other.NewValue, StringComparison.OrdinalIgnoreCase) ?? other.NewValue == null); } /// diff --git a/src/redmine-net20-api/Types/Error.cs b/src/redmine-net20-api/Types/Error.cs index 4898ec42..b5576229 100755 --- a/src/redmine-net20-api/Types/Error.cs +++ b/src/redmine-net20-api/Types/Error.cs @@ -43,7 +43,7 @@ public bool Equals(Error other) { if (other == null) return false; - return Info.Equals(other.Info); + return Info.Equals(other.Info, StringComparison.OrdinalIgnoreCase); } /// diff --git a/src/redmine-net20-api/Types/Group.cs b/src/redmine-net20-api/Types/Group.cs index 9b53f74f..b71ce6d1 100755 --- a/src/redmine-net20-api/Types/Group.cs +++ b/src/redmine-net20-api/Types/Group.cs @@ -112,9 +112,9 @@ public bool Equals(Group other) if (other == null) return false; return Id == other.Id && Name == other.Name - && (Users != null ? Users.Equals(other.Users) : other.Users == null) - && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null) - && (Memberships != null ? Memberships.Equals(other.Memberships) : other.Memberships == null); + && (Users?.Equals(other.Users) ?? other.Users == null) + && (CustomFields?.Equals(other.CustomFields) ?? other.CustomFields == null) + && (Memberships?.Equals(other.Memberships) ?? other.Memberships == null); } /// @@ -164,7 +164,7 @@ public override string ToString() /// /// /// - public int GetGroupUserId(object gu) + public static int GetGroupUserId(object gu) { return ((GroupUser)gu).Id; } diff --git a/src/redmine-net20-api/Types/IdentifiableName.cs b/src/redmine-net20-api/Types/IdentifiableName.cs index 35808375..5f585dfe 100755 --- a/src/redmine-net20-api/Types/IdentifiableName.cs +++ b/src/redmine-net20-api/Types/IdentifiableName.cs @@ -54,7 +54,7 @@ private void Initialize(XmlReader reader) /// /// The name. [XmlAttribute(RedmineKeys.NAME)] - public String Name { get; set; } + public string Name { get; set; } /// /// @@ -68,7 +68,7 @@ private void Initialize(XmlReader reader) /// public virtual void ReadXml(XmlReader reader) { - Id = Convert.ToInt32(reader.GetAttribute(RedmineKeys.ID)); + Id = Convert.ToInt32(reader.GetAttribute(RedmineKeys.ID), CultureInfo.InvariantCulture); Name = reader.GetAttribute(RedmineKeys.NAME); reader.Read(); } @@ -88,7 +88,7 @@ public virtual void WriteXml(XmlWriter writer) /// public override string ToString() { - return string.Format("[IdentifiableName: Id={0}, Name={1}]", Id, Name); + return string.Format(CultureInfo.InvariantCulture,"[IdentifiableName: Id={0}, Name={1}]", Id.ToString(CultureInfo.InvariantCulture), Name); } /// diff --git a/src/redmine-net20-api/Types/Issue.cs b/src/redmine-net20-api/Types/Issue.cs index cd33bcf8..4f11d017 100644 --- a/src/redmine-net20-api/Types/Issue.cs +++ b/src/redmine-net20-api/Types/Issue.cs @@ -80,14 +80,14 @@ public class Issue : Identifiable, IXmlSerializable, IEquatable, I /// /// The subject. [XmlElement(RedmineKeys.SUBJECT)] - public String Subject { get; set; } + public string Subject { get; set; } /// /// Gets or sets the description. /// /// The description. [XmlElement(RedmineKeys.DESCRIPTION)] - public String Description { get; set; } + public string Description { get; set; } /// /// Gets or sets the start date. @@ -540,21 +540,21 @@ public bool Equals(Issue other) && DueDate == other.DueDate && DoneRatio == other.DoneRatio && EstimatedHours == other.EstimatedHours - && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null) + && (CustomFields?.Equals(other.CustomFields) ?? other.CustomFields == null) && CreatedOn == other.CreatedOn && UpdatedOn == other.UpdatedOn && AssignedTo == other.AssignedTo && FixedVersion == other.FixedVersion && Notes == other.Notes - && (Watchers != null ? Watchers.Equals(other.Watchers) : other.Watchers == null) + && (Watchers?.Equals(other.Watchers) ?? other.Watchers == null) && ClosedOn == other.ClosedOn && SpentHours == other.SpentHours && PrivateNotes == other.PrivateNotes - && (Attachments != null ? Attachments.Equals(other.Attachments) : other.Attachments == null) - && (Changesets!= null ? Changesets.Equals(other.Changesets) : other.Changesets == null) - && (Children != null ? Children.Equals(other.Children) : other.Children == null) - && (Journals != null ? Journals.Equals(other.Journals) : other.Journals == null) - && (Relations != null ? Relations.Equals(other.Relations) : other.Relations == null) + && (Attachments?.Equals(other.Attachments) ?? other.Attachments == null) + && (Changesets?.Equals(other.Changesets) ?? other.Changesets == null) + && (Children?.Equals(other.Children) ?? other.Children == null) + && (Journals?.Equals(other.Journals) ?? other.Journals == null) + && (Relations?.Equals(other.Relations) ?? other.Relations == null) ); } diff --git a/src/redmine-net20-api/Types/IssueChild.cs b/src/redmine-net20-api/Types/IssueChild.cs index e85bc39b..392c5834 100755 --- a/src/redmine-net20-api/Types/IssueChild.cs +++ b/src/redmine-net20-api/Types/IssueChild.cs @@ -40,7 +40,7 @@ public class IssueChild : Identifiable, IXmlSerializable, IEquatable /// /// The subject. [XmlElement(RedmineKeys.SUBJECT)] - public String Subject { get; set; } + public string Subject { get; set; } /// /// diff --git a/src/redmine-net20-api/Types/IssueCustomField.cs b/src/redmine-net20-api/Types/IssueCustomField.cs index 994cf6b6..c21650a0 100755 --- a/src/redmine-net20-api/Types/IssueCustomField.cs +++ b/src/redmine-net20-api/Types/IssueCustomField.cs @@ -50,7 +50,7 @@ public class IssueCustomField : IdentifiableName, IEquatable, /// public override void ReadXml(XmlReader reader) { - Id = Convert.ToInt32(reader.GetAttribute(RedmineKeys.ID)); + Id = Convert.ToInt32(reader.GetAttribute(RedmineKeys.ID), CultureInfo.InvariantCulture); Name = reader.GetAttribute(RedmineKeys.NAME); Multiple = reader.ReadAttributeAsBoolean(RedmineKeys.MULTIPLE); @@ -139,8 +139,9 @@ public override int GetHashCode() /// /// /// - public string GetValue(object item) + public static string GetValue(object item) { + if (item == null) throw new ArgumentNullException(nameof(item)); return ((CustomFieldValue)item).Info; } } diff --git a/src/redmine-net20-api/Types/IssuePriority.cs b/src/redmine-net20-api/Types/IssuePriority.cs index f6fc1227..68643b3e 100755 --- a/src/redmine-net20-api/Types/IssuePriority.cs +++ b/src/redmine-net20-api/Types/IssuePriority.cs @@ -38,9 +38,10 @@ public class IssuePriority : IdentifiableName, IEquatable /// /// Generates an object from its XML representation. /// - /// The stream from which the object is deserialized. + /// The stream from which the object is deserialized. public override void ReadXml(XmlReader reader) { + if (reader == null) throw new ArgumentNullException(nameof(reader)); reader.Read(); while (!reader.EOF) { diff --git a/src/redmine-net20-api/Types/IssueRelation.cs b/src/redmine-net20-api/Types/IssueRelation.cs index fd2f59dc..59d49430 100755 --- a/src/redmine-net20-api/Types/IssueRelation.cs +++ b/src/redmine-net20-api/Types/IssueRelation.cs @@ -15,6 +15,7 @@ limitations under the License. */ using System; +using System.Globalization; using System.Xml; using System.Xml.Schema; using System.Xml.Serialization; @@ -69,6 +70,7 @@ public class IssueRelation : Identifiable, IXmlSerializable, IEqu /// public void ReadXml(XmlReader reader) { + if (reader == null) throw new ArgumentNullException(nameof(reader)); if (!reader.IsEmptyElement) reader.Read(); while (!reader.EOF) { @@ -125,7 +127,8 @@ public void ReadXml(XmlReader reader) /// public void WriteXml(XmlWriter writer) { - writer.WriteElementString(RedmineKeys.ISSUE_TO_ID, IssueToId.ToString()); + if (writer == null) throw new ArgumentNullException(nameof(writer)); + writer.WriteElementString(RedmineKeys.ISSUE_TO_ID, IssueToId.ToString(CultureInfo.InvariantCulture)); writer.WriteElementString(RedmineKeys.RELATION_TYPE, Type.ToString()); if (Type == IssueRelationType.precedes || Type == IssueRelationType.follows) writer.WriteValueOrEmpty(Delay, RedmineKeys.DELAY); diff --git a/src/redmine-net20-api/Types/IssueStatus.cs b/src/redmine-net20-api/Types/IssueStatus.cs index 2c1bf821..e0b24cf9 100755 --- a/src/redmine-net20-api/Types/IssueStatus.cs +++ b/src/redmine-net20-api/Types/IssueStatus.cs @@ -49,6 +49,7 @@ public class IssueStatus : IdentifiableName, IEquatable /// public override void ReadXml(XmlReader reader) { + if (reader == null) throw new ArgumentNullException(nameof(reader)); reader.Read(); while (!reader.EOF) { diff --git a/src/redmine-net20-api/Types/Journal.cs b/src/redmine-net20-api/Types/Journal.cs index 77ffa439..c83c3bcd 100644 --- a/src/redmine-net20-api/Types/Journal.cs +++ b/src/redmine-net20-api/Types/Journal.cs @@ -85,6 +85,7 @@ public class Journal : Identifiable, IEquatable, IXmlSerializa /// public void ReadXml(XmlReader reader) { + if (reader == null) throw new ArgumentNullException(nameof(reader)); Id = reader.ReadAttributeAsInt(RedmineKeys.ID); reader.Read(); @@ -131,7 +132,7 @@ public bool Equals(Journal other) && User == other.User && Notes == other.Notes && CreatedOn == other.CreatedOn - && (Details != null ? Details.Equals(other.Details) : other.Details == null ); + && (Details?.Equals(other.Details) ?? other.Details == null); } /// diff --git a/src/redmine-net20-api/Types/Membership.cs b/src/redmine-net20-api/Types/Membership.cs index e4b7f846..85d247ed 100755 --- a/src/redmine-net20-api/Types/Membership.cs +++ b/src/redmine-net20-api/Types/Membership.cs @@ -57,6 +57,7 @@ public class Membership : Identifiable, IEquatable, IXml /// public void ReadXml(XmlReader reader) { + if (reader == null) throw new ArgumentNullException(nameof(reader)); reader.Read(); while (!reader.EOF) { @@ -95,7 +96,7 @@ public bool Equals(Membership other) if (other == null) return false; return (Id == other.Id && (Project != null ? Project.Equals(other.Project) : other.Project == null) && - (Roles != null ? Roles.Equals(other.Roles) : other.Roles == null)); + (Roles?.Equals(other.Roles) ?? other.Roles == null)); } /// diff --git a/src/redmine-net20-api/Types/MembershipRole.cs b/src/redmine-net20-api/Types/MembershipRole.cs index 8978c685..be1500e4 100755 --- a/src/redmine-net20-api/Types/MembershipRole.cs +++ b/src/redmine-net20-api/Types/MembershipRole.cs @@ -15,6 +15,7 @@ limitations under the License. */ using System; +using System.Globalization; using System.Xml; using System.Xml.Serialization; using Redmine.Net.Api.Extensions; @@ -43,7 +44,8 @@ public class MembershipRole : IdentifiableName, IEquatable /// The reader. public override void ReadXml(XmlReader reader) { - Id = Convert.ToInt32(reader.GetAttribute(RedmineKeys.ID)); + if (reader == null) throw new ArgumentNullException(nameof(reader)); + Id = Convert.ToInt32(reader.GetAttribute(RedmineKeys.ID), CultureInfo.InvariantCulture); Name = reader.GetAttribute(RedmineKeys.NAME); Inherited = reader.ReadAttributeAsBoolean(RedmineKeys.INHERITED); reader.Read(); @@ -55,6 +57,7 @@ public override void ReadXml(XmlReader reader) /// public override void WriteXml(XmlWriter writer) { + if (writer == null) throw new ArgumentNullException(nameof(writer)); writer.WriteValue(Id); } diff --git a/src/redmine-net20-api/Types/News.cs b/src/redmine-net20-api/Types/News.cs index ccf885ab..f5921e75 100755 --- a/src/redmine-net20-api/Types/News.cs +++ b/src/redmine-net20-api/Types/News.cs @@ -48,21 +48,21 @@ public class News : Identifiable, IEquatable, IXmlSerializable /// /// The title. [XmlElement(RedmineKeys.TITLE)] - public String Title { get; set; } + public string Title { get; set; } /// /// Gets or sets the summary. /// /// The summary. [XmlElement(RedmineKeys.SUMMARY)] - public String Summary { get; set; } + public string Summary { get; set; } /// /// Gets or sets the description. /// /// The description. [XmlElement(RedmineKeys.DESCRIPTION)] - public String Description { get; set; } + public string Description { get; set; } /// /// Gets or sets the created on. @@ -83,6 +83,7 @@ public class News : Identifiable, IEquatable, IXmlSerializable /// public void ReadXml(XmlReader reader) { + if (reader == null) throw new ArgumentNullException(nameof(reader)); reader.Read(); while (!reader.EOF) { diff --git a/src/redmine-net20-api/Types/Permission.cs b/src/redmine-net20-api/Types/Permission.cs index 01148283..dcd3e137 100755 --- a/src/redmine-net20-api/Types/Permission.cs +++ b/src/redmine-net20-api/Types/Permission.cs @@ -39,7 +39,7 @@ public class Permission : IEquatable /// public bool Equals(Permission other) { - return Info == other.Info; + return other != null && Info == other.Info; } /// diff --git a/src/redmine-net20-api/Types/Project.cs b/src/redmine-net20-api/Types/Project.cs index ab9481b3..3505f376 100644 --- a/src/redmine-net20-api/Types/Project.cs +++ b/src/redmine-net20-api/Types/Project.cs @@ -16,6 +16,7 @@ limitations under the License. using System; using System.Collections.Generic; +using System.Globalization; using System.Xml; using System.Xml.Serialization; using Redmine.Net.Api.Extensions; @@ -34,14 +35,14 @@ public class Project : IdentifiableName, IEquatable /// /// The identifier. [XmlElement(RedmineKeys.IDENTIFIER)] - public String Identifier { get; set; } + public string Identifier { get; set; } /// /// Gets or sets the description. /// /// The description. [XmlElement(RedmineKeys.DESCRIPTION)] - public String Description { get; set; } + public string Description { get; set; } /// /// Gets or sets the parent. @@ -55,7 +56,7 @@ public class Project : IdentifiableName, IEquatable /// /// The home page. [XmlElement(RedmineKeys.HOMEPAGE)] - public String HomePage { get; set; } + public string HomePage { get; set; } /// /// Gets or sets the created on. @@ -152,6 +153,7 @@ public class Project : IdentifiableName, IEquatable /// The stream from which the object is deserialized. public override void ReadXml(XmlReader reader) { + if (reader == null) throw new ArgumentNullException(nameof(reader)); reader.Read(); while (!reader.EOF) { @@ -205,11 +207,12 @@ public override void ReadXml(XmlReader reader) /// public override void WriteXml(XmlWriter writer) { + if (writer == null) throw new ArgumentNullException(nameof(writer)); writer.WriteElementString(RedmineKeys.NAME, Name); writer.WriteElementString(RedmineKeys.IDENTIFIER, Identifier); writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); //writer.WriteElementString(RedmineKeys.INHERIT_MEMBERS, InheritMembers.ToString().ToLowerInvariant()); - writer.WriteElementString(RedmineKeys.IS_PUBLIC, IsPublic.ToString().ToLowerInvariant()); + writer.WriteElementString(RedmineKeys.IS_PUBLIC, IsPublic.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); writer.WriteIdOrEmpty(Parent, RedmineKeys.PARENT_ID); writer.WriteElementString(RedmineKeys.HOMEPAGE, HomePage); @@ -243,19 +246,19 @@ public bool Equals(Project other) if (other == null) return false; return ( Id == other.Id - && Identifier.Equals(other.Identifier) - && Description.Equals(other.Description) + && Identifier.Equals(other.Identifier, StringComparison.OrdinalIgnoreCase) + && Description.Equals(other.Description, StringComparison.OrdinalIgnoreCase) && (Parent != null ? Parent.Equals(other.Parent) : other.Parent == null) - && (HomePage != null ? HomePage.Equals(other.HomePage) : other.HomePage == null) + && (HomePage?.Equals(other.HomePage, StringComparison.OrdinalIgnoreCase) ?? other.HomePage == null) && CreatedOn == other.CreatedOn && UpdatedOn == other.UpdatedOn && Status == other.Status && IsPublic == other.IsPublic && InheritMembers == other.InheritMembers - && (Trackers != null ? Trackers.Equals(other.Trackers) : other.Trackers == null) - && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null) - && (IssueCategories != null ? IssueCategories.Equals(other.IssueCategories) : other.IssueCategories == null) - && (EnabledModules != null ? EnabledModules.Equals(other.EnabledModules) : other.EnabledModules == null) + && (Trackers?.Equals(other.Trackers) ?? other.Trackers == null) + && (CustomFields?.Equals(other.CustomFields) ?? other.CustomFields == null) + && (IssueCategories?.Equals(other.IssueCategories) ?? other.IssueCategories == null) + && (EnabledModules?.Equals(other.EnabledModules) ?? other.EnabledModules == null) ); } diff --git a/src/redmine-net20-api/Types/ProjectMembership.cs b/src/redmine-net20-api/Types/ProjectMembership.cs index 962d4d77..3dc0e494 100755 --- a/src/redmine-net20-api/Types/ProjectMembership.cs +++ b/src/redmine-net20-api/Types/ProjectMembership.cs @@ -94,6 +94,7 @@ public bool Equals(ProjectMembership other) /// public void ReadXml(XmlReader reader) { + if (reader == null) throw new ArgumentNullException(nameof(reader)); reader.Read(); while (!reader.EOF) { diff --git a/src/redmine-net20-api/Types/ProjectTracker.cs b/src/redmine-net20-api/Types/ProjectTracker.cs index 5aa09b55..518a419c 100755 --- a/src/redmine-net20-api/Types/ProjectTracker.cs +++ b/src/redmine-net20-api/Types/ProjectTracker.cs @@ -14,6 +14,7 @@ You may obtain a copy of the License at limitations under the License. */ +using System.Globalization; using System.Xml.Serialization; namespace Redmine.Net.Api.Types @@ -27,7 +28,7 @@ public class ProjectTracker : IdentifiableName, IValue /// /// /// - public string Value{get{return Id.ToString ();}} + public string Value{get{return Id.ToString (CultureInfo.InvariantCulture);}} /// /// diff --git a/src/redmine-net20-api/Types/Role.cs b/src/redmine-net20-api/Types/Role.cs index a2cdaaac..774c03ea 100755 --- a/src/redmine-net20-api/Types/Role.cs +++ b/src/redmine-net20-api/Types/Role.cs @@ -45,6 +45,7 @@ public class Role : IdentifiableName, IEquatable /// public override void ReadXml(XmlReader reader) { + if (reader == null) throw new ArgumentNullException(nameof(reader)); reader.Read(); while (!reader.EOF) { diff --git a/src/redmine-net20-api/Types/TimeEntry.cs b/src/redmine-net20-api/Types/TimeEntry.cs index 4caa32f9..d5de2b17 100644 --- a/src/redmine-net20-api/Types/TimeEntry.cs +++ b/src/redmine-net20-api/Types/TimeEntry.cs @@ -82,20 +82,10 @@ public class TimeEntry : Identifiable, ICloneable, IEquatable /// The comments. [XmlAttribute(RedmineKeys.COMMENTS)] - public String Comments + public string Comments { get { return comments; } - set - { - if (!string.IsNullOrEmpty(value)) - { - if (value.Length > 255) - { - value = value.Substring(0, 255); - } - } - comments = value; - } + set { comments = value.Truncate(255); } } /// @@ -224,7 +214,7 @@ public bool Equals(TimeEntry other) && User == other.User && CreatedOn == other.CreatedOn && UpdatedOn == other.UpdatedOn - && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null)); + && (CustomFields?.Equals(other.CustomFields) ?? other.CustomFields == null)); } /// diff --git a/src/redmine-net20-api/Types/TimeEntryActivity.cs b/src/redmine-net20-api/Types/TimeEntryActivity.cs index a7a01886..36f8d338 100755 --- a/src/redmine-net20-api/Types/TimeEntryActivity.cs +++ b/src/redmine-net20-api/Types/TimeEntryActivity.cs @@ -38,7 +38,7 @@ public class TimeEntryActivity : IdentifiableName, IEquatable /// /// Generates an object from its XML representation. /// - /// The stream from which the object is deserialized. + /// The stream from which the object is deserialized. public override void ReadXml(XmlReader reader) { reader.Read(); diff --git a/src/redmine-net20-api/Types/Tracker.cs b/src/redmine-net20-api/Types/Tracker.cs index fa9c579c..de431525 100755 --- a/src/redmine-net20-api/Types/Tracker.cs +++ b/src/redmine-net20-api/Types/Tracker.cs @@ -35,7 +35,7 @@ public override void WriteXml(XmlWriter writer) { } /// /// Generates an object from its XML representation. /// - /// The stream from which the object is deserialized. + /// The stream from which the object is deserialized. public override void ReadXml(XmlReader reader) { reader.Read(); diff --git a/src/redmine-net20-api/Types/Upload.cs b/src/redmine-net20-api/Types/Upload.cs index e8fc55ae..f7476ecc 100755 --- a/src/redmine-net20-api/Types/Upload.cs +++ b/src/redmine-net20-api/Types/Upload.cs @@ -72,10 +72,10 @@ public class Upload : IEquatable public bool Equals(Upload other) { return other != null - && Token.Equals(other.Token) - && FileName.Equals(other.FileName) - && Description.Equals(other.Description) - && ContentType.Equals(other.ContentType); + && Token.Equals(other.Token, StringComparison.OrdinalIgnoreCase) + && FileName.Equals(other.FileName, StringComparison.OrdinalIgnoreCase) + && Description.Equals(other.Description, StringComparison.OrdinalIgnoreCase) + && ContentType.Equals(other.ContentType, StringComparison.OrdinalIgnoreCase); } /// diff --git a/src/redmine-net20-api/Types/User.cs b/src/redmine-net20-api/Types/User.cs index 214b70e0..c9d89852 100644 --- a/src/redmine-net20-api/Types/User.cs +++ b/src/redmine-net20-api/Types/User.cs @@ -36,7 +36,7 @@ public class User : Identifiable, IXmlSerializable, IEquatable /// /// The login. [XmlElement(RedmineKeys.LOGIN)] - public String Login { get; set; } + public string Login { get; set; } /// /// Gets or sets the user password. @@ -50,21 +50,21 @@ public class User : Identifiable, IXmlSerializable, IEquatable /// /// The first name. [XmlElement(RedmineKeys.FIRSTNAME)] - public String FirstName { get; set; } + public string FirstName { get; set; } /// /// Gets or sets the last name. /// /// The last name. [XmlElement(RedmineKeys.LASTNAME)] - public String LastName { get; set; } + public string LastName { get; set; } /// /// Gets or sets the email. /// /// The email. [XmlElement(RedmineKeys.MAIL)] - public String Email { get; set; } + public string Email { get; set; } /// /// Gets or sets the authentication mode id. @@ -73,7 +73,7 @@ public class User : Identifiable, IXmlSerializable, IEquatable /// The authentication mode id. /// [XmlElement(RedmineKeys.AUTH_SOURCE_ID, IsNullable = true)] - public Int32? AuthenticationModeId { get; set; } + public int? AuthenticationModeId { get; set; } /// /// Gets or sets the created on. @@ -230,7 +230,7 @@ public void WriteXml(XmlWriter writer) writer.WriteValueOrEmpty(AuthenticationModeId, RedmineKeys.AUTH_SOURCE_ID); } - writer.WriteElementString(RedmineKeys.MUST_CHANGE_PASSWD, MustChangePassword.ToString().ToLowerInvariant()); + writer.WriteElementString(RedmineKeys.MUST_CHANGE_PASSWD, MustChangePassword.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); writer.WriteElementString(RedmineKeys.STATUS, ((int)Status).ToString(CultureInfo.InvariantCulture)); if(CustomFields != null) { @@ -248,21 +248,21 @@ public bool Equals(User other) if (other == null) return false; return ( Id == other.Id - && Login.Equals(other.Login) + && Login.Equals(other.Login, StringComparison.OrdinalIgnoreCase) //&& Password.Equals(other.Password) - && FirstName.Equals(other.FirstName) - && LastName.Equals(other.LastName) - && Email.Equals(other.Email) - && MailNotification.Equals(other.MailNotification) - && (ApiKey != null ? ApiKey.Equals(other.ApiKey) : other.ApiKey == null) + && FirstName.Equals(other.FirstName, StringComparison.OrdinalIgnoreCase) + && LastName.Equals(other.LastName, StringComparison.OrdinalIgnoreCase) + && Email.Equals(other.Email, StringComparison.OrdinalIgnoreCase) + && MailNotification.Equals(other.MailNotification, StringComparison.OrdinalIgnoreCase) + && (ApiKey?.Equals(other.ApiKey, StringComparison.OrdinalIgnoreCase) ?? other.ApiKey == null) && AuthenticationModeId == other.AuthenticationModeId && CreatedOn == other.CreatedOn && LastLoginOn == other.LastLoginOn && Status == other.Status && MustChangePassword == other.MustChangePassword - && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null) - && (Memberships != null ? Memberships.Equals(other.Memberships): other.Memberships == null) - && (Groups != null ? Groups.Equals(other.Groups) : other.Groups == null) + && (CustomFields?.Equals(other.CustomFields) ?? other.CustomFields == null) + && (Memberships?.Equals(other.Memberships) ?? other.Memberships == null) + && (Groups?.Equals(other.Groups) ?? other.Groups == null) ); } diff --git a/src/redmine-net20-api/Types/Version.cs b/src/redmine-net20-api/Types/Version.cs index 287823d2..e239f53c 100755 --- a/src/redmine-net20-api/Types/Version.cs +++ b/src/redmine-net20-api/Types/Version.cs @@ -41,7 +41,7 @@ public class Version : IdentifiableName, IEquatable /// /// The description. [XmlElement(RedmineKeys.DESCRIPTION)] - public String Description { get; set; } + public string Description { get; set; } /// /// Gets or sets the status. @@ -158,7 +158,7 @@ public bool Equals(Version other) && Sharing == other.Sharing && CreatedOn == other.CreatedOn && UpdatedOn == other.UpdatedOn - && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null)); + && (CustomFields?.Equals(other.CustomFields) ?? other.CustomFields == null)); } /// diff --git a/src/redmine-net20-api/Types/Watcher.cs b/src/redmine-net20-api/Types/Watcher.cs index 3afdacd5..5eb461bf 100755 --- a/src/redmine-net20-api/Types/Watcher.cs +++ b/src/redmine-net20-api/Types/Watcher.cs @@ -15,6 +15,7 @@ limitations under the License. */ using System; +using System.Globalization; using System.Xml.Serialization; namespace Redmine.Net.Api.Types @@ -33,7 +34,7 @@ public string Value { get { - return Id.ToString(); + return Id.ToString(CultureInfo.InvariantCulture); } } diff --git a/src/redmine-net20-api/redmine-net20-api.csproj b/src/redmine-net20-api/redmine-net20-api.csproj index 2c490e79..762bbb62 100644 --- a/src/redmine-net20-api/redmine-net20-api.csproj +++ b/src/redmine-net20-api/redmine-net20-api.csproj @@ -1,163 +1,188 @@  - - - - Debug - AnyCPU - {0E6B9B72-445D-4E71-8D29-48C4A009AB03} - Library - Properties + + + + net48 + net20;net40;net45;net451;net452;net46;net461;net462;net47;net471;net472;net48; + + false + Redmine.Net.Api - redmine-net20-api - v2.0 - 512 - + redmine-net-api + + true + + TRACE + + x64 or x86 + + Debug;Release + + PackageReference + + AnyCPU;x64 + + + + + + NET20;NETFULL + + + + NET40;NETFULL + + + + NET45;NETFULL + + + + NET451;NETFULL + + + + NET452;NETFULL + + + + NET46;NETFULL + + + + NET461;NETFULL + + + + NET462;NETFULL + + + + + NET47;NETFULL - + + + NET471;NETFULL + + + + NET472;NETFULL + + + + NET48;NETFULL + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + full true + + + + full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - bin\Debug\redmine-net20-api.XML - - - none - true - bin\Release\ - TRACE - prompt - 4 - bin\Release\redmine-net20-api.XML + true - + + + ..\bin\Release\ + pdbonly true - bin\DebugXML\ - DEBUG;TRACE - full - AnyCPU - prompt - MinimumRecommendedRules.ruleset - 4 - false - + + + ..\bin\Release\ + pdbonly true - bin\DebugJSON\ - DEBUG;TRACE - full - AnyCPU - prompt - MinimumRecommendedRules.ruleset - 4 - false - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Component - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + <_Parameter1>$(MSBuildProjectName).Tests + - - - - + \ No newline at end of file diff --git a/src/redmine-net40-api-signed/redmine-net40-api-signed.csproj b/src/redmine-net40-api-signed/redmine-net40-api-signed.csproj index 9d7fe5f8..7726d305 100644 --- a/src/redmine-net40-api-signed/redmine-net40-api-signed.csproj +++ b/src/redmine-net40-api-signed/redmine-net40-api-signed.csproj @@ -281,8 +281,8 @@ Types\WikiPage.cs - - Async\RedmineManagerAsync.cs + + Async\RedmineManagerAsync40.cs Extensions\CollectionExtensions.cs diff --git a/src/redmine-net40-api/Extensions/CollectionExtensions.cs b/src/redmine-net40-api/Extensions/CollectionExtensions.cs deleted file mode 100755 index 78ff07ff..00000000 --- a/src/redmine-net40-api/Extensions/CollectionExtensions.cs +++ /dev/null @@ -1,62 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; - -namespace Redmine.Net.Api.Extensions -{ - /// - /// - /// - - - public static class CollectionExtensions - { - /// - /// Clones the specified list to clone. - /// - /// - /// The list to clone. - /// - public static IList Clone(this IList listToClone) where T : ICloneable - { - if (listToClone == null) return null; - IList clonedList = new List(); - foreach (var item in listToClone) - clonedList.Add((T) item.Clone()); - return clonedList; - } - - - /// - /// Equalses the specified list to compare. - /// - /// - /// The list. - /// The list to compare. - /// - public static bool Equals(this IList list, IList listToCompare) where T : class - { - if (listToCompare == null) return false; - - var set = new HashSet(list); - var setToCompare = new HashSet(listToCompare); - - return set.SetEquals(setToCompare); - } - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/Extensions/WebExtensions.cs b/src/redmine-net40-api/Extensions/WebExtensions.cs deleted file mode 100755 index 10f38336..00000000 --- a/src/redmine-net40-api/Extensions/WebExtensions.cs +++ /dev/null @@ -1,121 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Exceptions; - -namespace Redmine.Net.Api.Extensions -{ - /// - /// - /// - public static class WebExtensions - { - /// - /// Handles the web exception. - /// - /// The exception. - /// The method. - /// The MIME format. - /// Timeout! - /// Bad domain name! - /// - /// - /// - /// - /// The page that you are trying to update is staled! - /// - /// - /// - public static void HandleWebException(this WebException exception, string method, MimeFormat mimeFormat) - { - if (exception == null) return; - - switch (exception.Status) - { - case WebExceptionStatus.Timeout: throw new RedmineTimeoutException("Timeout!", exception); - case WebExceptionStatus.NameResolutionFailure: throw new NameResolutionFailureException("Bad domain name!", exception); - case WebExceptionStatus.ProtocolError: - { - var response = (HttpWebResponse)exception.Response; - switch ((int)response.StatusCode) - { - - case (int)HttpStatusCode.NotFound: - throw new NotFoundException (response.StatusDescription, exception); - - case (int)HttpStatusCode.InternalServerError: - throw new InternalServerErrorException(response.StatusDescription, exception); - - case (int)HttpStatusCode.Unauthorized: - throw new UnauthorizedException(response.StatusDescription, exception); - - case (int)HttpStatusCode.Forbidden: - throw new ForbiddenException(response.StatusDescription, exception); - - case (int)HttpStatusCode.Conflict: - throw new ConflictException("The page that you are trying to update is staled!", exception); - - case 422: - - var errors = GetRedmineExceptions(exception.Response, mimeFormat); - string message = string.Empty; - if (errors != null) - { - message = errors.Aggregate(message, (current, error) => current + (error.Info + "\n")); - } - throw new RedmineException(method + " has invalid or missing attribute parameters: " + message, exception); - - case (int)HttpStatusCode.NotAcceptable: throw new NotAcceptableException(response.StatusDescription, exception); - } - } - break; - - default: throw new RedmineException(exception.Message, exception); - } - } - - /// - /// Gets the redmine exceptions. - /// - /// The web response. - /// The MIME format. - /// - private static List GetRedmineExceptions(this WebResponse webResponse, MimeFormat mimeFormat) - { - using (var dataStream = webResponse.GetResponseStream()) - { - if (dataStream == null) return null; - using (var reader = new StreamReader(dataStream)) - { - var responseFromServer = reader.ReadToEnd(); - - if (responseFromServer.Trim().Length > 0) - { - var errors = RedmineSerializer.DeserializeList(responseFromServer, mimeFormat); - return errors.Objects; - } - } - return null; - } - } - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/Extensions/XmlReaderExtensions.cs b/src/redmine-net40-api/Extensions/XmlReaderExtensions.cs deleted file mode 100755 index 2a572410..00000000 --- a/src/redmine-net40-api/Extensions/XmlReaderExtensions.cs +++ /dev/null @@ -1,219 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Xml; -using System.Xml.Serialization; - -namespace Redmine.Net.Api.Extensions -{ - /// - /// - public static class XmlReaderExtensions - { - /// - /// Reads the attribute as int. - /// - /// The reader. - /// Name of the attribute. - /// - public static int ReadAttributeAsInt(this XmlReader reader, string attributeName) - { - var attribute = reader.GetAttribute(attributeName); - int result; - if (string.IsNullOrWhiteSpace(attribute) || - !int.TryParse(attribute, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out result)) - return default(int); - return result; - } - - /// - /// Reads the attribute as nullable int. - /// - /// The reader. - /// Name of the attribute. - /// - public static int? ReadAttributeAsNullableInt(this XmlReader reader, string attributeName) - { - var attribute = reader.GetAttribute(attributeName); - int result; - if (string.IsNullOrWhiteSpace(attribute) || - !int.TryParse(attribute, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out result)) return null; - return result; - } - - /// - /// Reads the attribute as boolean. - /// - /// The reader. - /// Name of the attribute. - /// - public static bool ReadAttributeAsBoolean(this XmlReader reader, string attributeName) - { - var attribute = reader.GetAttribute(attributeName); - bool result; - if (string.IsNullOrWhiteSpace(attribute) || !bool.TryParse(attribute, out result)) return false; - - return result; - } - - /// - /// Reads the element content as nullable date time. - /// - /// The reader. - /// - public static DateTime? ReadElementContentAsNullableDateTime(this XmlReader reader) - { - var str = reader.ReadElementContentAsString(); - - // Format for journals, attachments etc. - var format = "yyyy'-'MM'-'dd HH':'mm':'ss UTC"; - - DateTime result; - if (string.IsNullOrWhiteSpace(str) || !DateTime.TryParse(str, out result)) - { - if (!DateTime.TryParseExact(str, format, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) - return null; - } - return result; - } - - /// - /// Reads the element content as nullable float. - /// - /// The reader. - /// - public static float? ReadElementContentAsNullableFloat(this XmlReader reader) - { - var str = reader.ReadElementContentAsString(); - - float result; - if (string.IsNullOrWhiteSpace(str) || - !float.TryParse(str, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out result)) return null; - - return result; - } - - /// - /// Reads the element content as nullable int. - /// - /// The reader. - /// - public static int? ReadElementContentAsNullableInt(this XmlReader reader) - { - var str = reader.ReadElementContentAsString(); - - int result; - if (string.IsNullOrWhiteSpace(str) || - !int.TryParse(str, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out result)) return null; - - return result; - } - - /// - /// Reads the element content as nullable decimal. - /// - /// The reader. - /// - public static decimal? ReadElementContentAsNullableDecimal(this XmlReader reader) - { - var str = reader.ReadElementContentAsString(); - - decimal result; - if (string.IsNullOrWhiteSpace(str) || - !decimal.TryParse(str, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out result)) return null; - - return result; - } - - /// - /// Reads the element content as collection. - /// - /// - /// The reader. - /// - public static List ReadElementContentAsCollection(this XmlReader reader) where T : class - { - var result = new List(); - var serializer = new XmlSerializer(typeof(T)); - var xml = reader.ReadOuterXml(); - using (var sr = new StringReader(xml)) - { - var r = new XmlTextReader(sr); - r.ReadStartElement(); - while (!r.EOF) - { - if (r.NodeType == XmlNodeType.EndElement) - { - r.ReadEndElement(); - continue; - } - - T temp; - - if (r.IsEmptyElement && r.HasAttributes) - { - temp = serializer.Deserialize(r) as T; - } - else - { - var subTree = r.ReadSubtree(); - temp = serializer.Deserialize(subTree) as T; - } - if (temp != null) result.Add(temp); - if (!r.IsEmptyElement) r.Read(); - } - } - return result; - } - - /// - /// Reads the element content as collection. - /// - /// The reader. - /// The type. - /// - public static ArrayList ReadElementContentAsCollection(this XmlReader reader, Type type) - { - var result = new ArrayList(); - var serializer = new XmlSerializer(type); - var xml = reader.ReadOuterXml(); - using (var sr = new StringReader(xml)) - { - var r = new XmlTextReader(sr); - r.ReadStartElement(); - while (!r.EOF) - { - if (r.NodeType == XmlNodeType.EndElement) - { - r.ReadEndElement(); - continue; - } - - var subTree = r.ReadSubtree(); - var temp = serializer.Deserialize(subTree); - if (temp != null) result.Add(temp); - r.Read(); - } - } - return result; - } - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/Internals/RedmineSerializer.cs b/src/redmine-net40-api/Internals/RedmineSerializer.cs deleted file mode 100755 index d420391c..00000000 --- a/src/redmine-net40-api/Internals/RedmineSerializer.cs +++ /dev/null @@ -1,230 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.IO; -using System.Linq; -using System.Xml; -using System.Xml.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Exceptions; - -namespace Redmine.Net.Api.Internals -{ - internal static partial class RedmineSerializer - { - /// - /// Serializes the specified System.Object and writes the XML document to a string. - /// - /// The type of objects to serialize. - /// The object to serialize. - /// - /// The System.String that contains the XML document. - /// - /// - // ReSharper disable once InconsistentNaming - private static string ToXML(T obj) where T : class - { - var xws = new XmlWriterSettings {OmitXmlDeclaration = true}; - using (var stringWriter = new StringWriter()) - { - using (var xmlWriter = XmlWriter.Create(stringWriter, xws)) - { - var sr = new XmlSerializer(typeof (T)); - sr.Serialize(xmlWriter, obj); - return stringWriter.ToString(); - } - } - } - - /// - /// Deserializes the XML document contained by the specific System.String. - /// - /// The type of objects to deserialize. - /// The System.String that contains the XML document to deserialize. - /// - /// The T object being deserialized. - /// - /// An error occurred during deserialization. The original exception is available - /// using the System.Exception.InnerException property. - // ReSharper disable once InconsistentNaming - private static T FromXML(string xml) where T : class - { - using (var text = new StringReader(xml)) - { - var sr = new XmlSerializer(typeof (T)); - return sr.Deserialize(text) as T; - } - } - - /// - /// Serializes the specified type T and writes the XML document to a string. - /// - /// - /// The object. - /// The MIME format. - /// - /// Serialization error - public static string Serialize(T obj, MimeFormat mimeFormat) where T : class, new() - { - try - { - if (mimeFormat == MimeFormat.Json) - { - return JsonSerializer(obj); - } - return ToXML(obj); - } - catch (Exception ex) - { - throw new RedmineException("Serialization error", ex); - } - } - - /// - /// Deserializes the XML document contained by the specific System.String. - /// - /// - /// The response. - /// The MIME format. - /// - /// - /// Could not deserialize null! - /// or - /// Deserialization error - /// - /// - /// - /// - public static T Deserialize(string response, MimeFormat mimeFormat) where T : class, new() - { - if (string.IsNullOrEmpty(response)) throw new RedmineException("Could not deserialize null!"); - try - { - if (mimeFormat == MimeFormat.Json) - { - var type = typeof (T); - var jsonRoot = (string) null; - if (type == typeof (IssueCategory)) jsonRoot = RedmineKeys.ISSUE_CATEGORY; - if (type == typeof (IssueRelation)) jsonRoot = RedmineKeys.RELATION; - if (type == typeof (TimeEntry)) jsonRoot = RedmineKeys.TIME_ENTRY; - if (type == typeof (ProjectMembership)) jsonRoot = RedmineKeys.MEMBERSHIP; - if (type == typeof (WikiPage)) jsonRoot = RedmineKeys.WIKI_PAGE; - return JsonDeserialize(response, jsonRoot); - } - - return FromXML(response); - } - catch (Exception ex) - { - throw new RedmineException("Deserialization error",ex); - } - } - - /// - /// Deserializes the list. - /// - /// - /// The response. - /// The MIME format. - /// - /// - /// Could not deserialize null! - /// or - /// Deserialization error - /// - public static PaginatedObjects DeserializeList(string response, MimeFormat mimeFormat) - where T : class, new() - { - try - { - if (string.IsNullOrWhiteSpace(response)) throw new RedmineException("Could not deserialize null!"); - - if (mimeFormat == MimeFormat.Json) - { - return JSonDeserializeList(response); - } - - return XmlDeserializeList(response); - } - - catch (Exception ex) - { - throw new RedmineException("Deserialization error", ex); - } - } - - /// - /// js the son deserialize list. - /// - /// - /// The response. - /// - private static PaginatedObjects JSonDeserializeList(string response) where T : class, new() - { - - int totalItems, offset; - var type = typeof(T); - var jsonRoot = (string)null; - if (type == typeof(Error)) jsonRoot = RedmineKeys.ERRORS; - if (type == typeof(WikiPage)) jsonRoot = RedmineKeys.WIKI_PAGES; - if (type == typeof(IssuePriority)) jsonRoot = RedmineKeys.ISSUE_PRIORITIES; - if (type == typeof(TimeEntryActivity)) jsonRoot = RedmineKeys.TIME_ENTRY_ACTIVITIES; - - if (string.IsNullOrEmpty(jsonRoot)) - jsonRoot = RedmineManager.Sufixes[type]; - - var result = JsonDeserializeToList(response, jsonRoot, out totalItems, out offset); - - return new PaginatedObjects() - { - TotalCount = totalItems, - Offset = offset, - Objects = result.ToList() - }; - } - - /// - /// XMLs the deserialize list. - /// - /// - /// The response. - /// - private static PaginatedObjects XmlDeserializeList(string response) where T : class, new() - { - using (var stringReader = new StringReader(response)) - { - using (var xmlReader = new XmlTextReader(stringReader)) - { - xmlReader.WhitespaceHandling = WhitespaceHandling.None; - xmlReader.Read(); - xmlReader.Read(); - - var totalItems = xmlReader.ReadAttributeAsInt(RedmineKeys.TOTAL_COUNT); - var offset = xmlReader.ReadAttributeAsInt(RedmineKeys.OFFSET); - var result = xmlReader.ReadElementContentAsCollection(); - return new PaginatedObjects() - { - TotalCount = totalItems, - Offset = offset, - Objects = result - }; - } - } - } - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/MimeFormat.cs b/src/redmine-net40-api/MimeFormat.cs deleted file mode 100755 index b99bf3c6..00000000 --- a/src/redmine-net40-api/MimeFormat.cs +++ /dev/null @@ -1,33 +0,0 @@ -/* -Copyright 2011 - 2019 Adrian Popescu. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - - -namespace Redmine.Net.Api -{ - /// - /// - /// - public enum MimeFormat - { - /// - /// - Xml, - /// - /// The json - /// - Json - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/redmine-net40-api.csproj b/src/redmine-net40-api/redmine-net40-api.csproj index 67dcf442..a413fc34 100644 --- a/src/redmine-net40-api/redmine-net40-api.csproj +++ b/src/redmine-net40-api/redmine-net40-api.csproj @@ -270,8 +270,6 @@ Types\WikiPage.cs - - @@ -313,45 +311,15 @@ - - - - - Types\IValue.cs - - - Exceptions\NotFoundException.cs - - - Exceptions\RedmineException.cs - - - Exceptions\RedmineTimeoutException.cs - - - Exceptions\NameResolutionFailureException.cs - - - Exceptions\InternalServerErrorException.cs - - - Exceptions\UnauthorizedException.cs - - - Exceptions\ForbiddenException.cs - - - Exceptions\ConflictException.cs - - - Exceptions\NotAcceptableException.cs - + + + - \ No newline at end of file diff --git a/src/redmine-net40-api/Async/RedmineManagerAsyncExtensions.cs b/src/redmine-net40-api/Async/RedmineManagerAsyncExtensions.cs deleted file mode 100755 index 30fb842f..00000000 --- a/src/redmine-net40-api/Async/RedmineManagerAsyncExtensions.cs +++ /dev/null @@ -1,324 +0,0 @@ -/* - Copyright 2011 - 2016 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Globalization; -using System.Net; -using System.Text; -using System.Threading.Tasks; -using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.Extensions -{ - public static class RedmineManagerAsyncExtensions - { - public static Task GetCurrentUserAsync(this RedmineManager redmineManager, NameValueCollection parameters = null) - { - var task = Task.Factory.StartNew(() => - { - var uri = UrlHelper.GetCurrentUserUrl(redmineManager); - - using (var wc = redmineManager.CreateWebClient(parameters)) - { - return wc.DownloadString(uri); - } - }); - - return task.ContinueWith(t => RedmineSerializer.Deserialize(t.Result, redmineManager.MimeFormat)); - } - - public static Task CreateOrUpdateWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage) - { - var task = Task.Factory.StartNew(() => - { - var uri = UrlHelper.GetWikiCreateOrUpdaterUrl(redmineManager, projectId, pageName); - var data = RedmineSerializer.Serialize(wikiPage,redmineManager.MimeFormat); - - using (var wc = redmineManager.CreateWebClient(null)) - { - var response = wc.UploadString(uri, RedmineManager.PUT, data); - return RedmineSerializer.Deserialize(response, redmineManager.MimeFormat); - } - }, TaskCreationOptions.LongRunning); - - return task; - } - - public static Task DeleteWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName) - { - var uri = UrlHelper.GetDeleteWikirUrl(redmineManager, projectId, pageName); - return Task.Factory.StartNew(() => - { - using (var wc = redmineManager.CreateWebClient(null)) - { - wc.UploadString(uri, RedmineManager.DELETE, string.Empty); - } - }, TaskCreationOptions.LongRunning); - } - - public static Task GetWikiPageAsync(this RedmineManager redmineManager, string projectId, NameValueCollection parameters, string pageName, uint version = 0) - { - var task = Task.Factory.StartNew(() => - { - var uri = UrlHelper.GetWikiPageUrl(redmineManager, projectId, parameters, pageName, version); - using (var wc = redmineManager.CreateWebClient(parameters)) - { - try - { - var response = wc.DownloadString(uri); - return RedmineSerializer.Deserialize(response, redmineManager.MimeFormat); - } - catch (WebException wex) - { - wex.HandleWebException("GetWikiPageAsync", redmineManager.MimeFormat); - } - return null; - } - }, TaskCreationOptions.LongRunning); - return task; - } - - public static Task> GetAllWikiPagesAsync(this RedmineManager redmineManager, NameValueCollection parameters, string projectId) - { - var task = Task.Factory.StartNew(() => - { - var uri = UrlHelper.GetWikisUrl(redmineManager, projectId); - using (var wc = redmineManager.CreateWebClient(parameters)) - { - var response = wc.DownloadString(uri); - return RedmineSerializer.DeserializeList(response, redmineManager.MimeFormat); - } - }, TaskCreationOptions.LongRunning); - return task; - } - - public static Task AddUserToGroupAsync(this RedmineManager redmineManager, int groupId, int userId) - { - var data = redmineManager.MimeFormat == MimeFormat.xml - ? "" + userId + "" - : "{\"user_id\":\"" + userId + "\"}"; - var task = Task.Factory.StartNew(() => - { - var uri = UrlHelper.GetAddUserToGroupUrl(redmineManager, groupId); - using (var wc = redmineManager.CreateWebClient(null)) - { - wc.UploadString(uri, RedmineManager.POST, data); - } - }, TaskCreationOptions.LongRunning); - return task; - } - - public static Task RemoveUserFromGroupAsync(this RedmineManager redmineManager, int groupId, int userId) - { - var task = Task.Factory.StartNew(() => - { - var uri = UrlHelper.GetRemoveUserFromGroupUrl(redmineManager, groupId, userId); - using (var wc = redmineManager.CreateWebClient(null)) - { - wc.UploadString(uri, RedmineManager.DELETE, string.Empty); - } - }, TaskCreationOptions.LongRunning); - return task; - } - - public static Task AddWatcherToIssueAsync(this RedmineManager redmineManager, int issueId, int userId) - { - var data = redmineManager.MimeFormat == MimeFormat.xml - ? "" + userId + "" - : "{\"user_id\":\"" + userId + "\"}"; - var task = Task.Factory.StartNew(() => - { - var uri = UrlHelper.GetAddWatcherUrl(redmineManager, issueId, userId); - - using (var wc = redmineManager.CreateWebClient(null)) - { - wc.UploadString(uri, RedmineManager.POST, data); - } - }, TaskCreationOptions.LongRunning); - return task; - } - - public static Task RemoveWatcherFromIssueAsync(this RedmineManager redmineManager, int issueId, int userId) - { - var task = Task.Factory.StartNew(() => - { - var uri = UrlHelper.GetRemoveWatcherUrl(redmineManager, issueId, userId); - using (var wc = redmineManager.CreateWebClient(null)) - { - wc.UploadString(uri, RedmineManager.DELETE, string.Empty); - } - }, TaskCreationOptions.LongRunning); - return task; - } - - public static Task GetObjectAsync(this RedmineManager redmineManager, string id, NameValueCollection parameters) where T : class, new() - { - var task = Task.Factory.StartNew(() => - { - var url = UrlHelper.GetGetUrl(redmineManager, id); - using (var wc = redmineManager.CreateWebClient(parameters)) - { - try - { - var response = wc.DownloadString(url); - return RedmineSerializer.Deserialize(response, redmineManager.MimeFormat); - } - catch (WebException wex) - { - wex.HandleWebException("GetObject", redmineManager.MimeFormat); - } - return null; - } - }, TaskCreationOptions.LongRunning); - return task; - } - - public static Task CreateObjectAsync(this RedmineManager redmineManager, T obj) where T : class, new() - { - return CreateObjectAsync(redmineManager, obj, null); - } - - public static Task CreateObjectAsync(this RedmineManager redmineManager, T obj, string ownerId) where T : class, new() - { - var task = Task.Factory.StartNew(() => - { - var url = UrlHelper.GetCreateUrl(redmineManager, ownerId); - var data = RedmineSerializer.Serialize(obj,redmineManager.MimeFormat); - - using (var wc = redmineManager.CreateWebClient(null)) - { - var response = wc.UploadString(url, RedmineManager.POST, data); - return RedmineSerializer.Deserialize(response, redmineManager.MimeFormat); - } - }, TaskCreationOptions.LongRunning); - return task; - } - - public static Task> GetPaginatedObjectsAsync(this RedmineManager redmineManager, NameValueCollection parameters) where T : class, new() - { - var task = Task.Factory.StartNew(() => - { - var url = UrlHelper.GetListUrl(redmineManager, parameters); - using (var wc = redmineManager.CreateWebClient(parameters)) - { - var response = wc.DownloadString(url); - return RedmineSerializer.DeserializeList(response, redmineManager.MimeFormat); - } - }, TaskCreationOptions.LongRunning); - return task; - } - - public static Task> GetObjectsAsync(this RedmineManager redmineManager, NameValueCollection parameters) where T : class, new() - { - var task = Task.Factory.StartNew(() => - { - int totalCount = 0, pageSize; - List resultList = null; - if (parameters == null) parameters = new NameValueCollection(); - int offset = 0; - int.TryParse(parameters[RedmineKeys.LIMIT], out pageSize); - if (pageSize == default(int)) - { - pageSize = redmineManager.PageSize > 0 ? redmineManager.PageSize : 25; - parameters.Set(RedmineKeys.LIMIT, pageSize.ToString(CultureInfo.InvariantCulture)); - } - do - { - parameters.Set(RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)); - var requestTask = redmineManager.GetPaginatedObjectsAsync(parameters).ContinueWith(t => - { - if (t.Result != null) - { - if (resultList == null) - { - resultList = t.Result.Objects; - totalCount = t.Result.TotalCount; - } - else - resultList.AddRange(t.Result.Objects); - } - offset += pageSize; - }); - requestTask.Wait(TimeSpan.FromMilliseconds(5000)); - } while (offset < totalCount); - return resultList; - }); - return task; - } - - public static Task UpdateObjectAsync(this RedmineManager redmineManager, string id, T obj, string projectId = null) where T : class, new() - { - var task = Task.Factory.StartNew(() => - { - var url = UrlHelper.GetUploadUrl(redmineManager, id, obj, projectId); - using (var wc = redmineManager.CreateWebClient(null)) - { - var data = RedmineSerializer.Serialize(obj,redmineManager.MimeFormat); - wc.UploadString(url, RedmineManager.PUT, data); - } - }, TaskCreationOptions.LongRunning); - return task; - } - - public static Task DeleteObjectAsync(this RedmineManager redmineManager, string id, NameValueCollection parameters) where T : class, new() - { - var task = Task.Factory.StartNew(() => - { - var uri = UrlHelper.GetDeleteUrl(redmineManager, id); - - using (var wc = redmineManager.CreateWebClient(parameters)) - { - wc.UploadString(uri, RedmineManager.DELETE, string.Empty); - } - }, TaskCreationOptions.LongRunning); - return task; - } - - public static Task UploadFileAsync(this RedmineManager redmineManager, byte[] data) - { - var task = Task.Factory.StartNew(() => - { - var uri = UrlHelper.GetUploadFileUrl(redmineManager); - using (var wc = redmineManager.CreateWebClient(null, true)) - { - var response = wc.UploadData(uri, RedmineManager.POST, data); - - var responseString = Encoding.ASCII.GetString(response); - return RedmineSerializer.Deserialize(responseString, redmineManager.MimeFormat); - } - }, TaskCreationOptions.LongRunning); - - return task; - } - - public static Task DownloadFileAsync(this RedmineManager redmineManager, string address) - { - var task = Task.Factory.StartNew(() => - { - using (var wc = redmineManager.CreateWebClient(null)) - { - wc.Headers.Add(HttpRequestHeader.Accept, "application/octet-stream"); - var response = wc.DownloadData(address); - return response; - } - }, TaskCreationOptions.LongRunning); - return task; - } - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/Extensions/JObjectExtensions.cs b/src/redmine-net40-api/Extensions/JObjectExtensions.cs deleted file mode 100755 index 729bf8eb..00000000 --- a/src/redmine-net40-api/Extensions/JObjectExtensions.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Web.Script.Serialization; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Redmine.Net.Api.JSonConverters; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.Extensions -{ - public static class JObjectExtensions - { - public static IdentifiableName GetValueAsIdentifiableName(this JObject obj, string key) - { - JToken val; - - if (!obj.TryGetValue(key, out val)) return null; - - //var ser = new JavaScriptSerializer(); - //ser.RegisterConverters(new[] { new IdentifiableNameConverter() }); - - //var result = ser.ConvertToType(val); - //return result; - - return val.ToObject(); - } - - public static List GetValueAsCollection(this JObject obj, string key) where T : new() - { - JToken val; - - if (!obj.TryGetValue(key, out val)) return null; - - //var ser = new JavaScriptSerializer(); - //ser.RegisterConverters(new[] { RedmineSerializer.JsonConverters[typeof(T)] }); - - //var list = new List(); - - //var arrayList = val as ArrayList; - //if (arrayList != null) - //{ - // list.AddRange(from object item in arrayList select ser.ConvertToType(item)); - //} - //else - //{ - // var dict = val as Dictionary; - // if (dict != null) - // { - // list.AddRange(dict.Select(pair => ser.ConvertToType(pair.Value))); - // } - //} - //return list; - - return null; - } - } -} diff --git a/src/redmine-net40-api/Internals/RedmineSerializerJson2.cs b/src/redmine-net40-api/Internals/RedmineSerializerJson2.cs deleted file mode 100755 index a0f197b5..00000000 --- a/src/redmine-net40-api/Internals/RedmineSerializerJson2.cs +++ /dev/null @@ -1,212 +0,0 @@ -/* - Copyright 2011 - 2016 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Redmine.Net.Api.JSonConverters2; -using Redmine.Net.Api.Types; -using Version = Redmine.Net.Api.Types.Version; - -namespace Redmine.Net.Api.Internals -{ - internal static partial class RedmineSerializer - { - private static readonly Dictionary jsonConverters = new Dictionary - { - {typeof (Issue), new IssueConverter()}, - {typeof (UploadConverter), new UploadConverter()}, - {typeof (IssueCustomField), new IssueCustomFieldConverter()} - //{typeof (Issue), new IssueConverter()}, - //{typeof (Project), new ProjectConverter()}, - //{typeof (User), new UserConverter()}, - //{typeof (UserGroup), new UserGroupConverter()}, - //{typeof (News), new NewsConverter()}, - //{typeof (Query), new QueryConverter()}, - //{typeof (Version), new VersionConverter()}, - //{typeof (Attachment), new AttachmentConverter()}, - //{typeof (Attachments), new AttachmentsConverter()}, - //{typeof (IssueRelation), new IssueRelationConverter()}, - //{typeof (TimeEntry), new TimeEntryConverter()}, - //{typeof (IssueStatus),new IssueStatusConverter()}, - //{typeof (Tracker),new TrackerConverter()}, - //{typeof (TrackerCustomField),new TrackerCustomFieldConverter()}, - //{typeof (IssueCategory), new IssueCategoryConverter()}, - //{typeof (Role), new RoleConverter()}, - //{typeof (ProjectMembership), new ProjectMembershipConverter()}, - //{typeof (Group), new GroupConverter()}, - //{typeof (GroupUser), new GroupUserConverter()}, - //{typeof (Error), new ErrorConverter()}, - //{typeof (IssueCustomField), new IssueCustomFieldConverter()}, - //{typeof (ProjectTracker), new ProjectTrackerConverter()}, - //{typeof (Journal), new JournalConverter()}, - //{typeof (TimeEntryActivity), new TimeEntryActivityConverter()}, - //{typeof (IssuePriority), new IssuePriorityConverter()}, - //{typeof (WikiPage), new WikiPageConverter()}, - //{typeof (Detail), new DetailConverter()}, - //{typeof (ChangeSet), new ChangeSetConverter()}, - //{typeof (Membership), new MembershipConverter()}, - //{typeof (MembershipRole), new MembershipRoleConverter()}, - //{typeof (IdentifiableName), new IdentifiableNameConverter()}, - //{typeof (Permission), new PermissionConverter()}, - //{typeof (IssueChild), new IssueChildConverter()}, - //{typeof (ProjectIssueCategory), new ProjectIssueCategoryConverter()}, - //{typeof (Watcher), new WatcherConverter()}, - //{typeof (Upload), new UploadConverter()}, - //{typeof (ProjectEnabledModule), new ProjectEnabledModuleConverter()}, - //{typeof (CustomField), new CustomFieldConverter()}, - //{typeof (CustomFieldRole), new CustomFieldRoleConverter()}, - //{typeof (CustomFieldPossibleValue), new CustomFieldPossibleValueConverter()} - }; - - public static Dictionary JsonConverters { get { return jsonConverters; } } - - public static string JsonSerializer(T type) where T : new() - { - var sb = new StringBuilder(); - - using (var sw = new StringWriter(sb)) - { - using (JsonWriter writer = new JsonTextWriter(sw)) - { - writer.Formatting = Formatting.Indented; - var converter = jsonConverters[typeof(T)]; - var serializer = new JsonSerializer(); - converter.Serialize(writer, type, serializer); - - return sb.ToString(); - } - } - } - - /// - /// JSON Deserialization - /// - public static List JsonDeserializeToList(string jsonString, string root) where T : class, new() - { - int totalCount; - return JsonDeserializeToList(jsonString, root, out totalCount); - } - - /// - /// JSON Deserialization - /// - public static List JsonDeserializeToList(string jsonString, string root, out int totalCount) where T : class,new() - { - var result = JsonDeserializeToList(jsonString, root, typeof(T), out totalCount); - return result == null ? null : ((ArrayList)result).OfType().ToList(); - } - - public static T JsonDeserialize(string jsonString, string root) where T : new() - { - var type = typeof(T); - var result = JsonDeserialize(jsonString, type, root); - - if (result == null) return default(T); - - return (T)result; - } - - public static object JsonDeserialize(string jsonString, Type type, string root) - { - if (string.IsNullOrEmpty(jsonString)) return null; - - var serializer = new JsonSerializer(); - var converter = jsonConverters[type]; - var jObject = JObject.Parse(jsonString); - var rootName = root ?? type.Name.ToLowerInvariant(); - var rootObject = jObject[rootName]; - - if (rootObject == null) return null; - - jObject = JObject.Parse(rootObject.ToString()); - - return converter.Deserialize(jObject, serializer); - } - - private static void AddToList(JsonSerializer serializer, IList list, Type type, JToken obj) - { - //foreach (var item in obj.ToObject()) - //{ - // if (item is ArrayList) - // { - // AddToList(serializer, list, type, new JToken(item)); - // } - // else - // { - // var converter = jsonConverters[type]; - // var o = converter.Deserialize(obj, serializer); - - // list.Add(o); - // } - //} - } - - private static object JsonDeserializeToList(string jsonString, string root, Type type, out int totalCount) - { - totalCount = 0; - - if (string.IsNullOrEmpty(jsonString)) return null; - - var serializer = new JsonSerializer(); - var converter = jsonConverters[type]; - var jObject = JObject.Parse(jsonString); - - JToken obj, tc; - - if (jObject.TryGetValue(RedmineKeys.TOTAL_COUNT, out tc)) totalCount = tc.Value(); - if (!jObject.TryGetValue(root.ToLowerInvariant(), out obj)) return null; - - jObject = JObject.Parse(obj.ToString()); - - var result = converter.Deserialize(jObject, serializer); - var arrayList = new ArrayList(); - - if (type == typeof(Error)) - { - string info = null; - - foreach (var item in jObject.ToObject()) - { - var innerArrayList = item as ArrayList; - if (innerArrayList != null) - { - info = innerArrayList.Cast() - .Aggregate(info, (current, item2) => current + (item2 as string + " ")); - } - else - { - info += item as string + " "; - } - } - - var err = new Error { Info = info }; - arrayList.Add(err); - } - else - { - AddToList(serializer, arrayList, type, obj); - } - - return arrayList; - } - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/AttachmentConverter.cs b/src/redmine-net40-api/JSonConverters/AttachmentConverter.cs deleted file mode 100755 index ac8fc98c..00000000 --- a/src/redmine-net40-api/JSonConverters/AttachmentConverter.cs +++ /dev/null @@ -1,96 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - - -namespace Redmine.Net.Api.JSonConverters -{ - /// - /// - /// - /// - internal class AttachmentConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// An instance of property data stored as name/value pairs. - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var attachment = new Attachment(); - - attachment.Id = dictionary.GetValue(RedmineKeys.ID); - attachment.Description = dictionary.GetValue(RedmineKeys.DESCRIPTION); - attachment.Author = dictionary.GetValueAsIdentifiableName(RedmineKeys.AUTHOR); - attachment.ContentType = dictionary.GetValue(RedmineKeys.CONTENT_TYPE); - attachment.ContentUrl = dictionary.GetValue(RedmineKeys.CONTENT_URL); - attachment.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); - attachment.FileName = dictionary.GetValue(RedmineKeys.FILENAME); - attachment.FileSize = dictionary.GetValue(RedmineKeys.FILESIZE); - - return attachment; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as Attachment; - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.FILENAME, entity.FileName); - result.Add(RedmineKeys.DESCRIPTION, entity.Description); - } - - var root = new Dictionary(); - root[RedmineKeys.ATTACHMENT] = result; - - return root; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(Attachment) }); } } - - #endregion - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/AttachmentsConverter.cs b/src/redmine-net40-api/JSonConverters/AttachmentsConverter.cs deleted file mode 100755 index 4a4fe365..00000000 --- a/src/redmine-net40-api/JSonConverters/AttachmentsConverter.cs +++ /dev/null @@ -1,79 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class AttachmentsConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// An instance of property data stored as name/value pairs. - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object’s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as Attachments; - var result = new Dictionary(); - - if (entity != null) - { - foreach (var entry in entity) - { - var attachment = new AttachmentConverter().Serialize(entry.Value, serializer); - result.Add(entry.Key.ToString(), attachment.First().Value); - } - } - - var root = new Dictionary(); - root[RedmineKeys.ATTACHMENTS] = result; - - return root; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(Attachments) }); } } - - #endregion - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/ChangeSetConverter.cs b/src/redmine-net40-api/JSonConverters/ChangeSetConverter.cs deleted file mode 100755 index 27db1d23..00000000 --- a/src/redmine-net40-api/JSonConverters/ChangeSetConverter.cs +++ /dev/null @@ -1,81 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class ChangeSetConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// An instance of property data stored as name/value pairs. - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var changeSet = new ChangeSet - { - Revision = dictionary.GetValue(RedmineKeys.REVISION), - Comments = dictionary.GetValue(RedmineKeys.COMMENTS), - User = dictionary.GetValueAsIdentifiableName(RedmineKeys.USER), - CommittedOn = dictionary.GetValue(RedmineKeys.COMMITTED_ON) - }; - - - return changeSet; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(ChangeSet)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/CustomFieldConverter.cs b/src/redmine-net40-api/JSonConverters/CustomFieldConverter.cs deleted file mode 100755 index 489c647d..00000000 --- a/src/redmine-net40-api/JSonConverters/CustomFieldConverter.cs +++ /dev/null @@ -1,92 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class CustomFieldConverter : IdentifiableNameConverter - { - #region Overrides of JavaScriptConverter - - /// - /// Deserializes the specified dictionary. - /// - /// The dictionary. - /// The type. - /// The serializer. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var customField = new CustomField(); - - customField.Id = dictionary.GetValue(RedmineKeys.ID); - customField.Name = dictionary.GetValue(RedmineKeys.NAME); - customField.CustomizedType = dictionary.GetValue(RedmineKeys.CUSTOMIZED_TYPE); - customField.FieldFormat = dictionary.GetValue(RedmineKeys.FIELD_FORMAT); - customField.Regexp = dictionary.GetValue(RedmineKeys.REGEXP); - customField.MinLength = dictionary.GetValue(RedmineKeys.MIN_LENGTH); - customField.MaxLength = dictionary.GetValue(RedmineKeys.MAX_LENGTH); - customField.IsRequired = dictionary.GetValue(RedmineKeys.IS_REQUIRED); - customField.IsFilter = dictionary.GetValue(RedmineKeys.IS_FILTER); - customField.Searchable = dictionary.GetValue(RedmineKeys.SEARCHABLE); - customField.Multiple = dictionary.GetValue(RedmineKeys.MULTIPLE); - customField.DefaultValue = dictionary.GetValue(RedmineKeys.DEFAULT_VALUE); - customField.Visible = dictionary.GetValue(RedmineKeys.VISIBLE); - customField.PossibleValues = - dictionary.GetValueAsCollection(RedmineKeys.POSSIBLE_VALUES); - customField.Trackers = dictionary.GetValueAsCollection(RedmineKeys.TRACKERS); - customField.Roles = dictionary.GetValueAsCollection(RedmineKeys.ROLES); - - - return customField; - } - - return null; - } - - /// - /// Serializes the specified object. - /// - /// The object. - /// The serializer. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// Gets the supported types. - /// - /// - /// The supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(CustomField)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/CustomFieldPossibleValueConverter.cs b/src/redmine-net40-api/JSonConverters/CustomFieldPossibleValueConverter.cs deleted file mode 100644 index 97962642..00000000 --- a/src/redmine-net40-api/JSonConverters/CustomFieldPossibleValueConverter.cs +++ /dev/null @@ -1,76 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class CustomFieldPossibleValueConverter : IdentifiableNameConverter - { - #region Overrides of JavaScriptConverter - - /// - /// Deserializes the specified dictionary. - /// - /// The dictionary. - /// The type. - /// The serializer. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var entity = new CustomFieldPossibleValue(); - - entity.Value = dictionary.GetValue(RedmineKeys.VALUE); - entity.Label = dictionary.GetValue(RedmineKeys.LABEL); - - return entity; - } - - return null; - } - - /// - /// Serializes the specified object. - /// - /// The object. - /// The serializer. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// Gets the supported types. - /// - /// - /// The supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(CustomFieldPossibleValue)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/CustomFieldRoleConverter.cs b/src/redmine-net40-api/JSonConverters/CustomFieldRoleConverter.cs deleted file mode 100755 index 34f47b05..00000000 --- a/src/redmine-net40-api/JSonConverters/CustomFieldRoleConverter.cs +++ /dev/null @@ -1,76 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class CustomFieldRoleConverter : IdentifiableNameConverter - { - #region Overrides of JavaScriptConverter - - /// - /// Deserializes the specified dictionary. - /// - /// The dictionary. - /// The type. - /// The serializer. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var entity = new CustomFieldRole(); - - entity.Id = dictionary.GetValue(RedmineKeys.ID); - entity.Name = dictionary.GetValue(RedmineKeys.NAME); - - return entity; - } - - return null; - } - - /// - /// Serializes the specified object. - /// - /// The object. - /// The serializer. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// Gets the supported types. - /// - /// - /// The supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(CustomFieldRole)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/DetailConverter.cs b/src/redmine-net40-api/JSonConverters/DetailConverter.cs deleted file mode 100755 index 8399bfea..00000000 --- a/src/redmine-net40-api/JSonConverters/DetailConverter.cs +++ /dev/null @@ -1,82 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class DetailConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var detail = new Detail(); - - detail.NewValue = dictionary.GetValue(RedmineKeys.NEW_VALUE); - detail.OldValue = dictionary.GetValue(RedmineKeys.OLD_VALUE); - detail.Property = dictionary.GetValue(RedmineKeys.PROPERTY); - detail.Name = dictionary.GetValue(RedmineKeys.NAME); - - return detail; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(Detail)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/ErrorConverter.cs b/src/redmine-net40-api/JSonConverters/ErrorConverter.cs deleted file mode 100755 index d9bf391b..00000000 --- a/src/redmine-net40-api/JSonConverters/ErrorConverter.cs +++ /dev/null @@ -1,76 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class ErrorConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var error = new Error {Info = dictionary.GetValue(RedmineKeys.ERROR)}; - return error; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(Error)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/FileConverter.cs b/src/redmine-net40-api/JSonConverters/FileConverter.cs deleted file mode 100755 index 4d849445..00000000 --- a/src/redmine-net40-api/JSonConverters/FileConverter.cs +++ /dev/null @@ -1,111 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class FileConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var file = new File { }; - - file.Author = dictionary.GetValueAsIdentifiableName(RedmineKeys.AUTHOR); - file.ContentType = dictionary.GetValue(RedmineKeys.CONTENT_TYPE); - file.ContentUrl = dictionary.GetValue(RedmineKeys.CONTENT_URL); - file.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); - file.Description = dictionary.GetValue(RedmineKeys.DESCRIPTION); - file.Digest = dictionary.GetValue(RedmineKeys.DIGEST); - file.Downloads = dictionary.GetValue(RedmineKeys.DOWNLOADS); - file.Filename = dictionary.GetValue(RedmineKeys.FILENAME); - file.Filesize = dictionary.GetValue(RedmineKeys.FILESIZE); - file.Id = dictionary.GetValue(RedmineKeys.ID); - file.Token = dictionary.GetValue(RedmineKeys.TOKEN); - var versionId = dictionary.GetValue(RedmineKeys.VERSION_ID); - if (versionId.HasValue) - { - file.Version = new IdentifiableName { Id = versionId.Value }; - } - else - { - file.Version = dictionary.GetValueAsIdentifiableName(RedmineKeys.VERSION); - } - return file; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object’s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as File; - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.TOKEN, entity.Token); - result.WriteIdIfNotNull(entity.Version, RedmineKeys.VERSION_ID); - result.Add(RedmineKeys.FILENAME, entity.Filename); - result.Add(RedmineKeys.DESCRIPTION, entity.Description); - - var root = new Dictionary(); - root[RedmineKeys.FILE] = result; - return root; - } - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] { typeof(File) }); } - } - - #endregion - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/GroupConverter.cs b/src/redmine-net40-api/JSonConverters/GroupConverter.cs deleted file mode 100755 index f0930865..00000000 --- a/src/redmine-net40-api/JSonConverters/GroupConverter.cs +++ /dev/null @@ -1,97 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class GroupConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var group = new Group(); - - group.Id = dictionary.GetValue(RedmineKeys.ID); - group.Name = dictionary.GetValue(RedmineKeys.NAME); - group.Users = dictionary.GetValueAsCollection(RedmineKeys.USERS); - group.CustomFields = dictionary.GetValueAsCollection(RedmineKeys.CUSTOM_FIELDS); - group.Memberships = dictionary.GetValueAsCollection(RedmineKeys.MEMBERSHIPS); - - return group; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as Group; - - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.NAME, entity.Name); - result.WriteIdsArray(RedmineKeys.USER_IDS, entity.Users); - - var root = new Dictionary(); - root[RedmineKeys.GROUP] = result; - return root; - } - - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(Group)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/GroupUserConverter.cs b/src/redmine-net40-api/JSonConverters/GroupUserConverter.cs deleted file mode 100755 index 8fccb468..00000000 --- a/src/redmine-net40-api/JSonConverters/GroupUserConverter.cs +++ /dev/null @@ -1,77 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - - -namespace Redmine.Net.Api.JSonConverters -{ - internal class GroupUserConverter : IdentifiableNameConverter - { - #region Overrides of JavaScriptConverter - - /// - /// Deserializes the specified dictionary. - /// - /// The dictionary. - /// The type. - /// The serializer. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var userGroup = new GroupUser(); - - userGroup.Id = dictionary.GetValue(RedmineKeys.ID); - userGroup.Name = dictionary.GetValue(RedmineKeys.NAME); - - return userGroup; - } - - return null; - } - - /// - /// Serializes the specified object. - /// - /// The object. - /// The serializer. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// Gets the supported types. - /// - /// - /// The supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(GroupUser)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/IdentifiableNameConverter.cs b/src/redmine-net40-api/JSonConverters/IdentifiableNameConverter.cs deleted file mode 100755 index 5c770a6f..00000000 --- a/src/redmine-net40-api/JSonConverters/IdentifiableNameConverter.cs +++ /dev/null @@ -1,91 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class IdentifiableNameConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var entity = new IdentifiableName(); - - entity.Id = dictionary.GetValue(RedmineKeys.ID); - entity.Name = dictionary.GetValue(RedmineKeys.NAME); - - return entity; - } - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as IdentifiableName; - var result = new Dictionary(); - - if (entity != null) - { - result.WriteIdIfNotNull(entity, RedmineKeys.ID); - - if (!string.IsNullOrEmpty(entity.Name)) - result.Add(RedmineKeys.NAME, entity.Name); - return result; - } - - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(IdentifiableName)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/IssueCategoryConverter.cs b/src/redmine-net40-api/JSonConverters/IssueCategoryConverter.cs deleted file mode 100755 index 112c1468..00000000 --- a/src/redmine-net40-api/JSonConverters/IssueCategoryConverter.cs +++ /dev/null @@ -1,97 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class IssueCategoryConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var issueCategory = new IssueCategory(); - - issueCategory.Id = dictionary.GetValue(RedmineKeys.ID); - issueCategory.Project = dictionary.GetValueAsIdentifiableName(RedmineKeys.PROJECT); - issueCategory.AsignTo = dictionary.GetValueAsIdentifiableName(RedmineKeys.ASSIGNED_TO); - issueCategory.Name = dictionary.GetValue(RedmineKeys.NAME); - - return issueCategory; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as IssueCategory; - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.NAME, entity.Name); - result.WriteIdIfNotNull(entity.Project, RedmineKeys.PROJECT_ID); - result.WriteIdIfNotNull(entity.AsignTo, RedmineKeys.ASSIGNED_TO_ID); - - var root = new Dictionary(); - - root[RedmineKeys.ISSUE_CATEGORY] = result; - return root; - } - - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(IssueCategory)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/IssueChildConverter.cs b/src/redmine-net40-api/JSonConverters/IssueChildConverter.cs deleted file mode 100755 index a16b4993..00000000 --- a/src/redmine-net40-api/JSonConverters/IssueChildConverter.cs +++ /dev/null @@ -1,75 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class IssueChildConverter : JavaScriptConverter - { - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(IssueChild)}); } - } - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// An instance of property data stored as name/value pairs. - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var issueChild = new IssueChild - { - Id = dictionary.GetValue(RedmineKeys.ID), - Tracker = dictionary.GetValueAsIdentifiableName(RedmineKeys.TRACKER), - Subject = dictionary.GetValue(RedmineKeys.SUBJECT) - }; - - return issueChild; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/IssueConverter.cs b/src/redmine-net40-api/JSonConverters/IssueConverter.cs deleted file mode 100644 index f31def65..00000000 --- a/src/redmine-net40-api/JSonConverters/IssueConverter.cs +++ /dev/null @@ -1,155 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - - -namespace Redmine.Net.Api.JSonConverters -{ - internal class IssueConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var issue = new Issue(); - - issue.Id = dictionary.GetValue(RedmineKeys.ID); - issue.Description = dictionary.GetValue(RedmineKeys.DESCRIPTION); - issue.Project = dictionary.GetValueAsIdentifiableName(RedmineKeys.PROJECT); - issue.Tracker = dictionary.GetValueAsIdentifiableName(RedmineKeys.TRACKER); - issue.Status = dictionary.GetValueAsIdentifiableName(RedmineKeys.STATUS); - issue.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); - issue.UpdatedOn = dictionary.GetValue(RedmineKeys.UPDATED_ON); - issue.ClosedOn = dictionary.GetValue(RedmineKeys.CLOSED_ON); - issue.Priority = dictionary.GetValueAsIdentifiableName(RedmineKeys.PRIORITY); - issue.Author = dictionary.GetValueAsIdentifiableName(RedmineKeys.AUTHOR); - issue.AssignedTo = dictionary.GetValueAsIdentifiableName(RedmineKeys.ASSIGNED_TO); - issue.Category = dictionary.GetValueAsIdentifiableName(RedmineKeys.CATEGORY); - issue.FixedVersion = dictionary.GetValueAsIdentifiableName(RedmineKeys.FIXED_VERSION); - issue.Subject = dictionary.GetValue(RedmineKeys.SUBJECT); - issue.Notes = dictionary.GetValue(RedmineKeys.NOTES); - issue.IsPrivate = dictionary.GetValue(RedmineKeys.IS_PRIVATE); - issue.StartDate = dictionary.GetValue(RedmineKeys.START_DATE); - issue.DueDate = dictionary.GetValue(RedmineKeys.DUE_DATE); - issue.SpentHours = dictionary.GetValue(RedmineKeys.SPENT_HOURS); - issue.TotalSpentHours = dictionary.GetValue(RedmineKeys.TOTAL_SPENT_HOURS); - issue.DoneRatio = dictionary.GetValue(RedmineKeys.DONE_RATIO); - issue.EstimatedHours = dictionary.GetValue(RedmineKeys.ESTIMATED_HOURS); - issue.TotalEstimatedHours = dictionary.GetValue(RedmineKeys.TOTAL_ESTIMATED_HOURS); - issue.ParentIssue = dictionary.GetValueAsIdentifiableName(RedmineKeys.PARENT); - - issue.CustomFields = dictionary.GetValueAsCollection(RedmineKeys.CUSTOM_FIELDS); - issue.Attachments = dictionary.GetValueAsCollection(RedmineKeys.ATTACHMENTS); - issue.Relations = dictionary.GetValueAsCollection(RedmineKeys.RELATIONS); - issue.Journals = dictionary.GetValueAsCollection(RedmineKeys.JOURNALS); - issue.Changesets = dictionary.GetValueAsCollection(RedmineKeys.CHANGESETS); - issue.Watchers = dictionary.GetValueAsCollection(RedmineKeys.WATCHERS); - issue.Children = dictionary.GetValueAsCollection(RedmineKeys.CHILDREN); - return issue; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as Issue; - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.SUBJECT, entity.Subject); - result.Add(RedmineKeys.DESCRIPTION, entity.Description); - result.Add(RedmineKeys.NOTES, entity.Notes); - if (entity.Id != 0) - { - result.Add(RedmineKeys.PRIVATE_NOTES, entity.PrivateNotes.ToString().ToLowerInvariant()); - } - result.Add(RedmineKeys.IS_PRIVATE, entity.IsPrivate.ToString().ToLowerInvariant()); - result.WriteIdIfNotNull(entity.Project, RedmineKeys.PROJECT_ID); - result.WriteIdIfNotNull(entity.Priority, RedmineKeys.PRIORITY_ID); - result.WriteIdIfNotNull(entity.Status, RedmineKeys.STATUS_ID); - result.WriteIdIfNotNull(entity.Category, RedmineKeys.CATEGORY_ID); - result.WriteIdIfNotNull(entity.Tracker, RedmineKeys.TRACKER_ID); - result.WriteIdIfNotNull(entity.AssignedTo, RedmineKeys.ASSIGNED_TO_ID); - result.WriteIdIfNotNull(entity.FixedVersion, RedmineKeys.FIXED_VERSION_ID); - result.WriteValueOrEmpty(entity.EstimatedHours, RedmineKeys.ESTIMATED_HOURS); - - result.WriteIdOrEmpty(entity.ParentIssue, RedmineKeys.PARENT_ISSUE_ID); - result.WriteDateOrEmpty(entity.StartDate, RedmineKeys.START_DATE); - result.WriteDateOrEmpty(entity.DueDate, RedmineKeys.DUE_DATE); - result.WriteDateOrEmpty(entity.UpdatedOn, RedmineKeys.UPDATED_ON); - - if (entity.DoneRatio != null) - result.Add(RedmineKeys.DONE_RATIO, entity.DoneRatio.Value.ToString(CultureInfo.InvariantCulture)); - - if (entity.SpentHours != null) - result.Add(RedmineKeys.SPENT_HOURS, entity.SpentHours.Value.ToString(CultureInfo.InvariantCulture)); - - result.WriteArray(RedmineKeys.UPLOADS, entity.Uploads, new UploadConverter(), serializer); - result.WriteArray(RedmineKeys.CUSTOM_FIELDS, entity.CustomFields, new IssueCustomFieldConverter(), - serializer); - - result.WriteIdsArray(RedmineKeys.WATCHER_USER_IDS, entity.Watchers); - - var root = new Dictionary(); - root[RedmineKeys.ISSUE] = result; - return root; - } - - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(Issue)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/IssueCustomFieldConverter.cs b/src/redmine-net40-api/JSonConverters/IssueCustomFieldConverter.cs deleted file mode 100755 index 3f34926b..00000000 --- a/src/redmine-net40-api/JSonConverters/IssueCustomFieldConverter.cs +++ /dev/null @@ -1,119 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class IssueCustomFieldConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var customField = new IssueCustomField(); - - customField.Id = dictionary.GetValue(RedmineKeys.ID); - customField.Name = dictionary.GetValue(RedmineKeys.NAME); - customField.Multiple = dictionary.GetValue(RedmineKeys.MULTIPLE); - - var val = dictionary.GetValue(RedmineKeys.VALUE); - - if (val != null) - { - if (customField.Values == null) customField.Values = new List(); - var list = val as ArrayList; - if (list != null) - { - foreach (var value in list) - { - customField.Values.Add(new CustomFieldValue {Info = Convert.ToString(value)}); - } - } - else - { - customField.Values.Add(new CustomFieldValue {Info = Convert.ToString(val)}); - } - } - return customField; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as IssueCustomField; - - var result = new Dictionary(); - - if (entity == null) return result; - if (entity.Values == null) return null; - var itemsCount = entity.Values.Count; - - result.Add(RedmineKeys.ID, entity.Id); - if (itemsCount > 1) - { - result.Add(RedmineKeys.VALUE, entity.Values.Select(x => x.Info).ToArray()); - } - else - { - result.Add(RedmineKeys.VALUE, itemsCount > 0 ? entity.Values[0].Info : null); - } - - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(IssueCustomField)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/IssuePriorityConverter.cs b/src/redmine-net40-api/JSonConverters/IssuePriorityConverter.cs deleted file mode 100755 index f3176403..00000000 --- a/src/redmine-net40-api/JSonConverters/IssuePriorityConverter.cs +++ /dev/null @@ -1,77 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class IssuePriorityConverter : JavaScriptConverter - { - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(IssuePriority)}); } - } - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var issuePriority = new IssuePriority(); - - issuePriority.Id = dictionary.GetValue(RedmineKeys.ID); - issuePriority.Name = dictionary.GetValue(RedmineKeys.NAME); - issuePriority.IsDefault = dictionary.GetValue(RedmineKeys.IS_DEFAULT); - - return issuePriority; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/IssueRelationConverter.cs b/src/redmine-net40-api/JSonConverters/IssueRelationConverter.cs deleted file mode 100755 index c0529205..00000000 --- a/src/redmine-net40-api/JSonConverters/IssueRelationConverter.cs +++ /dev/null @@ -1,99 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class IssueRelationConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var issueRelation = new IssueRelation(); - - issueRelation.Id = dictionary.GetValue(RedmineKeys.ID); - issueRelation.IssueId = dictionary.GetValue(RedmineKeys.ISSUE_ID); - issueRelation.IssueToId = dictionary.GetValue(RedmineKeys.ISSUE_TO_ID); - issueRelation.Type = dictionary.GetValue(RedmineKeys.RELATION_TYPE); - issueRelation.Delay = dictionary.GetValue(RedmineKeys.DELAY); - - return issueRelation; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as IssueRelation; - - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.ISSUE_TO_ID, entity.IssueToId); - result.Add(RedmineKeys.RELATION_TYPE, entity.Type.ToString()); - if (entity.Type == IssueRelationType.precedes || entity.Type == IssueRelationType.follows) - result.WriteValueOrEmpty(entity.Delay, RedmineKeys.DELAY); - - var root = new Dictionary(); - root[RedmineKeys.RELATION] = result; - return root; - } - - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(IssueRelation)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/IssueStatusConverter.cs b/src/redmine-net40-api/JSonConverters/IssueStatusConverter.cs deleted file mode 100755 index d42e1b98..00000000 --- a/src/redmine-net40-api/JSonConverters/IssueStatusConverter.cs +++ /dev/null @@ -1,81 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class IssueStatusConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var issueStatus = new IssueStatus(); - - issueStatus.Id = dictionary.GetValue(RedmineKeys.ID); - issueStatus.Name = dictionary.GetValue(RedmineKeys.NAME); - issueStatus.IsClosed = dictionary.GetValue(RedmineKeys.IS_CLOSED); - issueStatus.IsDefault = dictionary.GetValue(RedmineKeys.IS_DEFAULT); - return issueStatus; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(IssueStatus)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/JournalConverter.cs b/src/redmine-net40-api/JSonConverters/JournalConverter.cs deleted file mode 100644 index b9e83051..00000000 --- a/src/redmine-net40-api/JSonConverters/JournalConverter.cs +++ /dev/null @@ -1,84 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class JournalConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var journal = new Journal(); - - journal.Id = dictionary.GetValue(RedmineKeys.ID); - journal.Notes = dictionary.GetValue(RedmineKeys.NOTES); - journal.User = dictionary.GetValueAsIdentifiableName(RedmineKeys.USER); - journal.PrivateNotes = dictionary.GetValue(RedmineKeys.PRIVATE_NOTES); - journal.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); - journal.Details = dictionary.GetValueAsCollection(RedmineKeys.DETAILS); - - return journal; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(Journal)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/MembershipConverter.cs b/src/redmine-net40-api/JSonConverters/MembershipConverter.cs deleted file mode 100755 index 9bafc7d7..00000000 --- a/src/redmine-net40-api/JSonConverters/MembershipConverter.cs +++ /dev/null @@ -1,81 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class MembershipConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var membership = new Membership(); - - membership.Id = dictionary.GetValue(RedmineKeys.ID); - membership.Project = dictionary.GetValueAsIdentifiableName(RedmineKeys.PROJECT); - membership.Roles = dictionary.GetValueAsCollection(RedmineKeys.ROLES); - - return membership; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(Membership)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/MembershipRoleConverter.cs b/src/redmine-net40-api/JSonConverters/MembershipRoleConverter.cs deleted file mode 100755 index 0992a1d4..00000000 --- a/src/redmine-net40-api/JSonConverters/MembershipRoleConverter.cs +++ /dev/null @@ -1,81 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class MembershipRoleConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var membershipRole = new MembershipRole(); - - membershipRole.Id = dictionary.GetValue(RedmineKeys.ID); - membershipRole.Inherited = dictionary.GetValue(RedmineKeys.INHERITED); - membershipRole.Name = dictionary.GetValue(RedmineKeys.NAME); - - return membershipRole; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(MembershipRole)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/NewsConverter.cs b/src/redmine-net40-api/JSonConverters/NewsConverter.cs deleted file mode 100755 index 75df6ef1..00000000 --- a/src/redmine-net40-api/JSonConverters/NewsConverter.cs +++ /dev/null @@ -1,84 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class NewsConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var news = new News(); - - news.Id = dictionary.GetValue(RedmineKeys.ID); - news.Author = dictionary.GetValueAsIdentifiableName(RedmineKeys.AUTHOR); - news.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); - news.Description = dictionary.GetValue(RedmineKeys.DESCRIPTION); - news.Project = dictionary.GetValueAsIdentifiableName(RedmineKeys.PROJECT); - news.Summary = dictionary.GetValue(RedmineKeys.SUMMARY); - news.Title = dictionary.GetValue(RedmineKeys.TITLE); - - return news; - } - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(News)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/PermissionConverter.cs b/src/redmine-net40-api/JSonConverters/PermissionConverter.cs deleted file mode 100755 index 4e8a1d22..00000000 --- a/src/redmine-net40-api/JSonConverters/PermissionConverter.cs +++ /dev/null @@ -1,72 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class PermissionConverter : JavaScriptConverter - { - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(Permission)}); } - } - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var permission = new Permission {Info = dictionary.GetValue(RedmineKeys.PERMISSION)}; - return permission; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/ProjectConverter.cs b/src/redmine-net40-api/JSonConverters/ProjectConverter.cs deleted file mode 100755 index 70642bb5..00000000 --- a/src/redmine-net40-api/JSonConverters/ProjectConverter.cs +++ /dev/null @@ -1,116 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class ProjectConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var project = new Project(); - - project.Id = dictionary.GetValue(RedmineKeys.ID); - project.Description = dictionary.GetValue(RedmineKeys.DESCRIPTION); - project.HomePage = dictionary.GetValue(RedmineKeys.HOMEPAGE); - project.Name = dictionary.GetValue(RedmineKeys.NAME); - project.Identifier = dictionary.GetValue(RedmineKeys.IDENTIFIER); - project.Status = dictionary.GetValue(RedmineKeys.STATUS); - project.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); - project.UpdatedOn = dictionary.GetValue(RedmineKeys.UPDATED_ON); - project.Trackers = dictionary.GetValueAsCollection(RedmineKeys.TRACKERS); - project.CustomFields = dictionary.GetValueAsCollection(RedmineKeys.CUSTOM_FIELDS); - project.IsPublic = dictionary.GetValue(RedmineKeys.IS_PUBLIC); - project.Parent = dictionary.GetValueAsIdentifiableName(RedmineKeys.PARENT); - project.IssueCategories = dictionary.GetValueAsCollection(RedmineKeys.ISSUE_CATEGORIES); - project.EnabledModules = dictionary.GetValueAsCollection(RedmineKeys.ENABLED_MODULES); - project.TimeEntryActivities = dictionary.GetValueAsCollection(RedmineKeys.TIME_ENTRY_ACTIVITIES); - return project; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object’s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as Project; - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.NAME, entity.Name); - result.Add(RedmineKeys.IDENTIFIER, entity.Identifier); - result.Add(RedmineKeys.DESCRIPTION, entity.Description); - result.Add(RedmineKeys.HOMEPAGE, entity.HomePage); - //result.Add(RedmineKeys.INHERIT_MEMBERS, entity.InheritMembers.ToString().ToLowerInvariant()); - result.Add(RedmineKeys.IS_PUBLIC, entity.IsPublic.ToString().ToLowerInvariant()); - result.WriteIdOrEmpty(entity.Parent, RedmineKeys.PARENT_ID, string.Empty); - result.WriteIdsArray(RedmineKeys.TRACKER_IDS, entity.Trackers); - result.WriteNamesArray(RedmineKeys.ENABLED_MODULE_NAMES, entity.EnabledModules); - if (entity.Id > 0) - { - result.WriteArray(RedmineKeys.CUSTOM_FIELDS, entity.CustomFields, new IssueCustomFieldConverter(), - serializer); - } - var root = new Dictionary(); - root[RedmineKeys.PROJECT] = result; - return root; - } - - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] { typeof(Project) }); } - } - - #endregion - } -} diff --git a/src/redmine-net40-api/JSonConverters/ProjectEnabledModuleConverter.cs b/src/redmine-net40-api/JSonConverters/ProjectEnabledModuleConverter.cs deleted file mode 100755 index 6a2e093b..00000000 --- a/src/redmine-net40-api/JSonConverters/ProjectEnabledModuleConverter.cs +++ /dev/null @@ -1,77 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class ProjectEnabledModuleConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var projectEnableModule = new ProjectEnabledModule(); - projectEnableModule.Id = dictionary.GetValue(RedmineKeys.ID); - projectEnableModule.Name = dictionary.GetValue(RedmineKeys.NAME); - return projectEnableModule; - } - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(ProjectEnabledModule)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/ProjectIssueCategoryConverter.cs b/src/redmine-net40-api/JSonConverters/ProjectIssueCategoryConverter.cs deleted file mode 100755 index 4c33e2f2..00000000 --- a/src/redmine-net40-api/JSonConverters/ProjectIssueCategoryConverter.cs +++ /dev/null @@ -1,89 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class ProjectIssueCategoryConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var projectTracker = new ProjectIssueCategory(); - projectTracker.Id = dictionary.GetValue(RedmineKeys.ID); - projectTracker.Name = dictionary.GetValue(RedmineKeys.NAME); - return projectTracker; - } - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as ProjectIssueCategory; - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.ID, entity.Id); - result.Add(RedmineKeys.NAME, entity.Name); - - var root = new Dictionary(); - root[RedmineKeys.ISSUE_CATEGORY] = result; - return root; - } - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(ProjectIssueCategory)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/ProjectMembershipConverter.cs b/src/redmine-net40-api/JSonConverters/ProjectMembershipConverter.cs deleted file mode 100755 index fa782dc9..00000000 --- a/src/redmine-net40-api/JSonConverters/ProjectMembershipConverter.cs +++ /dev/null @@ -1,95 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class ProjectMembershipConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var projectMembership = new ProjectMembership(); - - projectMembership.Id = dictionary.GetValue(RedmineKeys.ID); - projectMembership.Group = dictionary.GetValueAsIdentifiableName(RedmineKeys.GROUP); - projectMembership.Project = dictionary.GetValueAsIdentifiableName(RedmineKeys.PROJECT); - projectMembership.Roles = dictionary.GetValueAsCollection(RedmineKeys.ROLES); - projectMembership.User = dictionary.GetValueAsIdentifiableName(RedmineKeys.USER); - - return projectMembership; - } - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as ProjectMembership; - var result = new Dictionary(); - - if (entity != null) - { - result.WriteIdIfNotNull(entity.User, RedmineKeys.USER_ID); - result.WriteIdsArray(RedmineKeys.ROLE_IDS, entity.Roles); - - var root = new Dictionary(); - root[RedmineKeys.MEMBERSHIP] = result; - return root; - } - - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(ProjectMembership)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/ProjectTrackerConverter.cs b/src/redmine-net40-api/JSonConverters/ProjectTrackerConverter.cs deleted file mode 100755 index e1183213..00000000 --- a/src/redmine-net40-api/JSonConverters/ProjectTrackerConverter.cs +++ /dev/null @@ -1,77 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class ProjectTrackerConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var projectTracker = new ProjectTracker(); - projectTracker.Id = dictionary.GetValue(RedmineKeys.ID); - projectTracker.Name = dictionary.GetValue(RedmineKeys.NAME); - return projectTracker; - } - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(ProjectTracker)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/QueryConverter.cs b/src/redmine-net40-api/JSonConverters/QueryConverter.cs deleted file mode 100755 index f92ff804..00000000 --- a/src/redmine-net40-api/JSonConverters/QueryConverter.cs +++ /dev/null @@ -1,82 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class QueryConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var query = new Query(); - - query.Id = dictionary.GetValue(RedmineKeys.ID); - query.IsPublic = dictionary.GetValue(RedmineKeys.IS_PUBLIC); - query.ProjectId = dictionary.GetValue(RedmineKeys.PROJECT_ID); - query.Name = dictionary.GetValue(RedmineKeys.NAME); - - return query; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(Query)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/RoleConverter.cs b/src/redmine-net40-api/JSonConverters/RoleConverter.cs deleted file mode 100755 index af14d6f1..00000000 --- a/src/redmine-net40-api/JSonConverters/RoleConverter.cs +++ /dev/null @@ -1,91 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class RoleConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var role = new Role(); - - role.Id = dictionary.GetValue(RedmineKeys.ID); - role.Name = dictionary.GetValue(RedmineKeys.NAME); - - var permissions = dictionary.GetValue(RedmineKeys.PERMISSIONS); - if (permissions != null) - { - role.Permissions = new List(); - foreach (var permission in permissions) - { - var perms = new Permission {Info = permission.ToString()}; - role.Permissions.Add(perms); - } - } - - return role; - } - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(Role)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/TimeEntryActivityConverter.cs b/src/redmine-net40-api/JSonConverters/TimeEntryActivityConverter.cs deleted file mode 100755 index 22e88762..00000000 --- a/src/redmine-net40-api/JSonConverters/TimeEntryActivityConverter.cs +++ /dev/null @@ -1,77 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class TimeEntryActivityConverter : JavaScriptConverter - { - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(TimeEntryActivity)}); } - } - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var timeEntryActivity = new TimeEntryActivity - { - Id = dictionary.GetValue(RedmineKeys.ID), - Name = dictionary.GetValue(RedmineKeys.NAME), - IsDefault = dictionary.GetValue(RedmineKeys.IS_DEFAULT) - }; - return timeEntryActivity; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/TimeEntryConverter.cs b/src/redmine-net40-api/JSonConverters/TimeEntryConverter.cs deleted file mode 100755 index 227ec6e8..00000000 --- a/src/redmine-net40-api/JSonConverters/TimeEntryConverter.cs +++ /dev/null @@ -1,120 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class TimeEntryConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var timeEntry = new TimeEntry(); - - timeEntry.Id = dictionary.GetValue(RedmineKeys.ID); - timeEntry.Activity = - dictionary.GetValueAsIdentifiableName(dictionary.ContainsKey(RedmineKeys.ACTIVITY) - ? RedmineKeys.ACTIVITY - : RedmineKeys.ACTIVITY_ID); - timeEntry.Comments = dictionary.GetValue(RedmineKeys.COMMENTS); - timeEntry.Hours = dictionary.GetValue(RedmineKeys.HOURS); - timeEntry.Issue = - dictionary.GetValueAsIdentifiableName(dictionary.ContainsKey(RedmineKeys.ISSUE) - ? RedmineKeys.ISSUE - : RedmineKeys.ISSUE_ID); - timeEntry.Project = - dictionary.GetValueAsIdentifiableName(dictionary.ContainsKey(RedmineKeys.PROJECT) - ? RedmineKeys.PROJECT - : RedmineKeys.PROJECT_ID); - timeEntry.SpentOn = dictionary.GetValue(RedmineKeys.SPENT_ON); - timeEntry.User = dictionary.GetValueAsIdentifiableName(RedmineKeys.USER); - timeEntry.CustomFields = dictionary.GetValueAsCollection(RedmineKeys.CUSTOM_FIELDS); - timeEntry.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); - timeEntry.UpdatedOn = dictionary.GetValue(RedmineKeys.UPDATED_ON); - - return timeEntry; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as TimeEntry; - var result = new Dictionary(); - - if (entity != null) - { - result.WriteIdIfNotNull(entity.Issue, RedmineKeys.ISSUE_ID); - result.WriteIdIfNotNull(entity.Project, RedmineKeys.PROJECT_ID); - result.WriteIdIfNotNull(entity.Activity, RedmineKeys.ACTIVITY_ID); - - if (!entity.SpentOn.HasValue) entity.SpentOn = DateTime.Now; - - result.WriteDateOrEmpty(entity.SpentOn, RedmineKeys.SPENT_ON); - result.Add(RedmineKeys.HOURS, entity.Hours); - result.Add(RedmineKeys.COMMENTS, entity.Comments); - result.WriteArray(RedmineKeys.CUSTOM_FIELDS, entity.CustomFields, new IssueCustomFieldConverter(), - serializer); - - var root = new Dictionary(); - root[RedmineKeys.TIME_ENTRY] = result; - return root; - } - - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(TimeEntry)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/TrackerConverter.cs b/src/redmine-net40-api/JSonConverters/TrackerConverter.cs deleted file mode 100755 index 4fd15eab..00000000 --- a/src/redmine-net40-api/JSonConverters/TrackerConverter.cs +++ /dev/null @@ -1,80 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class TrackerConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var tracker = new Tracker - { - Id = dictionary.GetValue(RedmineKeys.ID), - Name = dictionary.GetValue(RedmineKeys.NAME) - }; - return tracker; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(Tracker)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/TrackerCustomFieldConverter.cs b/src/redmine-net40-api/JSonConverters/TrackerCustomFieldConverter.cs deleted file mode 100755 index 79ae155b..00000000 --- a/src/redmine-net40-api/JSonConverters/TrackerCustomFieldConverter.cs +++ /dev/null @@ -1,67 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class TrackerCustomFieldConverter : IdentifiableNameConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var entity = new TrackerCustomField(); - - entity.Id = dictionary.GetValue(RedmineKeys.ID); - entity.Name = dictionary.GetValue(RedmineKeys.NAME); - - return entity; - } - - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(TrackerCustomField)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/UploadConverter.cs b/src/redmine-net40-api/JSonConverters/UploadConverter.cs deleted file mode 100755 index 42e0e689..00000000 --- a/src/redmine-net40-api/JSonConverters/UploadConverter.cs +++ /dev/null @@ -1,92 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class UploadConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var upload = new Upload(); - - upload.ContentType = dictionary.GetValue(RedmineKeys.CONTENT_TYPE); - upload.FileName = dictionary.GetValue(RedmineKeys.FILENAME); - upload.Token = dictionary.GetValue(RedmineKeys.TOKEN); - upload.Description = dictionary.GetValue(RedmineKeys.DESCRIPTION); - return upload; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as Upload; - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.CONTENT_TYPE, entity.ContentType); - result.Add(RedmineKeys.FILENAME, entity.FileName); - result.Add(RedmineKeys.TOKEN, entity.Token); - result.Add(RedmineKeys.DESCRIPTION, entity.Description); - } - - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(Upload)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/UserConverter.cs b/src/redmine-net40-api/JSonConverters/UserConverter.cs deleted file mode 100644 index 9c41557b..00000000 --- a/src/redmine-net40-api/JSonConverters/UserConverter.cs +++ /dev/null @@ -1,126 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class UserConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var user = new User(); - user.Login = dictionary.GetValue(RedmineKeys.LOGIN); - user.Id = dictionary.GetValue(RedmineKeys.ID); - user.FirstName = dictionary.GetValue(RedmineKeys.FIRSTNAME); - user.LastName = dictionary.GetValue(RedmineKeys.LASTNAME); - user.Email = dictionary.GetValue(RedmineKeys.MAIL); - user.MailNotification = dictionary.GetValue(RedmineKeys.MAIL_NOTIFICATION); - user.AuthenticationModeId = dictionary.GetValue(RedmineKeys.AUTH_SOURCE_ID); - user.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); - user.LastLoginOn = dictionary.GetValue(RedmineKeys.LAST_LOGIN_ON); - user.ApiKey = dictionary.GetValue(RedmineKeys.API_KEY); - user.Status = dictionary.GetValue(RedmineKeys.STATUS); - user.MustChangePassword = dictionary.GetValue(RedmineKeys.MUST_CHANGE_PASSWD); - user.CustomFields = dictionary.GetValueAsCollection(RedmineKeys.CUSTOM_FIELDS); - user.Memberships = dictionary.GetValueAsCollection(RedmineKeys.MEMBERSHIPS); - user.Groups = dictionary.GetValueAsCollection(RedmineKeys.GROUPS); - - return user; - } - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as User; - - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.LOGIN, entity.Login); - result.Add(RedmineKeys.FIRSTNAME, entity.FirstName); - result.Add(RedmineKeys.LASTNAME, entity.LastName); - result.Add(RedmineKeys.MAIL, entity.Email); - if(!string.IsNullOrWhiteSpace(entity.MailNotification)) - { - result.Add(RedmineKeys.MAIL_NOTIFICATION, entity.MailNotification); - } - - if(!string.IsNullOrWhiteSpace(entity.Password)) - { - result.Add(RedmineKeys.PASSWORD, entity.Password); - } - - result.Add(RedmineKeys.MUST_CHANGE_PASSWD, entity.MustChangePassword.ToString().ToLowerInvariant()); - result.Add(RedmineKeys.STATUS, ((int)entity.Status).ToString(CultureInfo.InvariantCulture)); - - if(entity.AuthenticationModeId.HasValue) - { - result.WriteValueOrEmpty(entity.AuthenticationModeId, RedmineKeys.AUTH_SOURCE_ID); - } - result.WriteArray(RedmineKeys.CUSTOM_FIELDS, entity.CustomFields, new IssueCustomFieldConverter(), - serializer); - - var root = new Dictionary(); - root[RedmineKeys.USER] = result; - return root; - } - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(User)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/UserGroupConverter.cs b/src/redmine-net40-api/JSonConverters/UserGroupConverter.cs deleted file mode 100755 index c406e66b..00000000 --- a/src/redmine-net40-api/JSonConverters/UserGroupConverter.cs +++ /dev/null @@ -1,67 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class UserGroupConverter : IdentifiableNameConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(UserGroup)}); } - } - - #endregion - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var userGroup = new UserGroup(); - - userGroup.Id = dictionary.GetValue(RedmineKeys.ID); - userGroup.Name = dictionary.GetValue(RedmineKeys.NAME); - - return userGroup; - } - - return null; - } - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/VersionConverter.cs b/src/redmine-net40-api/JSonConverters/VersionConverter.cs deleted file mode 100755 index 6e57cc47..00000000 --- a/src/redmine-net40-api/JSonConverters/VersionConverter.cs +++ /dev/null @@ -1,106 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; -using Version = Redmine.Net.Api.Types.Version; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class VersionConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var version = new Version(); - - version.Id = dictionary.GetValue(RedmineKeys.ID); - version.Description = dictionary.GetValue(RedmineKeys.DESCRIPTION); - version.Name = dictionary.GetValue(RedmineKeys.NAME); - version.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); - version.UpdatedOn = dictionary.GetValue(RedmineKeys.UPDATED_ON); - version.DueDate = dictionary.GetValue(RedmineKeys.DUE_DATE); - version.Project = dictionary.GetValueAsIdentifiableName(RedmineKeys.PROJECT); - version.Sharing = dictionary.GetValue(RedmineKeys.SHARING); - version.Status = dictionary.GetValue(RedmineKeys.STATUS); - version.CustomFields = dictionary.GetValueAsCollection(RedmineKeys.CUSTOM_FIELDS); - - return version; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as Version; - - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.NAME, entity.Name); - result.Add(RedmineKeys.STATUS, entity.Status.ToString().ToLowerInvariant()); - result.Add(RedmineKeys.SHARING, entity.Sharing.ToString().ToLowerInvariant()); - result.Add(RedmineKeys.DESCRIPTION, entity.Description); - - var root = new Dictionary(); - result.WriteDateOrEmpty(entity.DueDate, RedmineKeys.DUE_DATE); - root[RedmineKeys.VERSION] = result; - return root; - } - - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(Version)}); } - } - - #endregion - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/WatcherConverter.cs b/src/redmine-net40-api/JSonConverters/WatcherConverter.cs deleted file mode 100755 index d41a3062..00000000 --- a/src/redmine-net40-api/JSonConverters/WatcherConverter.cs +++ /dev/null @@ -1,84 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class WatcherConverter : JavaScriptConverter - { - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] {typeof(Watcher)}); } - } - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var watcher = new Watcher(); - - watcher.Id = dictionary.GetValue(RedmineKeys.ID); - watcher.Name = dictionary.GetValue(RedmineKeys.NAME); - - return watcher; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as Watcher; - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.ID, entity.Id); - } - - return result; - } - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters/WikiPageConverter.cs b/src/redmine-net40-api/JSonConverters/WikiPageConverter.cs deleted file mode 100755 index d907ba94..00000000 --- a/src/redmine-net40-api/JSonConverters/WikiPageConverter.cs +++ /dev/null @@ -1,98 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class WikiPageConverter : JavaScriptConverter - { - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new List(new[] { typeof(WikiPage) }); } - } - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var tracker = new WikiPage(); - - tracker.Id = dictionary.GetValue(RedmineKeys.ID); - tracker.Author = dictionary.GetValueAsIdentifiableName(RedmineKeys.AUTHOR); - tracker.Comments = dictionary.GetValue(RedmineKeys.COMMENTS); - tracker.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); - tracker.Text = dictionary.GetValue(RedmineKeys.TEXT); - tracker.Title = dictionary.GetValue(RedmineKeys.TITLE); - tracker.UpdatedOn = dictionary.GetValue(RedmineKeys.UPDATED_ON); - tracker.Version = dictionary.GetValue(RedmineKeys.VERSION); - tracker.Attachments = dictionary.GetValueAsCollection(RedmineKeys.ATTACHMENTS); - - return tracker; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as WikiPage; - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.TEXT, entity.Text); - result.Add(RedmineKeys.COMMENTS, entity.Comments); - result.WriteValueOrEmpty(entity.Version, RedmineKeys.VERSION); - result.WriteArray(RedmineKeys.UPLOADS, entity.Uploads, new UploadConverter(), serializer); - - var root = new Dictionary(); - root[RedmineKeys.WIKI_PAGE] = result; - return root; - } - - return result; - } - } -} \ No newline at end of file diff --git a/src/redmine-net40-api/JSonConverters2/IJsonConverter.cs b/src/redmine-net40-api/JSonConverters2/IJsonConverter.cs deleted file mode 100755 index 3617b563..00000000 --- a/src/redmine-net40-api/JSonConverters2/IJsonConverter.cs +++ /dev/null @@ -1,17 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; - -namespace Redmine.Net.Api.JSonConverters2 -{ - //public interface IJsonConverter : IJsonConverter - //{ - // void Serialize(JsonWriter writer, T obj, JsonSerializer serializer); - // new T Deserialize(JObject obj, JsonSerializer serializer); - //} - - public interface IJsonConverter - { - void Serialize(JsonWriter writer, object obj, JsonSerializer serializer); - object Deserialize(JObject obj, JsonSerializer serializer); - } -} diff --git a/src/redmine-net40-api/JSonConverters2/IssueConverter.cs b/src/redmine-net40-api/JSonConverters2/IssueConverter.cs deleted file mode 100755 index 704be682..00000000 --- a/src/redmine-net40-api/JSonConverters2/IssueConverter.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System; -using System.Globalization; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters2 -{ - public class IssueConverter : IJsonConverter - { - public void Serialize(JsonWriter writer, object obj, JsonSerializer serializer) - { - if (obj == null) return; - - var issue = (Issue) obj; - - writer.WriteStartObject(); - writer.WriteValue(RedmineKeys.ISSUE); - - writer.WriteProperty(RedmineKeys.SUBJECT, issue.Subject); - writer.WriteProperty(RedmineKeys.DESCRIPTION, issue.Description); - writer.WriteProperty(RedmineKeys.NOTES, issue.Notes); - - if (issue.Id != 0) - { - writer.WriteProperty(RedmineKeys.PRIVATE_NOTES, issue.PrivateNotes); - } - - writer.WriteProperty(RedmineKeys.IS_PRIVATE, issue.IsPrivate); - - writer.WriteIdIfNotNull(RedmineKeys.PROJECT_ID, issue.Project); - writer.WriteIdIfNotNull(RedmineKeys.PRIORITY_ID, issue.Priority); - writer.WriteIdIfNotNull(RedmineKeys.STATUS_ID, issue.Status); - writer.WriteIdIfNotNull(RedmineKeys.CATEGORY_ID, issue.Category); - writer.WriteIdIfNotNull(RedmineKeys.TRACKER_ID, issue.Tracker); - writer.WriteIdIfNotNull(RedmineKeys.ASSIGNED_TO_ID, issue.AssignedTo); - writer.WriteIdIfNotNull(RedmineKeys.FIXED_VERSION_ID, issue.FixedVersion); - writer.WriteValueOrEmpty(RedmineKeys.ESTIMATED_HOURS, issue.EstimatedHours); - - writer.WriteIdOrEmpty(RedmineKeys.PARENT_ISSUE_ID, issue.ParentIssue); - writer.WriteDateOrEmpty(RedmineKeys.START_DATE, issue.StartDate); - writer.WriteDateOrEmpty(RedmineKeys.DUE_DATE, issue.DueDate); - writer.WriteDateOrEmpty(RedmineKeys.UPDATED_ON, issue.DueDate); - - if (issue.DoneRatio != null) - writer.WriteProperty(RedmineKeys.DONE_RATIO, issue.DoneRatio.Value.ToString(CultureInfo.InvariantCulture)); - - if (issue.SpentHours != null) - writer.WriteProperty(RedmineKeys.SPENT_HOURS, issue.SpentHours.Value.ToString(CultureInfo.InvariantCulture)); - - writer.WriteArray(RedmineKeys.UPLOADS, issue.Uploads, new UploadConverter(), serializer); - writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, issue.CustomFields, new IssueCustomFieldConverter(), serializer); - - writer.WriteIdsArray(RedmineKeys.WATCHER_USER_IDS, issue.Watchers); - - writer.WriteEndObject(); - } - - public object Deserialize(JObject obj, JsonSerializer serializer) - { - if (obj == null) return null; - - var issue = new Issue - { - Id = obj.Value(RedmineKeys.ID), - Description = obj.Value(RedmineKeys.DESCRIPTION), - - Project = obj.GetValueAsIdentifiableName(RedmineKeys.PROJECT), - Tracker = obj.GetValueAsIdentifiableName(RedmineKeys.TRACKER), - Status = obj.GetValueAsIdentifiableName(RedmineKeys.STATUS), - - CreatedOn = obj.Value(RedmineKeys.CREATED_ON), - UpdatedOn = obj.Value(RedmineKeys.UPDATED_ON), - ClosedOn = obj.Value(RedmineKeys.CLOSED_ON), - - Priority = obj.GetValueAsIdentifiableName(RedmineKeys.PRIORITY), - Author = obj.GetValueAsIdentifiableName(RedmineKeys.AUTHOR), - AssignedTo = obj.GetValueAsIdentifiableName(RedmineKeys.ASSIGNED_TO), - Category = obj.GetValueAsIdentifiableName(RedmineKeys.CATEGORY), - FixedVersion = obj.GetValueAsIdentifiableName(RedmineKeys.FIXED_VERSION), - - Subject = obj.Value(RedmineKeys.SUBJECT), - Notes = obj.Value(RedmineKeys.NOTES), - IsPrivate = obj.Value(RedmineKeys.IS_PRIVATE), - StartDate = obj.Value(RedmineKeys.START_DATE), - DueDate = obj.Value(RedmineKeys.DUE_DATE), - SpentHours = obj.Value(RedmineKeys.SPENT_HOURS), - DoneRatio = obj.Value(RedmineKeys.DONE_RATIO), - EstimatedHours = obj.Value(RedmineKeys.ESTIMATED_HOURS), - - ParentIssue = obj.GetValueAsIdentifiableName(RedmineKeys.PARENT), - CustomFields = obj.GetValueAsCollection(RedmineKeys.CUSTOM_FIELDS), - Attachments = obj.GetValueAsCollection(RedmineKeys.ATTACHMENTS), - Relations = obj.GetValueAsCollection(RedmineKeys.RELATIONS), - Journals = obj.GetValueAsCollection(RedmineKeys.JOURNALS), - Changesets = obj.GetValueAsCollection(RedmineKeys.CHANGESETS), - Watchers = obj.GetValueAsCollection(RedmineKeys.WATCHERS), - Children = obj.GetValueAsCollection(RedmineKeys.CHILDREN) - }; - - return issue; - } - } -} diff --git a/src/redmine-net40-api/JSonConverters2/IssueCustomFieldConverter.cs b/src/redmine-net40-api/JSonConverters2/IssueCustomFieldConverter.cs deleted file mode 100755 index d93d870a..00000000 --- a/src/redmine-net40-api/JSonConverters2/IssueCustomFieldConverter.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters2 -{ - public class IssueCustomFieldConverter : IJsonConverter - { - public void Serialize(JsonWriter writer, object obj, JsonSerializer serializer) - { - if (obj == null) return; - - var item = (IssueCustomField)obj; - - if (item.Values == null) return; - - var count = item.Values.Count; - - if (count > 1) - { - writer.WriteProperty(RedmineKeys.VALUE, item.Values.Select(x => x.Info).ToArray()); - } - else - { - writer.WriteProperty(RedmineKeys.VALUE, count > 0 ? item.Values[0].Info : null); - } - } - - public object Deserialize(JObject obj, JsonSerializer serializer) - { - if (obj == null) return null; - - var customField = new IssueCustomField - { - Id = obj.Value(RedmineKeys.ID), - Name = obj.Value(RedmineKeys.NAME), - Multiple = obj.Value(RedmineKeys.MULTIPLE) - }; - - var val = obj.Value(RedmineKeys.VALUE); - var items = serializer.Deserialize(new JTokenReader(val.ToString()), typeof(List)) as List; - - customField.Values = items; - - if (items == null) return customField; - if (customField.Values == null) customField.Values = new List(); - - var list = val as ArrayList; - - if (list != null) - { - foreach (string value in list) - { - customField.Values.Add(new CustomFieldValue {Info = value}); - } - } - else - { - customField.Values.Add(new CustomFieldValue {Info = val as string}); - } - - return customField; - } - } -} diff --git a/src/redmine-net40-api/JSonConverters2/UploadConverter.cs b/src/redmine-net40-api/JSonConverters2/UploadConverter.cs deleted file mode 100755 index ea0bc540..00000000 --- a/src/redmine-net40-api/JSonConverters2/UploadConverter.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Newtonsoft.Json.Linq; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters2 -{ - public class UploadConverter : IJsonConverter - { - public void Serialize(Newtonsoft.Json.JsonWriter writer, object obj, Newtonsoft.Json.JsonSerializer serializer) - { - if (obj == null) return; - - var item = (Upload)obj; - - writer.WriteProperty(RedmineKeys.CONTENT_TYPE, item.ContentType); - writer.WriteProperty(RedmineKeys.FILENAME, item.FileName); - writer.WriteProperty(RedmineKeys.TOKEN, item.Token); - writer.WriteProperty(RedmineKeys.DESCRIPTION, item.Description); - } - - public object Deserialize(JObject obj, Newtonsoft.Json.JsonSerializer serializer) - { - if (obj == null) return null; - - var upload = new Upload - { - ContentType = obj.Value(RedmineKeys.CONTENT_TYPE), - FileName = obj.Value(RedmineKeys.FILENAME), - Token = obj.Value(RedmineKeys.TOKEN), - Description = obj.Value(RedmineKeys.DESCRIPTION) - }; - - return upload; - } - } -} diff --git a/src/redmine-net40-api/Properties/AssemblyInfo.cs b/src/redmine-net40-api/Properties/AssemblyInfo.cs deleted file mode 100644 index 84914b31..00000000 --- a/src/redmine-net40-api/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("redmine-net40-api")] -[assembly: AssemblyDescription("redmine-net-api is a library for communicating with a Redmine project management application.")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("redmine-net40-api")] -[assembly: AssemblyCopyright("Copyright © Adrian Popescu 2011 - 2019")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("8b72d103-5fba-4423-9698-ad097635a743")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.4")] -[assembly: AssemblyFileVersion("1.0.4")] diff --git a/src/redmine-net40-api/redmine-net40-api.csproj b/src/redmine-net40-api/redmine-net40-api.csproj deleted file mode 100644 index a413fc34..00000000 --- a/src/redmine-net40-api/redmine-net40-api.csproj +++ /dev/null @@ -1,331 +0,0 @@ - - - - - Debug - AnyCPU - {22492A69-B890-4D5B-A2FC-E2F6C63935B8} - Library - Properties - Redmine.Net.Api - redmine-net40-api - v4.0 - 512 - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - bin\Debug\redmine-net40-api.XML - - - none - true - bin\Release\ - TRACE - prompt - 4 - bin\Release\redmine-net40-api.XML - - - true - bin\DebugXML\ - DEBUG;TRACE - full - AnyCPU - prompt - MinimumRecommendedRules.ruleset - 4 - false - - - true - bin\DebugJSON\ - DEBUG;TRACE - full - AnyCPU - prompt - MinimumRecommendedRules.ruleset - 4 - false - - - - - - - - - - - - - - Extensions\Extensions.cs - - - Internals\HashCodeHelper.cs - - - Internals\UrlHelper.cs - - - Internals\DataHelper.cs - - - Extensions\NameValueCollectionExtensions.cs - - - HttpVerbs.cs - - - Internals\WebApiHelper.cs - - - Logging\ColorConsoleLogger.cs - - - Logging\ConsoleLogger.cs - - - Logging\ILogger.cs - - - Logging\LogEntry.cs - - - Logging\Logger.cs - - - Logging\LoggerExtensions.cs - - - Logging\LoggingEventType.cs - - - Logging\RedmineConsoleTraceListener.cs - - - Logging\TraceLogger.cs - - - RedmineKeys.cs - - - RedmineManager.cs - - - RedmineWebClient.cs - Component - - - Types\Attachment.cs - - - Types\Attachments.cs - - - Types\ChangeSet.cs - - - Types\CustomField.cs - - - Types\CustomFieldPossibleValue.cs - - - Types\CustomFieldRole.cs - - - Types\CustomFieldValue.cs - - - Types\Detail.cs - - - Types\Error.cs - - - Types\File.cs - - - Types\Group.cs - - - Types\GroupUser.cs - - - Types\Identifiable.cs - - - Types\IdentifiableName.cs - - - Types\IRedmineManager.cs - - - Types\IRedmineWebClient.cs - - - Types\Issue.cs - - - Types\IssueCategory.cs - - - Types\IssueChild.cs - - - Types\IssueCustomField.cs - - - Types\IssuePriority.cs - - - Types\IssueRelation.cs - - - Types\IssueRelationType.cs - - - Types\IssueStatus.cs - - - Types\Journal.cs - - - Types\Membership.cs - - - Types\MembershipRole.cs - - - Types\News.cs - - - Types\PaginatedObjects.cs - - - Types\Permission.cs - - - Types\Project.cs - - - Types\ProjectEnabledModule.cs - - - Types\ProjectIssueCategory.cs - - - Types\ProjectMembership.cs - - - Types\ProjectStatus.cs - - - Types\ProjectTracker.cs - - - Types\Query.cs - - - Types\Role.cs - - - Types\TimeEntry.cs - - - Types\TimeEntryActivity.cs - - - Types\Tracker.cs - - - Types\TrackerCustomField.cs - - - Types\Upload.cs - - - Types\User.cs - - - Types\UserGroup.cs - - - Types\UserStatus.cs - - - Types\Version.cs - - - Types\Watcher.cs - - - Types\WikiPage.cs - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Types\IValue.cs - - - - - - - - - \ No newline at end of file diff --git a/src/redmine-net450-api-signed/Properties/AssemblyInfo.cs b/src/redmine-net450-api-signed/Properties/AssemblyInfo.cs deleted file mode 100644 index 6e7337ce..00000000 --- a/src/redmine-net450-api-signed/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("redmine-net45-api-signed")] -[assembly: AssemblyDescription("redmine-net-api is a library for communicating with a Redmine project management application.")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("redmine-net45-api-signed")] -[assembly: AssemblyCopyright("Copyright © Adrian Popescu 2011 - 2019")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("1d1fb5e7-61a9-4ac5-9a84-5714f08e09cb")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.4")] -[assembly: AssemblyFileVersion("1.0.4")] diff --git a/src/redmine-net450-api-signed/redmine-net-api.snk b/src/redmine-net450-api-signed/redmine-net-api.snk deleted file mode 100755 index 6d40dc4bcf3a42efef28da84214dbd403deeb96e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50096IN-UAP2W7Z8Taq6HFlob3_zbGu}!n%`6YC2Vydx!NK+V zp`C9Rl^AXD@alQQf6TU;crI}udV(guj5H)*NQxvl^&$`zNi{a459MV*S05dQcQmf` zI$7fmYu~1esGxWp-hS=Af}lAbvrp2c_y5|i2m;JL!0Nnh?AW#RbcST{j(*V+V~?shO8SZNI?@B31WDMLDn9D3Dof3n&O^Ip zugL!?xxb@*0?a|hARq7-i|P_6divVkIr62Ee2=d)&>*{6A?_!)a9wo+Pm0@#Z@A zNJCL>`vp`*no - - - - Debug - AnyCPU - {028B9120-A7FC-4B23-AA9C-F18087058F76} - Library - Properties - Redmine.Net.Api - redmine-net45-api-signed - v4.5 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - bin\Debug\redmine-net45-api-signed.XML - - - none - true - bin\Release\ - TRACE - prompt - 4 - bin\Release\redmine-net45-api-signed.XML - - - true - - - redmine-net-api.snk - - - true - bin\DebugXML\ - DEBUG;TRACE - full - AnyCPU - prompt - MinimumRecommendedRules.ruleset - 4 - false - - - true - bin\DebugJSON\ - DEBUG;TRACE - full - AnyCPU - prompt - MinimumRecommendedRules.ruleset - 4 - false - - - - - - - - - - - - - - - Internals\HashCodeHelper.cs - - - Internals\DataHelper.cs - - - Extensions\NameValueCollectionExtensions.cs - - - HttpVerbs.cs - - - Internals\UrlHelper.cs - - - Extensions\XmlWriterExtensions.cs - - - Internals\WebApiHelper.cs - - - Logging\ColorConsoleLogger.cs - - - Logging\ConsoleLogger.cs - - - Logging\ILogger.cs - - - Logging\LogEntry.cs - - - Logging\Logger.cs - - - Logging\LoggerExtensions.cs - - - Logging\LoggingEventType.cs - - - Logging\RedmineConsoleTraceListener.cs - - - Logging\TraceLogger.cs - - - RedmineKeys.cs - - - RedmineManager.cs - - - RedmineWebClient.cs - Component - - - Types\Attachment.cs - - - Types\Attachments.cs - - - Types\ChangeSet.cs - - - Types\CustomField.cs - - - Types\CustomFieldPossibleValue.cs - - - Types\CustomFieldRole.cs - - - Types\CustomFieldValue.cs - - - Types\Detail.cs - - - Types\Error.cs - - - Types\File.cs - - - Types\Group.cs - - - Types\GroupUser.cs - - - Types\Identifiable.cs - - - Types\IdentifiableName.cs - - - Types\IRedmineManager.cs - - - Types\IRedmineWebClient.cs - - - Types\Issue.cs - - - Types\IssueCategory.cs - - - Types\IssueChild.cs - - - Types\IssueCustomField.cs - - - Types\IssuePriority.cs - - - Types\IssueRelation.cs - - - Types\IssueRelationType.cs - - - Types\IssueStatus.cs - - - Types\IValue.cs - - - Types\Journal.cs - - - Types\Membership.cs - - - Types\MembershipRole.cs - - - Types\News.cs - - - Types\PaginatedObjects.cs - - - Types\Permission.cs - - - Types\Project.cs - - - Types\ProjectEnabledModule.cs - - - Types\ProjectIssueCategory.cs - - - Types\ProjectMembership.cs - - - Types\ProjectStatus.cs - - - Types\ProjectTracker.cs - - - Types\Query.cs - - - Types\Role.cs - - - Types\TimeEntry.cs - - - Types\TimeEntryActivity.cs - - - Types\Tracker.cs - - - Types\TrackerCustomField.cs - - - Types\Upload.cs - - - Types\User.cs - - - Types\UserGroup.cs - - - Types\UserStatus.cs - - - Types\Version.cs - - - Types\Watcher.cs - - - Types\WikiPage.cs - - - Extensions\CollectionExtensions.cs - - - Extensions\JsonExtensions.cs - - - Extensions\WebExtensions.cs - - - Extensions\XmlReaderExtensions.cs - - - Internals\RedmineSerializer.cs - - - Internals\RedmineSerializerJson.cs - - - JsonConverters\AttachmentConverter.cs - - - JsonConverters\AttachmentsConverter.cs - - - JsonConverters\ChangeSetConverter.cs - - - JsonConverters\CustomFieldConverter.cs - - - JsonConverters\CustomFieldPossibleValueConverter.cs - - - JsonConverters\CustomFieldRoleConverter.cs - - - JsonConverters\DetailConverter.cs - - - JsonConverters\ErrorConverter.cs - - - JsonConverters\FileConverter.cs - - - JsonConverters\GroupConverter.cs - - - JsonConverters\GroupUserConverter.cs - - - JsonConverters\IdentifiableNameConverter.cs - - - JsonConverters\IssueCategoryConverter.cs - - - JsonConverters\IssueChildConverter.cs - - - JsonConverters\IssueConverter.cs - - - JsonConverters\IssueCustomFieldConverter.cs - - - JsonConverters\IssuePriorityConverter.cs - - - JsonConverters\IssueRelationConverter.cs - - - JsonConverters\IssueStatusConverter.cs - - - JsonConverters\JournalConverter.cs - - - JsonConverters\MembershipConverter.cs - - - JsonConverters\MembershipRoleConverter.cs - - - JsonConverters\NewsConverter.cs - - - JsonConverters\PermissionConverter.cs - - - JsonConverters\ProjectConverter.cs - - - JsonConverters\ProjectEnabledModuleConverter.cs - - - JsonConverters\ProjectIssueCategoryConverter.cs - - - JsonConverters\ProjectMembershipConverter.cs - - - JsonConverters\ProjectTrackerConverter.cs - - - JsonConverters\QueryConverter.cs - - - JsonConverters\RoleConverter.cs - - - JsonConverters\TimeEntryActivityConverter.cs - - - JsonConverters\TimeEntryConverter.cs - - - JsonConverters\TrackerConverter.cs - - - JsonConverters\TrackerCustomFieldConverter.cs - - - JsonConverters\UploadConverter.cs - - - JsonConverters\UserConverter.cs - - - JsonConverters\UserGroupConverter.cs - - - JsonConverters\VersionConverter.cs - - - JsonConverters\WatcherConverter.cs - - - JsonConverters\WikiPageConverter.cs - - - MimeFormat.cs - - - Async\RedmineManagerAsync.cs - - - Extensions\DisposableExtension.cs - - - Extensions\FunctionalExtensions.cs - - - Extensions\TaskExtensions.cs - - - Internals\WebApiAsyncHelper.cs - - - - Exceptions\NotFoundException.cs - - - Exceptions\RedmineException.cs - - - Exceptions\RedmineTimeoutException.cs - - - Exceptions\NameResolutionFailureException.cs - - - InternalServerErrorException.cs - - - Exceptions\UnauthorizedException.cs - - - Exceptions\ForbiddenException.cs - - - Exceptions\ConflictException.cs - - - Exceptions\NotAcceptableException.cs - - - - - - - - - \ No newline at end of file diff --git a/src/redmine-net450-api/Extensions/DisposableExtension.cs b/src/redmine-net450-api/Extensions/DisposableExtension.cs deleted file mode 100755 index 95f6e26a..00000000 --- a/src/redmine-net450-api/Extensions/DisposableExtension.cs +++ /dev/null @@ -1,40 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; - -namespace Redmine.Net.Api.Extensions -{ - /// - /// - /// - public static class DisposableExtension - { - /// - /// Usings the specified resource factory. - /// - /// The type of the resource. - /// The type of the result. - /// The resource factory. - /// The function. - /// - public static TResult Using(Func resourceFactory, Func fn) - where TResource : IDisposable - { - using (var resource = resourceFactory()) return fn(resource); - } - } -} \ No newline at end of file diff --git a/src/redmine-net450-api/Extensions/FunctionalExtensions.cs b/src/redmine-net450-api/Extensions/FunctionalExtensions.cs deleted file mode 100755 index b0a94161..00000000 --- a/src/redmine-net450-api/Extensions/FunctionalExtensions.cs +++ /dev/null @@ -1,105 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; - -namespace Redmine.Net.Api.Extensions -{ - /// - /// - /// - public static class FunctionalExtensions - { - - /// - /// The Tee extension method takes it�s name from the corresponding UNIX command which is used in command pipelines to cause a side-effect with a given input and return the original value. - /// - /// This. - /// Action. - /// The 1st type parameter. - public static T Tee(this T @this, Action action) - { - action(@this); - return @this; - } - - /// - /// Maps the specified function. - /// - /// The type of the source. - /// The type of the result. - /// The this. - /// The function. - /// - public static TResult Map(this TSource @this, Func fn) - { - return fn(@this); - } - - /// - /// Curries the specified function. - /// - /// - /// - /// - /// The function. - /// - public static Func> Curry(this Func func) - { - return a => b => func(a, b); - } - - /// - /// Curries the specified function. - /// - /// - /// - /// - /// - /// The function. - /// - public static Func>> Curry(this Func func) - { - return a => b => c => func(a, b, c); - } - - /// - /// Caches the specified function. - /// - /// - /// The function. - /// The interval. - /// - public static Func Cache(Func func, int interval) - { - var cachedValue = func(); - var timeCached= DateTime.Now; - - Func cachedFunc = () => - { - if((DateTime.Now - timeCached).Seconds >= interval) - { - timeCached = DateTime.Now; - cachedValue = func(); - } - - return cachedValue; - }; - - return cachedFunc; - } - } -} \ No newline at end of file diff --git a/src/redmine-net450-api/Extensions/TaskExtensions.cs b/src/redmine-net450-api/Extensions/TaskExtensions.cs deleted file mode 100755 index b379e6fa..00000000 --- a/src/redmine-net450-api/Extensions/TaskExtensions.cs +++ /dev/null @@ -1,136 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Threading.Tasks; - -namespace Redmine.Net.Api.Extensions -{ - - /// - /// - /// - public static class TaskExtensions - { - - /// - /// Maps the asynchronous. - /// - /// The type of the source. - /// The type of the result. - /// The this. - /// The function. - /// - public static async Task MapAsync(this Task @this, Func> fn) - { - return await fn(await @this); - } - - /// - /// Maps the asynchronous. - /// - /// The type of the source. - /// The type of the result. - /// The this. - /// The function. - /// - public static async Task MapAsync(this TSource @this, Func> fn) - { - return await fn(@this); - } - - /// - /// Maps the asynchronous. - /// - /// The type of the source. - /// The type of the result. - /// The this. - /// The function. - /// - public static async Task MapAsync(this Task @this, Func fn) - { - return fn(await @this); - } - - /// - /// Tees the asynchronous. - /// - /// The type of the source. - /// The type of the result. - /// The this. - /// The act. - /// - public static async Task TeeAsync(this TSource @this, Func> act) - { - await act(@this); - return @this; - } - - /// - /// Tees the asynchronous. - /// - /// - /// The this. - /// The act. - /// - public static async Task TeeAsync(this Task @this, Action act) - { - act(await @this); - return await @this; - } - - /// - /// Tees the asynchronous. - /// - /// The type of the source. - /// The type of the result. - /// The this. - /// The act. - /// - public static async Task TeeAsync(this Task @this, Func> act) - { - await act(await @this); - return await @this; - } - - /// - /// Tees the asynchronous. - /// - /// The type of the source. - /// The this. - /// The act. - /// - public static async Task TeeAsync(this TSource @this, Func> act) - { - await act(@this); - return @this; - } - - /// - /// Tees the specified act. - /// - /// The type of the source. - /// The type of the result. - /// The this. - /// The act. - /// - public static async Task Tee(this Task @this, Func act) - { - act(await @this); - return await @this; - } - } -} \ No newline at end of file diff --git a/src/redmine-net450-api/Properties/AssemblyInfo.cs b/src/redmine-net450-api/Properties/AssemblyInfo.cs deleted file mode 100644 index 1e553dcd..00000000 --- a/src/redmine-net450-api/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("redmine-net45-api")] -[assembly: AssemblyDescription("redmine-net-api is a library for communicating with a Redmine project management application.")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("redmine-net45-api")] -[assembly: AssemblyCopyright("Copyright © Adrian Popescu 2011 - 2019")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("310d3e49-5865-4b90-b645-dad29b388ac8")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.4")] -[assembly: AssemblyFileVersion("1.0.4")] diff --git a/src/redmine-net450-api/redmine-net450-api.csproj b/src/redmine-net450-api/redmine-net450-api.csproj deleted file mode 100644 index e8190258..00000000 --- a/src/redmine-net450-api/redmine-net450-api.csproj +++ /dev/null @@ -1,464 +0,0 @@ - - - - - Debug - AnyCPU - {AEDFD095-F4B0-4630-B41A-9A22169456E9} - Library - Properties - Redmine.Net.Api - redmine-net45-api - v4.5 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - bin\Debug\redmine-net45-api.XML - - - none - true - bin\Release\ - TRACE - prompt - 4 - bin\Release\redmine-net45-api.XML - - - true - bin\DebugXML\ - DEBUG;TRACE - full - AnyCPU - prompt - MinimumRecommendedRules.ruleset - 4 - false - - - true - bin\DebugJSON\ - DEBUG;TRACE - full - AnyCPU - prompt - MinimumRecommendedRules.ruleset - 4 - false - - - - - - - - - - - - - - - Internals\HashCodeHelper.cs - - - Internals\DataHelper.cs - - - Extensions\XmlWriterExtensions.cs - - - Extensions\NameValueCollectionExtensions.cs - - - HttpVerbs.cs - - - Internals\UrlHelper.cs - - - Internals\WebApiHelper.cs - - - Logging\ColorConsoleLogger.cs - - - Logging\ConsoleLogger.cs - - - Logging\ILogger.cs - - - Logging\LogEntry.cs - - - Logging\Logger.cs - - - Logging\LoggerExtensions.cs - - - Logging\LoggingEventType.cs - - - Logging\RedmineConsoleTraceListener.cs - - - Logging\TraceLogger.cs - - - RedmineKeys.cs - - - RedmineManager.cs - - - RedmineWebClient.cs - Component - - - Types\Attachment.cs - - - Types\Attachments.cs - - - Types\ChangeSet.cs - - - Types\CustomField.cs - - - Types\CustomFieldPossibleValue.cs - - - Types\CustomFieldRole.cs - - - Types\CustomFieldValue.cs - - - Types\Detail.cs - - - Types\Error.cs - - - Types\File.cs - - - Types\Group.cs - - - Types\GroupUser.cs - - - Types\Identifiable.cs - - - Types\IdentifiableName.cs - - - Types\IRedmineManager.cs - - - Types\IRedmineWebClient.cs - - - Types\Issue.cs - - - Types\IssueCategory.cs - - - Types\IssueChild.cs - - - Types\IssueCustomField.cs - - - Types\IssuePriority.cs - - - Types\IssueRelation.cs - - - Types\IssueRelationType.cs - - - Types\IssueStatus.cs - - - Types\Journal.cs - - - Types\Membership.cs - - - Types\MembershipRole.cs - - - Types\News.cs - - - Types\PaginatedObjects.cs - - - Types\Permission.cs - - - Types\Project.cs - - - Types\ProjectEnabledModule.cs - - - Types\ProjectIssueCategory.cs - - - Types\ProjectMembership.cs - - - Types\ProjectStatus.cs - - - Types\ProjectTracker.cs - - - Types\Query.cs - - - Types\Role.cs - - - Types\TimeEntry.cs - - - Types\TimeEntryActivity.cs - - - Types\Tracker.cs - - - Types\TrackerCustomField.cs - - - Types\Upload.cs - - - Types\User.cs - - - Types\UserGroup.cs - - - Types\UserStatus.cs - - - Types\Version.cs - - - Types\Watcher.cs - - - Types\WikiPage.cs - - - Extensions\CollectionExtensions.cs - - - Extensions\JsonExtensions.cs - - - Extensions\WebExtensions.cs - - - Extensions\XmlReaderExtensions.cs - - - Internals\RedmineSerializer.cs - - - Internals\RedmineSerializerJson.cs - - - JSonConverters\AttachmentConverter.cs - - - JSonConverters\AttachmentsConverter.cs - - - JSonConverters\ChangeSetConverter.cs - - - JSonConverters\CustomFieldConverter.cs - - - JSonConverters\CustomFieldPossibleValueConverter.cs - - - JSonConverters\CustomFieldRoleConverter.cs - - - JSonConverters\DetailConverter.cs - - - JSonConverters\ErrorConverter.cs - - - JsonConverters\FileConverter.cs - - - JSonConverters\GroupConverter.cs - - - JSonConverters\GroupUserConverter.cs - - - JSonConverters\IdentifiableNameConverter.cs - - - JSonConverters\IssueCategoryConverter.cs - - - JSonConverters\IssueChildConverter.cs - - - JSonConverters\IssueConverter.cs - - - JSonConverters\IssueCustomFieldConverter.cs - - - JSonConverters\IssuePriorityConverter.cs - - - JSonConverters\IssueRelationConverter.cs - - - JSonConverters\IssueStatusConverter.cs - - - JSonConverters\JournalConverter.cs - - - JSonConverters\MembershipConverter.cs - - - JSonConverters\MembershipRoleConverter.cs - - - JSonConverters\NewsConverter.cs - - - JSonConverters\PermissionConverter.cs - - - JSonConverters\ProjectConverter.cs - - - JSonConverters\ProjectEnabledModuleConverter.cs - - - JSonConverters\ProjectIssueCategoryConverter.cs - - - JSonConverters\ProjectMembershipConverter.cs - - - JSonConverters\ProjectTrackerConverter.cs - - - JSonConverters\QueryConverter.cs - - - JSonConverters\RoleConverter.cs - - - JSonConverters\TimeEntryActivityConverter.cs - - - JSonConverters\TimeEntryConverter.cs - - - JSonConverters\TrackerConverter.cs - - - JSonConverters\TrackerCustomFieldConverter.cs - - - JSonConverters\UploadConverter.cs - - - JSonConverters\UserConverter.cs - - - JSonConverters\UserGroupConverter.cs - - - JSonConverters\VersionConverter.cs - - - JSonConverters\WatcherConverter.cs - - - JSonConverters\WikiPageConverter.cs - - - MimeFormat.cs - - - - - - - Types\IValue.cs - - - Exceptions\NotFoundException.cs - - - Exceptions\RedmineException.cs - - - Exceptions\RedmineTimeoutException.cs - - - Exceptions\NameResolutionFailureException.cs - - - Exceptions\InternalServerErrorException.cs - - - Exceptions\UnauthorizedException.cs - - - Exceptions\ForbiddenException.cs - - - Exceptions\ConflictException.cs - - - Exceptions\NotAcceptableException.cs - - - - - - - - - \ No newline at end of file diff --git a/src/redmine-net451-api-signed/Properties/AssemblyInfo.cs b/src/redmine-net451-api-signed/Properties/AssemblyInfo.cs deleted file mode 100644 index 5fead8c4..00000000 --- a/src/redmine-net451-api-signed/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("redmine-net451-api-signed")] -[assembly: AssemblyDescription("redmine-net-api is a library for communicating with a Redmine project management application.")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("redmine-net451-api-signed")] -[assembly: AssemblyCopyright("Copyright © Adrian Popescu 2011 - 2019")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("42de2d03-79b1-475d-9f39-4a1acea67297")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.4.0")] -[assembly: AssemblyFileVersion("1.0.4.0")] diff --git a/src/redmine-net451-api-signed/redmine-net-api.snk b/src/redmine-net451-api-signed/redmine-net-api.snk deleted file mode 100755 index 9bc3c18fa396f7f21f34cb39af87c94da753e446..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50097JhH$|Om5knNbcroe2Z&qJt2a_rxL?`8Y-_p1Hlja}r`6m`Q;mf7#QGzQrhxCY?fqYn+e7$m`H3ETq#B z1s1D$y)N0w9JpDEbC=#6?mi@J2nYoygYW_H!bbX*5r&;GFR!J_^`<6BjvQS3V?b38 zq*6Bf{0ztRc-{Q>b334}+O}G!``r64EWLEDki1+)5shPCi4n%h$NEV^%+?*>r8PLz zFEpgWH_*vcio!WQZUU4)Z$8`1-9y%4|IJ>k3-mmFRlIOC!@s_1Fx5maAEh4oIo{7^ zV(S_-Vmi!uca%g&p=vjbD}(bYwH-5e<+IZ6+n+%rplsIH8_c-tXy2ttp+y5QjX>te zv>U)+-XHrE#bR+&6o%v^UPJVF;w?7!8V56@4)R#HI^&noK7%iYTy&bN=}L_iN-?Pe zs$<+Z99tzgv zYddaxg8|Lc(61YXk{<7fo?4w^c1cKzJJZmSZP!M=s+Byd-gq9|l@WmYu0zJ`j5E@l iRu(`pg{ek4Nqo%YyKq>e%VaH*xx1o|qf9Bj(5AdBKp} - - - - Debug - AnyCPU - {7FB65A2A-946B-4ACD-A6A2-85FA6D517CC2} - Library - Properties - Redmine.Net.Api - redmine-net451-api-signed - v4.5.1 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - bin\Debug\redmine-net451-api-signed.XML - - - none - true - bin\Release\ - TRACE - prompt - 4 - bin\Release\redmine-net451-api-signed.XML - - - true - - - redmine-net-api.snk - - - - - - - - - - - - - - Internals\HashCodeHelper.cs - - - Internals\DataHelper.cs - - - Extensions\XmlWriterExtensions.cs - - - Extensions\NameValueCollectionExtensions.cs - - - HttpVerbs.cs - - - Internals\UrlHelper.cs - - - Internals\WebApiHelper.cs - - - Logging\ColorConsoleLogger.cs - - - Logging\ConsoleLogger.cs - - - Logging\ILogger.cs - - - Logging\LogEntry.cs - - - Logging\Logger.cs - - - Logging\LoggerExtensions.cs - - - Logging\LoggingEventType.cs - - - Logging\RedmineConsoleTraceListener.cs - - - Logging\TraceLogger.cs - - - RedmineKeys.cs - - - RedmineManager.cs - - - RedmineWebClient.cs - Component - - - Types\Attachment.cs - - - Types\Attachments.cs - - - Types\ChangeSet.cs - - - Types\CustomField.cs - - - Types\CustomFieldPossibleValue.cs - - - Types\CustomFieldRole.cs - - - Types\CustomFieldValue.cs - - - Types\Detail.cs - - - Types\Error.cs - - - Types\File.cs - - - Types\Group.cs - - - Types\GroupUser.cs - - - Types\Identifiable.cs - - - Types\IdentifiableName.cs - - - Types\IRedmineManager.cs - - - Types\IRedmineWebClient.cs - - - Types\Issue.cs - - - Types\IssueCategory.cs - - - Types\IssueChild.cs - - - Types\IssueCustomField.cs - - - Types\IssuePriority.cs - - - Types\IssueRelation.cs - - - Types\IssueRelationType.cs - - - Types\IssueStatus.cs - - - Types\IValue.cs - - - Types\Journal.cs - - - Types\Membership.cs - - - Types\MembershipRole.cs - - - Types\News.cs - - - Types\PaginatedObjects.cs - - - Types\Permission.cs - - - Types\Project.cs - - - Types\ProjectEnabledModule.cs - - - Types\ProjectIssueCategory.cs - - - Types\ProjectMembership.cs - - - Types\ProjectStatus.cs - - - Types\ProjectTracker.cs - - - Types\Query.cs - - - Types\Role.cs - - - Types\TimeEntry.cs - - - Types\TimeEntryActivity.cs - - - Types\Tracker.cs - - - Types\TrackerCustomField.cs - - - Types\Upload.cs - - - Types\User.cs - - - Types\UserGroup.cs - - - Types\UserStatus.cs - - - Types\Version.cs - - - Types\Watcher.cs - - - Types\WikiPage.cs - - - Extensions\CollectionExtensions.cs - - - Extensions\JsonExtensions.cs - - - Extensions\WebExtensions.cs - - - Extensions\XmlReaderExtensions.cs - - - Internals\RedmineSerializer.cs - - - Internals\RedmineSerializerJson.cs - - - JsonConverters\AttachmentConverter.cs - - - JsonConverters\AttachmentsConverter.cs - - - JsonConverters\ChangeSetConverter.cs - - - JsonConverters\CustomFieldConverter.cs - - - JsonConverters\CustomFieldPossibleValueConverter.cs - - - JsonConverters\CustomFieldRoleConverter.cs - - - JsonConverters\DetailConverter.cs - - - JsonConverters\ErrorConverter.cs - - - JsonConverters\FileConverter.cs - - - JsonConverters\GroupConverter.cs - - - JsonConverters\GroupUserConverter.cs - - - JsonConverters\IdentifiableNameConverter.cs - - - JsonConverters\IssueCategoryConverter.cs - - - JsonConverters\IssueChildConverter.cs - - - JsonConverters\IssueConverter.cs - - - JsonConverters\IssueCustomFieldConverter.cs - - - JsonConverters\IssuePriorityConverter.cs - - - JsonConverters\IssueRelationConverter.cs - - - JsonConverters\IssueStatusConverter.cs - - - JsonConverters\JournalConverter.cs - - - JsonConverters\MembershipConverter.cs - - - JsonConverters\MembershipRoleConverter.cs - - - JsonConverters\NewsConverter.cs - - - JsonConverters\PermissionConverter.cs - - - JsonConverters\ProjectConverter.cs - - - JsonConverters\ProjectEnabledModuleConverter.cs - - - JsonConverters\ProjectIssueCategoryConverter.cs - - - JsonConverters\ProjectMembershipConverter.cs - - - JsonConverters\ProjectTrackerConverter.cs - - - JsonConverters\QueryConverter.cs - - - JsonConverters\RoleConverter.cs - - - JsonConverters\TimeEntryActivityConverter.cs - - - JsonConverters\TimeEntryConverter.cs - - - JsonConverters\TrackerConverter.cs - - - JsonConverters\TrackerCustomFieldConverter.cs - - - JsonConverters\UploadConverter.cs - - - JsonConverters\UserConverter.cs - - - JsonConverters\UserGroupConverter.cs - - - JsonConverters\VersionConverter.cs - - - JsonConverters\WatcherConverter.cs - - - JsonConverters\WikiPageConverter.cs - - - MimeFormat.cs - - - Async\RedmineManagerAsync.cs - - - Extensions\DisposableExtension.cs - - - Extensions\FunctionalExtensions.cs - - - Extensions\TaskExtensions.cs - - - Internals\WebApiAsyncHelper.cs - - - - Exceptions\NotFoundException.cs - - - Exceptions\RedmineException.cs - - - Exceptions\RedmineTimeoutException.cs - - - Exceptions\NameResolutionFailureException.cs - - - Exceptions\InternalServerErrorException.cs - - - Exceptions\UnauthorizedException.cs - - - Exceptions\ForbiddenException.cs - - - Exceptions\ConflictException.cs - - - Exceptions\NotAcceptableException.cs - - - - - - - - \ No newline at end of file diff --git a/src/redmine-net451-api/Properties/AssemblyInfo.cs b/src/redmine-net451-api/Properties/AssemblyInfo.cs deleted file mode 100644 index 3119f037..00000000 --- a/src/redmine-net451-api/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("redmine-net451-api")] -[assembly: AssemblyDescription("redmine-net-api is a library for communicating with a Redmine project management application.")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("redmine-net451-api")] -[assembly: AssemblyCopyright("Copyright © Adrian Popescu 2011 - 2019")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("dc861b73-64ca-4bf8-a6ba-73db00934aa9")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.4.0")] -[assembly: AssemblyFileVersion("1.0.4.0")] diff --git a/src/redmine-net451-api/redmine-net451-api.csproj b/src/redmine-net451-api/redmine-net451-api.csproj deleted file mode 100644 index d5516638..00000000 --- a/src/redmine-net451-api/redmine-net451-api.csproj +++ /dev/null @@ -1,449 +0,0 @@ - - - - - Debug - AnyCPU - {B67F0035-336C-4CDA-80A8-DE94EEDF5627} - Library - Properties - Redmine.Net.Api - redmine-net451-api - v4.5.1 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - bin\Debug\redmine-net451-api.XML - - - none - true - bin\Release\ - TRACE - prompt - 4 - bin\Release\redmine-net451-api.XML - - - - - - - - - - - - - - Internals\HashCodeHelper.cs - - - Internals\DataHelper.cs - - - Extensions\NameValueCollectionExtensions.cs - - - Extensions\XmlWriterExtensions.cs - - - HttpVerbs.cs - - - Internals\UrlHelper.cs - - - Internals\WebApiHelper.cs - - - Logging\ColorConsoleLogger.cs - - - Logging\ConsoleLogger.cs - - - Logging\ILogger.cs - - - Logging\LogEntry.cs - - - Logging\Logger.cs - - - Logging\LoggerExtensions.cs - - - Logging\LoggingEventType.cs - - - Logging\RedmineConsoleTraceListener.cs - - - Logging\TraceLogger.cs - - - RedmineKeys.cs - - - RedmineManager.cs - - - RedmineWebClient.cs - Component - - - Types\Attachment.cs - - - Types\Attachments.cs - - - Types\ChangeSet.cs - - - Types\CustomField.cs - - - Types\CustomFieldPossibleValue.cs - - - Types\CustomFieldRole.cs - - - Types\CustomFieldValue.cs - - - Types\Detail.cs - - - Types\Error.cs - - - Types\File.cs - - - Types\Group.cs - - - Types\GroupUser.cs - - - Types\Identifiable.cs - - - Types\IdentifiableName.cs - - - Types\IRedmineManager.cs - - - Types\IRedmineWebClient.cs - - - Types\Issue.cs - - - Types\IssueCategory.cs - - - Types\IssueChild.cs - - - Types\IssueCustomField.cs - - - Types\IssuePriority.cs - - - Types\IssueRelation.cs - - - Types\IssueRelationType.cs - - - Types\IssueStatus.cs - - - Types\IValue.cs - - - Types\Journal.cs - - - Types\Membership.cs - - - Types\MembershipRole.cs - - - Types\News.cs - - - Types\PaginatedObjects.cs - - - Types\Permission.cs - - - Types\Project.cs - - - Types\ProjectEnabledModule.cs - - - Types\ProjectIssueCategory.cs - - - Types\ProjectMembership.cs - - - Types\ProjectStatus.cs - - - Types\ProjectTracker.cs - - - Types\Query.cs - - - Types\Role.cs - - - Types\TimeEntry.cs - - - Types\TimeEntryActivity.cs - - - Types\Tracker.cs - - - Types\TrackerCustomField.cs - - - Types\Upload.cs - - - Types\User.cs - - - Types\UserGroup.cs - - - Types\UserStatus.cs - - - Types\Version.cs - - - Types\Watcher.cs - - - Types\WikiPage.cs - - - Extensions\CollectionExtensions.cs - - - Extensions\JsonExtensions.cs - - - Extensions\WebExtensions.cs - - - Extensions\XmlReaderExtensions.cs - - - Internals\RedmineSerializer.cs - - - Internals\RedmineSerializerJson.cs - - - JsonConverters\AttachmentConverter.cs - - - JsonConverters\AttachmentsConverter.cs - - - JsonConverters\ChangeSetConverter.cs - - - JsonConverters\CustomFieldConverter.cs - - - JsonConverters\CustomFieldPossibleValueConverter.cs - - - JsonConverters\CustomFieldRoleConverter.cs - - - JsonConverters\DetailConverter.cs - - - JsonConverters\ErrorConverter.cs - - - JsonConverters\FileConverter.cs - - - JsonConverters\GroupConverter.cs - - - JsonConverters\GroupUserConverter.cs - - - JsonConverters\IdentifiableNameConverter.cs - - - JsonConverters\IssueCategoryConverter.cs - - - JsonConverters\IssueChildConverter.cs - - - JsonConverters\IssueConverter.cs - - - JsonConverters\IssueCustomFieldConverter.cs - - - JsonConverters\IssuePriorityConverter.cs - - - JsonConverters\IssueRelationConverter.cs - - - JsonConverters\IssueStatusConverter.cs - - - JsonConverters\JournalConverter.cs - - - JsonConverters\MembershipConverter.cs - - - JsonConverters\MembershipRoleConverter.cs - - - JsonConverters\NewsConverter.cs - - - JsonConverters\PermissionConverter.cs - - - JsonConverters\ProjectConverter.cs - - - JsonConverters\ProjectEnabledModuleConverter.cs - - - JsonConverters\ProjectIssueCategoryConverter.cs - - - JsonConverters\ProjectMembershipConverter.cs - - - JsonConverters\ProjectTrackerConverter.cs - - - JsonConverters\QueryConverter.cs - - - JsonConverters\RoleConverter.cs - - - JsonConverters\TimeEntryActivityConverter.cs - - - JsonConverters\TimeEntryConverter.cs - - - JsonConverters\TrackerConverter.cs - - - JsonConverters\TrackerCustomFieldConverter.cs - - - JsonConverters\UploadConverter.cs - - - JsonConverters\UserConverter.cs - - - JsonConverters\UserGroupConverter.cs - - - JsonConverters\VersionConverter.cs - - - JsonConverters\WatcherConverter.cs - - - JsonConverters\WikiPageConverter.cs - - - MimeFormat.cs - - - Async\RedmineManagerAsync.cs - - - Extensions\DisposableExtension.cs - - - Extensions\FunctionalExtensions.cs - - - Extensions\TaskExtensions.cs - - - Internals\WebApiAsyncHelper.cs - - - - Exceptions\NotFoundException.cs - - - Exceptions\RedmineException.cs - - - Exceptions\RedmineTimeoutException.cs - - - Exceptions\NameResolutionFailureException.cs - - - Exceptions\InternalServerErrorException.cs - - - Exceptions\UnauthorizedException.cs - - - Exceptions\ForbiddenException.cs - - - Exceptions\ConflictException.cs - - - Exceptions\NotAcceptableException.cs - - - - - \ No newline at end of file diff --git a/src/redmine-net452-api-signed/Internals/HashCodeHelper.cs b/src/redmine-net452-api-signed/Internals/HashCodeHelper.cs deleted file mode 100755 index 1cde5a51..00000000 --- a/src/redmine-net452-api-signed/Internals/HashCodeHelper.cs +++ /dev/null @@ -1,75 +0,0 @@ -/* - Copyright 2011 - 2017 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System.Collections.Generic; - -namespace Redmine.Net.Api.Internals -{ - /// - /// - /// - internal static class HashCodeHelper - { - /// - /// Returns a hash code for the list. - /// - /// - /// The list. - /// The hash. - /// - /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. - /// - public static int GetHashCode(IList list, int hash) - { - unchecked - { - var hashCode = hash; - if (list != null) - { - hashCode = (hashCode * 13) + list.Count; - foreach (T t in list) - { - hashCode *= 13; - if (t != null) hashCode = hashCode + t.GetHashCode(); - } - } - - return hashCode; - } - } - - /// - /// Returns a hash code for this instance. - /// - /// - /// The entity. - /// The hash. - /// - /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table. - /// - public static int GetHashCode(T entity, int hash) - { - unchecked - { - var hashCode = hash; - - hashCode = (hashCode * 397) ^ (entity == null ? 0 : entity.GetHashCode()); - - return hashCode; - } - } - } -} \ No newline at end of file diff --git a/src/redmine-net452-api-signed/Properties/AssemblyInfo.cs b/src/redmine-net452-api-signed/Properties/AssemblyInfo.cs deleted file mode 100644 index 69fe8e03..00000000 --- a/src/redmine-net452-api-signed/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("redmine-net452-api-signed")] -[assembly: AssemblyDescription("redmine-net-api is a library for communicating with a Redmine project management application.")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("redmine-net452-api-signed")] -[assembly: AssemblyCopyright("Copyright © Adrian Popescu 2011 - 2019")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("0ae35da8-1d10-4fa4-8cf7-d1ec18a42fb7")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/redmine-net452-api-signed/redmine-net-api.snk b/src/redmine-net452-api-signed/redmine-net-api.snk deleted file mode 100644 index 232ce5648120f24d8b71496087763fe5ac612f90..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50098i*9JV*{ydH2BtR6AWtvZfy$bvQ zaF;`STQp38W|Mw*xI(29_nz6ewAF+1rvrgI&!(XbRes!v`U81-B|BC0(|`{nrj9CU zfkrN5sA&P>(JgcRtKpkA`om#v_Wueky?&})8q9_$5r4%`?*{+_!T9^18)? zEQmqHrK|as_>I9l%Mf8aNcd&FtItY+wZ(&`hd$Wx+)?AX1V()o$5auptv+11FGeI= zWk3!Dqn=(C&A{=;YU-0m*Rl!^qDFiAYpe}PkJ>9VvC6yW;F@^yk*6c(BQ|eQ zrb>sRKl6ZKhT6WpR=OX|U%v^l{$bI{jrJA|1P8Sdx*-YF?b2kFp`N-ZBRNv;2zwm; zZk%4MW^?b;v)^CVAZfVF%^uwz=&(h~c!_w+(>9M7IF=Xl#!*)u6fsCF-oIR5VPc0r`PQwi+^d9&c_x#cjDli23V*v5cOEhS?@G)tEY^Or> zE0Mx?>uDcmLs>MHrPr(ljAVDV*||p9;f(0=&tY`;C}- - - - - Debug - AnyCPU - {6CBF5FC3-7783-44E7-90CA-8D12B165B9C3} - Library - Properties - Redmine.Net.Api - redmine-net452-api-signed - v4.5.2 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - bin\Debug\redmine-net452-api-signed.xml - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - bin\Release\redmine-net452-api-signed.xml - - - true - - - redmine-net-api.snk - - - - - - - - - - - - - - Internals\HashCodeHelper.cs - - - Internals\DataHelper.cs - - - Extensions\NameValueCollectionExtensions.cs - - - Extensions\XmlWriterExtensions.cs - - - HttpVerbs.cs - - - Internals\UrlHelper.cs - - - Internals\WebApiHelper.cs - - - Logging\ColorConsoleLogger.cs - - - Logging\ConsoleLogger.cs - - - Logging\ILogger.cs - - - Logging\LogEntry.cs - - - Logging\Logger.cs - - - Logging\LoggerExtensions.cs - - - Logging\LoggingEventType.cs - - - Logging\RedmineConsoleTraceListener.cs - - - Logging\TraceLogger.cs - - - RedmineKeys.cs - - - RedmineManager.cs - - - RedmineWebClient.cs - Component - - - Types\Attachment.cs - - - Types\Attachments.cs - - - Types\ChangeSet.cs - - - Types\CustomField.cs - - - Types\CustomFieldPossibleValue.cs - - - Types\CustomFieldRole.cs - - - Types\CustomFieldValue.cs - - - Types\Detail.cs - - - Types\Error.cs - - - Types\File.cs - - - Types\Group.cs - - - Types\GroupUser.cs - - - Types\Identifiable.cs - - - Types\IdentifiableName.cs - - - Types\IRedmineManager.cs - - - Types\IRedmineWebClient.cs - - - Types\Issue.cs - - - Types\IssueCategory.cs - - - Types\IssueChild.cs - - - Types\IssueCustomField.cs - - - Types\IssuePriority.cs - - - Types\IssueRelation.cs - - - Types\IssueRelationType.cs - - - Types\IssueStatus.cs - - - Types\IValue.cs - - - Types\Journal.cs - - - Types\Membership.cs - - - Types\MembershipRole.cs - - - Types\News.cs - - - Types\PaginatedObjects.cs - - - Types\Permission.cs - - - Types\Project.cs - - - Types\ProjectEnabledModule.cs - - - Types\ProjectIssueCategory.cs - - - Types\ProjectMembership.cs - - - Types\ProjectStatus.cs - - - Types\ProjectTracker.cs - - - Types\Query.cs - - - Types\Role.cs - - - Types\TimeEntry.cs - - - Types\TimeEntryActivity.cs - - - Types\Tracker.cs - - - Types\TrackerCustomField.cs - - - Types\Upload.cs - - - Types\User.cs - - - Types\UserGroup.cs - - - Types\UserStatus.cs - - - Types\Version.cs - - - Types\Watcher.cs - - - Types\WikiPage.cs - - - Extensions\CollectionExtensions.cs - - - Extensions\JsonExtensions.cs - - - Extensions\WebExtensions.cs - - - Extensions\XmlReaderExtensions.cs - - - Internals\RedmineSerializer.cs - - - Internals\RedmineSerializerJson.cs - - - JsonConverters\AttachmentConverter.cs - - - JsonConverters\AttachmentsConverter.cs - - - JsonConverters\ChangeSetConverter.cs - - - JsonConverters\CustomFieldConverter.cs - - - JsonConverters\CustomFieldPossibleValueConverter.cs - - - JsonConverters\CustomFieldRoleConverter.cs - - - JsonConverters\DetailConverter.cs - - - JsonConverters\ErrorConverter.cs - - - JsonConverters\FileConverter.cs - - - JsonConverters\GroupConverter.cs - - - JsonConverters\GroupUserConverter.cs - - - JsonConverters\IdentifiableNameConverter.cs - - - JsonConverters\IssueCategoryConverter.cs - - - JsonConverters\IssueChildConverter.cs - - - JsonConverters\IssueConverter.cs - - - JsonConverters\IssueCustomFieldConverter.cs - - - JsonConverters\IssuePriorityConverter.cs - - - JsonConverters\IssueRelationConverter.cs - - - JsonConverters\IssueStatusConverter.cs - - - JsonConverters\JournalConverter.cs - - - JsonConverters\MembershipConverter.cs - - - JsonConverters\MembershipRoleConverter.cs - - - JsonConverters\NewsConverter.cs - - - JsonConverters\PermissionConverter.cs - - - JsonConverters\ProjectConverter.cs - - - JsonConverters\ProjectEnabledModuleConverter.cs - - - JsonConverters\ProjectIssueCategoryConverter.cs - - - JsonConverters\ProjectMembershipConverter.cs - - - JsonConverters\ProjectTrackerConverter.cs - - - JsonConverters\QueryConverter.cs - - - JsonConverters\RoleConverter.cs - - - JsonConverters\TimeEntryActivityConverter.cs - - - JsonConverters\TimeEntryConverter.cs - - - JsonConverters\TrackerConverter.cs - - - JsonConverters\TrackerCustomFieldConverter.cs - - - JsonConverters\UploadConverter.cs - - - JsonConverters\UserConverter.cs - - - JsonConverters\UserGroupConverter.cs - - - JsonConverters\VersionConverter.cs - - - JsonConverters\WatcherConverter.cs - - - JsonConverters\WikiPageConverter.cs - - - MimeFormat.cs - - - Async\RedmineManagerAsync.cs - - - Extensions\DisposableExtension.cs - - - Extensions\FunctionalExtensions.cs - - - Extensions\TaskExtensions.cs - - - Internals\WebApiAsyncHelper.cs - - - - Exceptions\NotFoundException.cs - - - Exceptions\RedmineException.cs - - - Exceptions\RedmineTimeoutException.cs - - - Exceptions\NameResolutionFailureException.cs - - - Exceptions\InternalServerErrorException.cs - - - Exceptions\UnauthorizedException.cs - - - Exceptions\ForbiddenException.cs - - - Exceptions\ConflictException.cs - - - Exceptions\NotAcceptableException.cs - - - - - - - \ No newline at end of file diff --git a/src/redmine-net452-api/Properties/AssemblyInfo.cs b/src/redmine-net452-api/Properties/AssemblyInfo.cs deleted file mode 100644 index 1451e4e9..00000000 --- a/src/redmine-net452-api/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("redmine-net452-api")] -[assembly: AssemblyDescription("redmine-net-api is a library for communicating with a Redmine project management application.")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("redmine-net452-api")] -[assembly: AssemblyCopyright("Copyright © Adrian Popescu 2011 - 2019")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("404b264f-363b-44ad-ae8d-2587c2e6fa82")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/redmine-net452-api/redmine-net452-api.csproj b/src/redmine-net452-api/redmine-net452-api.csproj deleted file mode 100644 index 5522823a..00000000 --- a/src/redmine-net452-api/redmine-net452-api.csproj +++ /dev/null @@ -1,442 +0,0 @@ - - - - - Debug - AnyCPU - {4EE7D8D8-AA65-442B-A928-580B4604B9AF} - Library - Properties - Redmine.Net.Api - redmine-net452-api - v4.5.2 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - bin\Debug\redmine-net452-api.xml - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - bin\Release\redmine-net452-api.xml - - - - - - - - - - - - - - Internals\HashCodeHelper.cs - - - Internals\DataHelper.cs - - - Extensions\NameValueCollectionExtensions.cs - - - Extensions\XmlWriterExtensions.cs - - - HttpVerbs.cs - - - Internals\UrlHelper.cs - - - Internals\WebApiHelper.cs - - - Logging\ColorConsoleLogger.cs - - - Logging\ConsoleLogger.cs - - - Logging\ILogger.cs - - - Logging\LogEntry.cs - - - Logging\Logger.cs - - - Logging\LoggerExtensions.cs - - - Logging\LoggingEventType.cs - - - Logging\RedmineConsoleTraceListener.cs - - - Logging\TraceLogger.cs - - - RedmineKeys.cs - - - RedmineManager.cs - - - RedmineWebClient.cs - Component - - - Types\Attachment.cs - - - Types\Attachments.cs - - - Types\ChangeSet.cs - - - Types\CustomField.cs - - - Types\CustomFieldPossibleValue.cs - - - Types\CustomFieldRole.cs - - - Types\CustomFieldValue.cs - - - Types\Detail.cs - - - Types\Error.cs - - - Types\File.cs - - - Types\Group.cs - - - Types\GroupUser.cs - - - Types\Identifiable.cs - - - Types\IdentifiableName.cs - - - Types\IRedmineManager.cs - - - Types\IRedmineWebClient.cs - - - Types\Issue.cs - - - Types\IssueCategory.cs - - - Types\IssueChild.cs - - - Types\IssueCustomField.cs - - - Types\IssuePriority.cs - - - Types\IssueRelation.cs - - - Types\IssueRelationType.cs - - - Types\IssueStatus.cs - - - Types\IValue.cs - - - Types\Journal.cs - - - Types\Membership.cs - - - Types\MembershipRole.cs - - - Types\News.cs - - - Types\PaginatedObjects.cs - - - Types\Permission.cs - - - Types\Project.cs - - - Types\ProjectEnabledModule.cs - - - Types\ProjectIssueCategory.cs - - - Types\ProjectMembership.cs - - - Types\ProjectStatus.cs - - - Types\ProjectTracker.cs - - - Types\Query.cs - - - Types\Role.cs - - - Types\TimeEntry.cs - - - Types\TimeEntryActivity.cs - - - Types\Tracker.cs - - - Types\TrackerCustomField.cs - - - Types\Upload.cs - - - Types\User.cs - - - Types\UserGroup.cs - - - Types\UserStatus.cs - - - Types\Version.cs - - - Types\Watcher.cs - - - Types\WikiPage.cs - - - Extensions\CollectionExtensions.cs - - - Extensions\JsonExtensions.cs - - - Extensions\WebExtensions.cs - - - Extensions\XmlReaderExtensions.cs - - - Internals\RedmineSerializer.cs - - - Internals\RedmineSerializerJson.cs - - - JsonConverters\AttachmentConverter.cs - - - JsonConverters\AttachmentsConverter.cs - - - JsonConverters\ChangeSetConverter.cs - - - JsonConverters\CustomFieldConverter.cs - - - JsonConverters\CustomFieldPossibleValueConverter.cs - - - JsonConverters\CustomFieldRoleConverter.cs - - - JsonConverters\DetailConverter.cs - - - JsonConverters\ErrorConverter.cs - - - JsonConverters\FileConverter.cs - - - JsonConverters\GroupConverter.cs - - - JsonConverters\GroupUserConverter.cs - - - JsonConverters\IdentifiableNameConverter.cs - - - JsonConverters\IssueCategoryConverter.cs - - - JsonConverters\IssueChildConverter.cs - - - JsonConverters\IssueConverter.cs - - - JsonConverters\IssueCustomFieldConverter.cs - - - JsonConverters\IssuePriorityConverter.cs - - - JsonConverters\IssueRelationConverter.cs - - - JsonConverters\IssueStatusConverter.cs - - - JsonConverters\JournalConverter.cs - - - JsonConverters\MembershipConverter.cs - - - JsonConverters\MembershipRoleConverter.cs - - - JsonConverters\NewsConverter.cs - - - JsonConverters\PermissionConverter.cs - - - JsonConverters\ProjectConverter.cs - - - JsonConverters\ProjectEnabledModuleConverter.cs - - - JsonConverters\ProjectIssueCategoryConverter.cs - - - JsonConverters\ProjectMembershipConverter.cs - - - JsonConverters\ProjectTrackerConverter.cs - - - JsonConverters\QueryConverter.cs - - - JsonConverters\RoleConverter.cs - - - JsonConverters\TimeEntryActivityConverter.cs - - - JsonConverters\TimeEntryConverter.cs - - - JsonConverters\TrackerConverter.cs - - - JsonConverters\TrackerCustomFieldConverter.cs - - - JsonConverters\UploadConverter.cs - - - JsonConverters\UserConverter.cs - - - JsonConverters\UserGroupConverter.cs - - - JsonConverters\VersionConverter.cs - - - JsonConverters\WatcherConverter.cs - - - JsonConverters\WikiPageConverter.cs - - - MimeFormat.cs - - - Async\RedmineManagerAsync.cs - - - Extensions\DisposableExtension.cs - - - Extensions\FunctionalExtensions.cs - - - Extensions\TaskExtensions.cs - - - Internals\WebApiAsyncHelper.cs - - - - Exceptions\NotFoundException.cs - - - Exceptions\RedmineException.cs - - - Exceptions\RedmineTimeoutException.cs - - - Exceptions\NameResolutionFailureException.cs - - - Exceptions\InternalServerErrorException.cs - - - Exceptions\UnauthorizedException.cs - - - Exceptions\ForbiddenException.cs - - - Exceptions\ConflictException.cs - - - Exceptions\NotAcceptableException.cs - - - - \ No newline at end of file From b0951c2ac1830dc55399cc1b2064408ccdffec87 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Tue, 19 Nov 2019 15:05:33 +0200 Subject: [PATCH 006/549] Rename project --- src/redmine-net-api.Tests/redmine-net-api.Tests.csproj | 5 ++--- src/redmine-net-api.sln | 2 +- .../Async/RedmineManagerAsync.cs | 0 .../Async/RedmineManagerAsync40.cs | 0 .../Async/RedmineManagerAsync45.cs | 0 .../Exceptions/ConflictException.cs | 0 .../Exceptions/ForbiddenException.cs | 0 .../Exceptions/InternalServerErrorException.cs | 0 .../Exceptions/NameResolutionFailureException.cs | 0 .../Exceptions/NotAcceptableException.cs | 0 .../Exceptions/NotFoundException.cs | 0 .../Exceptions/RedmineException.cs | 0 .../Exceptions/RedmineTimeoutException.cs | 0 .../Exceptions/UnauthorizedException.cs | 0 .../Extensions/CollectionExtensions.cs | 0 .../Extensions/ExtensionAttribute.cs | 0 .../Extensions/JsonExtensions.cs | 0 .../Extensions/LoggerExtensions.cs | 0 .../Extensions/NameValueCollectionExtensions.cs | 0 .../Extensions/StringExtensions.cs | 0 .../Extensions/WebExtensions.cs | 0 .../Extensions/XmlReaderExtensions.cs | 0 .../Extensions/XmlWriterExtensions.cs | 0 src/{redmine-net20-api => redmine-net-api}/HttpVerbs.cs | 0 .../IRedmineManager.cs | 0 .../IRedmineWebClient.cs | 0 .../Internals/DataHelper.cs | 0 src/{redmine-net20-api => redmine-net-api}/Internals/Func.cs | 0 .../Internals/HashCodeHelper.cs | 0 .../Internals/RedmineSerializer.cs | 0 .../Internals/RedmineSerializerJson.cs | 0 .../Internals/UrlHelper.cs | 0 .../Internals/WebApiAsyncHelper.cs | 0 .../Internals/WebApiHelper.cs | 0 .../JSonConverters/AttachmentConverter.cs | 0 .../JSonConverters/AttachmentsConverter.cs | 0 .../JSonConverters/ChangeSetConverter.cs | 0 .../JSonConverters/CustomFieldConverter.cs | 0 .../JSonConverters/CustomFieldPossibleValueConverter.cs | 0 .../JSonConverters/CustomFieldRoleConverter.cs | 0 .../JSonConverters/DetailConverter.cs | 0 .../JSonConverters/ErrorConverter.cs | 0 .../JSonConverters/FileConverter.cs | 0 .../JSonConverters/GroupConverter.cs | 0 .../JSonConverters/GroupUserConverter.cs | 0 .../JSonConverters/IdentifiableNameConverter.cs | 0 .../JSonConverters/IssueCategoryConverter.cs | 0 .../JSonConverters/IssueChildConverter.cs | 0 .../JSonConverters/IssueConverter.cs | 0 .../JSonConverters/IssueCustomFieldConverter.cs | 0 .../JSonConverters/IssuePriorityConverter.cs | 0 .../JSonConverters/IssueRelationConverter.cs | 0 .../JSonConverters/IssueStatusConverter.cs | 0 .../JSonConverters/JournalConverter.cs | 0 .../JSonConverters/MembershipConverter.cs | 0 .../JSonConverters/MembershipRoleConverter.cs | 0 .../JSonConverters/NewsConverter.cs | 0 .../JSonConverters/PermissionConverter.cs | 0 .../JSonConverters/ProjectConverter.cs | 0 .../JSonConverters/ProjectEnabledModuleConverter.cs | 0 .../JSonConverters/ProjectIssueCategoryConverter.cs | 0 .../JSonConverters/ProjectMembershipConverter.cs | 0 .../JSonConverters/ProjectTrackerConverter.cs | 0 .../JSonConverters/QueryConverter.cs | 0 .../JSonConverters/RoleConverter.cs | 0 .../JSonConverters/TimeEntryActivityConverter.cs | 0 .../JSonConverters/TimeEntryConverter.cs | 0 .../JSonConverters/TrackerConverter.cs | 0 .../JSonConverters/TrackerCustomFieldConverter.cs | 0 .../JSonConverters/UploadConverter.cs | 0 .../JSonConverters/UserConverter.cs | 0 .../JSonConverters/UserGroupConverter.cs | 0 .../JSonConverters/VersionConverter.cs | 0 .../JSonConverters/WatcherConverter.cs | 0 .../JSonConverters/WikiPageConverter.cs | 0 .../Logging/ColorConsoleLogger.cs | 0 .../Logging/ConsoleLogger.cs | 0 .../Logging/ILogger.cs | 0 .../Logging/LogEntry.cs | 0 src/{redmine-net20-api => redmine-net-api}/Logging/Logger.cs | 0 .../Logging/LoggerExtensions.cs | 0 .../Logging/LoggingEventType.cs | 0 .../Logging/RedmineConsoleTraceListener.cs | 0 .../Logging/TraceLogger.cs | 0 src/{redmine-net20-api => redmine-net-api}/MimeFormat.cs | 0 .../Properties/AssemblyInfo.cs | 0 src/{redmine-net20-api => redmine-net-api}/RedmineKeys.cs | 0 src/{redmine-net20-api => redmine-net-api}/RedmineManager.cs | 0 .../RedmineWebClient.cs | 0 .../Types/Attachment.cs | 0 .../Types/Attachments.cs | 0 .../Types/ChangeSet.cs | 0 .../Types/CustomField.cs | 0 .../Types/CustomFieldPossibleValue.cs | 0 .../Types/CustomFieldRole.cs | 0 .../Types/CustomFieldValue.cs | 0 src/{redmine-net20-api => redmine-net-api}/Types/Detail.cs | 0 src/{redmine-net20-api => redmine-net-api}/Types/Error.cs | 0 src/{redmine-net20-api => redmine-net-api}/Types/File.cs | 0 src/{redmine-net20-api => redmine-net-api}/Types/Group.cs | 0 .../Types/GroupUser.cs | 0 src/{redmine-net20-api => redmine-net-api}/Types/IValue.cs | 0 .../Types/Identifiable.cs | 0 .../Types/IdentifiableName.cs | 0 src/{redmine-net20-api => redmine-net-api}/Types/Issue.cs | 0 .../Types/IssueCategory.cs | 0 .../Types/IssueChild.cs | 0 .../Types/IssueCustomField.cs | 0 .../Types/IssuePriority.cs | 0 .../Types/IssueRelation.cs | 0 .../Types/IssueRelationType.cs | 0 .../Types/IssueStatus.cs | 0 src/{redmine-net20-api => redmine-net-api}/Types/Journal.cs | 0 .../Types/Membership.cs | 0 .../Types/MembershipRole.cs | 0 src/{redmine-net20-api => redmine-net-api}/Types/News.cs | 0 .../Types/PaginatedObjects.cs | 0 .../Types/Permission.cs | 0 src/{redmine-net20-api => redmine-net-api}/Types/Project.cs | 0 .../Types/ProjectEnabledModule.cs | 0 .../Types/ProjectIssueCategory.cs | 0 .../Types/ProjectMembership.cs | 0 .../Types/ProjectStatus.cs | 0 .../Types/ProjectTracker.cs | 0 src/{redmine-net20-api => redmine-net-api}/Types/Query.cs | 0 src/{redmine-net20-api => redmine-net-api}/Types/Role.cs | 0 .../Types/TimeEntry.cs | 0 .../Types/TimeEntryActivity.cs | 0 src/{redmine-net20-api => redmine-net-api}/Types/Tracker.cs | 0 .../Types/TrackerCustomField.cs | 0 src/{redmine-net20-api => redmine-net-api}/Types/Upload.cs | 0 src/{redmine-net20-api => redmine-net-api}/Types/User.cs | 0 .../Types/UserGroup.cs | 0 .../Types/UserStatus.cs | 0 src/{redmine-net20-api => redmine-net-api}/Types/Version.cs | 0 src/{redmine-net20-api => redmine-net-api}/Types/Watcher.cs | 0 src/{redmine-net20-api => redmine-net-api}/Types/WikiPage.cs | 0 .../redmine-net-api.csproj} | 0 138 files changed, 3 insertions(+), 4 deletions(-) rename src/{redmine-net20-api => redmine-net-api}/Async/RedmineManagerAsync.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Async/RedmineManagerAsync40.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Async/RedmineManagerAsync45.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Exceptions/ConflictException.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Exceptions/ForbiddenException.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Exceptions/InternalServerErrorException.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Exceptions/NameResolutionFailureException.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Exceptions/NotAcceptableException.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Exceptions/NotFoundException.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Exceptions/RedmineException.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Exceptions/RedmineTimeoutException.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Exceptions/UnauthorizedException.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Extensions/CollectionExtensions.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Extensions/ExtensionAttribute.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Extensions/JsonExtensions.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Extensions/LoggerExtensions.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Extensions/NameValueCollectionExtensions.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Extensions/StringExtensions.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Extensions/WebExtensions.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Extensions/XmlReaderExtensions.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Extensions/XmlWriterExtensions.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/HttpVerbs.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/IRedmineManager.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/IRedmineWebClient.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Internals/DataHelper.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Internals/Func.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Internals/HashCodeHelper.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Internals/RedmineSerializer.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Internals/RedmineSerializerJson.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Internals/UrlHelper.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Internals/WebApiAsyncHelper.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Internals/WebApiHelper.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/AttachmentConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/AttachmentsConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/ChangeSetConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/CustomFieldConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/CustomFieldPossibleValueConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/CustomFieldRoleConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/DetailConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/ErrorConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/FileConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/GroupConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/GroupUserConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/IdentifiableNameConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/IssueCategoryConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/IssueChildConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/IssueConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/IssueCustomFieldConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/IssuePriorityConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/IssueRelationConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/IssueStatusConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/JournalConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/MembershipConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/MembershipRoleConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/NewsConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/PermissionConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/ProjectConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/ProjectEnabledModuleConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/ProjectIssueCategoryConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/ProjectMembershipConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/ProjectTrackerConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/QueryConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/RoleConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/TimeEntryActivityConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/TimeEntryConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/TrackerConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/TrackerCustomFieldConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/UploadConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/UserConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/UserGroupConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/VersionConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/WatcherConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/JSonConverters/WikiPageConverter.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Logging/ColorConsoleLogger.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Logging/ConsoleLogger.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Logging/ILogger.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Logging/LogEntry.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Logging/Logger.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Logging/LoggerExtensions.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Logging/LoggingEventType.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Logging/RedmineConsoleTraceListener.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Logging/TraceLogger.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/MimeFormat.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Properties/AssemblyInfo.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/RedmineKeys.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/RedmineManager.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/RedmineWebClient.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/Attachment.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/Attachments.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/ChangeSet.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/CustomField.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/CustomFieldPossibleValue.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/CustomFieldRole.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/CustomFieldValue.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/Detail.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/Error.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/File.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/Group.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/GroupUser.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/IValue.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/Identifiable.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/IdentifiableName.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/Issue.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/IssueCategory.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/IssueChild.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/IssueCustomField.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/IssuePriority.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/IssueRelation.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/IssueRelationType.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/IssueStatus.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/Journal.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/Membership.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/MembershipRole.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/News.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/PaginatedObjects.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/Permission.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/Project.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/ProjectEnabledModule.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/ProjectIssueCategory.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/ProjectMembership.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/ProjectStatus.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/ProjectTracker.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/Query.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/Role.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/TimeEntry.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/TimeEntryActivity.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/Tracker.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/TrackerCustomField.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/Upload.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/User.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/UserGroup.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/UserStatus.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/Version.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/Watcher.cs (100%) rename src/{redmine-net20-api => redmine-net-api}/Types/WikiPage.cs (100%) rename src/{redmine-net20-api/redmine-net20-api.csproj => redmine-net-api/redmine-net-api.csproj} (100%) diff --git a/src/redmine-net-api.Tests/redmine-net-api.Tests.csproj b/src/redmine-net-api.Tests/redmine-net-api.Tests.csproj index d7fffce9..73725264 100644 --- a/src/redmine-net-api.Tests/redmine-net-api.Tests.csproj +++ b/src/redmine-net-api.Tests/redmine-net-api.Tests.csproj @@ -85,9 +85,8 @@ - - - + + diff --git a/src/redmine-net-api.sln b/src/redmine-net-api.sln index 40373ace..02c0b032 100644 --- a/src/redmine-net-api.sln +++ b/src/redmine-net-api.sln @@ -7,7 +7,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0DFF4758-5C1 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{F3F4278D-6271-4F77-BA88-41555D53CBD1}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "redmine-net20-api", "redmine-net20-api\redmine-net20-api.csproj", "{0E6B9B72-445D-4E71-8D29-48C4A009AB03}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "redmine-net-api", "redmine-net-api\redmine-net-api.csproj", "{0E6B9B72-445D-4E71-8D29-48C4A009AB03}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "redmine-net-api.Tests", "redmine-net-api.Tests\redmine-net-api.Tests.csproj", "{900EF0B3-0233-45DA-811F-4C59483E8452}" EndProject diff --git a/src/redmine-net20-api/Async/RedmineManagerAsync.cs b/src/redmine-net-api/Async/RedmineManagerAsync.cs similarity index 100% rename from src/redmine-net20-api/Async/RedmineManagerAsync.cs rename to src/redmine-net-api/Async/RedmineManagerAsync.cs diff --git a/src/redmine-net20-api/Async/RedmineManagerAsync40.cs b/src/redmine-net-api/Async/RedmineManagerAsync40.cs similarity index 100% rename from src/redmine-net20-api/Async/RedmineManagerAsync40.cs rename to src/redmine-net-api/Async/RedmineManagerAsync40.cs diff --git a/src/redmine-net20-api/Async/RedmineManagerAsync45.cs b/src/redmine-net-api/Async/RedmineManagerAsync45.cs similarity index 100% rename from src/redmine-net20-api/Async/RedmineManagerAsync45.cs rename to src/redmine-net-api/Async/RedmineManagerAsync45.cs diff --git a/src/redmine-net20-api/Exceptions/ConflictException.cs b/src/redmine-net-api/Exceptions/ConflictException.cs similarity index 100% rename from src/redmine-net20-api/Exceptions/ConflictException.cs rename to src/redmine-net-api/Exceptions/ConflictException.cs diff --git a/src/redmine-net20-api/Exceptions/ForbiddenException.cs b/src/redmine-net-api/Exceptions/ForbiddenException.cs similarity index 100% rename from src/redmine-net20-api/Exceptions/ForbiddenException.cs rename to src/redmine-net-api/Exceptions/ForbiddenException.cs diff --git a/src/redmine-net20-api/Exceptions/InternalServerErrorException.cs b/src/redmine-net-api/Exceptions/InternalServerErrorException.cs similarity index 100% rename from src/redmine-net20-api/Exceptions/InternalServerErrorException.cs rename to src/redmine-net-api/Exceptions/InternalServerErrorException.cs diff --git a/src/redmine-net20-api/Exceptions/NameResolutionFailureException.cs b/src/redmine-net-api/Exceptions/NameResolutionFailureException.cs similarity index 100% rename from src/redmine-net20-api/Exceptions/NameResolutionFailureException.cs rename to src/redmine-net-api/Exceptions/NameResolutionFailureException.cs diff --git a/src/redmine-net20-api/Exceptions/NotAcceptableException.cs b/src/redmine-net-api/Exceptions/NotAcceptableException.cs similarity index 100% rename from src/redmine-net20-api/Exceptions/NotAcceptableException.cs rename to src/redmine-net-api/Exceptions/NotAcceptableException.cs diff --git a/src/redmine-net20-api/Exceptions/NotFoundException.cs b/src/redmine-net-api/Exceptions/NotFoundException.cs similarity index 100% rename from src/redmine-net20-api/Exceptions/NotFoundException.cs rename to src/redmine-net-api/Exceptions/NotFoundException.cs diff --git a/src/redmine-net20-api/Exceptions/RedmineException.cs b/src/redmine-net-api/Exceptions/RedmineException.cs similarity index 100% rename from src/redmine-net20-api/Exceptions/RedmineException.cs rename to src/redmine-net-api/Exceptions/RedmineException.cs diff --git a/src/redmine-net20-api/Exceptions/RedmineTimeoutException.cs b/src/redmine-net-api/Exceptions/RedmineTimeoutException.cs similarity index 100% rename from src/redmine-net20-api/Exceptions/RedmineTimeoutException.cs rename to src/redmine-net-api/Exceptions/RedmineTimeoutException.cs diff --git a/src/redmine-net20-api/Exceptions/UnauthorizedException.cs b/src/redmine-net-api/Exceptions/UnauthorizedException.cs similarity index 100% rename from src/redmine-net20-api/Exceptions/UnauthorizedException.cs rename to src/redmine-net-api/Exceptions/UnauthorizedException.cs diff --git a/src/redmine-net20-api/Extensions/CollectionExtensions.cs b/src/redmine-net-api/Extensions/CollectionExtensions.cs similarity index 100% rename from src/redmine-net20-api/Extensions/CollectionExtensions.cs rename to src/redmine-net-api/Extensions/CollectionExtensions.cs diff --git a/src/redmine-net20-api/Extensions/ExtensionAttribute.cs b/src/redmine-net-api/Extensions/ExtensionAttribute.cs similarity index 100% rename from src/redmine-net20-api/Extensions/ExtensionAttribute.cs rename to src/redmine-net-api/Extensions/ExtensionAttribute.cs diff --git a/src/redmine-net20-api/Extensions/JsonExtensions.cs b/src/redmine-net-api/Extensions/JsonExtensions.cs similarity index 100% rename from src/redmine-net20-api/Extensions/JsonExtensions.cs rename to src/redmine-net-api/Extensions/JsonExtensions.cs diff --git a/src/redmine-net20-api/Extensions/LoggerExtensions.cs b/src/redmine-net-api/Extensions/LoggerExtensions.cs similarity index 100% rename from src/redmine-net20-api/Extensions/LoggerExtensions.cs rename to src/redmine-net-api/Extensions/LoggerExtensions.cs diff --git a/src/redmine-net20-api/Extensions/NameValueCollectionExtensions.cs b/src/redmine-net-api/Extensions/NameValueCollectionExtensions.cs similarity index 100% rename from src/redmine-net20-api/Extensions/NameValueCollectionExtensions.cs rename to src/redmine-net-api/Extensions/NameValueCollectionExtensions.cs diff --git a/src/redmine-net20-api/Extensions/StringExtensions.cs b/src/redmine-net-api/Extensions/StringExtensions.cs similarity index 100% rename from src/redmine-net20-api/Extensions/StringExtensions.cs rename to src/redmine-net-api/Extensions/StringExtensions.cs diff --git a/src/redmine-net20-api/Extensions/WebExtensions.cs b/src/redmine-net-api/Extensions/WebExtensions.cs similarity index 100% rename from src/redmine-net20-api/Extensions/WebExtensions.cs rename to src/redmine-net-api/Extensions/WebExtensions.cs diff --git a/src/redmine-net20-api/Extensions/XmlReaderExtensions.cs b/src/redmine-net-api/Extensions/XmlReaderExtensions.cs similarity index 100% rename from src/redmine-net20-api/Extensions/XmlReaderExtensions.cs rename to src/redmine-net-api/Extensions/XmlReaderExtensions.cs diff --git a/src/redmine-net20-api/Extensions/XmlWriterExtensions.cs b/src/redmine-net-api/Extensions/XmlWriterExtensions.cs similarity index 100% rename from src/redmine-net20-api/Extensions/XmlWriterExtensions.cs rename to src/redmine-net-api/Extensions/XmlWriterExtensions.cs diff --git a/src/redmine-net20-api/HttpVerbs.cs b/src/redmine-net-api/HttpVerbs.cs similarity index 100% rename from src/redmine-net20-api/HttpVerbs.cs rename to src/redmine-net-api/HttpVerbs.cs diff --git a/src/redmine-net20-api/IRedmineManager.cs b/src/redmine-net-api/IRedmineManager.cs similarity index 100% rename from src/redmine-net20-api/IRedmineManager.cs rename to src/redmine-net-api/IRedmineManager.cs diff --git a/src/redmine-net20-api/IRedmineWebClient.cs b/src/redmine-net-api/IRedmineWebClient.cs similarity index 100% rename from src/redmine-net20-api/IRedmineWebClient.cs rename to src/redmine-net-api/IRedmineWebClient.cs diff --git a/src/redmine-net20-api/Internals/DataHelper.cs b/src/redmine-net-api/Internals/DataHelper.cs similarity index 100% rename from src/redmine-net20-api/Internals/DataHelper.cs rename to src/redmine-net-api/Internals/DataHelper.cs diff --git a/src/redmine-net20-api/Internals/Func.cs b/src/redmine-net-api/Internals/Func.cs similarity index 100% rename from src/redmine-net20-api/Internals/Func.cs rename to src/redmine-net-api/Internals/Func.cs diff --git a/src/redmine-net20-api/Internals/HashCodeHelper.cs b/src/redmine-net-api/Internals/HashCodeHelper.cs similarity index 100% rename from src/redmine-net20-api/Internals/HashCodeHelper.cs rename to src/redmine-net-api/Internals/HashCodeHelper.cs diff --git a/src/redmine-net20-api/Internals/RedmineSerializer.cs b/src/redmine-net-api/Internals/RedmineSerializer.cs similarity index 100% rename from src/redmine-net20-api/Internals/RedmineSerializer.cs rename to src/redmine-net-api/Internals/RedmineSerializer.cs diff --git a/src/redmine-net20-api/Internals/RedmineSerializerJson.cs b/src/redmine-net-api/Internals/RedmineSerializerJson.cs similarity index 100% rename from src/redmine-net20-api/Internals/RedmineSerializerJson.cs rename to src/redmine-net-api/Internals/RedmineSerializerJson.cs diff --git a/src/redmine-net20-api/Internals/UrlHelper.cs b/src/redmine-net-api/Internals/UrlHelper.cs similarity index 100% rename from src/redmine-net20-api/Internals/UrlHelper.cs rename to src/redmine-net-api/Internals/UrlHelper.cs diff --git a/src/redmine-net20-api/Internals/WebApiAsyncHelper.cs b/src/redmine-net-api/Internals/WebApiAsyncHelper.cs similarity index 100% rename from src/redmine-net20-api/Internals/WebApiAsyncHelper.cs rename to src/redmine-net-api/Internals/WebApiAsyncHelper.cs diff --git a/src/redmine-net20-api/Internals/WebApiHelper.cs b/src/redmine-net-api/Internals/WebApiHelper.cs similarity index 100% rename from src/redmine-net20-api/Internals/WebApiHelper.cs rename to src/redmine-net-api/Internals/WebApiHelper.cs diff --git a/src/redmine-net20-api/JSonConverters/AttachmentConverter.cs b/src/redmine-net-api/JSonConverters/AttachmentConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/AttachmentConverter.cs rename to src/redmine-net-api/JSonConverters/AttachmentConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/AttachmentsConverter.cs b/src/redmine-net-api/JSonConverters/AttachmentsConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/AttachmentsConverter.cs rename to src/redmine-net-api/JSonConverters/AttachmentsConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/ChangeSetConverter.cs b/src/redmine-net-api/JSonConverters/ChangeSetConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/ChangeSetConverter.cs rename to src/redmine-net-api/JSonConverters/ChangeSetConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/CustomFieldConverter.cs b/src/redmine-net-api/JSonConverters/CustomFieldConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/CustomFieldConverter.cs rename to src/redmine-net-api/JSonConverters/CustomFieldConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/CustomFieldPossibleValueConverter.cs b/src/redmine-net-api/JSonConverters/CustomFieldPossibleValueConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/CustomFieldPossibleValueConverter.cs rename to src/redmine-net-api/JSonConverters/CustomFieldPossibleValueConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/CustomFieldRoleConverter.cs b/src/redmine-net-api/JSonConverters/CustomFieldRoleConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/CustomFieldRoleConverter.cs rename to src/redmine-net-api/JSonConverters/CustomFieldRoleConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/DetailConverter.cs b/src/redmine-net-api/JSonConverters/DetailConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/DetailConverter.cs rename to src/redmine-net-api/JSonConverters/DetailConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/ErrorConverter.cs b/src/redmine-net-api/JSonConverters/ErrorConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/ErrorConverter.cs rename to src/redmine-net-api/JSonConverters/ErrorConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/FileConverter.cs b/src/redmine-net-api/JSonConverters/FileConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/FileConverter.cs rename to src/redmine-net-api/JSonConverters/FileConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/GroupConverter.cs b/src/redmine-net-api/JSonConverters/GroupConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/GroupConverter.cs rename to src/redmine-net-api/JSonConverters/GroupConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/GroupUserConverter.cs b/src/redmine-net-api/JSonConverters/GroupUserConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/GroupUserConverter.cs rename to src/redmine-net-api/JSonConverters/GroupUserConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/IdentifiableNameConverter.cs b/src/redmine-net-api/JSonConverters/IdentifiableNameConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/IdentifiableNameConverter.cs rename to src/redmine-net-api/JSonConverters/IdentifiableNameConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/IssueCategoryConverter.cs b/src/redmine-net-api/JSonConverters/IssueCategoryConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/IssueCategoryConverter.cs rename to src/redmine-net-api/JSonConverters/IssueCategoryConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/IssueChildConverter.cs b/src/redmine-net-api/JSonConverters/IssueChildConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/IssueChildConverter.cs rename to src/redmine-net-api/JSonConverters/IssueChildConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/IssueConverter.cs b/src/redmine-net-api/JSonConverters/IssueConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/IssueConverter.cs rename to src/redmine-net-api/JSonConverters/IssueConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/IssueCustomFieldConverter.cs b/src/redmine-net-api/JSonConverters/IssueCustomFieldConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/IssueCustomFieldConverter.cs rename to src/redmine-net-api/JSonConverters/IssueCustomFieldConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/IssuePriorityConverter.cs b/src/redmine-net-api/JSonConverters/IssuePriorityConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/IssuePriorityConverter.cs rename to src/redmine-net-api/JSonConverters/IssuePriorityConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/IssueRelationConverter.cs b/src/redmine-net-api/JSonConverters/IssueRelationConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/IssueRelationConverter.cs rename to src/redmine-net-api/JSonConverters/IssueRelationConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/IssueStatusConverter.cs b/src/redmine-net-api/JSonConverters/IssueStatusConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/IssueStatusConverter.cs rename to src/redmine-net-api/JSonConverters/IssueStatusConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/JournalConverter.cs b/src/redmine-net-api/JSonConverters/JournalConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/JournalConverter.cs rename to src/redmine-net-api/JSonConverters/JournalConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/MembershipConverter.cs b/src/redmine-net-api/JSonConverters/MembershipConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/MembershipConverter.cs rename to src/redmine-net-api/JSonConverters/MembershipConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/MembershipRoleConverter.cs b/src/redmine-net-api/JSonConverters/MembershipRoleConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/MembershipRoleConverter.cs rename to src/redmine-net-api/JSonConverters/MembershipRoleConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/NewsConverter.cs b/src/redmine-net-api/JSonConverters/NewsConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/NewsConverter.cs rename to src/redmine-net-api/JSonConverters/NewsConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/PermissionConverter.cs b/src/redmine-net-api/JSonConverters/PermissionConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/PermissionConverter.cs rename to src/redmine-net-api/JSonConverters/PermissionConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/ProjectConverter.cs b/src/redmine-net-api/JSonConverters/ProjectConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/ProjectConverter.cs rename to src/redmine-net-api/JSonConverters/ProjectConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/ProjectEnabledModuleConverter.cs b/src/redmine-net-api/JSonConverters/ProjectEnabledModuleConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/ProjectEnabledModuleConverter.cs rename to src/redmine-net-api/JSonConverters/ProjectEnabledModuleConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/ProjectIssueCategoryConverter.cs b/src/redmine-net-api/JSonConverters/ProjectIssueCategoryConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/ProjectIssueCategoryConverter.cs rename to src/redmine-net-api/JSonConverters/ProjectIssueCategoryConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/ProjectMembershipConverter.cs b/src/redmine-net-api/JSonConverters/ProjectMembershipConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/ProjectMembershipConverter.cs rename to src/redmine-net-api/JSonConverters/ProjectMembershipConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/ProjectTrackerConverter.cs b/src/redmine-net-api/JSonConverters/ProjectTrackerConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/ProjectTrackerConverter.cs rename to src/redmine-net-api/JSonConverters/ProjectTrackerConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/QueryConverter.cs b/src/redmine-net-api/JSonConverters/QueryConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/QueryConverter.cs rename to src/redmine-net-api/JSonConverters/QueryConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/RoleConverter.cs b/src/redmine-net-api/JSonConverters/RoleConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/RoleConverter.cs rename to src/redmine-net-api/JSonConverters/RoleConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/TimeEntryActivityConverter.cs b/src/redmine-net-api/JSonConverters/TimeEntryActivityConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/TimeEntryActivityConverter.cs rename to src/redmine-net-api/JSonConverters/TimeEntryActivityConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/TimeEntryConverter.cs b/src/redmine-net-api/JSonConverters/TimeEntryConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/TimeEntryConverter.cs rename to src/redmine-net-api/JSonConverters/TimeEntryConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/TrackerConverter.cs b/src/redmine-net-api/JSonConverters/TrackerConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/TrackerConverter.cs rename to src/redmine-net-api/JSonConverters/TrackerConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/TrackerCustomFieldConverter.cs b/src/redmine-net-api/JSonConverters/TrackerCustomFieldConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/TrackerCustomFieldConverter.cs rename to src/redmine-net-api/JSonConverters/TrackerCustomFieldConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/UploadConverter.cs b/src/redmine-net-api/JSonConverters/UploadConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/UploadConverter.cs rename to src/redmine-net-api/JSonConverters/UploadConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/UserConverter.cs b/src/redmine-net-api/JSonConverters/UserConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/UserConverter.cs rename to src/redmine-net-api/JSonConverters/UserConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/UserGroupConverter.cs b/src/redmine-net-api/JSonConverters/UserGroupConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/UserGroupConverter.cs rename to src/redmine-net-api/JSonConverters/UserGroupConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/VersionConverter.cs b/src/redmine-net-api/JSonConverters/VersionConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/VersionConverter.cs rename to src/redmine-net-api/JSonConverters/VersionConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/WatcherConverter.cs b/src/redmine-net-api/JSonConverters/WatcherConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/WatcherConverter.cs rename to src/redmine-net-api/JSonConverters/WatcherConverter.cs diff --git a/src/redmine-net20-api/JSonConverters/WikiPageConverter.cs b/src/redmine-net-api/JSonConverters/WikiPageConverter.cs similarity index 100% rename from src/redmine-net20-api/JSonConverters/WikiPageConverter.cs rename to src/redmine-net-api/JSonConverters/WikiPageConverter.cs diff --git a/src/redmine-net20-api/Logging/ColorConsoleLogger.cs b/src/redmine-net-api/Logging/ColorConsoleLogger.cs similarity index 100% rename from src/redmine-net20-api/Logging/ColorConsoleLogger.cs rename to src/redmine-net-api/Logging/ColorConsoleLogger.cs diff --git a/src/redmine-net20-api/Logging/ConsoleLogger.cs b/src/redmine-net-api/Logging/ConsoleLogger.cs similarity index 100% rename from src/redmine-net20-api/Logging/ConsoleLogger.cs rename to src/redmine-net-api/Logging/ConsoleLogger.cs diff --git a/src/redmine-net20-api/Logging/ILogger.cs b/src/redmine-net-api/Logging/ILogger.cs similarity index 100% rename from src/redmine-net20-api/Logging/ILogger.cs rename to src/redmine-net-api/Logging/ILogger.cs diff --git a/src/redmine-net20-api/Logging/LogEntry.cs b/src/redmine-net-api/Logging/LogEntry.cs similarity index 100% rename from src/redmine-net20-api/Logging/LogEntry.cs rename to src/redmine-net-api/Logging/LogEntry.cs diff --git a/src/redmine-net20-api/Logging/Logger.cs b/src/redmine-net-api/Logging/Logger.cs similarity index 100% rename from src/redmine-net20-api/Logging/Logger.cs rename to src/redmine-net-api/Logging/Logger.cs diff --git a/src/redmine-net20-api/Logging/LoggerExtensions.cs b/src/redmine-net-api/Logging/LoggerExtensions.cs similarity index 100% rename from src/redmine-net20-api/Logging/LoggerExtensions.cs rename to src/redmine-net-api/Logging/LoggerExtensions.cs diff --git a/src/redmine-net20-api/Logging/LoggingEventType.cs b/src/redmine-net-api/Logging/LoggingEventType.cs similarity index 100% rename from src/redmine-net20-api/Logging/LoggingEventType.cs rename to src/redmine-net-api/Logging/LoggingEventType.cs diff --git a/src/redmine-net20-api/Logging/RedmineConsoleTraceListener.cs b/src/redmine-net-api/Logging/RedmineConsoleTraceListener.cs similarity index 100% rename from src/redmine-net20-api/Logging/RedmineConsoleTraceListener.cs rename to src/redmine-net-api/Logging/RedmineConsoleTraceListener.cs diff --git a/src/redmine-net20-api/Logging/TraceLogger.cs b/src/redmine-net-api/Logging/TraceLogger.cs similarity index 100% rename from src/redmine-net20-api/Logging/TraceLogger.cs rename to src/redmine-net-api/Logging/TraceLogger.cs diff --git a/src/redmine-net20-api/MimeFormat.cs b/src/redmine-net-api/MimeFormat.cs similarity index 100% rename from src/redmine-net20-api/MimeFormat.cs rename to src/redmine-net-api/MimeFormat.cs diff --git a/src/redmine-net20-api/Properties/AssemblyInfo.cs b/src/redmine-net-api/Properties/AssemblyInfo.cs similarity index 100% rename from src/redmine-net20-api/Properties/AssemblyInfo.cs rename to src/redmine-net-api/Properties/AssemblyInfo.cs diff --git a/src/redmine-net20-api/RedmineKeys.cs b/src/redmine-net-api/RedmineKeys.cs similarity index 100% rename from src/redmine-net20-api/RedmineKeys.cs rename to src/redmine-net-api/RedmineKeys.cs diff --git a/src/redmine-net20-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs similarity index 100% rename from src/redmine-net20-api/RedmineManager.cs rename to src/redmine-net-api/RedmineManager.cs diff --git a/src/redmine-net20-api/RedmineWebClient.cs b/src/redmine-net-api/RedmineWebClient.cs similarity index 100% rename from src/redmine-net20-api/RedmineWebClient.cs rename to src/redmine-net-api/RedmineWebClient.cs diff --git a/src/redmine-net20-api/Types/Attachment.cs b/src/redmine-net-api/Types/Attachment.cs similarity index 100% rename from src/redmine-net20-api/Types/Attachment.cs rename to src/redmine-net-api/Types/Attachment.cs diff --git a/src/redmine-net20-api/Types/Attachments.cs b/src/redmine-net-api/Types/Attachments.cs similarity index 100% rename from src/redmine-net20-api/Types/Attachments.cs rename to src/redmine-net-api/Types/Attachments.cs diff --git a/src/redmine-net20-api/Types/ChangeSet.cs b/src/redmine-net-api/Types/ChangeSet.cs similarity index 100% rename from src/redmine-net20-api/Types/ChangeSet.cs rename to src/redmine-net-api/Types/ChangeSet.cs diff --git a/src/redmine-net20-api/Types/CustomField.cs b/src/redmine-net-api/Types/CustomField.cs similarity index 100% rename from src/redmine-net20-api/Types/CustomField.cs rename to src/redmine-net-api/Types/CustomField.cs diff --git a/src/redmine-net20-api/Types/CustomFieldPossibleValue.cs b/src/redmine-net-api/Types/CustomFieldPossibleValue.cs similarity index 100% rename from src/redmine-net20-api/Types/CustomFieldPossibleValue.cs rename to src/redmine-net-api/Types/CustomFieldPossibleValue.cs diff --git a/src/redmine-net20-api/Types/CustomFieldRole.cs b/src/redmine-net-api/Types/CustomFieldRole.cs similarity index 100% rename from src/redmine-net20-api/Types/CustomFieldRole.cs rename to src/redmine-net-api/Types/CustomFieldRole.cs diff --git a/src/redmine-net20-api/Types/CustomFieldValue.cs b/src/redmine-net-api/Types/CustomFieldValue.cs similarity index 100% rename from src/redmine-net20-api/Types/CustomFieldValue.cs rename to src/redmine-net-api/Types/CustomFieldValue.cs diff --git a/src/redmine-net20-api/Types/Detail.cs b/src/redmine-net-api/Types/Detail.cs similarity index 100% rename from src/redmine-net20-api/Types/Detail.cs rename to src/redmine-net-api/Types/Detail.cs diff --git a/src/redmine-net20-api/Types/Error.cs b/src/redmine-net-api/Types/Error.cs similarity index 100% rename from src/redmine-net20-api/Types/Error.cs rename to src/redmine-net-api/Types/Error.cs diff --git a/src/redmine-net20-api/Types/File.cs b/src/redmine-net-api/Types/File.cs similarity index 100% rename from src/redmine-net20-api/Types/File.cs rename to src/redmine-net-api/Types/File.cs diff --git a/src/redmine-net20-api/Types/Group.cs b/src/redmine-net-api/Types/Group.cs similarity index 100% rename from src/redmine-net20-api/Types/Group.cs rename to src/redmine-net-api/Types/Group.cs diff --git a/src/redmine-net20-api/Types/GroupUser.cs b/src/redmine-net-api/Types/GroupUser.cs similarity index 100% rename from src/redmine-net20-api/Types/GroupUser.cs rename to src/redmine-net-api/Types/GroupUser.cs diff --git a/src/redmine-net20-api/Types/IValue.cs b/src/redmine-net-api/Types/IValue.cs similarity index 100% rename from src/redmine-net20-api/Types/IValue.cs rename to src/redmine-net-api/Types/IValue.cs diff --git a/src/redmine-net20-api/Types/Identifiable.cs b/src/redmine-net-api/Types/Identifiable.cs similarity index 100% rename from src/redmine-net20-api/Types/Identifiable.cs rename to src/redmine-net-api/Types/Identifiable.cs diff --git a/src/redmine-net20-api/Types/IdentifiableName.cs b/src/redmine-net-api/Types/IdentifiableName.cs similarity index 100% rename from src/redmine-net20-api/Types/IdentifiableName.cs rename to src/redmine-net-api/Types/IdentifiableName.cs diff --git a/src/redmine-net20-api/Types/Issue.cs b/src/redmine-net-api/Types/Issue.cs similarity index 100% rename from src/redmine-net20-api/Types/Issue.cs rename to src/redmine-net-api/Types/Issue.cs diff --git a/src/redmine-net20-api/Types/IssueCategory.cs b/src/redmine-net-api/Types/IssueCategory.cs similarity index 100% rename from src/redmine-net20-api/Types/IssueCategory.cs rename to src/redmine-net-api/Types/IssueCategory.cs diff --git a/src/redmine-net20-api/Types/IssueChild.cs b/src/redmine-net-api/Types/IssueChild.cs similarity index 100% rename from src/redmine-net20-api/Types/IssueChild.cs rename to src/redmine-net-api/Types/IssueChild.cs diff --git a/src/redmine-net20-api/Types/IssueCustomField.cs b/src/redmine-net-api/Types/IssueCustomField.cs similarity index 100% rename from src/redmine-net20-api/Types/IssueCustomField.cs rename to src/redmine-net-api/Types/IssueCustomField.cs diff --git a/src/redmine-net20-api/Types/IssuePriority.cs b/src/redmine-net-api/Types/IssuePriority.cs similarity index 100% rename from src/redmine-net20-api/Types/IssuePriority.cs rename to src/redmine-net-api/Types/IssuePriority.cs diff --git a/src/redmine-net20-api/Types/IssueRelation.cs b/src/redmine-net-api/Types/IssueRelation.cs similarity index 100% rename from src/redmine-net20-api/Types/IssueRelation.cs rename to src/redmine-net-api/Types/IssueRelation.cs diff --git a/src/redmine-net20-api/Types/IssueRelationType.cs b/src/redmine-net-api/Types/IssueRelationType.cs similarity index 100% rename from src/redmine-net20-api/Types/IssueRelationType.cs rename to src/redmine-net-api/Types/IssueRelationType.cs diff --git a/src/redmine-net20-api/Types/IssueStatus.cs b/src/redmine-net-api/Types/IssueStatus.cs similarity index 100% rename from src/redmine-net20-api/Types/IssueStatus.cs rename to src/redmine-net-api/Types/IssueStatus.cs diff --git a/src/redmine-net20-api/Types/Journal.cs b/src/redmine-net-api/Types/Journal.cs similarity index 100% rename from src/redmine-net20-api/Types/Journal.cs rename to src/redmine-net-api/Types/Journal.cs diff --git a/src/redmine-net20-api/Types/Membership.cs b/src/redmine-net-api/Types/Membership.cs similarity index 100% rename from src/redmine-net20-api/Types/Membership.cs rename to src/redmine-net-api/Types/Membership.cs diff --git a/src/redmine-net20-api/Types/MembershipRole.cs b/src/redmine-net-api/Types/MembershipRole.cs similarity index 100% rename from src/redmine-net20-api/Types/MembershipRole.cs rename to src/redmine-net-api/Types/MembershipRole.cs diff --git a/src/redmine-net20-api/Types/News.cs b/src/redmine-net-api/Types/News.cs similarity index 100% rename from src/redmine-net20-api/Types/News.cs rename to src/redmine-net-api/Types/News.cs diff --git a/src/redmine-net20-api/Types/PaginatedObjects.cs b/src/redmine-net-api/Types/PaginatedObjects.cs similarity index 100% rename from src/redmine-net20-api/Types/PaginatedObjects.cs rename to src/redmine-net-api/Types/PaginatedObjects.cs diff --git a/src/redmine-net20-api/Types/Permission.cs b/src/redmine-net-api/Types/Permission.cs similarity index 100% rename from src/redmine-net20-api/Types/Permission.cs rename to src/redmine-net-api/Types/Permission.cs diff --git a/src/redmine-net20-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs similarity index 100% rename from src/redmine-net20-api/Types/Project.cs rename to src/redmine-net-api/Types/Project.cs diff --git a/src/redmine-net20-api/Types/ProjectEnabledModule.cs b/src/redmine-net-api/Types/ProjectEnabledModule.cs similarity index 100% rename from src/redmine-net20-api/Types/ProjectEnabledModule.cs rename to src/redmine-net-api/Types/ProjectEnabledModule.cs diff --git a/src/redmine-net20-api/Types/ProjectIssueCategory.cs b/src/redmine-net-api/Types/ProjectIssueCategory.cs similarity index 100% rename from src/redmine-net20-api/Types/ProjectIssueCategory.cs rename to src/redmine-net-api/Types/ProjectIssueCategory.cs diff --git a/src/redmine-net20-api/Types/ProjectMembership.cs b/src/redmine-net-api/Types/ProjectMembership.cs similarity index 100% rename from src/redmine-net20-api/Types/ProjectMembership.cs rename to src/redmine-net-api/Types/ProjectMembership.cs diff --git a/src/redmine-net20-api/Types/ProjectStatus.cs b/src/redmine-net-api/Types/ProjectStatus.cs similarity index 100% rename from src/redmine-net20-api/Types/ProjectStatus.cs rename to src/redmine-net-api/Types/ProjectStatus.cs diff --git a/src/redmine-net20-api/Types/ProjectTracker.cs b/src/redmine-net-api/Types/ProjectTracker.cs similarity index 100% rename from src/redmine-net20-api/Types/ProjectTracker.cs rename to src/redmine-net-api/Types/ProjectTracker.cs diff --git a/src/redmine-net20-api/Types/Query.cs b/src/redmine-net-api/Types/Query.cs similarity index 100% rename from src/redmine-net20-api/Types/Query.cs rename to src/redmine-net-api/Types/Query.cs diff --git a/src/redmine-net20-api/Types/Role.cs b/src/redmine-net-api/Types/Role.cs similarity index 100% rename from src/redmine-net20-api/Types/Role.cs rename to src/redmine-net-api/Types/Role.cs diff --git a/src/redmine-net20-api/Types/TimeEntry.cs b/src/redmine-net-api/Types/TimeEntry.cs similarity index 100% rename from src/redmine-net20-api/Types/TimeEntry.cs rename to src/redmine-net-api/Types/TimeEntry.cs diff --git a/src/redmine-net20-api/Types/TimeEntryActivity.cs b/src/redmine-net-api/Types/TimeEntryActivity.cs similarity index 100% rename from src/redmine-net20-api/Types/TimeEntryActivity.cs rename to src/redmine-net-api/Types/TimeEntryActivity.cs diff --git a/src/redmine-net20-api/Types/Tracker.cs b/src/redmine-net-api/Types/Tracker.cs similarity index 100% rename from src/redmine-net20-api/Types/Tracker.cs rename to src/redmine-net-api/Types/Tracker.cs diff --git a/src/redmine-net20-api/Types/TrackerCustomField.cs b/src/redmine-net-api/Types/TrackerCustomField.cs similarity index 100% rename from src/redmine-net20-api/Types/TrackerCustomField.cs rename to src/redmine-net-api/Types/TrackerCustomField.cs diff --git a/src/redmine-net20-api/Types/Upload.cs b/src/redmine-net-api/Types/Upload.cs similarity index 100% rename from src/redmine-net20-api/Types/Upload.cs rename to src/redmine-net-api/Types/Upload.cs diff --git a/src/redmine-net20-api/Types/User.cs b/src/redmine-net-api/Types/User.cs similarity index 100% rename from src/redmine-net20-api/Types/User.cs rename to src/redmine-net-api/Types/User.cs diff --git a/src/redmine-net20-api/Types/UserGroup.cs b/src/redmine-net-api/Types/UserGroup.cs similarity index 100% rename from src/redmine-net20-api/Types/UserGroup.cs rename to src/redmine-net-api/Types/UserGroup.cs diff --git a/src/redmine-net20-api/Types/UserStatus.cs b/src/redmine-net-api/Types/UserStatus.cs similarity index 100% rename from src/redmine-net20-api/Types/UserStatus.cs rename to src/redmine-net-api/Types/UserStatus.cs diff --git a/src/redmine-net20-api/Types/Version.cs b/src/redmine-net-api/Types/Version.cs similarity index 100% rename from src/redmine-net20-api/Types/Version.cs rename to src/redmine-net-api/Types/Version.cs diff --git a/src/redmine-net20-api/Types/Watcher.cs b/src/redmine-net-api/Types/Watcher.cs similarity index 100% rename from src/redmine-net20-api/Types/Watcher.cs rename to src/redmine-net-api/Types/Watcher.cs diff --git a/src/redmine-net20-api/Types/WikiPage.cs b/src/redmine-net-api/Types/WikiPage.cs similarity index 100% rename from src/redmine-net20-api/Types/WikiPage.cs rename to src/redmine-net-api/Types/WikiPage.cs diff --git a/src/redmine-net20-api/redmine-net20-api.csproj b/src/redmine-net-api/redmine-net-api.csproj similarity index 100% rename from src/redmine-net20-api/redmine-net20-api.csproj rename to src/redmine-net-api/redmine-net-api.csproj From 25683cebc2125fc668b5416e5e24bbd4c235ed5f Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Tue, 19 Nov 2019 15:16:56 +0200 Subject: [PATCH 007/549] Change solution folders structure --- src/redmine-net-api.sln => redmine-net-api.sln | 6 ++++-- src/redmine-net-api.sln.DotSettings | 3 --- {src => tests}/redmine-net-api.Tests/Helper.cs | 0 .../redmine-net-api.Tests/Infrastructure/CaseOrder.cs | 0 .../Infrastructure/CollectionOrderer.cs | 0 .../redmine-net-api.Tests/Infrastructure/OrderAttribute.cs | 0 .../Infrastructure/RedmineCollection.cs | 0 {src => tests}/redmine-net-api.Tests/RedmineFixture.cs | 0 .../Tests/Async/AttachmentAsyncTests.cs | 0 .../redmine-net-api.Tests/Tests/Async/IssueAsyncTests.cs | 0 .../redmine-net-api.Tests/Tests/Async/UserAsyncTests.cs | 0 .../redmine-net-api.Tests/Tests/Async/WikiPageAsyncTests.cs | 0 {src => tests}/redmine-net-api.Tests/Tests/RedmineTest.cs | 0 .../redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs | 0 .../redmine-net-api.Tests/Tests/Sync/CustomFieldTests.cs | 0 .../redmine-net-api.Tests/Tests/Sync/GroupTests.cs | 0 .../redmine-net-api.Tests/Tests/Sync/IssueCategoryTests.cs | 0 .../redmine-net-api.Tests/Tests/Sync/IssuePriorityTests.cs | 0 .../redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs | 0 .../redmine-net-api.Tests/Tests/Sync/IssueStatusTests.cs | 0 .../redmine-net-api.Tests/Tests/Sync/IssueTests.cs | 0 .../redmine-net-api.Tests/Tests/Sync/NewsTests.cs | 0 .../Tests/Sync/ProjectMembershipTests.cs | 0 .../redmine-net-api.Tests/Tests/Sync/ProjectTests.cs | 0 .../redmine-net-api.Tests/Tests/Sync/QueryTests.cs | 0 .../redmine-net-api.Tests/Tests/Sync/RoleTests.cs | 0 .../Tests/Sync/TimeEntryActivtiyTests.cs | 0 .../redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs | 0 .../redmine-net-api.Tests/Tests/Sync/TrackerTests.cs | 0 .../redmine-net-api.Tests/Tests/Sync/UserTests.cs | 0 .../redmine-net-api.Tests/Tests/Sync/VersionTests.cs | 0 .../redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs | 0 {src => tests}/redmine-net-api.Tests/packages.config | 0 .../redmine-net-api.Tests/redmine-net-api.Tests.csproj | 2 +- 34 files changed, 5 insertions(+), 6 deletions(-) rename src/redmine-net-api.sln => redmine-net-api.sln (89%) delete mode 100755 src/redmine-net-api.sln.DotSettings rename {src => tests}/redmine-net-api.Tests/Helper.cs (100%) rename {src => tests}/redmine-net-api.Tests/Infrastructure/CaseOrder.cs (100%) rename {src => tests}/redmine-net-api.Tests/Infrastructure/CollectionOrderer.cs (100%) rename {src => tests}/redmine-net-api.Tests/Infrastructure/OrderAttribute.cs (100%) rename {src => tests}/redmine-net-api.Tests/Infrastructure/RedmineCollection.cs (100%) rename {src => tests}/redmine-net-api.Tests/RedmineFixture.cs (100%) rename {src => tests}/redmine-net-api.Tests/Tests/Async/AttachmentAsyncTests.cs (100%) rename {src => tests}/redmine-net-api.Tests/Tests/Async/IssueAsyncTests.cs (100%) rename {src => tests}/redmine-net-api.Tests/Tests/Async/UserAsyncTests.cs (100%) rename {src => tests}/redmine-net-api.Tests/Tests/Async/WikiPageAsyncTests.cs (100%) rename {src => tests}/redmine-net-api.Tests/Tests/RedmineTest.cs (100%) rename {src => tests}/redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs (100%) rename {src => tests}/redmine-net-api.Tests/Tests/Sync/CustomFieldTests.cs (100%) rename {src => tests}/redmine-net-api.Tests/Tests/Sync/GroupTests.cs (100%) rename {src => tests}/redmine-net-api.Tests/Tests/Sync/IssueCategoryTests.cs (100%) rename {src => tests}/redmine-net-api.Tests/Tests/Sync/IssuePriorityTests.cs (100%) rename {src => tests}/redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs (100%) rename {src => tests}/redmine-net-api.Tests/Tests/Sync/IssueStatusTests.cs (100%) rename {src => tests}/redmine-net-api.Tests/Tests/Sync/IssueTests.cs (100%) rename {src => tests}/redmine-net-api.Tests/Tests/Sync/NewsTests.cs (100%) rename {src => tests}/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs (100%) rename {src => tests}/redmine-net-api.Tests/Tests/Sync/ProjectTests.cs (100%) rename {src => tests}/redmine-net-api.Tests/Tests/Sync/QueryTests.cs (100%) rename {src => tests}/redmine-net-api.Tests/Tests/Sync/RoleTests.cs (100%) rename {src => tests}/redmine-net-api.Tests/Tests/Sync/TimeEntryActivtiyTests.cs (100%) rename {src => tests}/redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs (100%) rename {src => tests}/redmine-net-api.Tests/Tests/Sync/TrackerTests.cs (100%) rename {src => tests}/redmine-net-api.Tests/Tests/Sync/UserTests.cs (100%) rename {src => tests}/redmine-net-api.Tests/Tests/Sync/VersionTests.cs (100%) rename {src => tests}/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs (100%) rename {src => tests}/redmine-net-api.Tests/packages.config (100%) rename {src => tests}/redmine-net-api.Tests/redmine-net-api.Tests.csproj (97%) diff --git a/src/redmine-net-api.sln b/redmine-net-api.sln similarity index 89% rename from src/redmine-net-api.sln rename to redmine-net-api.sln index 02c0b032..8f0cd011 100644 --- a/src/redmine-net-api.sln +++ b/redmine-net-api.sln @@ -7,9 +7,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0DFF4758-5C1 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{F3F4278D-6271-4F77-BA88-41555D53CBD1}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "redmine-net-api", "redmine-net-api\redmine-net-api.csproj", "{0E6B9B72-445D-4E71-8D29-48C4A009AB03}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "redmine-net-api", "src\redmine-net-api\redmine-net-api.csproj", "{0E6B9B72-445D-4E71-8D29-48C4A009AB03}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "redmine-net-api.Tests", "redmine-net-api.Tests\redmine-net-api.Tests.csproj", "{900EF0B3-0233-45DA-811F-4C59483E8452}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "redmine-net-api.Tests", "tests\redmine-net-api.Tests\redmine-net-api.Tests.csproj", "{900EF0B3-0233-45DA-811F-4C59483E8452}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionFolder", "SolutionFolder", "{E8C35EC2-DD90-46E8-9B63-84EFD5F2FDE3}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/redmine-net-api.sln.DotSettings b/src/redmine-net-api.sln.DotSettings deleted file mode 100755 index 372a1f5b..00000000 --- a/src/redmine-net-api.sln.DotSettings +++ /dev/null @@ -1,3 +0,0 @@ - - JSON - XML \ No newline at end of file diff --git a/src/redmine-net-api.Tests/Helper.cs b/tests/redmine-net-api.Tests/Helper.cs similarity index 100% rename from src/redmine-net-api.Tests/Helper.cs rename to tests/redmine-net-api.Tests/Helper.cs diff --git a/src/redmine-net-api.Tests/Infrastructure/CaseOrder.cs b/tests/redmine-net-api.Tests/Infrastructure/CaseOrder.cs similarity index 100% rename from src/redmine-net-api.Tests/Infrastructure/CaseOrder.cs rename to tests/redmine-net-api.Tests/Infrastructure/CaseOrder.cs diff --git a/src/redmine-net-api.Tests/Infrastructure/CollectionOrderer.cs b/tests/redmine-net-api.Tests/Infrastructure/CollectionOrderer.cs similarity index 100% rename from src/redmine-net-api.Tests/Infrastructure/CollectionOrderer.cs rename to tests/redmine-net-api.Tests/Infrastructure/CollectionOrderer.cs diff --git a/src/redmine-net-api.Tests/Infrastructure/OrderAttribute.cs b/tests/redmine-net-api.Tests/Infrastructure/OrderAttribute.cs similarity index 100% rename from src/redmine-net-api.Tests/Infrastructure/OrderAttribute.cs rename to tests/redmine-net-api.Tests/Infrastructure/OrderAttribute.cs diff --git a/src/redmine-net-api.Tests/Infrastructure/RedmineCollection.cs b/tests/redmine-net-api.Tests/Infrastructure/RedmineCollection.cs similarity index 100% rename from src/redmine-net-api.Tests/Infrastructure/RedmineCollection.cs rename to tests/redmine-net-api.Tests/Infrastructure/RedmineCollection.cs diff --git a/src/redmine-net-api.Tests/RedmineFixture.cs b/tests/redmine-net-api.Tests/RedmineFixture.cs similarity index 100% rename from src/redmine-net-api.Tests/RedmineFixture.cs rename to tests/redmine-net-api.Tests/RedmineFixture.cs diff --git a/src/redmine-net-api.Tests/Tests/Async/AttachmentAsyncTests.cs b/tests/redmine-net-api.Tests/Tests/Async/AttachmentAsyncTests.cs similarity index 100% rename from src/redmine-net-api.Tests/Tests/Async/AttachmentAsyncTests.cs rename to tests/redmine-net-api.Tests/Tests/Async/AttachmentAsyncTests.cs diff --git a/src/redmine-net-api.Tests/Tests/Async/IssueAsyncTests.cs b/tests/redmine-net-api.Tests/Tests/Async/IssueAsyncTests.cs similarity index 100% rename from src/redmine-net-api.Tests/Tests/Async/IssueAsyncTests.cs rename to tests/redmine-net-api.Tests/Tests/Async/IssueAsyncTests.cs diff --git a/src/redmine-net-api.Tests/Tests/Async/UserAsyncTests.cs b/tests/redmine-net-api.Tests/Tests/Async/UserAsyncTests.cs similarity index 100% rename from src/redmine-net-api.Tests/Tests/Async/UserAsyncTests.cs rename to tests/redmine-net-api.Tests/Tests/Async/UserAsyncTests.cs diff --git a/src/redmine-net-api.Tests/Tests/Async/WikiPageAsyncTests.cs b/tests/redmine-net-api.Tests/Tests/Async/WikiPageAsyncTests.cs similarity index 100% rename from src/redmine-net-api.Tests/Tests/Async/WikiPageAsyncTests.cs rename to tests/redmine-net-api.Tests/Tests/Async/WikiPageAsyncTests.cs diff --git a/src/redmine-net-api.Tests/Tests/RedmineTest.cs b/tests/redmine-net-api.Tests/Tests/RedmineTest.cs similarity index 100% rename from src/redmine-net-api.Tests/Tests/RedmineTest.cs rename to tests/redmine-net-api.Tests/Tests/RedmineTest.cs diff --git a/src/redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs similarity index 100% rename from src/redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs rename to tests/redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs diff --git a/src/redmine-net-api.Tests/Tests/Sync/CustomFieldTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/CustomFieldTests.cs similarity index 100% rename from src/redmine-net-api.Tests/Tests/Sync/CustomFieldTests.cs rename to tests/redmine-net-api.Tests/Tests/Sync/CustomFieldTests.cs diff --git a/src/redmine-net-api.Tests/Tests/Sync/GroupTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/GroupTests.cs similarity index 100% rename from src/redmine-net-api.Tests/Tests/Sync/GroupTests.cs rename to tests/redmine-net-api.Tests/Tests/Sync/GroupTests.cs diff --git a/src/redmine-net-api.Tests/Tests/Sync/IssueCategoryTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/IssueCategoryTests.cs similarity index 100% rename from src/redmine-net-api.Tests/Tests/Sync/IssueCategoryTests.cs rename to tests/redmine-net-api.Tests/Tests/Sync/IssueCategoryTests.cs diff --git a/src/redmine-net-api.Tests/Tests/Sync/IssuePriorityTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/IssuePriorityTests.cs similarity index 100% rename from src/redmine-net-api.Tests/Tests/Sync/IssuePriorityTests.cs rename to tests/redmine-net-api.Tests/Tests/Sync/IssuePriorityTests.cs diff --git a/src/redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs similarity index 100% rename from src/redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs rename to tests/redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs diff --git a/src/redmine-net-api.Tests/Tests/Sync/IssueStatusTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/IssueStatusTests.cs similarity index 100% rename from src/redmine-net-api.Tests/Tests/Sync/IssueStatusTests.cs rename to tests/redmine-net-api.Tests/Tests/Sync/IssueStatusTests.cs diff --git a/src/redmine-net-api.Tests/Tests/Sync/IssueTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/IssueTests.cs similarity index 100% rename from src/redmine-net-api.Tests/Tests/Sync/IssueTests.cs rename to tests/redmine-net-api.Tests/Tests/Sync/IssueTests.cs diff --git a/src/redmine-net-api.Tests/Tests/Sync/NewsTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/NewsTests.cs similarity index 100% rename from src/redmine-net-api.Tests/Tests/Sync/NewsTests.cs rename to tests/redmine-net-api.Tests/Tests/Sync/NewsTests.cs diff --git a/src/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs similarity index 100% rename from src/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs rename to tests/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs diff --git a/src/redmine-net-api.Tests/Tests/Sync/ProjectTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/ProjectTests.cs similarity index 100% rename from src/redmine-net-api.Tests/Tests/Sync/ProjectTests.cs rename to tests/redmine-net-api.Tests/Tests/Sync/ProjectTests.cs diff --git a/src/redmine-net-api.Tests/Tests/Sync/QueryTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/QueryTests.cs similarity index 100% rename from src/redmine-net-api.Tests/Tests/Sync/QueryTests.cs rename to tests/redmine-net-api.Tests/Tests/Sync/QueryTests.cs diff --git a/src/redmine-net-api.Tests/Tests/Sync/RoleTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/RoleTests.cs similarity index 100% rename from src/redmine-net-api.Tests/Tests/Sync/RoleTests.cs rename to tests/redmine-net-api.Tests/Tests/Sync/RoleTests.cs diff --git a/src/redmine-net-api.Tests/Tests/Sync/TimeEntryActivtiyTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryActivtiyTests.cs similarity index 100% rename from src/redmine-net-api.Tests/Tests/Sync/TimeEntryActivtiyTests.cs rename to tests/redmine-net-api.Tests/Tests/Sync/TimeEntryActivtiyTests.cs diff --git a/src/redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs similarity index 100% rename from src/redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs rename to tests/redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs diff --git a/src/redmine-net-api.Tests/Tests/Sync/TrackerTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/TrackerTests.cs similarity index 100% rename from src/redmine-net-api.Tests/Tests/Sync/TrackerTests.cs rename to tests/redmine-net-api.Tests/Tests/Sync/TrackerTests.cs diff --git a/src/redmine-net-api.Tests/Tests/Sync/UserTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/UserTests.cs similarity index 100% rename from src/redmine-net-api.Tests/Tests/Sync/UserTests.cs rename to tests/redmine-net-api.Tests/Tests/Sync/UserTests.cs diff --git a/src/redmine-net-api.Tests/Tests/Sync/VersionTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/VersionTests.cs similarity index 100% rename from src/redmine-net-api.Tests/Tests/Sync/VersionTests.cs rename to tests/redmine-net-api.Tests/Tests/Sync/VersionTests.cs diff --git a/src/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs similarity index 100% rename from src/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs rename to tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs diff --git a/src/redmine-net-api.Tests/packages.config b/tests/redmine-net-api.Tests/packages.config similarity index 100% rename from src/redmine-net-api.Tests/packages.config rename to tests/redmine-net-api.Tests/packages.config diff --git a/src/redmine-net-api.Tests/redmine-net-api.Tests.csproj b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj similarity index 97% rename from src/redmine-net-api.Tests/redmine-net-api.Tests.csproj rename to tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj index 73725264..efdec128 100644 --- a/src/redmine-net-api.Tests/redmine-net-api.Tests.csproj +++ b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj @@ -86,7 +86,7 @@ - + From d8b9f9d465c440cc97748bcfc11d972ff1407bbf Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Tue, 19 Nov 2019 15:26:39 +0200 Subject: [PATCH 008/549] Cleanup folders --- build/redmine-net-api-signed.nuspec | 38 ----------------- build/redmine-net-api.nuspec | 40 ------------------ .../docker-compose.yml => docker-compose.yml | 0 tools/NuGet/NuGet.exe | Bin 3957976 -> 0 bytes 4 files changed, 78 deletions(-) delete mode 100644 build/redmine-net-api-signed.nuspec delete mode 100644 build/redmine-net-api.nuspec rename build/docker-compose.yml => docker-compose.yml (100%) mode change 100755 => 100644 delete mode 100755 tools/NuGet/NuGet.exe diff --git a/build/redmine-net-api-signed.nuspec b/build/redmine-net-api-signed.nuspec deleted file mode 100644 index 2c7b8523..00000000 --- a/build/redmine-net-api-signed.nuspec +++ /dev/null @@ -1,38 +0,0 @@ - - - - redmine-api-signed - 0.0.0.0 - Redmine .NET API Signed - Adrian Popescu - Adrian Popescu - - Apache-2.0 - https://github.com/zapadi/redmine-net-api - https://github.com/zapadi/redmine-net-api/raw/master/logo.png - true - Redmine .NET API is a communication library for Redmine project management application. - - Copyright ©2011-2019 Adrian Popescu - en-US - Redmine API .NET C# - - Bug fixes and performance improvements - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/build/redmine-net-api.nuspec b/build/redmine-net-api.nuspec deleted file mode 100644 index 700c00d6..00000000 --- a/build/redmine-net-api.nuspec +++ /dev/null @@ -1,40 +0,0 @@ - - - - redmine-api - 0.0.0.0 - Redmine .NET API - Adrian Popescu - Adrian Popescu - - Apache-2.0 - https://github.com/zapadi/redmine-net-api - https://github.com/zapadi/redmine-net-api/raw/master/logo.png - true - Redmine .NET API is a communication library for Redmine project management application. - - Copyright ©2011-2019 Adrian Popescu - en-US - Redmine API .NET C# - - Bug fixes and performance improvements - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/build/docker-compose.yml b/docker-compose.yml old mode 100755 new mode 100644 similarity index 100% rename from build/docker-compose.yml rename to docker-compose.yml diff --git a/tools/NuGet/NuGet.exe b/tools/NuGet/NuGet.exe deleted file mode 100755 index 6bb79fe5379d098fabcabf69f3e5c9e8214c229b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3957976 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

u`~u-4R_QwH34fjNiJ#YXwiBMQTDPA`_yNKP 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

q?zIcAk{U|VH1Lr>$qLBDkJ%l8D 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%0Dr|q4 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®FbQ_ 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%0D-B$lcXj(!M5VY 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 &075Qv=WA+vP92B5(O=;-8ki!KUdW03zHFK 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

Kr!IOq6Q7?TzDzy)-fY;^MGt18Mq3kykJ!_S6 zOgX#-1Z{OPX1L(8Oix5$Q+TVPv$dh@mDo%0ApR0w_$%a)^$nXhg)i_P#CwCvY_glv zeJuPkkSrWFm>2zclV^^%yOTZ>U#t5n-J_VN_q6t(O3j8Z`vE#;JLcXjNIH<`_)aG7 z$J`UxH_jOZX&K-Xc%da6PqG90EYy;mI8`)o2jH&oY)l(&vpv0|l2uYIwDtSrb2t`m z_*+7@4{`Nqf_8a(xz>e##X^5fV$&v6?xOcV*mm7IXw(?1o6P2m~sv>vkx zAF$mT{0YO&j+&m~rs0lh&YX5BDQ4z0E4S^1JpkIAIW4>k&LGY$r_zq>#IughQ33dn zIu|SQklMb^;(wW%HgQ_y7ME@@PdA!JYTC?MSsR*;EB5Z%TAVmH;>+jYT1RqT($3_4 zT-nYkB*VfrcNf}MqHIg(`Co;t9)S#lZ@k0~vg4(8v^9h3L1MH7tiEtE=X}47KWugC zU{KMToS7KJhv;z|d-R}(0%dUmE;n6(9J-mf$0&o0-x~3>7LO4zcDzl|#9_9RVwA~% z7NcMFrpIK^>txW>b7*uQ>q&y9V-6t6+ToU)T>Dmy=||7eE-Ztnyw9?T+7Sv+tR0DG z!}hV%ZhLXN4L!oGINL(8&NW3@rP%!o3mfPwx&HbeDzvK@8YVycY(epLW?n?f+{kF3 zKCo}@pt^Jvu&gf0?um_Wo8?i5n-G-a>6F6iB=ouQv_f#&DH|eGZlgBWVjz{m2=`2XiMk1f%i%DzT) zs_CvOwYu&gs0tL`C1h1ORONX5_|drI#{}Vb z)s#x0K`aDSqblFCJ}4fEne_o{n5+*hVsL|3EMWa?shqW(GKs~cgpi*=NLS?^k#jFj z|MmpvC@%O4A<`L#WHMab5%E-(WT{HuLLnEdK8|8s08t^$(JHEJ4XJYPbC#^jY`B|~ za3?1mxsTjt4N>KWCXyvfUOGgYacj6|KH-wpE)LQyh|8k0&T7VM-N5DSgClkJOnRTej zwJH3mC7tylvTdEMh#fi)_*(>t@1jBH2x1%#)gZ#i?efSK#{#r z#c*Zz7cUsJP^v7`7B~^?q<}vozrKY+h8Mzpj<2mxZ4U*yno()r*qTr+;Ry`r!;1*3 za%)bd{fTq*Y&0$;kR7$mr+Nl(oa`Mg!@}nQtwhQ}%K}oTe=;dg<-V(eJO0tq+lv#K zl}M|_n9sLAEraNtt~FS@!Lz{fIe{S5)!hjcRCa~R+UY=o*tsY1Tob!ez{vuLX(`|o z0Yq#HI8^{;98^!kwPu&P=N4}%)#2i9V#b4TEWDWVN0(tEP$4eI&qppIjJE9zOX);h zja)7v{Ltm1H5P)Ai@f3P4^vy8Vv*DpB&Csy;B7-HT)XR^uB+?qM2@L|b$KeRsf7)h zUFTPJeUV!sXCzfc3*BLztuV*rw!(Bh&cWpVEq?FvlYFJ8cxKaHj;F9*f0cY@PfYtT z)hJd-g77JQ6(oEVcA<$6cr#3I2 zrCQrFy%_zGGyRWw5LotIJ=&i`?5AEpiEW#U>FKn9?Ff%Lg46P{*8gmZ6&>NsNA%h2 z7LFQID2y?kJqHvv9=<3RzQRu(zkYl}IZwRks$C_or*c&U`|ElXAilu(|xf>5zgLVpFJqkubv`D9{5 z_8N8c?-vj_dIAz^-{VP(!%3MT*)-Ok_Rf}Zpg3XnSP@?Nxk6o*gj(KVY;|g7e^uN| zzyz^e7g8Kl8njzVLlNlsRAd)wxOjjn;smY2o5XphwQG_HTA@jV7UY<4tI4>ftfMa@ z5@VTeE*8nr9F)SALe(y{v@gURoVp$h4vs-T!y)4415AstJ~6DyDMB*JWCYwAtsw@hK_JE5px9dbxOj?nCf(yWV{3#7Q{kO|EuzcyQiqf)52U6a&!0v z5*weuH82(l9b@JlujIRoAjj-npA*G&8jbUk0Km}Wqq8LWNIwI zbQZ@K!>6F*5ZmAC_*r=x0Oy37%v6mWKcZ&kpEb>&i!=rB~_^m}L{a(z(Y zB>m_J^x$;1IxI0lMdB>()PF$UT#W@%zIHh^>~b2^Wx7#6nvtFl_k2L6>1f6v-GU5h zTF1C&s#3JdWRkPvg}wJ|(XX_TMIaWZ$Kr_N$?6be{-IQYSJ4H%4D}IYT1u(4O1=iD z;T)bsiP@c3m!?!8Nz=3;P1Ds2Yfzj%NRigF)*@ExHxPWvw^-v=XW0$VS~Wm3ahtMt zw1&?kyljBxVankz!Azfj9-M%pyYhmv?)Rg^zi+TDJyczhpz%Kx< z!lZxi^1wASg$IF)9R#yE$ZI^e1N(DBc**t)wEc7VdE-2_#!ca`L}9+pp>t-buo2;l z^b_H)!3FX4gsZ&3BhNSRJhrE;-{$M3tBXw7+PRfj?bnNhC`Z4+rHO7UwVZj#nnTVW zy&#BhB3=;Rh|9OTAK1$ealMgkH_pVTDumX|`Vj<}F; zN@W!&Yah}OA&Bo8E$CB(@`I{53t~%(7KmeUk5>(qPuArn1?(O4c?#0-U*?j$*1DfvT}5;W0op zCBgIKdR9U_KcQ!(#`DkhtTcyvH>Xgq!)ulgvi8jq_bpS{VM)2ac{ zc*yg#Q?W?OMiMk08ew>=G%QU^tnB(LSqRjV<%p8&t}?swZ9mS|Er^J$ub4cux9|{r zFB?yu1eLVco0Fuh;`%Lk7W2R?%`RB)OOWVyFy1QU$nq))%KMDG>QlHX?=>p#USz|` zrhq7~Jg>ZpHKM!|iJ&3-Ch{iCzA>dF`n|FreY2)zU#>k%dP(-I`np!xzi#9U!KyEL zUTv`K>o*fFzND&pi!SNWmev1IQZ4AD%Be{)UPo_I!fCI%iMHiqtfXnR;Ylj)9FvOM z_PwYoozL%2N)8@s(SG`;`XsA9#(2LzNl=+ir1dEsKeXoKhvx`anM+H}_~Ch;|L5^T zn)W8ifaI5Hg0vBnXD(NG5aP$0F8%=)po@$p>=3LI@Y5gXOo0sIKbrftx$odcgu_+J2{nY-DndmkwKBfq6#X%PPv z)WFHS2C+hU9Y)1~?*Z(J-WTd`I9j*(cl=q_V5mbgLd&%e049W@!QU65*INccpuzII z6`n?DQ}`iJt#i@JPnNJ~B}hU?(%TqK{~%QKPdVe63z>PhlZSi)WHeyj{fICL5tw(c z7ZEF1l0wM5D}T8A;nd({RE!C`NKA_z1*hw!cm9}g%;Kyn2eEh%#DXn~5jqQT(Tup< zIQhRYLdNc_PgBN8pJp^1syRu96^9b6G0U(__Gn&>B2ymD=KmMrvWbF+X%0UDgF0un z++V}8Z}7eYik9|ms-su)dj{NBJUAe;g`EuSJN&#pPfFVq{@W!!dFI5zC&X>Dy7C6K zh`ww$BGX=~ny}m*SP;Ky?jOv33wLoYfhxx8DbdCvK2&#xYuZk$v zC{^Tbz#>NdD>aq30hJ|)h45;H8YF!+wHyqWXsSIoXsQerxu&YN7|j?g{zR$?(HJal z6wz*|C4>wX@;yy8su>qZQxmxMMA}YfeUYdv-=i~Dk#qM14e_dwiHc~YSVzl%i0)`{ z)7}*7E}ZR1Zu?B3V_euVSy(qMR^bvQvvN8mD@n<$M8^J(QvJ^398*qq9;0IrCSBSC z{ zof5q<&$EI4h@b5FNs$((Eb71(Evefqk^fp*IB!zL&v_jF2c{2DT?#{oX)m;468vMx z0K2+84>EC%S_daR1_qOjfV|9NM6&YF1ew)Q7bBd3fCgAIkQ9AU`l%Cl7hURm$>e{i zB130=mKV*`q+4q88AZ1t%PKis`}YNjzw z854d^Vj8jto(x%x*i&O$=^{$WpvDs`Q#iq_@7NpIf&9F_L(OM4=g=CY14(ZwYyUq5#dt;&7uv}- zysoj~`YK@?Veo89*gN|kUKTt=nPjv57=1`5VHF^)auF0M)}Zho`xN z!SqcQaTS*1Cst^_-8fg&eZOn-^W-KLHif6VG$zmNDX4ypGr286L6F-^BxXH)5H|ra zW<}T2>Y7ylQ|eeYsUa`;rkO1o|S+(-*~R(3kvN9!t{FD%T%>k|W&lCx!W9 zG=CcUED2w^WW6wn=V@5SSkEDHvhz>G`%A%h81Kh`g7-%dm@`&+i64i*vsNMi-Ln9- zGENZ7H5|rA3-_?(5J-!gaMYwVq;C>SlYF=g4*JzC()2ud7#~aQ^GQ4F zzIsH)&sZT^6YVaAXd=N%?j-$YR?(I1u_lT!v^`GBcJ^jVRZ$jv#4*g_-i!}tm7s(G zT!7g+t;Gu1GF&`aqvR!xJG}%C@1U6WImw19_B~0WwC_nOkrvNQkYDY@)#AB3RiXA* zm{H_VpQ+(<$A;iNLaTkJ2AC5Z%aV@EBO#JZ0P-;e9ZT#aY8WV@bwVL8Q1Z zje={k1+;FTN~-g_2Qhj?-nVnqiN@N6rs-4m&d;4-EzHv3qr0R=VAFV;1o1M!!zswL zvOEsRS?;VCP61d}q-+1HJxr?nja44Up=TY1&q$-k^lS=Y&@;DXWjS8rm#OW87ih8V zzAV(KEQNIY25;d#h?fV?#ys5c6ZF8&4~xbT0PR%VmZ-yYVMT?D`*5RmASxOUR6R~7 ze%`<;YkHh%@~TsD)#KbPeb@)`*fR)-9!K7A_f}~Gt5_t}O;S3&QE-3txg~0;p{zXw z%0AEJm~z+)!rF#${zP&*0rEvM+|o5n`yW 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� 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

atVm+*N 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

E-dX(#V9smO0CBt?_5MK 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

gt|@rrlN16 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

9zD!W!R&0Y)Zpyd+2o5G-fcVmRv z4Om4MeR(MiD#d|%c!OR6FTcJ?sCjS{TEA$%SzR}?-fZM9n|%8b@{oKzoq6ZsGrP8U zL9=*^bb`xHZ{L${n%wbb$D8KNVPVe09Zt8Em)zRkPef9yW+Q0>cSwuMrv zuMihk-dNh(jl9nR-*vBO4A4h{a$ldPJ!8Z)govj5;BUOU3(fyL9s=hV6?d}-n?nNqNy7trSX`mz!>K3N zl3K~BtR*tTrBZvW3+)%Jl8INSPgGt5Lhb1y#$GM^a`7g)g4sY(kSNgjh04_^HE6H+ zohWb=6^a3u+7nRIdR|Oyv3QJf>h&k5Ht)>Oy|!U$C-a?(BwX4ui#CJ4{WWcu!rz{T zIdnP!p@C7o`*hpRe!cVrn;zQ+q#3g+6UE61AF5@aD_RW*Ti#Vz1qHStn&kKmlaim5 zsN|Qi$x7E=zR!XyFd67GS>}~e!EM2l4qoGGso-K9 z2%(3X7z{B6&ei8pzs;^S3d`vI3qE5_L~QzAQwhBi0{ zizOynJ_3X#Z=g^Dwyw)frR5}u*Vav}9Mq9Bd!J3eEBYCu6U8dm3cFD##jTr=8y&is z79!1s+=po!`p1QleLi~kShQ_j#mNbF^Q1~-LsES$qSc=`MRd0RQtiXB9Rc5%pe?$qR9 zq@>Q{#;NYGqE)a{BZk89)@s>O+I2@;`#XyunBiJAE6T>uowgG3dumDwQR_27^NP+ql^$kjUI6;M!q$dL2Yvfq~~{Kj~lo> zJItna)TJ(fJ-adCr`y`qS0mu?_u7eLo`oJtj+7?7;%Li0=Y~ls>q=C+mL}yXQ<6WQ z@jnYYsY{BhoTQ-h<$+pyZ2JnVpi-U*a2*|dq4hQ3aZC9ns2vmniB^s**+|rhqVkJ! z8WX7{P5ICHeO^dbl9tz?08|ry0_8)#cK$S^i_2!q`;V2AjJ{la$V%L`f7?afzI zq;#}kxfCro1vD@+E=`kIwJt(4msMsG8@6~8kknwRemPES$0p_!j}SaC^-72l&*BSk z!jh%>gLJ!O4joSD^U4b7WN%y%7eXi!>aN4oo_4&A%~#`+sP@-fgFbj2mqpPK>;iUQ zp+MVb;Y5%_yGRIgYxJ{efw=ltV>Jawh7CeeR#m$+7J&7&kRJGQ=lNGJc<7R#mD)7> zuJ-9wgOv%k235oKqSlE`SN^s2vP-cb&hF0c4!b+E&ddUf4Nw$gja{QAcCi;!RMaTymsk?bXA(6<69jwo zi(fRx#QdM9A!PoUtr&iT~kge+4XgyKbuVQ3a$F|c`qQ1ut)8zeb1pkp^T zm{3|wh!*DK0dw@IQmZR0Y3j>+ugWyU3b>&#L18wUR6GEn`%u6WpOPs+3npPhijcZt zKYJ(@N-cE4N0e;~ui}ggdD+tq>%DW96W&BJdxLMIr1Gr-ht5Gb6k<=Q)vip ztoXQ#!BmnS_#JcytWmgwh*6$$Zsoh>3yUxj?X|aI2jrC+sNXQcC-?;(x$a$RL1LiMgvyCbG^HYzZjtZMqn2 z?Dfe8aE*!q^-y_c0qNGAZbQiH{#g)A3=7;M* zWb^Wy$*1$jAwb$*c42H(p^6baMwytN`ebUmg}~t`04Q-EC2rk|Hz9m&!?lZ(xO5MZ zX))*ceD_M-9(aEG0#p~cgI#nSxY9}R6g)Dr(TbWzgj6*o8xX=0njF(%5eq{4=hfV> zymbkHw#mQ~a0^desF2%niD$Vx35=)Sfq~mAO(df`&1@;W;1Wk?aDBkVSwaza-T69e$TxMiQGEY>iO_D+Ve_t=KZCNsB|73+ng=^cVvR zU*~r&z3$D&SJF{m7SzH(`62T_;2@s?l};j5u2uSR5tavN^xjIuc&{bKzPo{tbl0tV z|BQ3rKm8uWiZN77;gbrQ1?L<1BsZerkMsMuI^Ub}>i(Z>?HF!mDd>d22>f!-wu zuN7fGxq477(oW?>o?Xdc=Tr20pGWZuY!qUO4Y%U%6mZ1_Ja4Bb_gZ@3N8Llfyjs5a zBQam!4_hRIDb;RG6g+GM+-(DWDrwikO@+9pFc@_y=58ETF%5BufNey3CNJ?^0U5@# zc-f0M^Jj$^RrG&^E$bG6fk8EK2Bx<_VBvpWJpenWz!8DVu)^R1k0xNuCOi-$k!+6K z&4s-!f*e#}a)IRKyfA@6gN`G37dJP_bFgnT+n7eUG>+`y3m{muu#(`Ip8>uK%Fc)+ zK&qyJs*07(jx$Y*-Y9o|ZynX%E%r|#N92f3Bb-ubqh41@m#GVy*Scx445GYAp2n25<`01dl zz^Ay+BfS_`DZmICUyL7(&`dO8<3BNzP2_mJP!*OvdPCLWGQCD30^zrYL?YRM^9y{O z2XbYki@la&;CguwNQ~}-*6j;+lK3XsCPDzgP=cG|+Mq>L%uYz$HN1%+UQ)aa5QB(u zh;wWtjy{bzB*5loa5Xjr5W|>Iq-&`JLg8!^xxI{DDo{k^5v3VCeEEVEx!Sl5=5CH2pk@88&|C6l^_UpUB1FX>WXX9@)aRF3bdR} zXPmp~;hQ#|c9FP5A>^sUYf|UD|Kk<7_LQp5yLVq`q#UjeeD|?+MBiPB%ftkS*ND7; zaKvT9xJS^0J0XG3YiZKa8UZesOX13fIs_rEiej&wlXEtFZf>iA|61GAr{mI8;0p2< zRU9*EJQEz4R6xd@$yFvmoSGB{OKxYgBd*X17%G{z@&3}c32+9Zm~i3?rj?irv4&jh z@D3EJOjM;{41qkm|Hb7W*Ud%34YDCz$xWN3*o`P8= zm0XsL!W97idA<;-6jV(xJY|#-dZ{)#*{KABs=1#^pSNy+K6ZB0JcL}L)Ywm@%3F5< z&^P?7=S(Xj+ISgk6NAsADoAkWn3NH74$=?(#{o;PHJNCJHU#Fp>J z8EcrS17tdm2O+0%+*4X*XQFHebh3IW5w^>O&@c|3ZDyH>Xs9!Ase}yRf@|wEY~#xN z#h>FcOb=TaryC}Y0jnIhKl zGQC@eUNR)$&qA$^t7?bv;TNx`y9X9v!(BK_22%l-_N6npvEw_2$;kMxHMFPsBb5% z-6folYiD&j$T#4ME*IU{EEM6Zv}ey#u@9G3Z)Z%0H&$AXUEL{yb-_1x@GU8OI_#{Y zi|~zj?@z5FsK~gPg$EH*$Xy~Zh-NZj$>p8f7)iQELmbks?wrO2At2dJ- zy>Avm)Q4Axm(DMPHf$FbP-n=b=+M49y=7LvbUxiwaajk8(M`JcFnmrqeZ4iH4yyqw z?@ImV7V*M!FYKORHunj-{u2$UU1L#s(KN$mxwXT79qI|toN(_3Tqh8qp2%d9z%6cI zKN31uZg+U+;yYd7pEn;qZ__>3V0-<}56{?i!D{+W*quGJ%uQ!pVU~#_#6b1d+Uqv0 zzvP{B?|1WWzI6Sjdu|WqS2!RD2$Q+;d*aA1cE$iU5ThoW;6c{}ryQ>h*clKmD47L% z5a*??IFJu1$_IGgH*Z*vsnKi(l1>QdgE~B$C`hkx&##9*8gHxTCIVMhL+4kJWgoJT zD{72~)CR1>(eGuD6DlqBU1*|HG5>3uaYL7}F1Y|hJl6wjqpuUS>Iui%Ljj7z%Pa=x zVWYs(Ml16khd@FLoU7v)Kp%puh92nQPP$CGtyN<_!P!)Jxm^dCvXI3Bh~0`rgyJb` z0OF8NF{e_7rwPmfi6SUPt&IMs%XV@0m`kmig(OG@Y2nqcwcEJz4so)-%Qq@8pTa@% z_Khnca*T5xP2ok*jdZbnbDfKEg!R?{A34WqAs?)~fvy|gxJTcAioLrl z!z65y$|@vPA`1|Dxq<>bIH>~Y1s+Ryh(|O7k|Fe{hONrD3}Mw4HxD9>=>LL$m1^wmCN6%nHr2=eO;q~@|_r3l2nItemn&#%z zuuJ5WY&YfSeCrbIzRzRMX~wM{1O(4x8TQvH)D}Vu6LV1lhZVrWg7Th9NW~$p&>`tX zn3}K)L4;wWgD@nzO@Z1175rEQqjWh?0@Pe2m?(ufBgiqJ%%^!tt3ZVKr4>5R#)6B$ z68Ft)D?9`N22b*1q?weKaqViXeiT#O7P}t$2^jTZxKiPR^#Nf`NtplAJX9&yp|W6( zGF-5uNH76B^~-R_G3+WS=;5d+=0zM5X-Y#15+uXy!=-#kAI*qzv}EB( z{1kS^%(v7?oE7KfyYNH4V7Mtbc(IHREFFoV-UJ}k2$O)NOTv91yb$gKiK9xsIRij1 z1vpD(or?wO_xh*&`QZH9Ke*%;FG3ptAMf1^pYUtf!k>`)V7-LRXI*^vp0)3tzWTi% z-c5&bb6hk>9MC+C$YOf5Em_vk{L~fLF~fh5!PvqNu(%tJqNT-Pup#y)vxFPtmgFLy zJaDu}NpI6#K)Rb)9voPql1p=}FjNwaEu&>dibh=KK3-W7(gDaF1@U10L9fttWRI(A zPQGx0+)knm2 zhgFcb)3HxbEk;*R>?^}hp>4RrVzc#ea(chz`Ysj7pv1W6>1($z;Z&UdP()J3bLm=a zRXtZD#uJJi-qx|dw|{|`n7a)@yFdk^S=p`#VV}HJ?|UfBl-Pk;&wxf?Eqc! z^Q+d!ofAM-=`9V(tC#B@qou(#Sjg7}D;kVE!Hx3$VOqw6ALnM_#^}~LY1;2DWZ1Ns zNCtRD&H_SOYp&FToWfBI424)E>v%W<{cuJ)HHCgt@Oe!xPLyI=LP*Ae8+^n&%g@4U z1;;Z(rG&QtZd!jDu8siP(PVt_L-@=Dei9|*$q^c?LGrc=$c*J_^A#)cT#wE23TMu_ z(@iu7{L?fmHEB`4!N~40uQ@FfG|rX{cOZHg?jMaf`So)9D+Mn*o`RW<@imES7d9IV zi^cjqH-Op+i*hapy%_3fUL$pn4GxiT5??cQAw#aVEDS6~IvE7BZLl#m#GwMum{yvW zP#Ax2+0~5aTrlgRm^Hj#2p{V=tO2MM&Tw$s?pk0s1bZJIo+E6yimz6@7L*2GoRT31 z8Rs%kpYwB`xN&75hQ(hY?AUeGWO2e?9g7q01qlFr!rH%a<@s_!D9*J*73&Nf^f+I2 zHAv?DtFe<0T^;IMwQvMZ*XeLMaxP{M!hJU1d5HZ{p9jlWe_DREZtz7_#=5>N4z-#$ z=d|ERD#tsX78w{(6$P02!A5~X%$Zf;Mv?lOfaA(kx~)IhAzcGw56--m7fQrmPFxjv z5ox*vx;KtUB`zCy(#ge8)v@*eQ3w&#$Fe-v62lu0${3Foed%19bQ!xLu^rH^p=8rh zj^@ODk$dU;At3$DF$hH7RQ`M+fSvKff6zVND=oas|8y>|E8a={jun{HfY%} z^unB%&tN_>H1?T`JyP%}z`V;spD_CgD%DU5attAEPA@^WmN)`%U>4S7Gz&DeVMRfJ z8FsDC#g1{7lT$5Yp_R}}n&dG8bT9@kPokx!xqV6shixr#$Q*CB<;KlJgwEsux7!9a z5duZyyU>j&%`0!gQ)_fp zsIjhA~7`DQWzoamuGB0Eh*ER!DI#B0jFef`Q;MA9p653@FhR5`=2bZdjw-(}aT< z3=YmATGMqrBZJ)oLKvIhB{&q#?^lfAqmJ=yqhA+fHjfeO@X|W=pMYgFY0eEn*@ySc z1428tmZhR4_Lo8|Sb}rL7CD);%m#F<1Q#Tk7&xPXy_NB$C3&n?0xJ;`W77`w2570g z@|`#iQRf!NxS`de9}tFv!%s*Y+r?>_8`$ia&ZVCQHeNW1sUfrFM0Y1L>+R1TKTv^|-FfPz7yP|berm`M}}B=>v> z6}6kEwHYU!aLT z($?Xe#0DHkQ$Ab`9B5`2J#~fQpLQT>;E-Zj0}i|(O_o7v!9;4I0K|DT%!IJO6j;S> zaExCl`H(UM4h%jS(k7gavuwW*>C$v8AVOel;e6!wpAzIcksxI%LR5Z$=~3nUdO8~& zpP#BZk-Qf=)avMrIaRYJj_>b!6hSwV!*RqhhOjs8mWa4@v^_$fy1T>9wh(R)nB!0A zTsV1$TJ|WiuH$Jue!&SkF+E(R?4|Qyk}y6;4AiE_5yR%8ouMjGd|pcnCk?413p6t7 zJm$<$0Ru0pcG?}50q?-|d^BvABoMRABB)Z`NK)1kKMiA(HdR`Plb8qfBtvs{B7y6vDk7`7I z{blocH*Z?~fV01OlBaQSx7d4@?g$Cmn1Nd)++y7IUT($&KP}sEhtsD@WvG8q&EnY- z=-oVL;YV>D1>QJnh(%Gn+S1Kj6Umc2r3XBuUDwjifPhmlA&1}vw_&Y#MA#f|Ww5F^ z`{=F<#v}tKV5Pbf>zZs_ai{i#b4U?UW8{pHWKKUixukIxWjdS!K#v6w{=39D1B;AE z(p?+oT{cug#=Y5>bruwJ_l1Wqb`P}4(|gVYkn_iaQvuU%J&zu}$#6b@aQaq+xy~Z! zwh(38zfBmT`le?pyXw|=R@}eov0L-GX49ExY+5Zb_=7Z)*;>$QzG}tWx7?Gjzyh-F zoioKBZOmLSjReiZc7`h;GfzLp1HIOCQ#(wa*hP2DY%#qW8bo}18W_8AjVkw)#L%V} z%xr$b4hD8?MYA!}q6mv(pNWGwVMT`hh#?dvxNNe4z}C<=X}CdTw~Kwy2?xUp?##Co zvGwx%34l$qaDAbvTwGcVX`amr(t2mS4ttkJ+4?x9PzcbySzG*%om)aSaljLUS8m>< zz};ja-N1|>JqO@d)+KEM{sb#=?gtR9vo@Z#Mn2pZ&`FG!y)nT%N77K;ICKw)Yq?E%< zeQH%L+;ROg7noVJQ@C@N`xcOf3;5&;ow-;x)jlo;bnR=5ARLWwSi?vxg9af?gey-q z9?YHVFtwiPz-EBcHca7ln90W4ZrlV4IH4ge59o{t;KB+Iwy!`cfPu7Zvl0R2DsOS} z!~|pn40v#*#QFx*x%QSeIl_C$QMdiKZog^M`476Ds;>CZ6F9&C!Rb}gY`*H;P2azE z(-pUvy>`W{DbY=Lo$=0vH)5@M`)u46w1Rm2cU&Izo`4EWp$cXJ(q6a{j^Zm?li$P1 z_Av%D#VpH9mMn>BGS~^|;^d>Y02U>#YwNtazkS7zG^{`ky;^Jd(h|Q2zD>8^gamJ{ zf0%U-RNQrWaO=M5bY7YyrMVAT{oBqqFdL->YhSS1*wovayR;U_y;;vY{#Xas9XFb7 zZ>dQ)KYbUSDfFyJBxxc8cJv-927qEyM-kFJH${yuCw@8vN&_R0_!}?d-438I9JymK=@z!tv zOT1bVu#U+P!a(4oDkl?kB+n!oaJqm~W;Dqoh!!msj2PH33b$!7LbAN{)Hh)8f&r9; z6l>q3jxPkp1k`7f95Hh~#Ma44HV(RZu7ORTMYF``crzLum-f;XhsFq;dul&v8-d?C zi}N(zEMS;6HN+cPAIm!Lr77GVmn`-k#p=I$EAh>Nso^R*zJ}b!%|I0jy2nBJkMO&w z1aR(g3DAvpz0n|6+$mMCzCq_$6N6s1;hxSLEr5e=lET&@;y!fIS>a}i@tK*~gK{%b z;l}bDMDc#?F@N9;#rd`K?z+1_xO|-#>7&i}qWl2oC#U#4)i-Hjo1os}1qQMr#!pGD z5A+8x2mgKrMS2Da<<#WI%iq7|MjAJMe8YQd))pDgLam~?!<4w7R7hETBHsbbC(g_| zUksAM6)2A-J)b9C8t8wCxdjae?;?BQYDudF!WnQ#W33!?L1+@6DLGK7Pb`8!Kt*sa z4^zmpI4m55A}y2z_EyC~!dx~;c&tQ0DO0^6H z6?%rH=?G!b66&n`Ln75k@MiVa!lY-?4$>-%LIe@%37UDq z=4KI`5GcHu;3{lIuAt0O^|6;$iCQYb6ohW|rA6`nD_@Mc z$OmHxP}jm;gG$n-`Oqo+51_dLqK42k9{fqD8%|9T_8JSg zJ`JkIp@`YPMAP+H5=~@jyO~h*^a261xbgnuBcMdp5_p{D6wb;3xx$fghwECQ(ZKZG zyYh6kCv1A?fp>gyaBn{PV?Mg&>`f1!505>n@Iy4Kh5_SW%@+!S({s5^H(rJ#Hf9<& zCj-Yj%uG}iCsn>kUpa;3;TIlyO0@*PaR&$;lBYkg!MSHf{2U1Dg&x94HU_If`I+d3 zw869r6%1iIPy{O`ny!=kvU~;y$N

x4)BdgdV_?M1t7FU}ls>IKNlf6BeA(7z)_? zgMcq+S0mOMHmpHZGu;_f>4B7Fs5e|#bkKORB;}fMa^AxkM8#Ve>B7f`s7) zN#=H;d5xd~0MQhiz?^OS_@^kaWp#@3$^OOWA&~_8bZNHX1lB3@kBprQia#qhJKuCd zT}z8i92nhv;qEb~Z%4BOqwb4pAJx4n5cKD9N;_n4pKjNqBko0o{ry0FgyU6!N6`tx zy3LFLrbJH%n2Mp;Sg$Nw!3~@Xl;tb*G>j)9#&FF-u81cel(-31j)O_eLgVpDueo9_ z_RDJ85fNhyFNHSB^Avn~M$-QbDa9t7FPhcdr4|IW5qo9#XIK62_MX)ZzmS&GZ{KYufYnvcvHH z%Q;Nx=d&KpSl!E~3T$O?*D$)+oPQGLK<%SJLFzhhg3~IBv7xD|m{DN{NVv@;devwP zpy=wMMAG#gYZMciGQs1p@S7Uu;L0ZFZb=Pu0$JSc+Ytvt z>mP*snP>wXnjyB1dJ7)6bwEwO&lUwV3Q%kN_>%zZQ)@j?jhpc z#r4l*I#|o`^Q(AfiWekm1_RL)CduPX1pbIsfS!(GX;{8eNOBLa-DUUi##Ns(V>U>+ z&iNg3tk%!l*jo>TeE6ObA=X9_@KfYwn7d+Lbq*_ziBmLzjOLbSL8}%{4s9W|2Yg-2 zKnxd5&t-175&i(1E(F~aMbgmGg_I|O@YY28%R2iRXrGPWjYK|t&1Opc00Aek`AxLM_!1;!PH-I6M&5RZt(2^ApivfcN4jfW4ddKae(NQuDs^i(F zDF2y(qmA+sLwq&>`C(H9P7EKFxTpG+0Fx{)>D5PeMSPUCr`xJI&-AKsM9!#gOlFyL;!?yJQI_|OmX}9MV@qMCTU3gBbOvaJs&Vl=(HNyB3#E*O zfJHWS%Gyd{@|vHf;_5+h#DJz0 zKxB3z*Op-ykL|NC)Gxs*kVId0s#amt!TtL)S z;h7=mz>+B1f3&8C%4vH)+P}Ya4MrJG4(r32i zS`iGzA*bOP=v;}6%Pr$!;>N9znx=$x0>|ar0~{4fqazN&0}8gw>&q#*M9U`S9gt!6&>dJR!lc01c1bq8DPXBoD<9uQ6muY*w+#3dx_Ons)L(B8^)Mz}CK zMZ?+13gP<4lBa?^6w3K@|l)3a7)*s|UTj6&yOq03dLD@+c# ze2o9dBrG~SGWT@q7hzOC!EBgAWr*Dr%1u(Jzl{a!X6L5A157=HsGWe|BQt?qMlv$A zP_bR%oC2UJqL=;GY3{EKz z2&*Wh%|cT-Qz=(KO_Wf=R4!JP$bt6(0tkNrF6@1euoY8I?;43&&G8*SCgNg;+% z$?Vot!EXF*=t7Z`nsL8LvzMtltz>-5G{m1yN`HGo3!L`7Cd;}tV#7JiMo|fMUA}JM zmm{XTMRJT9p!D&@T?a=$_^n99F80wctK*@KT7*%my2wGQAR0;LXK4M&JWyhx)+gB0 zVufbDthaKS$seB6UV5L{V*$aLWD6c;=C*&#qE-u%8P%0q1!YteY7G@nWdBl1XB3e< z&~~S!s38HDqq)39bc_JsNM(!6MmDkwyxrN1>^hlZvyv3Xc!M%0&3wSDPKE}-GbPO=3jJY|2Vm%jR0&(h^D$U6mufF|p*DA43vCJ>l+)BwZO zu8JG=t!tx1I`VpmY;~G?ez5vh^T(v)iqEfM?v}h=-Ngi{6H{ghm+SHp86%(4S3q?2 z-8&Nb`LiEB=0qZqAK=wo)sAgd3$$LKw`Q;%azD*Rv}_^+{|ej=mT6AblvyiZ`Ow%v z{#OS6gO_n8QO=VWOsBHwthSowWG$R0s?$x0RC&!|hsB504jVq`@F7DJ14kU*Gruqd z|0G)ywscIJtx&0cc=2ym#RUHUlONx)YV}TY&nek!&Sm?4c-_@2|Jd`E$xG+m_uNab ztleqGBkTWu+Z|7ydfvlN?Y61ux$>RA^UD)<81wQY3od&3;m8rcd+?$C|1hOz`hPyU z>3{wjx%iyV&)+)Yyju794u?3O?78>q4-UTUq7!;P{aV}LcYk&6u;ZS(X~Z*QFF*XM z$G&sKEvJr~I)D3*XZGJ^=k%{WYLb1$e;1~w|MWf zbtLjMo?VV~f5AZf0?Wzv_&)wSk;tn^dn?{|M7qadP2k^!=#yJ^h(tcd=M$0lH~9V{ z-sj-?)yTgXdHRFKS)ktsK`I_ep=R}{VFvZBLJd*se92W1YaLDv`R#{Kza2%YhTG1d(Z9mdI=n&w?A9LGn2e z?BeKpwF-}JMrIF(gIvk!-o=6NNLHio(lX^zV2}zlo)glN%tm&@e4pQyhI_L%e>5kc z2sV8N-gB5aONC2wk^UgwC1JsgI+v4}=Q>leAeBK~Ip&<6$AMUvn$KZ40U49X#9FaF zWMxjuaPuDn(dw==KTMQEmBOnDsCIt8hfes_ZC7V=b(DCaF_Mho5- zTZPUcpKxPjThI=j)*FTY0`cZ8YTuijoXrRs@}@BYmPx+JNEN5EN|r60<9yuZ@8DFrza>sqk$7aX5l-^p(ak7g z;y4j}j(owU!yKk;r!G4Q+(48{ICLfQ+LBlbY&!58de5u7Iz)Nx%^|9om|ej{`xR(3 zo5WThT4B1zv>102igT6pJ~|K`<#NBX<3x#=psyA4a!3UGRdOVj!VM1hs~CFOBcixi z1W~tuHP~$ereZSPM+8NnW3m!*HNBrYo~$0@q8H(=PVpoyxLSxYS_Y zm^0`4B%I?hTfp?rGBf`Sd(;C88Nx@Nfhe#Pi7+N#Ih-k=eG0O9p@|KLMBc<&m#hcC zGDW5iq>++@vreYlPzgIJoPian7hpXaSg`ve-7v%2V5fknUX+4>+5_6 zZk!y0KI7f!3kVQ~UfAJZSacpg34W0F*pK+#KC{h zz4EIgu0DC=&Ij$b^poN5SC)PDWc3xh{a|zIkv*5Ko;%{ky;m9-pnI&b8I-@2mowNYzoKmXGu zFZ}G)o&R>^^qGlU-x$2H=Uzv@bK3EvdyU&Ev3Nz#TWa6fe)4N0FaG$U^D2*=o_%KF zHc!0$#iU;@-MMwq^l4KkR=-*?{jG1$UH+}QJyNHqe(}nxb)_30x$bxI$5yZW@XwD% zdfd_LlD$51mcHKp!bv9%etw&`n~uHy+RD2=D|_m-o4;6g)j5~FyyO|@#`G)yebUqi zUtBZb)jl77_mCNH9QIUp(eL+I{y#4*ynSl*U6H17*ZgAlCt`2@>XDI$eb!R>o#cm4 zPpUcW;?K7`p!_FCzWCC(caE?6eCAKz*lD-l_siXX_6-L)SqAJ$KnIJKgc$Q%`zl@`|JX`23Xf>xca4=GpVtq+Why!=8`4cKy4zw)Z;z zg00p+w@vRuPd@LqihZ6vZPN1}Z+p;-H_W;G{xheg&$(~AHG2-+s{LmN+`D=7?Y~@b zR?XO%4}S9ftIvC+@r=DDy?5eMLvFmd`Cm`nm7Vm?PJPe5J8{OWFE;<`jDP-O{F@K` z;DqB#X0B`8@2}^c_|u;@_nkVbdgUQ|zp(M9@&9vK`&FyvoOa#3-WNTbeK7NrS60-1 z@}nINxUJ&l)#LX3KX3>`YdrtVA zpFO7hkuUG(sU&uq2flw)3d=Ev)w{I5G#JpJsnUFN;8!^j6- zDEYzrFAn~E(@X#S>py=#_KUq=zkj^*=Cgl!{H=#}{ph_}8xH>9Qz!Ys6J!7O!M~ED zKh93w^zEmo57?&etOeUV@ahFUcKYk<(R&)7IQabozjNqCJO8chSHCJ3%>sh z@2}(84oKG@?}y;oAbcJJT6etr& zp_Irte|GBYaU8-lV~0fhB{#dZvYkoD4k-)8o4Zs#^)@skKxSZ=jCV*d4rL`llIJ|M z=p{iS1w$^*!$`jYXG-I-`{i!3Np>y`VkqX33pY|c6k_%yKDS5KA=}tu*=Y195j98B z+FY2(n;>$4M})~*gubWkzhUh;NSf;~6^ zFCJ#G(FqO*l9kzHd`KH7M~45_f)0C%HD=;Dc&|o=K3)brb7?@4JTyy0xO2@Qt8Vmv zfW%Ro*c**tO`k-f5jGJ|%-FLmj-yT6PZsaZV)9dCy?3mYjlATqP9+jEV-_`~`LQn4 zC!YorTLbkh)HoekgbeJJ+4xLr!nZIVTxOVrhbg?+OgkXcQHIAyb`<&`+fdsY{~=u# z1I0Sz`7YP4T(N3&`g3P%xVgSdd%aL~@sh z*d`t0I-F$rd+akLtF}$%JNe!ANAQ@5^SwrTkvyt4EdR6(KHnQ+#jS3b@JzHR7nzAk zFah-mfM+OVsJD@HxR=y_&Z%R@PG?d*p|t{{5~mG2LPk#5 zvaG>coC-ptETLe>t34|$ELf*`Xb{g3S(CqmfXksl0<`l`Bo-oZ5QOx=!sTW~Sl40* z{X&Z6RwC1EFOz@xgXbhwA}eZ9&3cTme{3sd+}Wv1P(TJel8t0QwwEQ#xlRtVtmrS~ z0VH&1M^Gqa%o(017G6SPSE%_DD`j?y|8P7T*%oV<%Q>3CE-JPM*$HH_K1k}$iC|K{ z?3##VF6;V}*~a--g=}OYlDoXnd2*dNwrb@5Q$WDwg?s`Or4*uJE?I|cBfb8ol(4d! zJeZOU6nZL#nnNq@CS-SoopQI$&XS5}{1%i4H)9n927M!JR&8ByvP=cDus#EjF=i`V zC5tmHvV~?j#cMNC9E&WYZI+<*&)MZCpyaT2tvnBk0t&J0g6FR6SIa~`Nb*Yg%czNRjHs2Z`MXQw5s1#-q#G~~a-QYk+%<8=0{OH$n1QSYf$yN9=2nw8qjUHZcj>0qK~tCp{mNImA~t!2Y1Mj>VjP z#G8>pPwmtr{(WgJZ&>Fmk=HN)R0+gExBCgt5t64u>}02(wcjV`Tod6W-q z*8Q<}>X>LX z_hdv_3T@|P8v?5(`v6seAy0b39XIqA%2hJ{|(ixLo@O|#bM|Mr0#YwrtwlCeNzxJ-|*u%@8 zv7+9w{=fTQaO2EGfAe#1nog+s#%0^Bjv>j&GAM$y;@LgF8gH0qAQRI;mFhY5TD3P# z27xU0uNBa%8{~K0&$dKvLkZ3;TYGGlt`RNIx8&4H6tt*pJXa^*F&GU}h`7k|JFx`3 zmAxf_b65%BC1g>(tSz{B8E1WRUS5Z(k2O+G+99MHp>2{Aa#ReYSn`x)m!H5iQUO;H zm61mUT9R2o6@f&FYaNrg%W6tJXCIErQJT#P!e+jMBastLKph%*TElNJ~QdI02I>UI;vAGK$0F1{}2J8e6PPgwB^T$MS+5rYH2QmQBfZ zMlSRm8LgUJF>KIasWCnd9yUByi9N&Dey$*7LH^8KY2M}COL~m{+g*=eGWn^G&b;V_ z-~I8!U(UaA`DcH6{M3V*=e*M=``Syf^rtVC_xk%OlQ+J4;;bV^?2y@ZxBZvC0wT1X z{Sfc7@&0qnfW`QHCO&V2&)>!SEWGpY61-2u`%m!OBG`S#(!S&OuBO3SH_>U?n9)Uc zA-t=&vunnunMl77Bxu;7(VBl{o=KKT)37daA4jvJ+S~13l4j;oWQba;UeM-4zfzje z=!phf?i9>?&){#Xpx$uSc$!6pxM8M=z*^TDk-1;cLgC-@hEEcFdMOlb4qgI;Hh>X-!8*k=mSZ8Etfu`8tPV8DdHl2xM*rYRk(!`*5a1t3 z%OC4N%sB)&0K9O>)fH0E6}UFw8g6X%;Xnhq2vr^`HB?io=Md=(ViR46imctM$DI8t zp?4TD=q?fB#uLL`MT*Yj6;!Lb2xNU6(zUr6o*Ufz;%eM#yDx?6B3n~5j7 z!u|k1p0;}2%|ujrE+S_65$6fs@6P&5pdD4*l0)E-fQz!?ha4+HT>k~23#(p&o62Rk zp!${dPH<}vESWSHMBN*D7sQJ%*<$G8(haK5L3y3TU{!(hh?a8kVoY->bKAzL6C2vK zh|X+r(bSkoRse2j2rHCpbPT}nc>V8QLg}?+QxO?lMv9&@cj(nBcdFp!3qaS9FrzV?~~&fVqDXcDah=bwH@dfyG(XBIra?849gd~fW!f!CgY_!w#q+rD0v z{NWyFpZXv&EW>L&g#Z8IeFNT?;l)3m@l#7U46^Mj(GUjas~I#FtL;}+4E~#gZ0FHy zN+M!Mbe@TFp6AN6J!?oIRe>3(dKWTJLQxEY zd~5F$OEXcZsDH>}k2TqD5^1;s9ulqxn5YIC&1x(ru;8jy4P8Mqe=}^-XgcEPqtQyV zPrT9ThiGnOEd%Jgd&xmVI)ngMzXZ;XO#{XN1fXfkgz_PS2M-!JOpPSATDoVIgNWE~ zu}}KiOW^%6&-iB~@V38sW%;h_?>TL^IaAJB^69e|=6e6;%GOJMan0JxPPui|rMo}) zv*|}YH6S|knNvQU`&|6zeg3%NyGu8}x3=!BiU}j$ZQ0?f4}b7+-CwW#@R@D`L8?s#!y%@@h(%O1v5+a0UaN08P(#GbAZ zFM4I|kKZft`%nxL3z0F8IJh%_b z_iQNKi#ubG26mP;T)mT!sb6QAyy{(tiXFI#nw7Jt*d%9IV?j*d<6M+0BU#caPlt*<%za z6|N6P%sG5I?nxp^;P9!zAW&)&hKpP@+bB^EP>ft{2j?XRRM`99aW*ZLMtx0nW6oJc z+Gyfp^ppwqblk20oy%V+jPP<`F83rLv`bJ8MU&F2Ck{#?@E2TfHW7gBfNu z;`=)V6<~hP%?79J7{SHj6XP~o0t)4P1eZ`sd>!grs|ZR_Fcb~|5I&QMIlX2BSRKY2 z46ATLAv!TZ@@)n;v!51C8b5FT_M%VsTy$DIDN znh`Z9yek@MC&_^fg;L(RylNjPN(Xnn^kh)TsQ($qZ@Map#N$4Y6 z7ug2sY!jj!01$Rbp>ItfnweR0nneSfFprseIHL_Pp3Lb`NinCl7gQy!wv)*ubh+eW zhpm|)7s9K-NUS~RS~2$-4~uA3TT4qk14jXxg78o_FV;SZlQnLl3XrjV2avB6qFB*D zIcr8_4iL%O+t9V$Kq_+6=JtPvHR!uu_rXGxs-jaqOb`V;3kS<{UCBjo7rRMqI9< zyk&<@(zyedE82q4ymn`fC+q#(*S+e`56pAJrkpLf!VB_A)&rS5kZCwj)rrJ{MXpjj z6rO(Yo0x-3`mE0%AngNv5OXH5xiV7N{2FnFU}a%DcuU~gFnBPP&P%3dSC>+hksb4; z41nUaA~q$esUL?9LvS|1v`c3Ym^72X4-mFes$Bw98rrOw1-9T=$;kf$+uJ4B5%JG# zZ*T3fZuuV*=d9eiZ0>2Nu72|LKRveXs)zpamsLB>an5YqcgR`4zH6^@cRqOjc@OV; z>3NIdH($70Q?H9>?eWwl+tl~CYWTm(ue!Cg?Am+J|NiwO_sibccbC{rd(Zj&rVCn6 zzjeo_pSkr{cW=CX>qyBRo2pvw;&JE+^Zs;q^Zq64n%-?)_x{sQtef}JE9(w?{o#l9 z+rRN)9+m#+zC};Z-SN(+&)gyLZ1uj^KX=LILtp6QEP3g$HzvKD{CLjGXKXy}u9uXPXb06oDPg{04e{Fh)tJ^2-^v+LD+Ii30f4#?6PyJz!e_rwJ zJ@0;c_`W9(8@AszZ~SrpdADrcW7rdOdNm*WuU;qZa%%6B`i<<<_S{i@E7 zHJ^VnrswBV$9{Y6y78Ac4V$>tZ!fI8FgauLHNTiQx%#@Jr+n-EC#F5K=9wAqjC!={ z(o5Q^|B?MJ*_S!cR`5ZdseEdEPX{RFp z0WgmojXZbb^%6dx4Ftbm>=%i=3;JuI*S-Q1&rFoJ8T6+hZvymI8&u^e^J0j1HsO#VZ5TphguOd$j?ffNZ z-;BIh;@S47Bh4(k;`uVvZz5>T#5)0gu0?+y1bPpk+#`@?KIol~@AvE+iIk!}N!0Ck zNOJ{fd$?&vSHcOLEwdaGz8!-)P?D zAB^e^_>UW`1|;NO`Ryw;+l|gxIN_UPR49x z0DAmDQmjE$8TVI#rq|(no$E=!D7IL~Nw}Y;>pLR~*{e5%v(QBdbIt zBmadgCH_)4CKl_;s|<~K!#ht#lN6(P{FV9wS$lVs)vJF`%#XqTq#XzH&^%R-@gn(!5TCpy#J$)FYlX^2)`HlyEg`0i zO2+4W^$0Sk@7OIN$U`3zq7d-MSNUVHIGJ8V0du#cfLSPjCwz=DVN@(#iu{(DY~($V z7Pbt}adr3$+Ao6vD?K3tD_N^>)|u!Z8gTvcht{v91%!!;vFRXHM&Bd?fi~lv;ba=h zqtcHoQiZ;v0OK*{&B!of0zN9J6XTGqAtzWHrO#S=lJ#+9?&FCkPH}(uS}?+kq1aI` zF}c%AY z`&Tyd6J#9h�Eh=}1aUP&S)O7&BA2tA#8ZAv9MV>9F@1Kg`uF*oJI?5B&T>J_$yo z@>G@(f%!Cc!J^ySc6WHi2%dyiA?blOX(5NVT;ysb8P3??98MYh!kksNvr(I*W?K!6 zA{_BU$l8n3q84luj<)37;yh#WJ)jy4wE~vMylhwo9Imc<319aoLC%W@UyqD?1t*>j zkp7*e>AC6UOOF{SoYI6#h(($^A3Dkw`}e(q%+X+lfpDb zLnL;i0X<|heW;@wR*v?BR^|q>R)7|VN1Y-y8yS{ts--9&1*>V-R$6W?cH0Dyb%WSF zc+v{Ah!V&z3rIY;Fts4F+YB!=dx6#GTnKaWX~^ORHF{Y{+v*IRm3k>ixFMq+30lP2 z6U9ot8`<2D-HxjGG!nY8r5z>YGwQU#Is7uRyFt8M;es=)LR?Jg>S>)-KSYMH6vEWv zx{^T|J6P&2P~zRd@L&%Hgy?OYEIMGuhhm1iY?5{%ps>d0)Q9Esg=@R+!8VjJI2$wG7kZYYBC z??jLrsp~0Y?0H(xK-{qUYo#|w#M{}`Meby6^0WKbQ`MJM<5BfZwJ!GiMAV2PMs?S0 zV9%oEBYdbsI??wH#ze zN=o-IUb z3WjH*(G=PvgJGblFPzciZ+g_Q8@QMfuUZ8@T`yQ5pRgb=eO8=Xyc~1D8_khRQB;9( z2xbk>OjS3nH^OruQGlhxB&h$gZfcZ>W3KptQN9gZ)U5m_`?a9%&(ysO6Rk!qEJASi ziB1C10`rC4nGEbAsP%Ybx&+z!1lwF(D?S&AN6tWg-4_REe`iZ|U1SZj2iK(v5&1T< zj?&2=%%VGdI|W|J&mP>N1UoG}Y9Aqc?_lXVQ^L|G7ugFI%|2aAiaQyQIMgZ?l)>)N z`Co-G<_?foqELv|MOGpExL{diQ7&-Yg4mUSlZhImhtM?+&zHg>BOyD;KZ8uKAekyW z%9x$gM0`?%;=Zs5&!TN3BW$lRPx(WxYdtw&HYj04fRUk%YPl}kVx z-P^tTdi?z#VKA{;S*V>;ZmdDp+05#Xk5bD$RUFQpBzmA3m1oqHT_rdn z*;@qZ%gB2$?HeFFkZO7`q&I)Ua6(we^qp`jb0DkNBqb|5Q2@-98t@R}FeDjjfw*mW z0Y!5KY;|luS4&p8Hq`1GWE@Tn-fjU|4tarEf}N*2EIPNRfD8#{BR@n|iFos8W&ag7 z0k%i(z$s5jxIBNRc~EjIBOfB!;BYdsf%=NEi^Fo3NPE+GF4u83UBOO3!ULGli`END zyR(r%JnD)i@K}jbE!?fQB72`8eZQI)`_fFGiOT@;qyTkv|~Xw(_5o)nF7_68v&1iS5OphKX?mqiw0CQAk3nHs4;-}5QG^6{|&rqgZ@ph?U@-k;f!mK zIpyOutg=?3H=>N~t8JnKkc8B;d<0Xvh!yn318BEIn8SYxL6_r`8MvLrfz<7xS`e{A z0`#O2G3WR>t-!INuaCA0j%WtYNsvNfQ4deR07v5%LJ8J~Q;`4xX>H)xVIe>e#Hmgp z2ws6ZNOwyx^x-HHD4-Cuive5NwL-32_FR%;&cWfI=wavj^+~+|2A>vuD!LN3u@}&R z{H#3#c#z#Kq`04Q&-@?rX}jR%Zjr!SfWzPD(HhxqYu zh39s}-NCW3E;>lU2o}IYMx)Hx-Su^JJw&t4_HrAj_yT2}9pULk7d!KH-)J9tKe>)S zuG3~;XE-YC^D0u`FR;B^f7cPpGy%;dSWTY^9>;0b&1|s%VrG!x!Z^MW*R*;ZN-oC0 z`#4tM%}qbJs3a{(+_A6}VLK_zJ8_jV*jP4U%-L&#iPvF2GM>s{%?#2C#hV!%+Kvku z&>nwzcG@Z{2&;^!K-iXG-luB?ega@&s5TKH49btiSMj<*QG51KsC*lTs;uRddXtJz z+)nJkR%6;PFSQkVl=1i{ioLKF^m{uo)Fh zvFL;iB__7#26zQ%_T3~(wNJ&jvO$c<507@^1*|K96od~5Qf}K zWHEgJ-NL!ymu`1E9*RDevlP|}_AyJZ2OymfY7I-4a6+LHv9l@2h{n|`(d)O~=S z;&*19WtdQXS~G5ldtdgJdO~x25{T7@w84O9qA3JN5`WHAEV&qXf-z@OzJIiMRb&2w zc@frUL()%lCEUp;n$ADD|A`Q&>;SCKUtf%Y6Ukh*Kc`I4j+ccV$_9B$`>VDQe2c|B z0}zCXOXDrLMM8wBKK1eoH#WDyQMh!v1qL*;!TlbqlKfn>R7_x0xJp=WcgwvN;i12K zZ39NfYC+6Nu?rVvACMh&q0$PKALKvqmqo<#iG5`*XAdmoOr(`SQc7`W*D2~X>pl^P zvle(iyGdnsNaEe+SSMTeDG;U%1tsk4i(>?}K51Ik=0izzoUCahgQE&!!oPh4u zmY`8s_R-c@NoQPuDNB1)wFv-H7;?+>#v$xbn@v|Pcadj58v5|t_EAwCBiMnx0x9e* z9-7AMxq||!Twi6#0aKq{+^)610HzMZ0M0^9;liIux44a$-W(OuHzkE-CZp7_9YB~2 zcH9suP=i9b(d1y>Kwk?E>3*&{q_GwRzLYzL(A#rHxeN!rW3-j$8iGu22QLe>g6Sun}ZYY_ebBQ~=sVuuFh{DGL@>FmHcif|OWeSvsKr;9&+3Fq+uy^>zp�H?s7>kZa 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

j5h_yN4(`Z(zwoll0)mCy`^ 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 z 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

P!%aBS7G9X@ z7pxBO$9-Qn_-H}?56d^8Du@fTPa98=VLXI}Tvt8x$p94ia84_>UMa>G-;^8e`P0+C zOU)QU0~egm)bQe{LG&)rqO1<4OawMxrllpB{JP&8vN_+rS;nnFzum^8tS}(uwf!Ug zmbM!>zHHX^<9M3q-NUqPc8r0+;+eDa8Lh!Jgnvl$h_sT=u>5< zQ;6!8OS1wHl!iE*S|=*Q7}%k-CV2< zr!69nGluCq1T$Pk7jj{%Fyk?H#1g($uYkY6F|;=oPje`f_BC{}P~!`p)ZcE0oT{(k zy2gm+NB3o59#@+NEHa;yPlx8YExk{@!@h4_qRmT8Pb~ee#z@>W&AF!;wXeS2=cI|1 zhd}rIP@3@{%EB#jiIo&=KQx~XrLB`oxSM`StNWsSv`IpJU^N8M;1`5nLXP^p?&<%$ zA%%uk&-wsprBEdp2*5Dn86B;<5Nv%JUz4HoA8MpGs@6*PL0 z;poyXYV8=Z2vbG&yYcv;af{aO91y+KenDSv6?#LvhTl*)ad^XLRwpP{71iTDtq)p< zV#YWOn@cdUiO@o&K%-hZgPiS_huJ4O#z=4DaGTs6j=&%sbW5#6yt*R3Zw|G=LN(*2?VL?j;BcY0SM^W-en>o)(Kz~tg|M0t>vN6AqE5jM*Eix!?Ycc) z+o{nMS`)jkM;objIVjji3B@qdWL(g)IL)-Ao!hSq{Ut=Y%O3RC44G)B3T$*l= z4#eHWZrPMl)uu^(rnePnqNc{~Xik6Q{Jp!7myD0g3Jn}|j(@dZGvzeq$|9%dekcNU zAEEX!GzKzu(YTpiotv|ffcE%q^ZIDSFeAE4c30Z&hyHiw{lebd2QTl`NHR+R^QggPQWXSxbuYHU@v}S{PB~QoZkk%EmIOImk z({mU!N~r13JsqIm>yL-6N3&n1%McH7Hx{voF&dh`mFT%RmkTKPmf+At=|~La+~@9< z!(fk5$W>0B+-#oW&T>+oPED1RcI4+u#~qz39mDrZz^l<3_PC=RQmsz;-jb-ZrFb2= zq7#7eEXHo`>AykoDS?{Uc(d2H#L%ol7Q2a4OMxCAZ=7IU!>OUrsJWy2yJvr`X_y5N6!mhMLv@Qfo6y7TzN^(zJY76NM2s&ioiGN4OX)mG%fIiWg^M@^$+Dbh&DYRX0^+w zQ$?CPloN(eSoa4LEIv%-#iRRBPjZdzFfSk9Sr}&6Jt1e|@hY9j!pTyYB++H97-NrW z@nMPk=~vM*LgQf@S54qLI83_UCtFf8w%FD0@+{p2u8h*tosIqb*mZ51H$C5_SHQ#) zN<~*05P8spo;s#;9Z(CR5Djv?aNApO^tOu#)^1&T{i@b9qWk3{i-<6;aZ&^@WHuHu zQ!Kb$s*Le+g|7`DRQ!UImeDTPS|W1DM4hIWQl8J1;R&(N#{6JHJH zV8tkqu_;kAUcV#v^1BjK_kEzZ3E}^7obbOv_P2XgB$o_A` z-cR5A(UQdeiD@zI0j)oH`;#@c$g}G*tV)Ymdza|5q4$Pf=cY)nKi6hGiPbH%Y-#0r zJWQ=T6tT%#`RT3bw|H2z+ObB@>X*@84fLIceKM3D+8xV<4qrRRICRAeT-zV?fOX{el5U)`ABi?@1DAQ{cy{L&Bh|(S!m?XV?t0xe zZ>+9G$^&y3;>jPKps{^i{Z=I&liK1vM!9zMhg|KOc3e4!Zn$rj7EsI9;fIr>T65^H z!@uQzPk-Rs1+0Lh&)F%_3pu#dm{u%dWRu8XX}vbi}N4C)S4D@Vu&M zL^fJUJ!=b2ZhH^3?L@38+C~jNr!N4l0|NIc+8_OSwJ%{CnUFpV05C1AleK8!bp&aH zgZCx!(6^BCcqe9_?)CcVzF)1T4F?w4wiaDvn4UO1%Y5lQhM68GXO!8jPq>adKK7~2 zD#0s<^A&e z##X!K>4LRP>bj#oT4aOGust=lgpl=6;8u;s2H~Jv*bg9tA_55c*Qr3A%nXcD80^r7 zhCs)e)}gd3bsgHxIV&<@g-NyvL-WTjU=-c&%J=W1ZwviNP95h4L+!f1yTy8MTm6n* z9aFpUbI)cGy79$INblRGbFEx$mu7dhV;dp|yr|apm0AQC>&Ot|3l&%Y{3X;C3=qff zqrNb8X;a{FXJ3aGR!90<*d!#=>3?%cu$VZvnE|Uoy+1{&kt%xJ@`gLRkP165HI6J| z&D1PfB%UT714Q}Qv&

bnXPPs~ejzL5rzu!~rZ+F(?mw6HQ?+G_I%>o0?vX#i##n zE~I}MZQ_h&ITl(NpY~TCkZE15h+Fiya@5U3xZuV^0fl*KcdKtS7}v$Svf?VW@Wj>Q zqCi2y5YU!kT&eTdu>_?8pZ>cI`O=1+O53Li@d$SeF#!5_*benz`Yfamw}YaJvlg$* zsRqGjjp@)Skj;OMUKd8Te~Ov=-^a=jx*t2&Ae2vidG@_1zf}K2DAoTTWOw%9wO@8y zaCI6SJv#08EL*~Vnzw|hqFExYw+HQ_u0~N%mU|iQsVx#bP`%nc{SU+Ok~JdzSvZ~V z)>BWlfv~23Ki|j$ztDBmSp3!%!TKSL8_k#fglTLW0f8TK*1oOwdf|ETY8>SK4`T}I zTVN~Q^jTMnBizWC5B-~)$3#! zQjtDx_w$n4koHkYO{t&Awl>WA#u)HDl4ckaA&`j)XKW}KH*F${&(23}Vj17Ka|!o} zF>dRYXejGsytd9}3UC|r;t@c)%#QD;b{}FA8l4+VY-?|~->V-^3}}DzE5F0hO`ORw z#))YO+r(fYjsLYSU|)!bl;?@3b{AUCq~uP0Aoqh>gt6&0Fcu5_B0bRw5!ug!VBd-R zK;hfbj@k9`Z6{;*A8!YlD$`+Ze2Gl#>@L3smI_hcPqf5Oq?c7dN;y6EXYSAB-@lgg zoH6kS4fV%QW?;G%Nv|09JcYw=+ldc9L&RQ}xm+2TV|1`> zn+0-yTF$I?&%NpY{Rs}nf9@yvdG}A`C!A;PcFSinnVWh~dL`skyEAf)^Zm&#YuL53 zgOahAzl4DY8s1>)HehfY<|-(c@K6X;Yx@j_lW%b|CwckRd^_-gl#Sg1mi6LXj1859 z@#+8CnzFumS=}TaH878D`l0{8mky=EuzYp7KlF=KoZ2|(2=Rhc;}iL-hd{vG;yFqw zrad~`a;N(Z)o>s`oXW77eHaaO5~S1>+DQveeSj8Zofj}XNx)OP=p z`laiV1I(ufHk?cgE7x6vZucd5YE2vN@I07%?qA4?9R8Ju{&xQh`FlW`^RaU2BQdQO zxW`VEjrSeMMAg@uE7O1XdsC@%jadI$F%EkwR}Rs74qX=Y8)GTY2c`pPn!}#7Q4U`}XbNq=qt;mX zL~^vRzY7q*$D6_2oA4Dpr5iSkT@=2s4{JO{kK-lfd|yhzb^<#23@p9RUet8aUCJj& z6@4OQKip}B)m*#-1YQ?5J-L0TrSK*gw--Agunh{i(2_tIBhLNXH1?|MkOw)*;CUMe zyL~7D)F8TNQev-Q&Bb=IMmhWKYD~r*G&Hz2B|I0McHOuGvNQhBz~8K1vG0ee*X%1| zEQ&{!ajhS9@#r6oKDr6R(fjlcA=Go<2gHZISiM6G0nmZL)zl?}pZ4!#QQ6nQj>*cP z;V@C$&r9%9d>%S!_}uDsw2XFY?jZPo)-_L^m$Hn9T~j4w{81~$^T$ye@;0WuaT&UO z_!LUeovv5ep-+fP9k>0}rZ8Tyuq|C! z0RSHi8iAqGM|ROW*IxE{`7~}?brsKwKu@pL+t%e=>ruv%I?_$a$I@0KMq^+H`B`XQ zle(!< zXH;khOJyCNlkCsoTYXz3q9e6Bd%Gd`q=sbCC&u~8yAl&RU-CeHLMPPcr1$wnDWM1Q z`Hy6r^vCl;9ec)VP#>1c#GXB!t_U(zfk9lMGQ$`ouIL~6^D%YdPKSg*9J5n1bt8D3 z35-oKT(OrsAPX1$-Cz~gE~x~|mqh8pq*87`^zroX@&0Hjr%>3EpkA{$}W;E@y$o_*E1C|wqSU+OvJV+9`y5I(7e5Q5CFQmwNDL)Ni? z4=aOxW8^S)6XUU?w;(~d&!$&&($rnMAILmH7n1j?p=wh3n`YUb|e!pLa4(H_hVM7*V_e9>g7I&!7af=7Sc(voM zohyE@t0(M)VZ^G#;}a~?4-ZArI~qSe86lxDjP)T@qCe1mbSBSstDw}hJwu5oZ(Lft zK1KU5oZHkVW}$*mVgY5IInInN@cvB27ZAKK>^l-3x<>@P8ni{wA2xbf8|7Y(Lkv-Y z9$bIH2*HUqON9{+bdXDEJi!MLCc|!D~rr5$Pplwa&MvZ{U1NUE@QP>3|3i0x6e!S%nx5#>N%gp^ts7>!V-8O&tAL6hS`y2NWX? zv%@RXw4-a`t?Q{6C5HtRbayaI32kfZpxsC92IEF?d*}+q*)pTF79oTSc;Yz&bmI_a z@E~+N&_zWE?0WiNH{_$Z{^O?2dNW}~G_Dh4)=fhyNN7^9)ip)ytT?m(PwBF%Hi z&F~Rx?3cj_A$s6DA~OuTad%mQ}5M61zFj~^MI+$7=*j`LaFr2@z14x4ohzn zrT=?5Bf-q}d$NT3J&x~SR^OARU0w)#EbxS~-!HFm4J@CD<^F|?N&Z5{Xn^(5Ck5Uv^X#9asX^>}O#%X_&V=kLmw|I`0E z)F`}9-9n22zlWAWH%zKF@l8DOtnJFMhl&DFQNVA9-@+Z0$9y*QbkM&>sNa+GSP)d9 z?`oc^MHprs!n}@1VR&oTC9~SrUlz(OA&kdPLHTfnKlNMtKl-)SzqQ!+wa>YFI~Y!( z^!}UG&g1?ajF|>np4FUdETQ}OtyCeb65}$w*=#)i;+SxM{4Z&jwT=!Zb0R z&aIaG3XY`%mgzS)oW^p1ZuIa)wX=AC4{z4x5c1m9_KQYl-^^UGp)jL+<1QbEg}YMnm3FJ zP?GvD+D0=CJVF4y@0S+jl61JPc!bU2#eQzBDUf518Oj@?L5pW~XX^**@#t{t_0hh= zY2v*J$gf>J@aRY;Zn3m%Y-siq$@fovw4l88ZV?mkD4l77NYWzji{HpB z3()fRj9@A#0lStp#z8Xi2v?`-Y{n;4e7Le)>x(O$-14v(9O!7*j3Le$!d%!-td>m3 zYsiP0a90AdDas^~n=7?p%M)U2?nxx%!DbkCT|FzQhx2LuyNc~;HUF?ivk7ov*kY?{ zQW}5!m#tdW*a>5g{%p{9HwT^?eEwtW8MpTR)>B0})$*XKLnwzB^8!ElG#gKRjZyrB>Al8(m&U+lSU{b!&>HwXwJbSZFEjPMv?# z;`}uriRHx~H);c;HH3Ep>bt$;!GS_U+oH!~ zX~o$EW6jxsZ)_$ILu;&Fg+h&cvGGEnMt2A*I^vxLrR^%fbQdgp61WK zTg_3dPto7R2@YV1r^c73YGYnMG3yU+)X>DZQdNs%#vZVu@EN9V%<*ifjJvmq8g!5l z+R!^HUnKCP)#Ne;&Uye1M!vdF!FEXK_&4+={2fg^xrVYq@P#FrxiAczaxZ`U&%ssV zARF(io?n^s4Jx%$^TzySEIV50a03f+3z@mAAT25^?h{YV@(pJm^?l2zNaWEb8sNe4 zijb{mtBh<#TXUc=%WIgWqiP%VF1Yz7S!6?FI!1e&3(Yhc%4tBAcoz1oqT*R0ul#jP4%VHWa}sml+c( z9-XAnRYMm=d#Cy+%q>3wpy8oAG{*Nu_GdBG2*QXKcxi|Ch3+W=(tVv9MtwY5Bec4B znt3c&Its!3ngr8l`JJzrzu!Yqx!PF4eNoa$OJsqduHtmDsF7F1F37bINFLYXGYdkvj#Pu zcnlrXp})p^F3?-jo(NhHLOJ?TJlWXOe5lInCIt3Q52`<8>|M3*$L#s~sN}xPxK(VeYv9=nYkLO!^qv8I=LeDFA1E$aH{VA5ezPSv` z5E|6}S;E9k?}hZ=_`61Pr{%DG9gEdogdQ0-9#iGXQTx%=24Mm5^zY7!Wrj%-%Q$Xb zJA%5iFz(Q#g$*k011X07f+~tQNNzRUA^ST5&nHA7K(twKQV>tAgRT2+`ej1|vLVA3tkptYZA9d#YF;CZZF*Ut;n z*uqJ}YrMJY8V}`l-ny6wgLI=Q%*1_)0~aMd2AnqM6dr~(-*ewx=s)+0PAbN|+Qr8B z@S^;Tt~qwk+f%h0U5gkd)9|>-@Pc-k4dLl`h|!I2cV)1BEP2GUfZEMHE!&h2yW{d# z-#v~Lc-{fkk4=f&a%vI=5qK_JACNcjGxhT=FE0WexAp@VOZy`A)s{YsB+6 zFD3N1w3MJJxr3+waD{WZVdT;84)$l#r>7wh61rqD)>qOQ28=Y31)Vrk=5R~`!d}Pj z!~HK3ImDT~fR}`~*$Zu2;rBz2bH~hi1#| z!M{J2GqCi3G+=IT_*uSrG5!tBn%i?o9xnJRv2@IQK=eRmWZFD9=7gEO#gb(p69D9$jBeN|}IJxKE#@k-^eJ=l69^}(KZ_B}r z^7+eSkKMReuZVu0xcB5f&_N2%(%U!afeyZTLow&%8|-bzAi0NpS-(fjg~fPBl-{*2 zi}z3m423QYI%C_ui9>|KXR)uuy`*Vk4)>OR20y~^9(QwDoD+5*x^#~0sIkd3uPBq2 z1+JkDhvVm364B?cYx=kxqHj0GH5cwq56nS#5_t_h`OZBP7|?XiFvup2a6P>1XVnGJ z8U?3U$BSp6t((>5YLAwWySO^#6S`t!9hm+cxn5WAUHo*9mM*q}hdEN!6I|QY*AZ`J z4`j<`vg`B{j|8>ZgchdO^FzN&u!o^=Ui=vwC3QwnA5%K+m>5$ zpQ7p)G0P36u3hHFNg5Y<a7^s+C2M$WQVWFa%Wkn~|y%&{* z(!i+CJ+GW%Iy1gFdvC4?gRzJ0J^QVIhPA!`%k5!hj>-CsFs>$`R zY>e4`QNy}=7r&Akz)Z`wo;_Ao(!)FtxMdBZycVaqTbIa|kI3ud2R=TUmdJp+X^8~x z;YoaXF})fi+L7_4alK_7Zi%niid1xstLwdnYs&u%aVc!0i1ObwV*31bPB_%ihLk>r z6i+nLn?_^ccDHKkb2S>zN&7QAYr&t7_}iN649g~t&B-g?eU?{r+byp)c~95YeHHiI z=i;#V_n!Y(mP_R4oBrP~{lD^7hP|IloAg8ZgV}uj?ML$4kL36Gx061-^qz&`d;V=3 zhzvUlGE6L>N3XNp-Jox`q(6?{1tvVO&EvZ759{y^Tb(dG$3PhUWt)V;@ZWtJ4+@7p z_V~Yo0qcoxMv&$>{7?yj8=pu@c|{0rS`!|1Y6e;Y^q-KNY|Apjov zs>e{sW)6`98)zBAy*S)yHa z*W=}DhEoJ|gKC4TK5xS?cL4KQkG%YJm&W$$@qDiZ8KaT4A*dg29EwYNY`~8J15OV2 zhbbRj4sQ&&H>IX9SAoDhENG1%89u&#SboN^6?0BF78^ewmRiN|#>oTlu#sDS$b#9g z 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

`$7q0mOk)0&n7L;_vmCI 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

S!LAcQfFx?Hrq0eITQ>_vDsF1 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 z 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... + Redmine .NET API Client 1.0.0 2.0.0 $(VersionSuffix) From ece9b6facfefdd8c1d52af27d32417164eaa8f50 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Thu, 21 Nov 2019 14:20:34 +0200 Subject: [PATCH 015/549] Pass an explicit TaskScheduler to Task.Factory.StartNew (.NET40) --- .../Async/RedmineManagerAsync40.cs | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/redmine-net-api/Async/RedmineManagerAsync40.cs b/src/redmine-net-api/Async/RedmineManagerAsync40.cs index ab178809..2b8a1f39 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync40.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync40.cs @@ -15,8 +15,8 @@ limitations under the License. */ +using System.Threading; #if NET40 - using System.Collections.Generic; using System.Collections.Specialized; using System.Threading.Tasks; @@ -37,7 +37,7 @@ public static class RedmineManagerAsync /// public static Task GetCurrentUserAsync(this RedmineManager redmineManager, NameValueCollection parameters = null) { - return Task.Factory.StartNew(() => redmineManager.GetCurrentUser(parameters)); + return Task.Factory.StartNew(() => redmineManager.GetCurrentUser(parameters), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); } /// @@ -50,7 +50,7 @@ public static Task GetCurrentUserAsync(this RedmineManager redmineManager, /// public static Task CreateOrUpdateWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage) { - return Task.Factory.StartNew(() => redmineManager.CreateOrUpdateWikiPage(projectId, pageName, wikiPage)); + return Task.Factory.StartNew(() => redmineManager.CreateOrUpdateWikiPage(projectId, pageName, wikiPage), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); } /// @@ -62,7 +62,7 @@ public static Task CreateOrUpdateWikiPageAsync(this RedmineManager red /// public static Task DeleteWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName) { - return Task.Factory.StartNew(() => redmineManager.DeleteWikiPage(projectId, pageName)); + return Task.Factory.StartNew(() => redmineManager.DeleteWikiPage(projectId, pageName), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); } /// @@ -76,7 +76,7 @@ public static Task DeleteWikiPageAsync(this RedmineManager redmineManager, strin /// public static Task GetWikiPageAsync(this RedmineManager redmineManager, string projectId, NameValueCollection parameters, string pageName, uint version = 0) { - return Task.Factory.StartNew(() => redmineManager.GetWikiPage(projectId, parameters, pageName, version)); + return Task.Factory.StartNew(() => redmineManager.GetWikiPage(projectId, parameters, pageName, version), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); } /// @@ -87,7 +87,7 @@ public static Task GetWikiPageAsync(this RedmineManager redmineManager /// public static Task> GetAllWikiPagesAsync(this RedmineManager redmineManager, string projectId) { - return Task.Factory.StartNew(() => redmineManager.GetAllWikiPages(projectId)); + return Task.Factory.StartNew(() => redmineManager.GetAllWikiPages(projectId), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); } /// @@ -99,7 +99,7 @@ public static Task> GetAllWikiPagesAsync(this RedmineManager redm /// public static Task AddUserToGroupAsync(this RedmineManager redmineManager, int groupId, int userId) { - return Task.Factory.StartNew(() => redmineManager.AddUserToGroup(groupId, userId)); + return Task.Factory.StartNew(() => redmineManager.AddUserToGroup(groupId, userId), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); } /// @@ -111,7 +111,7 @@ public static Task AddUserToGroupAsync(this RedmineManager redmineManager, int g /// public static Task RemoveUserFromGroupAsync(this RedmineManager redmineManager, int groupId, int userId) { - return Task.Factory.StartNew(() => redmineManager.RemoveUserFromGroup(groupId, userId)); + return Task.Factory.StartNew(() => redmineManager.RemoveUserFromGroup(groupId, userId), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); } /// @@ -123,7 +123,7 @@ public static Task RemoveUserFromGroupAsync(this RedmineManager redmineManager, /// public static Task AddWatcherToIssueAsync(this RedmineManager redmineManager, int issueId, int userId) { - return Task.Factory.StartNew(() => redmineManager.AddWatcherToIssue(issueId, userId)); + return Task.Factory.StartNew(() => redmineManager.AddWatcherToIssue(issueId, userId), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); } /// @@ -135,7 +135,7 @@ public static Task AddWatcherToIssueAsync(this RedmineManager redmineManager, in /// public static Task RemoveWatcherFromIssueAsync(this RedmineManager redmineManager, int issueId, int userId) { - return Task.Factory.StartNew(() => redmineManager.RemoveWatcherFromIssue(issueId, userId)); + return Task.Factory.StartNew(() => redmineManager.RemoveWatcherFromIssue(issueId, userId), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); } /// @@ -148,7 +148,7 @@ public static Task RemoveWatcherFromIssueAsync(this RedmineManager redmineManage /// public static Task GetObjectAsync(this RedmineManager redmineManager, string id, NameValueCollection parameters) where T : class, new() { - return Task.Factory.StartNew(() => redmineManager.GetObject(id, parameters)); + return Task.Factory.StartNew(() => redmineManager.GetObject(id, parameters), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); } /// @@ -173,7 +173,7 @@ public static Task RemoveWatcherFromIssueAsync(this RedmineManager redmineManage /// public static Task CountAsync(this RedmineManager redmineManager, params string[] include) where T : class, new() { - return Task.Factory.StartNew(()=> redmineManager.Count(include)); + return Task.Factory.StartNew(()=> redmineManager.Count(include), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); } /// @@ -185,7 +185,7 @@ public static Task RemoveWatcherFromIssueAsync(this RedmineManager redmineManage /// public static Task CountAsync(this RedmineManager redmineManager, NameValueCollection parameters) where T : class, new() { - return Task.Factory.StartNew(() => redmineManager.Count(parameters)); + return Task.Factory.StartNew(() => redmineManager.Count(parameters), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); } /// @@ -198,7 +198,7 @@ public static Task RemoveWatcherFromIssueAsync(this RedmineManager redmineManage /// public static Task CreateObjectAsync(this RedmineManager redmineManager, T obj, string ownerId) where T : class, new() { - return Task.Factory.StartNew(() => redmineManager.CreateObject(obj, ownerId)); + return Task.Factory.StartNew(() => redmineManager.CreateObject(obj, ownerId), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); } /// @@ -210,7 +210,7 @@ public static Task RemoveWatcherFromIssueAsync(this RedmineManager redmineManage /// public static Task> GetPaginatedObjectsAsync(this RedmineManager redmineManager, NameValueCollection parameters) where T : class, new() { - return Task.Factory.StartNew(() => redmineManager.GetPaginatedObjects(parameters)); + return Task.Factory.StartNew(() => redmineManager.GetPaginatedObjects(parameters), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); } /// @@ -222,7 +222,7 @@ public static Task RemoveWatcherFromIssueAsync(this RedmineManager redmineManage /// public static Task> GetObjectsAsync(this RedmineManager redmineManager, NameValueCollection parameters) where T : class, new() { - return Task.Factory.StartNew(() => redmineManager.GetObjects(parameters)); + return Task.Factory.StartNew(() => redmineManager.GetObjects(parameters), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); } /// @@ -236,7 +236,7 @@ public static Task RemoveWatcherFromIssueAsync(this RedmineManager redmineManage /// public static Task UpdateObjectAsync(this RedmineManager redmineManager, string id, T obj, string projectId = null) where T : class, new() { - return Task.Factory.StartNew(() => redmineManager.UpdateObject(id, obj, projectId)); + return Task.Factory.StartNew(() => redmineManager.UpdateObject(id, obj, projectId), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); } /// @@ -249,7 +249,7 @@ public static Task RemoveWatcherFromIssueAsync(this RedmineManager redmineManage /// public static Task DeleteObjectAsync(this RedmineManager redmineManager, string id, NameValueCollection parameters) where T : class, new() { - return Task.Factory.StartNew(() => redmineManager.DeleteObject(id)); + return Task.Factory.StartNew(() => redmineManager.DeleteObject(id), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); } /// @@ -260,7 +260,7 @@ public static Task RemoveWatcherFromIssueAsync(this RedmineManager redmineManage /// public static Task UploadFileAsync(this RedmineManager redmineManager, byte[] data) { - return Task.Factory.StartNew(() => redmineManager.UploadFile(data)); + return Task.Factory.StartNew(() => redmineManager.UploadFile(data), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); } /// @@ -271,7 +271,7 @@ public static Task UploadFileAsync(this RedmineManager redmineManager, b /// public static Task DownloadFileAsync(this RedmineManager redmineManager, string address) { - return Task.Factory.StartNew(() => redmineManager.DownloadFile(address)); + return Task.Factory.StartNew(() => redmineManager.DownloadFile(address), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); } } } From 760b14e73ccf5b9070136337d6d8e9620bbc5eee Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Thu, 21 Nov 2019 14:21:58 +0200 Subject: [PATCH 016/549] Add Serializable attribute to redmine exceptions --- src/redmine-net-api/Exceptions/ConflictException.cs | 10 ++++++++++ src/redmine-net-api/Exceptions/ForbiddenException.cs | 11 +++++++++++ .../Exceptions/InternalServerErrorException.cs | 11 +++++++++++ .../Exceptions/NameResolutionFailureException.cs | 10 ++++++++++ .../Exceptions/NotAcceptableException.cs | 10 ++++++++++ src/redmine-net-api/Exceptions/NotFoundException.cs | 11 +++++++++++ src/redmine-net-api/Exceptions/RedmineException.cs | 11 ++++++++++- .../Exceptions/RedmineTimeoutException.cs | 11 +++++++++++ .../Exceptions/UnauthorizedException.cs | 12 ++++++++++++ 9 files changed, 96 insertions(+), 1 deletion(-) diff --git a/src/redmine-net-api/Exceptions/ConflictException.cs b/src/redmine-net-api/Exceptions/ConflictException.cs index 0acc079f..43e4a5ad 100644 --- a/src/redmine-net-api/Exceptions/ConflictException.cs +++ b/src/redmine-net-api/Exceptions/ConflictException.cs @@ -23,6 +23,7 @@ namespace Redmine.Net.Api.Exceptions /// /// /// + [Serializable] public sealed class ConflictException : RedmineException { /// @@ -72,5 +73,14 @@ public ConflictException(string format, Exception innerException, params object[ { } + /// + /// + /// + /// + /// + private ConflictException(SerializationInfo serializationInfo, StreamingContext streamingContext):base(serializationInfo, streamingContext) + { + + } } } \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/ForbiddenException.cs b/src/redmine-net-api/Exceptions/ForbiddenException.cs index e9c2fa2c..de49d5f4 100644 --- a/src/redmine-net-api/Exceptions/ForbiddenException.cs +++ b/src/redmine-net-api/Exceptions/ForbiddenException.cs @@ -23,6 +23,7 @@ namespace Redmine.Net.Api.Exceptions /// /// /// + [Serializable] public sealed class ForbiddenException : RedmineException { /// @@ -72,5 +73,15 @@ public ForbiddenException(string format, Exception innerException, params object { } + /// + /// + /// + /// + /// + /// + private ForbiddenException(SerializationInfo serializationInfo, StreamingContext streamingContext):base(serializationInfo, streamingContext) + { + + } } } \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/InternalServerErrorException.cs b/src/redmine-net-api/Exceptions/InternalServerErrorException.cs index be8e3e5c..dea797e9 100644 --- a/src/redmine-net-api/Exceptions/InternalServerErrorException.cs +++ b/src/redmine-net-api/Exceptions/InternalServerErrorException.cs @@ -23,6 +23,7 @@ namespace Redmine.Net.Api.Exceptions /// /// /// + [Serializable] public sealed class InternalServerErrorException : RedmineException { /// @@ -72,5 +73,15 @@ public InternalServerErrorException(string format, Exception innerException, par { } + /// + /// + /// + /// + /// + /// + private InternalServerErrorException(SerializationInfo serializationInfo, StreamingContext streamingContext):base(serializationInfo, streamingContext) + { + + } } } \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/NameResolutionFailureException.cs b/src/redmine-net-api/Exceptions/NameResolutionFailureException.cs index 6a08cb46..dd0e48c0 100644 --- a/src/redmine-net-api/Exceptions/NameResolutionFailureException.cs +++ b/src/redmine-net-api/Exceptions/NameResolutionFailureException.cs @@ -23,6 +23,7 @@ namespace Redmine.Net.Api.Exceptions /// /// /// + [Serializable] public sealed class NameResolutionFailureException : RedmineException { /// @@ -72,5 +73,14 @@ public NameResolutionFailureException(string format, Exception innerException, p { } + /// + /// + /// + /// + /// + private NameResolutionFailureException(SerializationInfo serializationInfo, StreamingContext streamingContext):base(serializationInfo, streamingContext) + { + + } } } \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/NotAcceptableException.cs b/src/redmine-net-api/Exceptions/NotAcceptableException.cs index 281e0cfb..90aee858 100644 --- a/src/redmine-net-api/Exceptions/NotAcceptableException.cs +++ b/src/redmine-net-api/Exceptions/NotAcceptableException.cs @@ -23,6 +23,7 @@ namespace Redmine.Net.Api.Exceptions /// /// /// + [Serializable] public sealed class NotAcceptableException : RedmineException { /// @@ -72,5 +73,14 @@ public NotAcceptableException(string format, Exception innerException, params ob { } + /// + /// + /// + /// + /// + private NotAcceptableException(SerializationInfo serializationInfo, StreamingContext streamingContext):base(serializationInfo, streamingContext) + { + + } } } \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/NotFoundException.cs b/src/redmine-net-api/Exceptions/NotFoundException.cs index b28ca9d3..dbd29178 100644 --- a/src/redmine-net-api/Exceptions/NotFoundException.cs +++ b/src/redmine-net-api/Exceptions/NotFoundException.cs @@ -24,6 +24,7 @@ namespace Redmine.Net.Api.Exceptions /// Thrown in case the objects requested for could not be found. /// /// + [Serializable] public sealed class NotFoundException : RedmineException { /// @@ -72,5 +73,15 @@ public NotFoundException(string format, Exception innerException, params object[ : base(string.Format(CultureInfo.InvariantCulture,format, args), innerException) { } + + /// + /// + /// + /// + /// + private NotFoundException(SerializationInfo serializationInfo, StreamingContext streamingContext):base(serializationInfo, streamingContext) + { + + } } } \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/RedmineException.cs b/src/redmine-net-api/Exceptions/RedmineException.cs index d917780b..0867b830 100644 --- a/src/redmine-net-api/Exceptions/RedmineException.cs +++ b/src/redmine-net-api/Exceptions/RedmineException.cs @@ -24,6 +24,7 @@ namespace Redmine.Net.Api.Exceptions /// Thrown in case something went wrong in Redmine /// /// + [Serializable] public class RedmineException : Exception { /// @@ -73,6 +74,14 @@ public RedmineException(string format, Exception innerException, params object[] { } - + /// + /// + /// + /// + /// + protected RedmineException(SerializationInfo serializationInfo, StreamingContext streamingContext):base(serializationInfo, streamingContext) + { + + } } } \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/RedmineTimeoutException.cs b/src/redmine-net-api/Exceptions/RedmineTimeoutException.cs index cc4a17f9..8f0da618 100644 --- a/src/redmine-net-api/Exceptions/RedmineTimeoutException.cs +++ b/src/redmine-net-api/Exceptions/RedmineTimeoutException.cs @@ -23,6 +23,7 @@ namespace Redmine.Net.Api.Exceptions /// /// /// + [Serializable] public sealed class RedmineTimeoutException : RedmineException { /// @@ -74,5 +75,15 @@ public RedmineTimeoutException(string format, Exception innerException, params o : base(string.Format(CultureInfo.InvariantCulture,format, args), innerException) { } + + /// + /// + /// + /// + /// + private RedmineTimeoutException(SerializationInfo serializationInfo, StreamingContext streamingContext):base(serializationInfo, streamingContext) + { + + } } } \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/UnauthorizedException.cs b/src/redmine-net-api/Exceptions/UnauthorizedException.cs index 32991005..4283b14c 100644 --- a/src/redmine-net-api/Exceptions/UnauthorizedException.cs +++ b/src/redmine-net-api/Exceptions/UnauthorizedException.cs @@ -24,6 +24,7 @@ namespace Redmine.Net.Api.Exceptions /// Thrown in case something went wrong while trying to login. /// /// + [Serializable] public sealed class UnauthorizedException : RedmineException { /// @@ -75,5 +76,16 @@ public UnauthorizedException(string format, Exception innerException, params obj : base(string.Format(CultureInfo.InvariantCulture,format, args), innerException) { } + + /// + /// + /// + /// + /// + /// + private UnauthorizedException(SerializationInfo serializationInfo, StreamingContext streamingContext):base(serializationInfo, streamingContext) + { + + } } } \ No newline at end of file From cb57c40bb440b83554124511a72b3ec49e53187c Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Thu, 21 Nov 2019 14:22:50 +0200 Subject: [PATCH 017/549] Add missing ConfigureAwait(false) --- src/redmine-net-api/Async/RedmineManagerAsync45.cs | 2 +- src/redmine-net-api/Internals/WebApiAsyncHelper.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/redmine-net-api/Async/RedmineManagerAsync45.cs b/src/redmine-net-api/Async/RedmineManagerAsync45.cs index 4642e872..e2cae408 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync45.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync45.cs @@ -228,7 +228,7 @@ public static async Task RemoveWatcherFromIssueAsync(this RedmineManager redmine try { - var tempResult = await GetPaginatedObjectsAsync(redmineManager,parameters); + var tempResult = await GetPaginatedObjectsAsync(redmineManager,parameters).ConfigureAwait(false); if (tempResult != null) { totalCount = tempResult.TotalCount; diff --git a/src/redmine-net-api/Internals/WebApiAsyncHelper.cs b/src/redmine-net-api/Internals/WebApiAsyncHelper.cs index 704207fe..4912030a 100644 --- a/src/redmine-net-api/Internals/WebApiAsyncHelper.cs +++ b/src/redmine-net-api/Internals/WebApiAsyncHelper.cs @@ -162,7 +162,7 @@ public static async Task ExecuteDownloadFile(RedmineManager redmineManag { try { - return await wc.DownloadDataTaskAsync(address); + return await wc.DownloadDataTaskAsync(address).ConfigureAwait(false); } catch (WebException webException) { @@ -186,7 +186,7 @@ public static async Task ExecuteUploadFile(RedmineManager redmineManager { try { - var response = await wc.UploadDataTaskAsync(address, data); + var response = await wc.UploadDataTaskAsync(address, data).ConfigureAwait(false); var responseString = Encoding.ASCII.GetString(response); return RedmineSerializer.Deserialize(responseString, redmineManager.MimeFormat); } From a4ff18d281c0427cac44a64adec30341c209d4a6 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Thu, 21 Nov 2019 14:24:41 +0200 Subject: [PATCH 018/549] Add XmlTextReaderBuilder --- .../Extensions/XmlReaderExtensions.cs | 5 +- .../Internals/RedmineSerializer.cs | 6 +-- .../Internals/XmlTextReaderBuilder.cs | 54 +++++++++++++++++++ 3 files changed, 60 insertions(+), 5 deletions(-) create mode 100644 src/redmine-net-api/Internals/XmlTextReaderBuilder.cs diff --git a/src/redmine-net-api/Extensions/XmlReaderExtensions.cs b/src/redmine-net-api/Extensions/XmlReaderExtensions.cs index 9bc95d7c..00f9a7e5 100755 --- a/src/redmine-net-api/Extensions/XmlReaderExtensions.cs +++ b/src/redmine-net-api/Extensions/XmlReaderExtensions.cs @@ -21,6 +21,7 @@ limitations under the License. using System.IO; using System.Xml; using System.Xml.Serialization; +using Redmine.Net.Api.Internals; namespace Redmine.Net.Api.Extensions { @@ -171,7 +172,7 @@ public static List ReadElementContentAsCollection(this XmlReader reader) w using (var stringReader = new StringReader(outerXml)) { - using (var xmlTextReader = new XmlTextReader(stringReader)) + using (var xmlTextReader = XmlTextReaderBuilder.Create(stringReader)) { xmlTextReader.ReadStartElement(); while (!xmlTextReader.EOF) @@ -237,7 +238,7 @@ public static IEnumerable ReadElementContentAsEnumerable(this XmlReader re var outerXml = reader.ReadOuterXml(); using (var stringReader = new StringReader(outerXml)) { - using (var xmlTextReader = new XmlTextReader(stringReader)) + using (var xmlTextReader = XmlTextReaderBuilder.Create(stringReader)) { xmlTextReader.ReadStartElement(); while (!xmlTextReader.EOF) diff --git a/src/redmine-net-api/Internals/RedmineSerializer.cs b/src/redmine-net-api/Internals/RedmineSerializer.cs index 9cbb0943..8045bac5 100755 --- a/src/redmine-net-api/Internals/RedmineSerializer.cs +++ b/src/redmine-net-api/Internals/RedmineSerializer.cs @@ -68,7 +68,7 @@ private static T FromXML(string xml) where T : class { if(xml.IsNullOrWhiteSpace()) throw new ArgumentNullException(nameof(xml)); - using (var text = new XmlTextReader(xml)) + using (var text = XmlTextReaderBuilder.Create(xml)) { var sr = new XmlSerializer(typeof (T)); return sr.Deserialize(text) as T; @@ -214,9 +214,9 @@ public static PaginatedObjects DeserializeList(string response, MimeFormat { using (var stringReader = new StringReader(response)) { - using (var xmlReader = new XmlTextReader(stringReader)) + using (var xmlReader = XmlTextReaderBuilder.Create(stringReader)) { - xmlReader.WhitespaceHandling = WhitespaceHandling.None; + xmlReader.Read(); xmlReader.Read(); diff --git a/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs b/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs new file mode 100644 index 00000000..b5af4dd1 --- /dev/null +++ b/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs @@ -0,0 +1,54 @@ +using System.IO; +using System.Xml; + +namespace Redmine.Net.Api.Internals +{ + internal static class XmlTextReaderBuilder + { +#if NET20 + public static XmlReader Create(StringReader stringReader) + { + return XmlReader.Create(stringReader, new XmlReaderSettings() + { + ProhibitDtd = true, + XmlResolver = null, + IgnoreComments = true, + IgnoreWhitespace = true, + }); + + } + + public static XmlReader Create(string stringReader) + { + return XmlReader.Create(stringReader, new XmlReaderSettings() + { + ProhibitDtd = true, + XmlResolver = null, + IgnoreComments = true, + IgnoreWhitespace = true, + }); + + } +#else + public static XmlTextReader Create(StringReader stringReader) + { + return new XmlTextReader(stringReader) + { + DtdProcessing = DtdProcessing.Prohibit, + XmlResolver = null, + WhitespaceHandling = WhitespaceHandling.None + }; + } + + public static XmlTextReader Create(string stringReader) + { + return new XmlTextReader(stringReader) + { + DtdProcessing = DtdProcessing.Prohibit, + XmlResolver = null, + WhitespaceHandling = WhitespaceHandling.None + }; + } +#endif + } +} \ No newline at end of file From 466d143078708c4d867612853768afdd94cf7bde Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Thu, 21 Nov 2019 14:27:59 +0200 Subject: [PATCH 019/549] string.Format to interpolation --- src/redmine-net-api/Types/Attachment.cs | 4 ++-- src/redmine-net-api/Types/ChangeSet.cs | 2 +- src/redmine-net-api/Types/CustomField.cs | 4 ++-- src/redmine-net-api/Types/CustomFieldPossibleValue.cs | 2 +- src/redmine-net-api/Types/CustomFieldRole.cs | 2 +- src/redmine-net-api/Types/CustomFieldValue.cs | 4 ++-- src/redmine-net-api/Types/Detail.cs | 2 +- src/redmine-net-api/Types/Error.cs | 2 +- src/redmine-net-api/Types/File.cs | 2 +- src/redmine-net-api/Types/Group.cs | 5 +++-- src/redmine-net-api/Types/GroupUser.cs | 2 +- src/redmine-net-api/Types/Identifiable.cs | 2 +- src/redmine-net-api/Types/Issue.cs | 8 +++----- src/redmine-net-api/Types/IssueCategory.cs | 2 +- src/redmine-net-api/Types/IssueChild.cs | 4 ++-- src/redmine-net-api/Types/IssueCustomField.cs | 2 +- src/redmine-net-api/Types/IssuePriority.cs | 2 +- src/redmine-net-api/Types/IssueRelation.cs | 3 ++- src/redmine-net-api/Types/IssueStatus.cs | 2 +- src/redmine-net-api/Types/Journal.cs | 2 +- src/redmine-net-api/Types/Membership.cs | 2 +- src/redmine-net-api/Types/MembershipRole.cs | 2 +- src/redmine-net-api/Types/News.cs | 4 ++-- src/redmine-net-api/Types/Permission.cs | 2 +- src/redmine-net-api/Types/Project.cs | 6 +++--- src/redmine-net-api/Types/ProjectEnabledModule.cs | 2 +- src/redmine-net-api/Types/ProjectIssueCategory.cs | 2 +- src/redmine-net-api/Types/ProjectMembership.cs | 3 ++- src/redmine-net-api/Types/ProjectTracker.cs | 2 +- src/redmine-net-api/Types/Query.cs | 2 +- src/redmine-net-api/Types/Role.cs | 2 +- src/redmine-net-api/Types/TimeEntry.cs | 4 ++-- src/redmine-net-api/Types/TimeEntryActivity.cs | 2 +- src/redmine-net-api/Types/Tracker.cs | 2 +- src/redmine-net-api/Types/TrackerCustomField.cs | 2 +- src/redmine-net-api/Types/Upload.cs | 3 ++- src/redmine-net-api/Types/User.cs | 6 +++--- src/redmine-net-api/Types/UserGroup.cs | 2 +- src/redmine-net-api/Types/Version.cs | 6 +++--- src/redmine-net-api/Types/Watcher.cs | 2 +- src/redmine-net-api/Types/WikiPage.cs | 4 ++-- 41 files changed, 61 insertions(+), 59 deletions(-) diff --git a/src/redmine-net-api/Types/Attachment.cs b/src/redmine-net-api/Types/Attachment.cs index ac171889..4a125135 100755 --- a/src/redmine-net-api/Types/Attachment.cs +++ b/src/redmine-net-api/Types/Attachment.cs @@ -173,8 +173,8 @@ public override int GetHashCode() /// public override string ToString() { - return string.Format("[Attachment: {7}, FileName={0}, FileSize={1}, ContentType={2}, Description={3}, ContentUrl={4}, Author={5}, CreatedOn={6}]", - FileName, FileSize, ContentType, Description, ContentUrl, Author, CreatedOn, base.ToString()); + return + $"[Attachment: {base.ToString()}, FileName={FileName}, FileSize={FileSize}, ContentType={ContentType}, Description={Description}, ContentUrl={ContentUrl}, Author={Author}, CreatedOn={CreatedOn}]"; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/ChangeSet.cs b/src/redmine-net-api/Types/ChangeSet.cs index 10b26548..ec41ad24 100755 --- a/src/redmine-net-api/Types/ChangeSet.cs +++ b/src/redmine-net-api/Types/ChangeSet.cs @@ -146,7 +146,7 @@ public override int GetHashCode() /// public override string ToString() { - return string.Format("Revision: {0}, User: '{1}', CommitedOn: {2}, Comments: '{3}'", Revision, User, CommittedOn, Comments); + return $"Revision: {Revision}, User: '{User}', CommitedOn: {CommittedOn}, Comments: '{Comments}'"; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/CustomField.cs b/src/redmine-net-api/Types/CustomField.cs index a4bc7a69..f197cdbb 100755 --- a/src/redmine-net-api/Types/CustomField.cs +++ b/src/redmine-net-api/Types/CustomField.cs @@ -251,8 +251,8 @@ public override int GetHashCode() /// public override string ToString () { - return string.Format ("[CustomField: Id={0}, Name={1}, CustomizedType={2}, FieldFormat={3}, Regexp={4}, MinLength={5}, MaxLength={6}, IsRequired={7}, IsFilter={8}, Searchable={9}, Multiple={10}, DefaultValue={11}, Visible={12}, PossibleValues={13}, Trackers={14}, Roles={15}]", - Id, Name, CustomizedType, FieldFormat, Regexp, MinLength, MaxLength, IsRequired, IsFilter, Searchable, Multiple, DefaultValue, Visible, PossibleValues, Trackers, Roles); + return + $"[CustomField: Id={Id}, Name={Name}, CustomizedType={CustomizedType}, FieldFormat={FieldFormat}, Regexp={Regexp}, MinLength={MinLength}, MaxLength={MaxLength}, IsRequired={IsRequired}, IsFilter={IsFilter}, Searchable={Searchable}, Multiple={Multiple}, DefaultValue={DefaultValue}, Visible={Visible}, PossibleValues={PossibleValues}, Trackers={Trackers}, Roles={Roles}]"; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/CustomFieldPossibleValue.cs b/src/redmine-net-api/Types/CustomFieldPossibleValue.cs index 61ab24fa..ff138076 100755 --- a/src/redmine-net-api/Types/CustomFieldPossibleValue.cs +++ b/src/redmine-net-api/Types/CustomFieldPossibleValue.cs @@ -82,7 +82,7 @@ public override int GetHashCode() /// public override string ToString () { - return string.Format ("[CustomFieldPossibleValue: {0}]", base.ToString()); + return $"[CustomFieldPossibleValue: {base.ToString()}]"; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/CustomFieldRole.cs b/src/redmine-net-api/Types/CustomFieldRole.cs index 2c67f343..213b5948 100755 --- a/src/redmine-net-api/Types/CustomFieldRole.cs +++ b/src/redmine-net-api/Types/CustomFieldRole.cs @@ -30,7 +30,7 @@ public class CustomFieldRole : IdentifiableName /// public override string ToString () { - return string.Format ("[CustomFieldRole: {0}]", base.ToString()); + return $"[CustomFieldRole: {base.ToString()}]"; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/CustomFieldValue.cs b/src/redmine-net-api/Types/CustomFieldValue.cs index 56ea6263..31fa884e 100755 --- a/src/redmine-net-api/Types/CustomFieldValue.cs +++ b/src/redmine-net-api/Types/CustomFieldValue.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -75,7 +75,7 @@ public override int GetHashCode() /// public override string ToString() { - return string.Format("[CustomFieldValue: Info={0}]", Info); + return $"[CustomFieldValue: Info={Info}]"; } /// diff --git a/src/redmine-net-api/Types/Detail.cs b/src/redmine-net-api/Types/Detail.cs index 23838086..c0943f6d 100755 --- a/src/redmine-net-api/Types/Detail.cs +++ b/src/redmine-net-api/Types/Detail.cs @@ -157,7 +157,7 @@ public override int GetHashCode() /// public override string ToString() { - return string.Format("[Detail: Property={0}, Name={1}, OldValue={2}, NewValue={3}]", Property, Name, OldValue, NewValue); + return $"[Detail: Property={Property}, Name={Name}, OldValue={OldValue}, NewValue={NewValue}]"; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Error.cs b/src/redmine-net-api/Types/Error.cs index b5576229..96fb26b7 100755 --- a/src/redmine-net-api/Types/Error.cs +++ b/src/redmine-net-api/Types/Error.cs @@ -52,7 +52,7 @@ public bool Equals(Error other) /// public override string ToString() { - return string.Format("[Error: Info={0}]", Info); + return $"[Error: Info={Info}]"; } /// diff --git a/src/redmine-net-api/Types/File.cs b/src/redmine-net-api/Types/File.cs index 86142eb5..f7ca5b81 100644 --- a/src/redmine-net-api/Types/File.cs +++ b/src/redmine-net-api/Types/File.cs @@ -151,7 +151,7 @@ public override int GetHashCode() /// public override string ToString() { - return string.Format("[File: Id={0}, Name={1}]", Id, Filename); + return $"[File: Id={Id}, Name={Filename}]"; } /// diff --git a/src/redmine-net-api/Types/Group.cs b/src/redmine-net-api/Types/Group.cs index b71ce6d1..fe4e6900 100755 --- a/src/redmine-net-api/Types/Group.cs +++ b/src/redmine-net-api/Types/Group.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -156,7 +156,8 @@ public override int GetHashCode() /// public override string ToString() { - return string.Format("[Group: Id={0}, Name={1}, Users={2}, CustomFields={3}, Memberships={4}]", Id, Name, Users, CustomFields, Memberships); + return + $"[Group: Id={Id}, Name={Name}, Users={Users}, CustomFields={CustomFields}, Memberships={Memberships}]"; } /// diff --git a/src/redmine-net-api/Types/GroupUser.cs b/src/redmine-net-api/Types/GroupUser.cs index 415222bc..e3f10928 100755 --- a/src/redmine-net-api/Types/GroupUser.cs +++ b/src/redmine-net-api/Types/GroupUser.cs @@ -30,7 +30,7 @@ public class GroupUser : IdentifiableName /// public override string ToString () { - return string.Format ("[GroupUser: {0}]", base.ToString()); + return $"[GroupUser: {base.ToString()}]"; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Identifiable.cs b/src/redmine-net-api/Types/Identifiable.cs index e654dc09..d9934286 100755 --- a/src/redmine-net-api/Types/Identifiable.cs +++ b/src/redmine-net-api/Types/Identifiable.cs @@ -101,7 +101,7 @@ public override int GetHashCode() /// public override string ToString() { - return string.Format("[Identifiable: Id={0}]", Id); + return $"[Identifiable: Id={Id}]"; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Issue.cs b/src/redmine-net-api/Types/Issue.cs index 4f11d017..0c88696f 100644 --- a/src/redmine-net-api/Types/Issue.cs +++ b/src/redmine-net-api/Types/Issue.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2017 Adrian Popescu, Dorin Huzum. Licensed under the Apache License, Version 2.0 (the "License"); @@ -564,10 +564,8 @@ public bool Equals(Issue other) /// public override string ToString() { - return string.Format("[Issue: {30}, Project={0}, Tracker={1}, Status={2}, Priority={3}, Author={4}, Category={5}, Subject={6}, Description={7}, StartDate={8}, DueDate={9}, DoneRatio={10}, PrivateNotes={11}, EstimatedHours={12}, SpentHours={13}, CustomFields={14}, CreatedOn={15}, UpdatedOn={16}, ClosedOn={17}, Notes={18}, AssignedTo={19}, ParentIssue={20}, FixedVersion={21}, IsPrivate={22}, Journals={23}, Changesets={24}, Attachments={25}, Relations={26}, Children={27}, Uploads={28}, Watchers={29}]", - Project, Tracker, Status, Priority, Author, Category, Subject, Description, StartDate, DueDate, DoneRatio, PrivateNotes, - EstimatedHours, SpentHours, CustomFields, CreatedOn, UpdatedOn, ClosedOn, Notes, AssignedTo, ParentIssue, FixedVersion, - IsPrivate, Journals, Changesets, Attachments, Relations, Children, Uploads, Watchers, base.ToString()); + return + $"[Issue: {base.ToString()}, Project={Project}, Tracker={Tracker}, Status={Status}, Priority={Priority}, Author={Author}, Category={Category}, Subject={Subject}, Description={Description}, StartDate={StartDate}, DueDate={DueDate}, DoneRatio={DoneRatio}, PrivateNotes={PrivateNotes}, EstimatedHours={EstimatedHours}, SpentHours={SpentHours}, CustomFields={CustomFields}, CreatedOn={CreatedOn}, UpdatedOn={UpdatedOn}, ClosedOn={ClosedOn}, Notes={Notes}, AssignedTo={AssignedTo}, ParentIssue={ParentIssue}, FixedVersion={FixedVersion}, IsPrivate={IsPrivate}, Journals={Journals}, Changesets={Changesets}, Attachments={Attachments}, Relations={Relations}, Children={Children}, Uploads={Uploads}, Watchers={Watchers}]"; } /// diff --git a/src/redmine-net-api/Types/IssueCategory.cs b/src/redmine-net-api/Types/IssueCategory.cs index fbca4eb3..bc0a26ad 100755 --- a/src/redmine-net-api/Types/IssueCategory.cs +++ b/src/redmine-net-api/Types/IssueCategory.cs @@ -137,7 +137,7 @@ public override int GetHashCode() /// public override string ToString() { - return string.Format("[IssueCategory: {3}, Project={0}, AsignTo={1}, Name={2}]", Project, AsignTo, Name, base.ToString()); + return $"[IssueCategory: {base.ToString()}, Project={Project}, AsignTo={AsignTo}, Name={Name}]"; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/IssueChild.cs b/src/redmine-net-api/Types/IssueChild.cs index 392c5834..7dd10276 100755 --- a/src/redmine-net-api/Types/IssueChild.cs +++ b/src/redmine-net-api/Types/IssueChild.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -125,7 +125,7 @@ public override int GetHashCode() /// public override string ToString() { - return string.Format("[IssueChild: {0}, Tracker={1}, Subject={2}]", base.ToString(), Tracker, Subject); + return $"[IssueChild: {base.ToString()}, Tracker={Tracker}, Subject={Subject}]"; } } } diff --git a/src/redmine-net-api/Types/IssueCustomField.cs b/src/redmine-net-api/Types/IssueCustomField.cs index c21650a0..fb779dea 100755 --- a/src/redmine-net-api/Types/IssueCustomField.cs +++ b/src/redmine-net-api/Types/IssueCustomField.cs @@ -114,7 +114,7 @@ public object Clone() /// public override string ToString() { - return string.Format("[IssueCustomField: {2} Values={0}, Multiple={1}]", Values, Multiple, base.ToString()); + return $"[IssueCustomField: {base.ToString()} Values={Values}, Multiple={Multiple}]"; } /// diff --git a/src/redmine-net-api/Types/IssuePriority.cs b/src/redmine-net-api/Types/IssuePriority.cs index 68643b3e..87754916 100755 --- a/src/redmine-net-api/Types/IssuePriority.cs +++ b/src/redmine-net-api/Types/IssuePriority.cs @@ -120,7 +120,7 @@ public override int GetHashCode() /// public override string ToString() { - return string.Format("[IssuePriority: Id={0}, Name={1}, IsDefault={2}]", Id, Name, IsDefault); + return $"[IssuePriority: Id={Id}, Name={Name}, IsDefault={IsDefault}]"; } #endregion diff --git a/src/redmine-net-api/Types/IssueRelation.cs b/src/redmine-net-api/Types/IssueRelation.cs index 59d49430..fa8a533a 100755 --- a/src/redmine-net-api/Types/IssueRelation.cs +++ b/src/redmine-net-api/Types/IssueRelation.cs @@ -169,7 +169,8 @@ public override int GetHashCode() /// public override string ToString() { - return string.Format("[IssueRelation: {4}, IssueId={0}, IssueToId={1}, Type={2}, Delay={3}]", IssueId, IssueToId, Type, Delay, base.ToString()); + return + $"[IssueRelation: {base.ToString()}, IssueId={IssueId}, IssueToId={IssueToId}, Type={Type}, Delay={Delay}]"; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/IssueStatus.cs b/src/redmine-net-api/Types/IssueStatus.cs index e0b24cf9..63a2bd69 100755 --- a/src/redmine-net-api/Types/IssueStatus.cs +++ b/src/redmine-net-api/Types/IssueStatus.cs @@ -114,7 +114,7 @@ public override int GetHashCode() /// public override string ToString() { - return string.Format("[IssueStatus: {2}, IsDefault={0}, IsClosed={1}]", IsDefault, IsClosed, base.ToString()); + return $"[IssueStatus: {base.ToString()}, IsDefault={IsDefault}, IsClosed={IsClosed}]"; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Journal.cs b/src/redmine-net-api/Types/Journal.cs index c83c3bcd..66cdb5d7 100644 --- a/src/redmine-net-api/Types/Journal.cs +++ b/src/redmine-net-api/Types/Journal.cs @@ -172,7 +172,7 @@ public override int GetHashCode() /// public override string ToString() { - return string.Format("[Journal: Id={0}, User={1}, Notes={2}, CreatedOn={3}, Details={4}]", Id, User, Notes, CreatedOn, Details); + return $"[Journal: Id={Id}, User={User}, Notes={Notes}, CreatedOn={CreatedOn}, Details={Details}]"; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Membership.cs b/src/redmine-net-api/Types/Membership.cs index 85d247ed..44e22724 100755 --- a/src/redmine-net-api/Types/Membership.cs +++ b/src/redmine-net-api/Types/Membership.cs @@ -121,7 +121,7 @@ public override int GetHashCode() /// public override string ToString() { - return string.Format("[Membership: {2}, Project={0}, Roles={1}]", Project, Roles, base.ToString()); + return $"[Membership: {base.ToString()}, Project={Project}, Roles={Roles}]"; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/MembershipRole.cs b/src/redmine-net-api/Types/MembershipRole.cs index be1500e4..edfe3bae 100755 --- a/src/redmine-net-api/Types/MembershipRole.cs +++ b/src/redmine-net-api/Types/MembershipRole.cs @@ -94,7 +94,7 @@ public override int GetHashCode() /// public override string ToString() { - return string.Format("[MembershipRole: {1}, Inherited={0}]", Inherited, base.ToString()); + return $"[MembershipRole: {base.ToString()}, Inherited={Inherited}]"; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/News.cs b/src/redmine-net-api/Types/News.cs index f5921e75..80bd3664 100755 --- a/src/redmine-net-api/Types/News.cs +++ b/src/redmine-net-api/Types/News.cs @@ -162,8 +162,8 @@ public override int GetHashCode() /// public override string ToString() { - return string.Format("[News: {6}, Project={0}, Author={1}, Title={2}, Summary={3}, Description={4}, CreatedOn={5}]", - Project, Author, Title, Summary, Description, CreatedOn, base.ToString()); + return + $"[News: {base.ToString()}, Project={Project}, Author={Author}, Title={Title}, Summary={Summary}, Description={Description}, CreatedOn={CreatedOn}]"; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Permission.cs b/src/redmine-net-api/Types/Permission.cs index dcd3e137..9706629d 100755 --- a/src/redmine-net-api/Types/Permission.cs +++ b/src/redmine-net-api/Types/Permission.cs @@ -75,7 +75,7 @@ public override int GetHashCode() /// public override string ToString() { - return string.Format("[Permission: Info={0}]", Info); + return $"[Permission: Info={Info}]"; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs index 3505f376..3179e516 100644 --- a/src/redmine-net-api/Types/Project.cs +++ b/src/redmine-net-api/Types/Project.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -295,8 +295,8 @@ public override int GetHashCode() /// public override string ToString() { - return string.Format("[Project: {13}, Identifier={0}, Description={1}, Parent={2}, HomePage={3}, CreatedOn={4}, UpdatedOn={5}, Status={6}, IsPublic={7}, InheritMembers={8}, Trackers={9}, CustomFields={10}, IssueCategories={11}, EnabledModules={12}]", - Identifier, Description, Parent, HomePage, CreatedOn, UpdatedOn, Status, IsPublic, InheritMembers, Trackers, CustomFields, IssueCategories, EnabledModules, base.ToString()); + return + $"[Project: {base.ToString()}, Identifier={Identifier}, Description={Description}, Parent={Parent}, HomePage={HomePage}, CreatedOn={CreatedOn}, UpdatedOn={UpdatedOn}, Status={Status}, IsPublic={IsPublic}, InheritMembers={InheritMembers}, Trackers={Trackers}, CustomFields={CustomFields}, IssueCategories={IssueCategories}, EnabledModules={EnabledModules}]"; } } } diff --git a/src/redmine-net-api/Types/ProjectEnabledModule.cs b/src/redmine-net-api/Types/ProjectEnabledModule.cs index 7723082e..23e71027 100755 --- a/src/redmine-net-api/Types/ProjectEnabledModule.cs +++ b/src/redmine-net-api/Types/ProjectEnabledModule.cs @@ -40,7 +40,7 @@ public string Value /// public override string ToString() { - return string.Format("[ProjectEnabledModule: {0}]", base.ToString()); + return $"[ProjectEnabledModule: {base.ToString()}]"; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/ProjectIssueCategory.cs b/src/redmine-net-api/Types/ProjectIssueCategory.cs index ec2d4f51..4c4d58c3 100755 --- a/src/redmine-net-api/Types/ProjectIssueCategory.cs +++ b/src/redmine-net-api/Types/ProjectIssueCategory.cs @@ -30,7 +30,7 @@ public class ProjectIssueCategory : IdentifiableName /// public override string ToString () { - return string.Format ("[ProjectIssueCategory: {0}]", base.ToString()); + return $"[ProjectIssueCategory: {base.ToString()}]"; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/ProjectMembership.cs b/src/redmine-net-api/Types/ProjectMembership.cs index 3dc0e494..6175d16c 100755 --- a/src/redmine-net-api/Types/ProjectMembership.cs +++ b/src/redmine-net-api/Types/ProjectMembership.cs @@ -154,7 +154,8 @@ public override int GetHashCode() /// public override string ToString() { - return string.Format("[ProjectMembership: {4}, Project={0}, User={1}, Group={2}, Roles={3}]", Project, User, Group, Roles, base.ToString()); + return + $"[ProjectMembership: {base.ToString()}, Project={Project}, User={User}, Group={Group}, Roles={Roles}]"; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/ProjectTracker.cs b/src/redmine-net-api/Types/ProjectTracker.cs index 518a419c..ccd5d6e5 100755 --- a/src/redmine-net-api/Types/ProjectTracker.cs +++ b/src/redmine-net-api/Types/ProjectTracker.cs @@ -36,7 +36,7 @@ public class ProjectTracker : IdentifiableName, IValue /// public override string ToString () { - return string.Format ("[ProjectTracker: {0}]", base.ToString()); + return $"[ProjectTracker: {base.ToString()}]"; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Query.cs b/src/redmine-net-api/Types/Query.cs index 4239ba75..160deacf 100755 --- a/src/redmine-net-api/Types/Query.cs +++ b/src/redmine-net-api/Types/Query.cs @@ -113,7 +113,7 @@ public override int GetHashCode() /// public override string ToString() { - return string.Format("[Query: {2}, IsPublic={0}, ProjectId={1}]", IsPublic, ProjectId, base.ToString()); + return $"[Query: {base.ToString()}, IsPublic={IsPublic}, ProjectId={ProjectId}]"; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Role.cs b/src/redmine-net-api/Types/Role.cs index 774c03ea..fcf6fa93 100755 --- a/src/redmine-net-api/Types/Role.cs +++ b/src/redmine-net-api/Types/Role.cs @@ -120,7 +120,7 @@ public override int GetHashCode() /// public override string ToString() { - return string.Format("[Role: Id={0}, Name={1}, Permissions={2}]", Id, Name, Permissions); + return $"[Role: Id={Id}, Name={Name}, Permissions={Permissions}]"; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/TimeEntry.cs b/src/redmine-net-api/Types/TimeEntry.cs index d5de2b17..68f67281 100644 --- a/src/redmine-net-api/Types/TimeEntry.cs +++ b/src/redmine-net-api/Types/TimeEntry.cs @@ -246,8 +246,8 @@ public override int GetHashCode() /// public override string ToString() { - return string.Format("[TimeEntry: {10}, Issue={0}, Project={1}, SpentOn={2}, Hours={3}, Activity={4}, User={5}, Comments={6}, CreatedOn={7}, UpdatedOn={8}, CustomFields={9}]", - Issue, Project, SpentOn, Hours, Activity, User, Comments, CreatedOn, UpdatedOn, CustomFields, base.ToString()); + return + $"[TimeEntry: {base.ToString()}, Issue={Issue}, Project={Project}, SpentOn={SpentOn}, Hours={Hours}, Activity={Activity}, User={User}, Comments={Comments}, CreatedOn={CreatedOn}, UpdatedOn={UpdatedOn}, CustomFields={CustomFields}]"; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/TimeEntryActivity.cs b/src/redmine-net-api/Types/TimeEntryActivity.cs index 36f8d338..2c6a9f12 100755 --- a/src/redmine-net-api/Types/TimeEntryActivity.cs +++ b/src/redmine-net-api/Types/TimeEntryActivity.cs @@ -122,7 +122,7 @@ public override int GetHashCode() /// public override string ToString() { - return string.Format("[TimeEntryActivity: Id={0}, Name={1}, IsDefault={2}]", Id, Name, IsDefault); + return $"[TimeEntryActivity: Id={Id}, Name={Name}, IsDefault={IsDefault}]"; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Tracker.cs b/src/redmine-net-api/Types/Tracker.cs index de431525..74d1c5c2 100755 --- a/src/redmine-net-api/Types/Tracker.cs +++ b/src/redmine-net-api/Types/Tracker.cs @@ -106,7 +106,7 @@ public override int GetHashCode() /// public override string ToString() { - return string.Format("[Tracker: Id={0}, Name={1}]", Id, Name); + return $"[Tracker: Id={Id}, Name={Name}]"; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/TrackerCustomField.cs b/src/redmine-net-api/Types/TrackerCustomField.cs index a5b0fe6e..2ce4ba12 100755 --- a/src/redmine-net-api/Types/TrackerCustomField.cs +++ b/src/redmine-net-api/Types/TrackerCustomField.cs @@ -43,7 +43,7 @@ public override void ReadXml(XmlReader reader) /// public override string ToString () { - return string.Format ("[TrackerCustomField: {0}]", base.ToString()); + return $"[TrackerCustomField: {base.ToString()}]"; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Upload.cs b/src/redmine-net-api/Types/Upload.cs index f7476ecc..cef6b530 100755 --- a/src/redmine-net-api/Types/Upload.cs +++ b/src/redmine-net-api/Types/Upload.cs @@ -114,7 +114,8 @@ public override int GetHashCode() /// public override string ToString() { - return string.Format("[Upload: Token={0}, FileName={1}, ContentType={2}, Description={3}]", Token, FileName, ContentType, Description); + return + $"[Upload: Token={Token}, FileName={FileName}, ContentType={ContentType}, Description={Description}]"; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/User.cs b/src/redmine-net-api/Types/User.cs index c9d89852..e5e66cad 100644 --- a/src/redmine-net-api/Types/User.cs +++ b/src/redmine-net-api/Types/User.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -300,8 +300,8 @@ public override int GetHashCode() /// public override string ToString() { - return string.Format("[User: {14}, Login={0}, Password={1}, FirstName={2}, LastName={3}, Email={4}, EmailNotification={5}, AuthenticationModeId={6}, CreatedOn={7}, LastLoginOn={8}, ApiKey={9}, Status={10}, MustChangePassword={11}, CustomFields={12}, Memberships={13}, Groups={14}]", - Login, Password, FirstName, LastName, Email, MailNotification, AuthenticationModeId, CreatedOn, LastLoginOn, ApiKey, Status, MustChangePassword, CustomFields, Memberships, Groups, base.ToString()); + return + $"[User: {Groups}, Login={Login}, Password={Password}, FirstName={FirstName}, LastName={LastName}, Email={Email}, EmailNotification={MailNotification}, AuthenticationModeId={AuthenticationModeId}, CreatedOn={CreatedOn}, LastLoginOn={LastLoginOn}, ApiKey={ApiKey}, Status={Status}, MustChangePassword={MustChangePassword}, CustomFields={CustomFields}, Memberships={Memberships}, Groups={Groups}]"; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/UserGroup.cs b/src/redmine-net-api/Types/UserGroup.cs index c4726b26..f38211be 100755 --- a/src/redmine-net-api/Types/UserGroup.cs +++ b/src/redmine-net-api/Types/UserGroup.cs @@ -30,7 +30,7 @@ public class UserGroup : IdentifiableName /// public override string ToString () { - return string.Format ("[UserGroup: {0}]", base.ToString()); + return $"[UserGroup: {base.ToString()}]"; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Version.cs b/src/redmine-net-api/Types/Version.cs index e239f53c..64d3d66f 100755 --- a/src/redmine-net-api/Types/Version.cs +++ b/src/redmine-net-api/Types/Version.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -188,8 +188,8 @@ public override int GetHashCode() /// public override string ToString() { - return string.Format("[Version: {8}, Project={0}, Description={1}, Status={2}, DueDate={3}, Sharing={4}, CreatedOn={5}, UpdatedOn={6}, CustomFields={7}]", - Project, Description, Status, DueDate, Sharing, CreatedOn, UpdatedOn, CustomFields, base.ToString()); + return + $"[Version: {base.ToString()}, Project={Project}, Description={Description}, Status={Status}, DueDate={DueDate}, Sharing={Sharing}, CreatedOn={CreatedOn}, UpdatedOn={UpdatedOn}, CustomFields={CustomFields}]"; } } diff --git a/src/redmine-net-api/Types/Watcher.cs b/src/redmine-net-api/Types/Watcher.cs index 5eb461bf..adfcdd63 100755 --- a/src/redmine-net-api/Types/Watcher.cs +++ b/src/redmine-net-api/Types/Watcher.cs @@ -46,7 +46,7 @@ public string Value /// public override string ToString() { - return string.Format("[Watcher: {0}]", base.ToString()); + return $"[Watcher: {base.ToString()}]"; } /// diff --git a/src/redmine-net-api/Types/WikiPage.cs b/src/redmine-net-api/Types/WikiPage.cs index 3ca2a6da..9adfbff3 100755 --- a/src/redmine-net-api/Types/WikiPage.cs +++ b/src/redmine-net-api/Types/WikiPage.cs @@ -205,8 +205,8 @@ public override int GetHashCode() /// public override string ToString() { - return string.Format("[WikiPage: {8}, Title={0}, Text={1}, Comments={2}, Version={3}, Author={4}, CreatedOn={5}, UpdatedOn={6}, Attachments={7}]", - Title, Text, Comments, Version, Author, CreatedOn, UpdatedOn, Attachments, base.ToString()); + return + $"[WikiPage: {base.ToString()}, Title={Title}, Text={Text}, Comments={Comments}, Version={Version}, Author={Author}, CreatedOn={CreatedOn}, UpdatedOn={UpdatedOn}, Attachments={Attachments}]"; } #endregion From 7c697af0fad1589c5c967d966d13198110d82b7c Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Thu, 21 Nov 2019 14:33:26 +0200 Subject: [PATCH 020/549] Remove json converter extra List allocation --- src/redmine-net-api/JSonConverters/AttachmentConverter.cs | 5 ++++- src/redmine-net-api/JSonConverters/AttachmentsConverter.cs | 7 +++++-- src/redmine-net-api/JSonConverters/ChangeSetConverter.cs | 2 +- src/redmine-net-api/JSonConverters/CustomFieldConverter.cs | 2 +- .../JSonConverters/CustomFieldPossibleValueConverter.cs | 2 +- .../JSonConverters/CustomFieldRoleConverter.cs | 2 +- src/redmine-net-api/JSonConverters/DetailConverter.cs | 2 +- src/redmine-net-api/JSonConverters/ErrorConverter.cs | 2 +- src/redmine-net-api/JSonConverters/FileConverter.cs | 4 ++-- src/redmine-net-api/JSonConverters/GroupConverter.cs | 2 +- src/redmine-net-api/JSonConverters/GroupUserConverter.cs | 2 +- .../JSonConverters/IdentifiableNameConverter.cs | 2 +- .../JSonConverters/IssueCategoryConverter.cs | 2 +- src/redmine-net-api/JSonConverters/IssueChildConverter.cs | 2 +- src/redmine-net-api/JSonConverters/IssueConverter.cs | 2 +- .../JSonConverters/IssueCustomFieldConverter.cs | 2 +- .../JSonConverters/IssuePriorityConverter.cs | 2 +- .../JSonConverters/IssueRelationConverter.cs | 2 +- src/redmine-net-api/JSonConverters/IssueStatusConverter.cs | 2 +- src/redmine-net-api/JSonConverters/JournalConverter.cs | 2 +- src/redmine-net-api/JSonConverters/MembershipConverter.cs | 2 +- .../JSonConverters/MembershipRoleConverter.cs | 2 +- src/redmine-net-api/JSonConverters/NewsConverter.cs | 2 +- src/redmine-net-api/JSonConverters/PermissionConverter.cs | 2 +- src/redmine-net-api/JSonConverters/ProjectConverter.cs | 4 ++-- .../JSonConverters/ProjectEnabledModuleConverter.cs | 2 +- .../JSonConverters/ProjectIssueCategoryConverter.cs | 2 +- .../JSonConverters/ProjectMembershipConverter.cs | 2 +- .../JSonConverters/ProjectTrackerConverter.cs | 2 +- src/redmine-net-api/JSonConverters/QueryConverter.cs | 2 +- src/redmine-net-api/JSonConverters/RoleConverter.cs | 2 +- .../JSonConverters/TimeEntryActivityConverter.cs | 2 +- src/redmine-net-api/JSonConverters/TimeEntryConverter.cs | 2 +- src/redmine-net-api/JSonConverters/TrackerConverter.cs | 2 +- .../JSonConverters/TrackerCustomFieldConverter.cs | 2 +- src/redmine-net-api/JSonConverters/UploadConverter.cs | 2 +- src/redmine-net-api/JSonConverters/UserConverter.cs | 2 +- src/redmine-net-api/JSonConverters/UserGroupConverter.cs | 2 +- src/redmine-net-api/JSonConverters/VersionConverter.cs | 2 +- src/redmine-net-api/JSonConverters/WatcherConverter.cs | 2 +- src/redmine-net-api/JSonConverters/WikiPageConverter.cs | 2 +- 41 files changed, 50 insertions(+), 44 deletions(-) diff --git a/src/redmine-net-api/JSonConverters/AttachmentConverter.cs b/src/redmine-net-api/JSonConverters/AttachmentConverter.cs index 7122a92a..e8b608a8 100755 --- a/src/redmine-net-api/JSonConverters/AttachmentConverter.cs +++ b/src/redmine-net-api/JSonConverters/AttachmentConverter.cs @@ -91,7 +91,10 @@ public override IDictionary Serialize(object obj, JavaScriptSeri /// /// When overridden in a derived class, gets a collection of the supported types. /// - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(Attachment) }); } } + public override IEnumerable SupportedTypes + { + get { return new[] {typeof(Attachment)}; } + } #endregion } diff --git a/src/redmine-net-api/JSonConverters/AttachmentsConverter.cs b/src/redmine-net-api/JSonConverters/AttachmentsConverter.cs index 9b1b209d..149f21c7 100755 --- a/src/redmine-net-api/JSonConverters/AttachmentsConverter.cs +++ b/src/redmine-net-api/JSonConverters/AttachmentsConverter.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -74,7 +74,10 @@ public override IDictionary Serialize(object obj, JavaScriptSeri /// /// When overridden in a derived class, gets a collection of the supported types. /// - public override IEnumerable SupportedTypes { get { return new List(new[] { typeof(Attachments) }); } } + public override IEnumerable SupportedTypes + { + get { return new[] {typeof(Attachments)}; } + } #endregion } diff --git a/src/redmine-net-api/JSonConverters/ChangeSetConverter.cs b/src/redmine-net-api/JSonConverters/ChangeSetConverter.cs index f73ee73a..bcb1b7dd 100755 --- a/src/redmine-net-api/JSonConverters/ChangeSetConverter.cs +++ b/src/redmine-net-api/JSonConverters/ChangeSetConverter.cs @@ -73,7 +73,7 @@ public override IDictionary Serialize(object obj, JavaScriptSeri /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(ChangeSet)}); } + get { return new[] {typeof(ChangeSet)}; } } #endregion diff --git a/src/redmine-net-api/JSonConverters/CustomFieldConverter.cs b/src/redmine-net-api/JSonConverters/CustomFieldConverter.cs index db516280..98bcbf6f 100755 --- a/src/redmine-net-api/JSonConverters/CustomFieldConverter.cs +++ b/src/redmine-net-api/JSonConverters/CustomFieldConverter.cs @@ -84,7 +84,7 @@ public override IDictionary Serialize(object obj, JavaScriptSeri /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(CustomField)}); } + get { return new[] {typeof(CustomField)}; } } #endregion diff --git a/src/redmine-net-api/JSonConverters/CustomFieldPossibleValueConverter.cs b/src/redmine-net-api/JSonConverters/CustomFieldPossibleValueConverter.cs index cc36315a..434ae939 100644 --- a/src/redmine-net-api/JSonConverters/CustomFieldPossibleValueConverter.cs +++ b/src/redmine-net-api/JSonConverters/CustomFieldPossibleValueConverter.cs @@ -68,7 +68,7 @@ public override IDictionary Serialize(object obj, JavaScriptSeri /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(CustomFieldPossibleValue)}); } + get { return new[] {typeof(CustomFieldPossibleValue)}; } } #endregion diff --git a/src/redmine-net-api/JSonConverters/CustomFieldRoleConverter.cs b/src/redmine-net-api/JSonConverters/CustomFieldRoleConverter.cs index 6d93f6fe..dc18c821 100755 --- a/src/redmine-net-api/JSonConverters/CustomFieldRoleConverter.cs +++ b/src/redmine-net-api/JSonConverters/CustomFieldRoleConverter.cs @@ -68,7 +68,7 @@ public override IDictionary Serialize(object obj, JavaScriptSeri /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(CustomFieldRole)}); } + get { return new[] {typeof(CustomFieldRole)}; } } #endregion diff --git a/src/redmine-net-api/JSonConverters/DetailConverter.cs b/src/redmine-net-api/JSonConverters/DetailConverter.cs index 4984a2fd..721d8002 100755 --- a/src/redmine-net-api/JSonConverters/DetailConverter.cs +++ b/src/redmine-net-api/JSonConverters/DetailConverter.cs @@ -74,7 +74,7 @@ public override IDictionary Serialize(object obj, JavaScriptSeri /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(Detail)}); } + get { return new[] {typeof(Detail)}; } } #endregion diff --git a/src/redmine-net-api/JSonConverters/ErrorConverter.cs b/src/redmine-net-api/JSonConverters/ErrorConverter.cs index e19fdc00..178be9af 100755 --- a/src/redmine-net-api/JSonConverters/ErrorConverter.cs +++ b/src/redmine-net-api/JSonConverters/ErrorConverter.cs @@ -68,7 +68,7 @@ public override IDictionary Serialize(object obj, JavaScriptSeri /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(Error)}); } + get { return new[] {typeof(Error)}; } } #endregion diff --git a/src/redmine-net-api/JSonConverters/FileConverter.cs b/src/redmine-net-api/JSonConverters/FileConverter.cs index 8165083e..cf1ea60f 100755 --- a/src/redmine-net-api/JSonConverters/FileConverter.cs +++ b/src/redmine-net-api/JSonConverters/FileConverter.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -103,7 +103,7 @@ public override IDictionary Serialize(object obj, JavaScriptSeri /// public override IEnumerable SupportedTypes { - get { return new List(new[] { typeof(File) }); } + get { return new[] { typeof(File) }; } } #endregion diff --git a/src/redmine-net-api/JSonConverters/GroupConverter.cs b/src/redmine-net-api/JSonConverters/GroupConverter.cs index 02083800..7c647b57 100755 --- a/src/redmine-net-api/JSonConverters/GroupConverter.cs +++ b/src/redmine-net-api/JSonConverters/GroupConverter.cs @@ -89,7 +89,7 @@ public override IDictionary Serialize(object obj, JavaScriptSeri /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(Group)}); } + get { return new[] {typeof(Group)}; } } #endregion diff --git a/src/redmine-net-api/JSonConverters/GroupUserConverter.cs b/src/redmine-net-api/JSonConverters/GroupUserConverter.cs index 9b65c69a..2ac9d56f 100755 --- a/src/redmine-net-api/JSonConverters/GroupUserConverter.cs +++ b/src/redmine-net-api/JSonConverters/GroupUserConverter.cs @@ -69,7 +69,7 @@ public override IDictionary Serialize(object obj, JavaScriptSeri /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(GroupUser)}); } + get { return new[] {typeof(GroupUser)}; } } #endregion diff --git a/src/redmine-net-api/JSonConverters/IdentifiableNameConverter.cs b/src/redmine-net-api/JSonConverters/IdentifiableNameConverter.cs index 3b0e94f3..8a907d97 100755 --- a/src/redmine-net-api/JSonConverters/IdentifiableNameConverter.cs +++ b/src/redmine-net-api/JSonConverters/IdentifiableNameConverter.cs @@ -83,7 +83,7 @@ public override IDictionary Serialize(object obj, JavaScriptSeri /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(IdentifiableName)}); } + get { return new[] {typeof(IdentifiableName)}; } } #endregion diff --git a/src/redmine-net-api/JSonConverters/IssueCategoryConverter.cs b/src/redmine-net-api/JSonConverters/IssueCategoryConverter.cs index ae698a96..528c0b8d 100755 --- a/src/redmine-net-api/JSonConverters/IssueCategoryConverter.cs +++ b/src/redmine-net-api/JSonConverters/IssueCategoryConverter.cs @@ -89,7 +89,7 @@ public override IDictionary Serialize(object obj, JavaScriptSeri /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(IssueCategory)}); } + get { return new[] {typeof(IssueCategory)}; } } #endregion diff --git a/src/redmine-net-api/JSonConverters/IssueChildConverter.cs b/src/redmine-net-api/JSonConverters/IssueChildConverter.cs index 082eeb60..4a631515 100755 --- a/src/redmine-net-api/JSonConverters/IssueChildConverter.cs +++ b/src/redmine-net-api/JSonConverters/IssueChildConverter.cs @@ -29,7 +29,7 @@ internal class IssueChildConverter : JavaScriptConverter /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(IssueChild)}); } + get { return new[] {typeof(IssueChild)}; } } /// diff --git a/src/redmine-net-api/JSonConverters/IssueConverter.cs b/src/redmine-net-api/JSonConverters/IssueConverter.cs index d3f14331..5c5ba3a8 100644 --- a/src/redmine-net-api/JSonConverters/IssueConverter.cs +++ b/src/redmine-net-api/JSonConverters/IssueConverter.cs @@ -147,7 +147,7 @@ public override IDictionary Serialize(object obj, JavaScriptSeri /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(Issue)}); } + get { return new[] {typeof(Issue)}; } } #endregion diff --git a/src/redmine-net-api/JSonConverters/IssueCustomFieldConverter.cs b/src/redmine-net-api/JSonConverters/IssueCustomFieldConverter.cs index 19170bd3..c12ece96 100755 --- a/src/redmine-net-api/JSonConverters/IssueCustomFieldConverter.cs +++ b/src/redmine-net-api/JSonConverters/IssueCustomFieldConverter.cs @@ -113,7 +113,7 @@ public override IDictionary Serialize(object obj, JavaScriptSeri /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(IssueCustomField)}); } + get { return new[] {typeof(IssueCustomField)}; } } #endregion diff --git a/src/redmine-net-api/JSonConverters/IssuePriorityConverter.cs b/src/redmine-net-api/JSonConverters/IssuePriorityConverter.cs index 6205a98e..836e0374 100755 --- a/src/redmine-net-api/JSonConverters/IssuePriorityConverter.cs +++ b/src/redmine-net-api/JSonConverters/IssuePriorityConverter.cs @@ -29,7 +29,7 @@ internal class IssuePriorityConverter : JavaScriptConverter /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(IssuePriority)}); } + get { return new[] {typeof(IssuePriority)}; } } /// diff --git a/src/redmine-net-api/JSonConverters/IssueRelationConverter.cs b/src/redmine-net-api/JSonConverters/IssueRelationConverter.cs index 0c42b0c5..3fbefa61 100755 --- a/src/redmine-net-api/JSonConverters/IssueRelationConverter.cs +++ b/src/redmine-net-api/JSonConverters/IssueRelationConverter.cs @@ -93,7 +93,7 @@ public override IDictionary Serialize(object obj, JavaScriptSeri /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(IssueRelation)}); } + get { return new[] {typeof(IssueRelation)}; } } #endregion diff --git a/src/redmine-net-api/JSonConverters/IssueStatusConverter.cs b/src/redmine-net-api/JSonConverters/IssueStatusConverter.cs index 0965ce90..702afda1 100755 --- a/src/redmine-net-api/JSonConverters/IssueStatusConverter.cs +++ b/src/redmine-net-api/JSonConverters/IssueStatusConverter.cs @@ -73,7 +73,7 @@ public override IDictionary Serialize(object obj, JavaScriptSeri /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(IssueStatus)}); } + get { return new[] {typeof(IssueStatus)}; } } #endregion diff --git a/src/redmine-net-api/JSonConverters/JournalConverter.cs b/src/redmine-net-api/JSonConverters/JournalConverter.cs index 7336f90d..23226cb0 100644 --- a/src/redmine-net-api/JSonConverters/JournalConverter.cs +++ b/src/redmine-net-api/JSonConverters/JournalConverter.cs @@ -76,7 +76,7 @@ public override IDictionary Serialize(object obj, JavaScriptSeri /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(Journal)}); } + get { return new[] {typeof(Journal)}; } } #endregion diff --git a/src/redmine-net-api/JSonConverters/MembershipConverter.cs b/src/redmine-net-api/JSonConverters/MembershipConverter.cs index 784de771..27de7c51 100755 --- a/src/redmine-net-api/JSonConverters/MembershipConverter.cs +++ b/src/redmine-net-api/JSonConverters/MembershipConverter.cs @@ -73,7 +73,7 @@ public override IDictionary Serialize(object obj, JavaScriptSeri /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(Membership)}); } + get { return new[] {typeof(Membership)}; } } #endregion diff --git a/src/redmine-net-api/JSonConverters/MembershipRoleConverter.cs b/src/redmine-net-api/JSonConverters/MembershipRoleConverter.cs index cd9b57c0..6042ab8b 100755 --- a/src/redmine-net-api/JSonConverters/MembershipRoleConverter.cs +++ b/src/redmine-net-api/JSonConverters/MembershipRoleConverter.cs @@ -73,7 +73,7 @@ public override IDictionary Serialize(object obj, JavaScriptSeri /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(MembershipRole)}); } + get { return new[] {typeof(MembershipRole)}; } } #endregion diff --git a/src/redmine-net-api/JSonConverters/NewsConverter.cs b/src/redmine-net-api/JSonConverters/NewsConverter.cs index fb1ce0b2..d77bce89 100755 --- a/src/redmine-net-api/JSonConverters/NewsConverter.cs +++ b/src/redmine-net-api/JSonConverters/NewsConverter.cs @@ -76,7 +76,7 @@ public override IDictionary Serialize(object obj, JavaScriptSeri /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(News)}); } + get { return new[] {typeof(News)}; } } #endregion diff --git a/src/redmine-net-api/JSonConverters/PermissionConverter.cs b/src/redmine-net-api/JSonConverters/PermissionConverter.cs index dd4905d2..606ab8dc 100755 --- a/src/redmine-net-api/JSonConverters/PermissionConverter.cs +++ b/src/redmine-net-api/JSonConverters/PermissionConverter.cs @@ -29,7 +29,7 @@ internal class PermissionConverter : JavaScriptConverter /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(Permission)}); } + get { return new[] {typeof(Permission)}; } } /// diff --git a/src/redmine-net-api/JSonConverters/ProjectConverter.cs b/src/redmine-net-api/JSonConverters/ProjectConverter.cs index 9f897864..9966622e 100755 --- a/src/redmine-net-api/JSonConverters/ProjectConverter.cs +++ b/src/redmine-net-api/JSonConverters/ProjectConverter.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -110,7 +110,7 @@ public override IDictionary Serialize(object obj, JavaScriptSeri /// public override IEnumerable SupportedTypes { - get { return new List(new[] { typeof(Project) }); } + get { return new[] { typeof(Project) }; } } #endregion diff --git a/src/redmine-net-api/JSonConverters/ProjectEnabledModuleConverter.cs b/src/redmine-net-api/JSonConverters/ProjectEnabledModuleConverter.cs index 8ec50390..977667e7 100755 --- a/src/redmine-net-api/JSonConverters/ProjectEnabledModuleConverter.cs +++ b/src/redmine-net-api/JSonConverters/ProjectEnabledModuleConverter.cs @@ -69,7 +69,7 @@ public override IDictionary Serialize(object obj, JavaScriptSeri /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(ProjectEnabledModule)}); } + get { return new[] {typeof(ProjectEnabledModule)}; } } #endregion diff --git a/src/redmine-net-api/JSonConverters/ProjectIssueCategoryConverter.cs b/src/redmine-net-api/JSonConverters/ProjectIssueCategoryConverter.cs index 44d366d7..14d91a37 100755 --- a/src/redmine-net-api/JSonConverters/ProjectIssueCategoryConverter.cs +++ b/src/redmine-net-api/JSonConverters/ProjectIssueCategoryConverter.cs @@ -81,7 +81,7 @@ public override IDictionary Serialize(object obj, JavaScriptSeri /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(ProjectIssueCategory)}); } + get { return new[] {typeof(ProjectIssueCategory)}; } } #endregion diff --git a/src/redmine-net-api/JSonConverters/ProjectMembershipConverter.cs b/src/redmine-net-api/JSonConverters/ProjectMembershipConverter.cs index 9e22e06b..c5e16fac 100755 --- a/src/redmine-net-api/JSonConverters/ProjectMembershipConverter.cs +++ b/src/redmine-net-api/JSonConverters/ProjectMembershipConverter.cs @@ -87,7 +87,7 @@ public override IDictionary Serialize(object obj, JavaScriptSeri /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(ProjectMembership)}); } + get { return new[] {typeof(ProjectMembership)}; } } #endregion diff --git a/src/redmine-net-api/JSonConverters/ProjectTrackerConverter.cs b/src/redmine-net-api/JSonConverters/ProjectTrackerConverter.cs index cc41bdf4..dc529b06 100755 --- a/src/redmine-net-api/JSonConverters/ProjectTrackerConverter.cs +++ b/src/redmine-net-api/JSonConverters/ProjectTrackerConverter.cs @@ -69,7 +69,7 @@ public override IDictionary Serialize(object obj, JavaScriptSeri /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(ProjectTracker)}); } + get { return new[] {typeof(ProjectTracker)}; } } #endregion diff --git a/src/redmine-net-api/JSonConverters/QueryConverter.cs b/src/redmine-net-api/JSonConverters/QueryConverter.cs index 75bb5410..279e3509 100755 --- a/src/redmine-net-api/JSonConverters/QueryConverter.cs +++ b/src/redmine-net-api/JSonConverters/QueryConverter.cs @@ -74,7 +74,7 @@ public override IDictionary Serialize(object obj, JavaScriptSeri /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(Query)}); } + get { return new[] {typeof(Query)}; } } #endregion diff --git a/src/redmine-net-api/JSonConverters/RoleConverter.cs b/src/redmine-net-api/JSonConverters/RoleConverter.cs index 754a21f3..07da8eab 100755 --- a/src/redmine-net-api/JSonConverters/RoleConverter.cs +++ b/src/redmine-net-api/JSonConverters/RoleConverter.cs @@ -83,7 +83,7 @@ public override IDictionary Serialize(object obj, JavaScriptSeri /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(Role)}); } + get { return new[] {typeof(Role)}; } } #endregion diff --git a/src/redmine-net-api/JSonConverters/TimeEntryActivityConverter.cs b/src/redmine-net-api/JSonConverters/TimeEntryActivityConverter.cs index 8ee50dfb..126305bc 100755 --- a/src/redmine-net-api/JSonConverters/TimeEntryActivityConverter.cs +++ b/src/redmine-net-api/JSonConverters/TimeEntryActivityConverter.cs @@ -29,7 +29,7 @@ internal class TimeEntryActivityConverter : JavaScriptConverter /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(TimeEntryActivity)}); } + get { return new[] {typeof(TimeEntryActivity)}; } } /// diff --git a/src/redmine-net-api/JSonConverters/TimeEntryConverter.cs b/src/redmine-net-api/JSonConverters/TimeEntryConverter.cs index 1da798c6..c2113f7a 100755 --- a/src/redmine-net-api/JSonConverters/TimeEntryConverter.cs +++ b/src/redmine-net-api/JSonConverters/TimeEntryConverter.cs @@ -112,7 +112,7 @@ public override IDictionary Serialize(object obj, JavaScriptSeri /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(TimeEntry)}); } + get { return new[] {typeof(TimeEntry)}; } } #endregion diff --git a/src/redmine-net-api/JSonConverters/TrackerConverter.cs b/src/redmine-net-api/JSonConverters/TrackerConverter.cs index a402c1a8..58a25882 100755 --- a/src/redmine-net-api/JSonConverters/TrackerConverter.cs +++ b/src/redmine-net-api/JSonConverters/TrackerConverter.cs @@ -72,7 +72,7 @@ public override IDictionary Serialize(object obj, JavaScriptSeri /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(Tracker)}); } + get { return new[] {typeof(Tracker)}; } } #endregion diff --git a/src/redmine-net-api/JSonConverters/TrackerCustomFieldConverter.cs b/src/redmine-net-api/JSonConverters/TrackerCustomFieldConverter.cs index 51b699b0..84bceb12 100755 --- a/src/redmine-net-api/JSonConverters/TrackerCustomFieldConverter.cs +++ b/src/redmine-net-api/JSonConverters/TrackerCustomFieldConverter.cs @@ -59,7 +59,7 @@ public override object Deserialize(IDictionary dictionary, Type /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(TrackerCustomField)}); } + get { return new[] {typeof(TrackerCustomField)}; } } #endregion diff --git a/src/redmine-net-api/JSonConverters/UploadConverter.cs b/src/redmine-net-api/JSonConverters/UploadConverter.cs index 1400509c..954c5db0 100755 --- a/src/redmine-net-api/JSonConverters/UploadConverter.cs +++ b/src/redmine-net-api/JSonConverters/UploadConverter.cs @@ -84,7 +84,7 @@ public override IDictionary Serialize(object obj, JavaScriptSeri /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(Upload)}); } + get { return new[] {typeof(Upload)}; } } #endregion diff --git a/src/redmine-net-api/JSonConverters/UserConverter.cs b/src/redmine-net-api/JSonConverters/UserConverter.cs index 6a688835..b886085a 100644 --- a/src/redmine-net-api/JSonConverters/UserConverter.cs +++ b/src/redmine-net-api/JSonConverters/UserConverter.cs @@ -118,7 +118,7 @@ public override IDictionary Serialize(object obj, JavaScriptSeri /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(User)}); } + get { return new[] {typeof(User)}; } } #endregion diff --git a/src/redmine-net-api/JSonConverters/UserGroupConverter.cs b/src/redmine-net-api/JSonConverters/UserGroupConverter.cs index e295464a..d5664f3a 100755 --- a/src/redmine-net-api/JSonConverters/UserGroupConverter.cs +++ b/src/redmine-net-api/JSonConverters/UserGroupConverter.cs @@ -31,7 +31,7 @@ internal class UserGroupConverter : IdentifiableNameConverter /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(UserGroup)}); } + get { return new[] {typeof(UserGroup)}; } } #endregion diff --git a/src/redmine-net-api/JSonConverters/VersionConverter.cs b/src/redmine-net-api/JSonConverters/VersionConverter.cs index 8a081573..0ff9e247 100755 --- a/src/redmine-net-api/JSonConverters/VersionConverter.cs +++ b/src/redmine-net-api/JSonConverters/VersionConverter.cs @@ -98,7 +98,7 @@ public override IDictionary Serialize(object obj, JavaScriptSeri /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(Version)}); } + get { return new[] {typeof(Version)}; } } #endregion diff --git a/src/redmine-net-api/JSonConverters/WatcherConverter.cs b/src/redmine-net-api/JSonConverters/WatcherConverter.cs index 6752be08..684beb69 100755 --- a/src/redmine-net-api/JSonConverters/WatcherConverter.cs +++ b/src/redmine-net-api/JSonConverters/WatcherConverter.cs @@ -29,7 +29,7 @@ internal class WatcherConverter : JavaScriptConverter /// public override IEnumerable SupportedTypes { - get { return new List(new[] {typeof(Watcher)}); } + get { return new[] {typeof(Watcher)}; } } /// diff --git a/src/redmine-net-api/JSonConverters/WikiPageConverter.cs b/src/redmine-net-api/JSonConverters/WikiPageConverter.cs index d5111b6d..968bb544 100755 --- a/src/redmine-net-api/JSonConverters/WikiPageConverter.cs +++ b/src/redmine-net-api/JSonConverters/WikiPageConverter.cs @@ -29,7 +29,7 @@ internal class WikiPageConverter : JavaScriptConverter /// public override IEnumerable SupportedTypes { - get { return new List(new[] { typeof(WikiPage) }); } + get { return new[] { typeof(WikiPage) }; } } /// From 9446090cfd4edf9619942341de80f0b4296067b8 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Thu, 21 Nov 2019 14:40:59 +0200 Subject: [PATCH 021/549] Resolve CS1574 --- src/redmine-net-api/JSonConverters/AttachmentConverter.cs | 2 +- src/redmine-net-api/JSonConverters/AttachmentsConverter.cs | 4 ++-- src/redmine-net-api/JSonConverters/ChangeSetConverter.cs | 2 +- src/redmine-net-api/JSonConverters/DetailConverter.cs | 2 +- src/redmine-net-api/JSonConverters/ErrorConverter.cs | 2 +- src/redmine-net-api/JSonConverters/FileConverter.cs | 4 ++-- src/redmine-net-api/JSonConverters/GroupConverter.cs | 2 +- .../JSonConverters/IdentifiableNameConverter.cs | 2 +- src/redmine-net-api/JSonConverters/IssueCategoryConverter.cs | 2 +- src/redmine-net-api/JSonConverters/IssueChildConverter.cs | 2 +- src/redmine-net-api/JSonConverters/IssueConverter.cs | 2 +- .../JSonConverters/IssueCustomFieldConverter.cs | 2 +- src/redmine-net-api/JSonConverters/IssuePriorityConverter.cs | 2 +- src/redmine-net-api/JSonConverters/IssueRelationConverter.cs | 2 +- src/redmine-net-api/JSonConverters/IssueStatusConverter.cs | 2 +- src/redmine-net-api/JSonConverters/JournalConverter.cs | 4 ++-- src/redmine-net-api/JSonConverters/MembershipConverter.cs | 2 +- src/redmine-net-api/JSonConverters/MembershipRoleConverter.cs | 2 +- src/redmine-net-api/JSonConverters/NewsConverter.cs | 2 +- src/redmine-net-api/JSonConverters/PermissionConverter.cs | 2 +- src/redmine-net-api/JSonConverters/ProjectConverter.cs | 2 +- .../JSonConverters/ProjectEnabledModuleConverter.cs | 2 +- .../JSonConverters/ProjectIssueCategoryConverter.cs | 4 ++-- .../JSonConverters/ProjectMembershipConverter.cs | 4 ++-- src/redmine-net-api/JSonConverters/ProjectTrackerConverter.cs | 4 ++-- src/redmine-net-api/JSonConverters/QueryConverter.cs | 4 ++-- src/redmine-net-api/JSonConverters/RoleConverter.cs | 4 ++-- .../JSonConverters/TimeEntryActivityConverter.cs | 4 ++-- src/redmine-net-api/JSonConverters/TimeEntryConverter.cs | 4 ++-- src/redmine-net-api/JSonConverters/TrackerConverter.cs | 4 ++-- .../JSonConverters/TrackerCustomFieldConverter.cs | 4 ++-- src/redmine-net-api/JSonConverters/UploadConverter.cs | 4 ++-- src/redmine-net-api/JSonConverters/UserConverter.cs | 4 ++-- src/redmine-net-api/JSonConverters/UserGroupConverter.cs | 4 ++-- src/redmine-net-api/JSonConverters/VersionConverter.cs | 4 ++-- src/redmine-net-api/JSonConverters/WatcherConverter.cs | 4 ++-- src/redmine-net-api/JSonConverters/WikiPageConverter.cs | 4 ++-- 37 files changed, 55 insertions(+), 55 deletions(-) diff --git a/src/redmine-net-api/JSonConverters/AttachmentConverter.cs b/src/redmine-net-api/JSonConverters/AttachmentConverter.cs index e8b608a8..aab99206 100755 --- a/src/redmine-net-api/JSonConverters/AttachmentConverter.cs +++ b/src/redmine-net-api/JSonConverters/AttachmentConverter.cs @@ -36,7 +36,7 @@ internal class AttachmentConverter : JavaScriptConverter /// /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// - /// An instance of property data stored as name/value pairs. + /// An instance of property data stored as name/value pairs. /// The type of the resulting object. /// The instance. /// diff --git a/src/redmine-net-api/JSonConverters/AttachmentsConverter.cs b/src/redmine-net-api/JSonConverters/AttachmentsConverter.cs index 149f21c7..bc95d87e 100755 --- a/src/redmine-net-api/JSonConverters/AttachmentsConverter.cs +++ b/src/redmine-net-api/JSonConverters/AttachmentsConverter.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -32,7 +32,7 @@ internal class AttachmentsConverter : JavaScriptConverter /// /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// - /// An instance of property data stored as name/value pairs. + /// An instance of property data stored as name/value pairs. /// The type of the resulting object. /// The instance. /// diff --git a/src/redmine-net-api/JSonConverters/ChangeSetConverter.cs b/src/redmine-net-api/JSonConverters/ChangeSetConverter.cs index bcb1b7dd..56b6de4f 100755 --- a/src/redmine-net-api/JSonConverters/ChangeSetConverter.cs +++ b/src/redmine-net-api/JSonConverters/ChangeSetConverter.cs @@ -29,7 +29,7 @@ internal class ChangeSetConverter : JavaScriptConverter /// /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// - /// An instance of property data stored as name/value pairs. + /// An instance of property data stored as name/value pairs. /// The type of the resulting object. /// The instance. /// diff --git a/src/redmine-net-api/JSonConverters/DetailConverter.cs b/src/redmine-net-api/JSonConverters/DetailConverter.cs index 721d8002..85aec9dd 100755 --- a/src/redmine-net-api/JSonConverters/DetailConverter.cs +++ b/src/redmine-net-api/JSonConverters/DetailConverter.cs @@ -30,7 +30,7 @@ internal class DetailConverter : JavaScriptConverter /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// /// - /// An instance of property data stored + /// An instance of property data stored /// as name/value pairs. /// /// The type of the resulting object. diff --git a/src/redmine-net-api/JSonConverters/ErrorConverter.cs b/src/redmine-net-api/JSonConverters/ErrorConverter.cs index 178be9af..9e388f18 100755 --- a/src/redmine-net-api/JSonConverters/ErrorConverter.cs +++ b/src/redmine-net-api/JSonConverters/ErrorConverter.cs @@ -30,7 +30,7 @@ internal class ErrorConverter : JavaScriptConverter /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// /// - /// An instance of property data stored + /// An instance of property data stored /// as name/value pairs. /// /// The type of the resulting object. diff --git a/src/redmine-net-api/JSonConverters/FileConverter.cs b/src/redmine-net-api/JSonConverters/FileConverter.cs index cf1ea60f..1fe97b53 100755 --- a/src/redmine-net-api/JSonConverters/FileConverter.cs +++ b/src/redmine-net-api/JSonConverters/FileConverter.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -30,7 +30,7 @@ internal class FileConverter : JavaScriptConverter /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// /// - /// An instance of property data stored + /// An instance of property data stored /// as name/value pairs. /// /// The type of the resulting object. diff --git a/src/redmine-net-api/JSonConverters/GroupConverter.cs b/src/redmine-net-api/JSonConverters/GroupConverter.cs index 7c647b57..893888de 100755 --- a/src/redmine-net-api/JSonConverters/GroupConverter.cs +++ b/src/redmine-net-api/JSonConverters/GroupConverter.cs @@ -30,7 +30,7 @@ internal class GroupConverter : JavaScriptConverter /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// /// - /// An instance of property data stored + /// An instance of property data stored /// as name/value pairs. /// /// The type of the resulting object. diff --git a/src/redmine-net-api/JSonConverters/IdentifiableNameConverter.cs b/src/redmine-net-api/JSonConverters/IdentifiableNameConverter.cs index 8a907d97..4ea1bc91 100755 --- a/src/redmine-net-api/JSonConverters/IdentifiableNameConverter.cs +++ b/src/redmine-net-api/JSonConverters/IdentifiableNameConverter.cs @@ -30,7 +30,7 @@ internal class IdentifiableNameConverter : JavaScriptConverter /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// /// - /// An instance of property data stored + /// An instance of property data stored /// as name/value pairs. /// /// The type of the resulting object. diff --git a/src/redmine-net-api/JSonConverters/IssueCategoryConverter.cs b/src/redmine-net-api/JSonConverters/IssueCategoryConverter.cs index 528c0b8d..8a0f34f5 100755 --- a/src/redmine-net-api/JSonConverters/IssueCategoryConverter.cs +++ b/src/redmine-net-api/JSonConverters/IssueCategoryConverter.cs @@ -30,7 +30,7 @@ internal class IssueCategoryConverter : JavaScriptConverter /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// /// - /// An instance of property data stored + /// An instance of property data stored /// as name/value pairs. /// /// The type of the resulting object. diff --git a/src/redmine-net-api/JSonConverters/IssueChildConverter.cs b/src/redmine-net-api/JSonConverters/IssueChildConverter.cs index 4a631515..bfd7d233 100755 --- a/src/redmine-net-api/JSonConverters/IssueChildConverter.cs +++ b/src/redmine-net-api/JSonConverters/IssueChildConverter.cs @@ -35,7 +35,7 @@ public override IEnumerable SupportedTypes /// /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// - /// An instance of property data stored as name/value pairs. + /// An instance of property data stored as name/value pairs. /// The type of the resulting object. /// The instance. /// diff --git a/src/redmine-net-api/JSonConverters/IssueConverter.cs b/src/redmine-net-api/JSonConverters/IssueConverter.cs index 5c5ba3a8..fac26cef 100644 --- a/src/redmine-net-api/JSonConverters/IssueConverter.cs +++ b/src/redmine-net-api/JSonConverters/IssueConverter.cs @@ -32,7 +32,7 @@ internal class IssueConverter : JavaScriptConverter /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// /// - /// An instance of property data stored + /// An instance of property data stored /// as name/value pairs. /// /// The type of the resulting object. diff --git a/src/redmine-net-api/JSonConverters/IssueCustomFieldConverter.cs b/src/redmine-net-api/JSonConverters/IssueCustomFieldConverter.cs index c12ece96..f788e242 100755 --- a/src/redmine-net-api/JSonConverters/IssueCustomFieldConverter.cs +++ b/src/redmine-net-api/JSonConverters/IssueCustomFieldConverter.cs @@ -34,7 +34,7 @@ internal class IssueCustomFieldConverter : JavaScriptConverter /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// /// - /// An instance of property data stored + /// An instance of property data stored /// as name/value pairs. /// /// The type of the resulting object. diff --git a/src/redmine-net-api/JSonConverters/IssuePriorityConverter.cs b/src/redmine-net-api/JSonConverters/IssuePriorityConverter.cs index 836e0374..bb41d295 100755 --- a/src/redmine-net-api/JSonConverters/IssuePriorityConverter.cs +++ b/src/redmine-net-api/JSonConverters/IssuePriorityConverter.cs @@ -36,7 +36,7 @@ public override IEnumerable SupportedTypes /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// /// - /// An instance of property data stored + /// An instance of property data stored /// as name/value pairs. /// /// The type of the resulting object. diff --git a/src/redmine-net-api/JSonConverters/IssueRelationConverter.cs b/src/redmine-net-api/JSonConverters/IssueRelationConverter.cs index 3fbefa61..c5b0ce4a 100755 --- a/src/redmine-net-api/JSonConverters/IssueRelationConverter.cs +++ b/src/redmine-net-api/JSonConverters/IssueRelationConverter.cs @@ -32,7 +32,7 @@ internal class IssueRelationConverter : JavaScriptConverter /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// /// - /// An instance of property data stored + /// An instance of property data stored /// as name/value pairs. /// /// The type of the resulting object. diff --git a/src/redmine-net-api/JSonConverters/IssueStatusConverter.cs b/src/redmine-net-api/JSonConverters/IssueStatusConverter.cs index 702afda1..f3c6815f 100755 --- a/src/redmine-net-api/JSonConverters/IssueStatusConverter.cs +++ b/src/redmine-net-api/JSonConverters/IssueStatusConverter.cs @@ -30,7 +30,7 @@ internal class IssueStatusConverter : JavaScriptConverter /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// /// - /// An instance of property data stored + /// An instance of property data stored /// as name/value pairs. /// /// The type of the resulting object. diff --git a/src/redmine-net-api/JSonConverters/JournalConverter.cs b/src/redmine-net-api/JSonConverters/JournalConverter.cs index 23226cb0..f6a3ba5d 100644 --- a/src/redmine-net-api/JSonConverters/JournalConverter.cs +++ b/src/redmine-net-api/JSonConverters/JournalConverter.cs @@ -30,11 +30,11 @@ internal class JournalConverter : JavaScriptConverter /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// /// - /// An instance of property data stored + /// An instance of property data stored /// as name/value pairs. /// /// The type of the resulting object. - /// The instance. + /// The instance. /// /// The deserialized object. /// diff --git a/src/redmine-net-api/JSonConverters/MembershipConverter.cs b/src/redmine-net-api/JSonConverters/MembershipConverter.cs index 27de7c51..8ec11a74 100755 --- a/src/redmine-net-api/JSonConverters/MembershipConverter.cs +++ b/src/redmine-net-api/JSonConverters/MembershipConverter.cs @@ -30,7 +30,7 @@ internal class MembershipConverter : JavaScriptConverter /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// /// - /// An instance of property data stored + /// An instance of property data stored /// as name/value pairs. /// /// The type of the resulting object. diff --git a/src/redmine-net-api/JSonConverters/MembershipRoleConverter.cs b/src/redmine-net-api/JSonConverters/MembershipRoleConverter.cs index 6042ab8b..c1548591 100755 --- a/src/redmine-net-api/JSonConverters/MembershipRoleConverter.cs +++ b/src/redmine-net-api/JSonConverters/MembershipRoleConverter.cs @@ -30,7 +30,7 @@ internal class MembershipRoleConverter : JavaScriptConverter /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// /// - /// An instance of property data stored + /// An instance of property data stored /// as name/value pairs. /// /// The type of the resulting object. diff --git a/src/redmine-net-api/JSonConverters/NewsConverter.cs b/src/redmine-net-api/JSonConverters/NewsConverter.cs index d77bce89..71420ba4 100755 --- a/src/redmine-net-api/JSonConverters/NewsConverter.cs +++ b/src/redmine-net-api/JSonConverters/NewsConverter.cs @@ -30,7 +30,7 @@ internal class NewsConverter : JavaScriptConverter /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// /// - /// An instance of property data stored + /// An instance of property data stored /// as name/value pairs. /// /// The type of the resulting object. diff --git a/src/redmine-net-api/JSonConverters/PermissionConverter.cs b/src/redmine-net-api/JSonConverters/PermissionConverter.cs index 606ab8dc..7e2895d6 100755 --- a/src/redmine-net-api/JSonConverters/PermissionConverter.cs +++ b/src/redmine-net-api/JSonConverters/PermissionConverter.cs @@ -36,7 +36,7 @@ public override IEnumerable SupportedTypes /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// /// - /// An instance of property data stored + /// An instance of property data stored /// as name/value pairs. /// /// The type of the resulting object. diff --git a/src/redmine-net-api/JSonConverters/ProjectConverter.cs b/src/redmine-net-api/JSonConverters/ProjectConverter.cs index 9966622e..da50fe18 100755 --- a/src/redmine-net-api/JSonConverters/ProjectConverter.cs +++ b/src/redmine-net-api/JSonConverters/ProjectConverter.cs @@ -32,7 +32,7 @@ internal class ProjectConverter : JavaScriptConverter /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// /// - /// An instance of property data stored + /// An instance of property data stored /// as name/value pairs. /// /// The type of the resulting object. diff --git a/src/redmine-net-api/JSonConverters/ProjectEnabledModuleConverter.cs b/src/redmine-net-api/JSonConverters/ProjectEnabledModuleConverter.cs index 977667e7..089d9f4b 100755 --- a/src/redmine-net-api/JSonConverters/ProjectEnabledModuleConverter.cs +++ b/src/redmine-net-api/JSonConverters/ProjectEnabledModuleConverter.cs @@ -30,7 +30,7 @@ internal class ProjectEnabledModuleConverter : JavaScriptConverter /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// /// - /// An instance of property data stored + /// An instance of property data stored /// as name/value pairs. /// /// The type of the resulting object. diff --git a/src/redmine-net-api/JSonConverters/ProjectIssueCategoryConverter.cs b/src/redmine-net-api/JSonConverters/ProjectIssueCategoryConverter.cs index 14d91a37..0319ccbc 100755 --- a/src/redmine-net-api/JSonConverters/ProjectIssueCategoryConverter.cs +++ b/src/redmine-net-api/JSonConverters/ProjectIssueCategoryConverter.cs @@ -30,11 +30,11 @@ internal class ProjectIssueCategoryConverter : JavaScriptConverter /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// /// - /// An instance of property data stored + /// An instance of property data stored /// as name/value pairs. /// /// The type of the resulting object. - /// The instance. + /// The instance. /// /// The deserialized object. /// diff --git a/src/redmine-net-api/JSonConverters/ProjectMembershipConverter.cs b/src/redmine-net-api/JSonConverters/ProjectMembershipConverter.cs index c5e16fac..595f31fb 100755 --- a/src/redmine-net-api/JSonConverters/ProjectMembershipConverter.cs +++ b/src/redmine-net-api/JSonConverters/ProjectMembershipConverter.cs @@ -30,11 +30,11 @@ internal class ProjectMembershipConverter : JavaScriptConverter /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// /// - /// An instance of property data stored + /// An instance of property data stored /// as name/value pairs. /// /// The type of the resulting object. - /// The instance. + /// The instance. /// /// The deserialized object. /// diff --git a/src/redmine-net-api/JSonConverters/ProjectTrackerConverter.cs b/src/redmine-net-api/JSonConverters/ProjectTrackerConverter.cs index dc529b06..5697d888 100755 --- a/src/redmine-net-api/JSonConverters/ProjectTrackerConverter.cs +++ b/src/redmine-net-api/JSonConverters/ProjectTrackerConverter.cs @@ -30,11 +30,11 @@ internal class ProjectTrackerConverter : JavaScriptConverter /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// /// - /// An instance of property data stored + /// An instance of property data stored /// as name/value pairs. /// /// The type of the resulting object. - /// The instance. + /// The instance. /// /// The deserialized object. /// diff --git a/src/redmine-net-api/JSonConverters/QueryConverter.cs b/src/redmine-net-api/JSonConverters/QueryConverter.cs index 279e3509..88f8fc62 100755 --- a/src/redmine-net-api/JSonConverters/QueryConverter.cs +++ b/src/redmine-net-api/JSonConverters/QueryConverter.cs @@ -30,11 +30,11 @@ internal class QueryConverter : JavaScriptConverter /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// /// - /// An instance of property data stored + /// An instance of property data stored /// as name/value pairs. /// /// The type of the resulting object. - /// The instance. + /// The instance. /// /// The deserialized object. /// diff --git a/src/redmine-net-api/JSonConverters/RoleConverter.cs b/src/redmine-net-api/JSonConverters/RoleConverter.cs index 07da8eab..b14af70a 100755 --- a/src/redmine-net-api/JSonConverters/RoleConverter.cs +++ b/src/redmine-net-api/JSonConverters/RoleConverter.cs @@ -31,11 +31,11 @@ internal class RoleConverter : JavaScriptConverter /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// /// - /// An instance of property data stored + /// An instance of property data stored /// as name/value pairs. /// /// The type of the resulting object. - /// The instance. + /// The instance. /// /// The deserialized object. /// diff --git a/src/redmine-net-api/JSonConverters/TimeEntryActivityConverter.cs b/src/redmine-net-api/JSonConverters/TimeEntryActivityConverter.cs index 126305bc..435b29ef 100755 --- a/src/redmine-net-api/JSonConverters/TimeEntryActivityConverter.cs +++ b/src/redmine-net-api/JSonConverters/TimeEntryActivityConverter.cs @@ -36,11 +36,11 @@ public override IEnumerable SupportedTypes /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// /// - /// An instance of property data stored + /// An instance of property data stored /// as name/value pairs. /// /// The type of the resulting object. - /// The instance. + /// The instance. /// /// The deserialized object. /// diff --git a/src/redmine-net-api/JSonConverters/TimeEntryConverter.cs b/src/redmine-net-api/JSonConverters/TimeEntryConverter.cs index c2113f7a..3e001bd0 100755 --- a/src/redmine-net-api/JSonConverters/TimeEntryConverter.cs +++ b/src/redmine-net-api/JSonConverters/TimeEntryConverter.cs @@ -30,11 +30,11 @@ internal class TimeEntryConverter : JavaScriptConverter /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// /// - /// An instance of property data stored + /// An instance of property data stored /// as name/value pairs. /// /// The type of the resulting object. - /// The instance. + /// The instance. /// /// The deserialized object. /// diff --git a/src/redmine-net-api/JSonConverters/TrackerConverter.cs b/src/redmine-net-api/JSonConverters/TrackerConverter.cs index 58a25882..221dbae4 100755 --- a/src/redmine-net-api/JSonConverters/TrackerConverter.cs +++ b/src/redmine-net-api/JSonConverters/TrackerConverter.cs @@ -30,11 +30,11 @@ internal class TrackerConverter : JavaScriptConverter /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// /// - /// An instance of property data stored + /// An instance of property data stored /// as name/value pairs. /// /// The type of the resulting object. - /// The instance. + /// The instance. /// /// The deserialized object. /// diff --git a/src/redmine-net-api/JSonConverters/TrackerCustomFieldConverter.cs b/src/redmine-net-api/JSonConverters/TrackerCustomFieldConverter.cs index 84bceb12..415480b3 100755 --- a/src/redmine-net-api/JSonConverters/TrackerCustomFieldConverter.cs +++ b/src/redmine-net-api/JSonConverters/TrackerCustomFieldConverter.cs @@ -30,11 +30,11 @@ internal class TrackerCustomFieldConverter : IdentifiableNameConverter /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// /// - /// An instance of property data stored + /// An instance of property data stored /// as name/value pairs. /// /// The type of the resulting object. - /// The instance. + /// The instance. /// /// The deserialized object. /// diff --git a/src/redmine-net-api/JSonConverters/UploadConverter.cs b/src/redmine-net-api/JSonConverters/UploadConverter.cs index 954c5db0..1df7d6f1 100755 --- a/src/redmine-net-api/JSonConverters/UploadConverter.cs +++ b/src/redmine-net-api/JSonConverters/UploadConverter.cs @@ -30,11 +30,11 @@ internal class UploadConverter : JavaScriptConverter /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// /// - /// An instance of property data stored + /// An instance of property data stored /// as name/value pairs. /// /// The type of the resulting object. - /// The instance. + /// The instance. /// /// The deserialized object. /// diff --git a/src/redmine-net-api/JSonConverters/UserConverter.cs b/src/redmine-net-api/JSonConverters/UserConverter.cs index b886085a..ee34bed5 100644 --- a/src/redmine-net-api/JSonConverters/UserConverter.cs +++ b/src/redmine-net-api/JSonConverters/UserConverter.cs @@ -31,11 +31,11 @@ internal class UserConverter : JavaScriptConverter /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// /// - /// An instance of property data stored + /// An instance of property data stored /// as name/value pairs. /// /// The type of the resulting object. - /// The instance. + /// The instance. /// /// The deserialized object. /// diff --git a/src/redmine-net-api/JSonConverters/UserGroupConverter.cs b/src/redmine-net-api/JSonConverters/UserGroupConverter.cs index d5664f3a..2137bb10 100755 --- a/src/redmine-net-api/JSonConverters/UserGroupConverter.cs +++ b/src/redmine-net-api/JSonConverters/UserGroupConverter.cs @@ -40,11 +40,11 @@ public override IEnumerable SupportedTypes /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// /// - /// An instance of property data stored + /// An instance of property data stored /// as name/value pairs. /// /// The type of the resulting object. - /// The instance. + /// The instance. /// /// The deserialized object. /// diff --git a/src/redmine-net-api/JSonConverters/VersionConverter.cs b/src/redmine-net-api/JSonConverters/VersionConverter.cs index 0ff9e247..b3f7bf34 100755 --- a/src/redmine-net-api/JSonConverters/VersionConverter.cs +++ b/src/redmine-net-api/JSonConverters/VersionConverter.cs @@ -31,11 +31,11 @@ internal class VersionConverter : JavaScriptConverter /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// /// - /// An instance of property data stored + /// An instance of property data stored /// as name/value pairs. /// /// The type of the resulting object. - /// The instance. + /// The instance. /// /// The deserialized object. /// diff --git a/src/redmine-net-api/JSonConverters/WatcherConverter.cs b/src/redmine-net-api/JSonConverters/WatcherConverter.cs index 684beb69..3c3a1d31 100755 --- a/src/redmine-net-api/JSonConverters/WatcherConverter.cs +++ b/src/redmine-net-api/JSonConverters/WatcherConverter.cs @@ -36,11 +36,11 @@ public override IEnumerable SupportedTypes /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// /// - /// An instance of property data stored + /// An instance of property data stored /// as name/value pairs. /// /// The type of the resulting object. - /// The instance. + /// The instance. /// /// The deserialized object. /// diff --git a/src/redmine-net-api/JSonConverters/WikiPageConverter.cs b/src/redmine-net-api/JSonConverters/WikiPageConverter.cs index 968bb544..c5c6af0a 100755 --- a/src/redmine-net-api/JSonConverters/WikiPageConverter.cs +++ b/src/redmine-net-api/JSonConverters/WikiPageConverter.cs @@ -36,11 +36,11 @@ public override IEnumerable SupportedTypes /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. /// /// - /// An instance of property data stored + /// An instance of property data stored /// as name/value pairs. /// /// The type of the resulting object. - /// The instance. + /// The instance. /// /// The deserialized object. /// From e8c7c4c7df2b94bfd1a67188a956534ea403ee3d Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Thu, 21 Nov 2019 14:44:14 +0200 Subject: [PATCH 022/549] Add ToLowerInv(string|bool)) extensions --- src/redmine-net-api/Extensions/JsonExtensions.cs | 10 ++++++++++ src/redmine-net-api/Extensions/StringExtensions.cs | 10 ++++++++++ src/redmine-net-api/Internals/RedmineSerializerJson.cs | 5 +++-- src/redmine-net-api/JSonConverters/IssueConverter.cs | 4 ++-- src/redmine-net-api/JSonConverters/ProjectConverter.cs | 6 +++--- src/redmine-net-api/JSonConverters/UserConverter.cs | 2 +- 6 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/redmine-net-api/Extensions/JsonExtensions.cs b/src/redmine-net-api/Extensions/JsonExtensions.cs index 91292ae1..34a8d00a 100644 --- a/src/redmine-net-api/Extensions/JsonExtensions.cs +++ b/src/redmine-net-api/Extensions/JsonExtensions.cs @@ -217,6 +217,16 @@ public static IdentifiableName GetValueAsIdentifiableName(this IDictionary + /// + /// + /// + /// + public static string ToLowerInv(this bool value) + { + return !value ? "false" : "true"; + } } } #endif \ No newline at end of file diff --git a/src/redmine-net-api/Extensions/StringExtensions.cs b/src/redmine-net-api/Extensions/StringExtensions.cs index bc9f69c9..5f563fa4 100644 --- a/src/redmine-net-api/Extensions/StringExtensions.cs +++ b/src/redmine-net-api/Extensions/StringExtensions.cs @@ -49,5 +49,15 @@ public static string Truncate(this string text, int maximumLength) return text; } + + /// + /// Lower case based on invariant culture. + /// + /// + /// + public static string ToLowerInv(this string text) + { + return text.IsNullOrWhiteSpace() ? text : text.ToLowerInvariant(); + } } } \ No newline at end of file diff --git a/src/redmine-net-api/Internals/RedmineSerializerJson.cs b/src/redmine-net-api/Internals/RedmineSerializerJson.cs index 2395db87..8dff8bb8 100755 --- a/src/redmine-net-api/Internals/RedmineSerializerJson.cs +++ b/src/redmine-net-api/Internals/RedmineSerializerJson.cs @@ -21,6 +21,7 @@ limitations under the License. using System.Linq; using Redmine.Net.Api.JSonConverters; using Redmine.Net.Api.Types; +using Redmine.Net.Api.Extensions; using Version = Redmine.Net.Api.Types.Version; namespace Redmine.Net.Api.Internals @@ -158,7 +159,7 @@ public static object JsonDeserialize(string jsonString, Type type, string root) if (dictionary == null) return null; object obj; - return !dictionary.TryGetValue(root ?? type.Name.ToLowerInvariant(), out obj) ? null : serializer.ConvertToType(obj, type); + return !dictionary.TryGetValue(root ?? type.Name.ToLowerInv(), out obj) ? null : serializer.ConvertToType(obj, type); } /// @@ -211,7 +212,7 @@ private static object JsonDeserializeToList(string jsonString, string root, Type if (dictionary.TryGetValue(RedmineKeys.OFFSET, out off)) offset = (int)off; - if (!dictionary.TryGetValue(root.ToLowerInvariant(), out obj)) return null; + if (!dictionary.TryGetValue(root.ToLowerInv(), out obj)) return null; var arrayList = new ArrayList(); if (type == typeof(Error)) diff --git a/src/redmine-net-api/JSonConverters/IssueConverter.cs b/src/redmine-net-api/JSonConverters/IssueConverter.cs index fac26cef..a67167dc 100644 --- a/src/redmine-net-api/JSonConverters/IssueConverter.cs +++ b/src/redmine-net-api/JSonConverters/IssueConverter.cs @@ -105,9 +105,9 @@ public override IDictionary Serialize(object obj, JavaScriptSeri result.Add(RedmineKeys.NOTES, entity.Notes); if (entity.Id != 0) { - result.Add(RedmineKeys.PRIVATE_NOTES, entity.PrivateNotes.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + result.Add(RedmineKeys.PRIVATE_NOTES, entity.PrivateNotes.ToLowerInv()); } - result.Add(RedmineKeys.IS_PRIVATE, entity.IsPrivate.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + result.Add(RedmineKeys.IS_PRIVATE, entity.IsPrivate.ToLowerInv()); result.WriteIdIfNotNull(entity.Project, RedmineKeys.PROJECT_ID); result.WriteIdIfNotNull(entity.Priority, RedmineKeys.PRIORITY_ID); result.WriteIdIfNotNull(entity.Status, RedmineKeys.STATUS_ID); diff --git a/src/redmine-net-api/JSonConverters/ProjectConverter.cs b/src/redmine-net-api/JSonConverters/ProjectConverter.cs index da50fe18..7ba99aa5 100755 --- a/src/redmine-net-api/JSonConverters/ProjectConverter.cs +++ b/src/redmine-net-api/JSonConverters/ProjectConverter.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -87,8 +87,8 @@ public override IDictionary Serialize(object obj, JavaScriptSeri result.Add(RedmineKeys.IDENTIFIER, entity.Identifier); result.Add(RedmineKeys.DESCRIPTION, entity.Description); result.Add(RedmineKeys.HOMEPAGE, entity.HomePage); - //result.Add(RedmineKeys.INHERIT_MEMBERS, entity.InheritMembers.ToString().ToLowerInvariant()); - result.Add(RedmineKeys.IS_PUBLIC, entity.IsPublic.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + //result.Add(RedmineKeys.INHERIT_MEMBERS, entity.InheritMembers.ToLowerInv()); + result.Add(RedmineKeys.IS_PUBLIC, entity.IsPublic.ToLowerInv()); result.WriteIdOrEmpty(entity.Parent, RedmineKeys.PARENT_ID, string.Empty); result.WriteIdsArray(RedmineKeys.TRACKER_IDS, entity.Trackers); result.WriteNamesArray(RedmineKeys.ENABLED_MODULE_NAMES, entity.EnabledModules); diff --git a/src/redmine-net-api/JSonConverters/UserConverter.cs b/src/redmine-net-api/JSonConverters/UserConverter.cs index ee34bed5..d5055a9c 100644 --- a/src/redmine-net-api/JSonConverters/UserConverter.cs +++ b/src/redmine-net-api/JSonConverters/UserConverter.cs @@ -96,7 +96,7 @@ public override IDictionary Serialize(object obj, JavaScriptSeri result.Add(RedmineKeys.PASSWORD, entity.Password); } - result.Add(RedmineKeys.MUST_CHANGE_PASSWD, entity.MustChangePassword.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + result.Add(RedmineKeys.MUST_CHANGE_PASSWD, entity.MustChangePassword.ToLowerInv()); result.Add(RedmineKeys.STATUS, ((int)entity.Status).ToString(CultureInfo.InvariantCulture)); if(entity.AuthenticationModeId.HasValue) From ec29e0c2df3abfce9a08d112acc214c9526852f8 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Thu, 21 Nov 2019 14:46:54 +0200 Subject: [PATCH 023/549] Replace xml bool toLowerInvariant with XmlConvert.ToString(bool) --- src/redmine-net-api/Types/Issue.cs | 7 ++++--- src/redmine-net-api/Types/Project.cs | 4 ++-- src/redmine-net-api/Types/User.cs | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/redmine-net-api/Types/Issue.cs b/src/redmine-net-api/Types/Issue.cs index 0c88696f..1889566e 100644 --- a/src/redmine-net-api/Types/Issue.cs +++ b/src/redmine-net-api/Types/Issue.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2017 Adrian Popescu, Dorin Huzum. Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ limitations under the License. using System; using System.Collections.Generic; +using System.Globalization; using System.Xml; using System.Xml.Schema; using System.Xml.Serialization; @@ -459,12 +460,12 @@ public void WriteXml(XmlWriter writer) if (Id != 0) { - writer.WriteElementString(RedmineKeys.PRIVATE_NOTES, PrivateNotes.ToString().ToLowerInvariant()); + writer.WriteElementString(RedmineKeys.PRIVATE_NOTES, XmlConvert.ToString(PrivateNotes)); } writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); writer.WriteStartElement(RedmineKeys.IS_PRIVATE); - writer.WriteValue(IsPrivate.ToString().ToLowerInvariant()); + writer.WriteValue(XmlConvert.ToString(IsPrivate)); writer.WriteEndElement(); writer.WriteIdIfNotNull(Project, RedmineKeys.PROJECT_ID); diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs index 3179e516..dbc171aa 100644 --- a/src/redmine-net-api/Types/Project.cs +++ b/src/redmine-net-api/Types/Project.cs @@ -211,8 +211,8 @@ public override void WriteXml(XmlWriter writer) writer.WriteElementString(RedmineKeys.NAME, Name); writer.WriteElementString(RedmineKeys.IDENTIFIER, Identifier); writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); - //writer.WriteElementString(RedmineKeys.INHERIT_MEMBERS, InheritMembers.ToString().ToLowerInvariant()); - writer.WriteElementString(RedmineKeys.IS_PUBLIC, IsPublic.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + //writer.WriteElementString(RedmineKeys.INHERIT_MEMBERS, XmlConvert.ToString(InheritMembers)); + writer.WriteElementString(RedmineKeys.IS_PUBLIC, XmlConvert.ToString(IsPublic)); writer.WriteIdOrEmpty(Parent, RedmineKeys.PARENT_ID); writer.WriteElementString(RedmineKeys.HOMEPAGE, HomePage); diff --git a/src/redmine-net-api/Types/User.cs b/src/redmine-net-api/Types/User.cs index e5e66cad..b901c77c 100644 --- a/src/redmine-net-api/Types/User.cs +++ b/src/redmine-net-api/Types/User.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -230,7 +230,7 @@ public void WriteXml(XmlWriter writer) writer.WriteValueOrEmpty(AuthenticationModeId, RedmineKeys.AUTH_SOURCE_ID); } - writer.WriteElementString(RedmineKeys.MUST_CHANGE_PASSWD, MustChangePassword.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + writer.WriteElementString(RedmineKeys.MUST_CHANGE_PASSWD, XmlConvert.ToString(MustChangePassword)); writer.WriteElementString(RedmineKeys.STATUS, ((int)Status).ToString(CultureInfo.InvariantCulture)); if(CustomFields != null) { From 1980ab47300479c36b1254b11b4caa3191608ac1 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Thu, 21 Nov 2019 14:49:13 +0200 Subject: [PATCH 024/549] Add Format(xml|json) property on RedmineManager --- src/redmine-net-api/RedmineManager.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index bc393d61..5f645a1e 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -106,6 +106,7 @@ public RedmineManager(string host, MimeFormat mimeFormat = MimeFormat.Xml, bool Host = host; MimeFormat = mimeFormat; + Format = mimeFormat == MimeFormat.Xml ? "xml" : "json"; Proxy = proxy; SecurityProtocolType = securityProtocolType; @@ -187,6 +188,11 @@ public static Dictionary Sufixes get { return routes; } } + /// + /// + /// + public string Format { get; } + /// /// Gets the host. /// From 7aee97f03d322cc328eae4e10ca2ece79de48005 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Thu, 21 Nov 2019 14:52:49 +0200 Subject: [PATCH 025/549] Add missing ToString(CultureInfo.InvariantCulture) --- .../Extensions/XmlWriterExtensions.cs | 27 +++-- src/redmine-net-api/Internals/UrlHelper.cs | 98 ++++++++++--------- .../JSonConverters/VersionConverter.cs | 6 +- .../JSonConverters/WatcherConverter.cs | 4 +- src/redmine-net-api/RedmineManager.cs | 8 +- src/redmine-net-api/Types/CustomFieldValue.cs | 4 +- src/redmine-net-api/Types/IssueChild.cs | 5 +- src/redmine-net-api/Types/Version.cs | 7 +- 8 files changed, 89 insertions(+), 70 deletions(-) diff --git a/src/redmine-net-api/Extensions/XmlWriterExtensions.cs b/src/redmine-net-api/Extensions/XmlWriterExtensions.cs index 4fa3e5c1..29701be8 100755 --- a/src/redmine-net-api/Extensions/XmlWriterExtensions.cs +++ b/src/redmine-net-api/Extensions/XmlWriterExtensions.cs @@ -30,6 +30,10 @@ namespace Redmine.Net.Api.Extensions /// public static partial class XmlExtensions { + + private static readonly Type[] emptyTypeArray = new Type[0]; + private static readonly XmlAttributeOverrides xmlAttributeOverrides = new XmlAttributeOverrides(); + /// /// Writes the id if not null. /// @@ -71,13 +75,16 @@ public static void WriteArray(this XmlWriter writer, IEnumerable collection, str /// The f. public static void WriteArrayIds(this XmlWriter writer, IEnumerable collection, string elementName, Type type, Func f) { - if (collection == null) return; + if (collection == null || f == null) return; + writer.WriteStartElement(elementName); writer.WriteAttributeString("type", "array"); + var serializer = new XmlSerializer(type); + foreach (var item in collection) { - new XmlSerializer(type).Serialize(writer, f.Invoke(item)); + serializer.Serialize(writer, f.Invoke(item)); } writer.WriteEndElement(); @@ -95,18 +102,18 @@ public static void WriteArrayIds(this XmlWriter writer, IEnumerable collection, public static void WriteArray(this XmlWriter writer, IEnumerable collection, string elementName, Type type, string root, string defaultNamespace = null) { if (collection == null) return; + writer.WriteStartElement(elementName); writer.WriteAttributeString("type", "array"); + var rootAttribute = new XmlRootAttribute(root); + + var serializer = new XmlSerializer(type, xmlAttributeOverrides, emptyTypeArray, rootAttribute, + defaultNamespace); + foreach (var item in collection) { - #if (NET20 || NET40 || NET45 || NET451 || NET452) - new XmlSerializer(type, new XmlAttributeOverrides(), new Type[]{}, new XmlRootAttribute(root), - defaultNamespace).Serialize(writer, item); -#else - new XmlSerializer(type, new XmlAttributeOverrides(), Array.Empty(), new XmlRootAttribute(root), - defaultNamespace).Serialize(writer, item); - #endif + serializer.Serialize(writer, item); } writer.WriteEndElement(); @@ -121,7 +128,7 @@ public static void WriteArray(this XmlWriter writer, IEnumerable collection, str /// The func to invoke. public static void WriteArrayStringElement(this XmlWriter writer, IEnumerable collection, string elementName, Func f) { - if (collection == null) return; + if (collection == null || f == null) return; writer.WriteStartElement(elementName); writer.WriteAttributeString("type", "array"); diff --git a/src/redmine-net-api/Internals/UrlHelper.cs b/src/redmine-net-api/Internals/UrlHelper.cs index f869daa1..c4c9b221 100644 --- a/src/redmine-net-api/Internals/UrlHelper.cs +++ b/src/redmine-net-api/Internals/UrlHelper.cs @@ -21,6 +21,7 @@ limitations under the License. using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Types; +using File = Redmine.Net.Api.Types.File; using Version = Redmine.Net.Api.Types.Version; namespace Redmine.Net.Api.Internals @@ -83,8 +84,8 @@ public static string GetUploadUrl(RedmineManager redmineManager, string id, T if (!RedmineManager.Sufixes.ContainsKey(type)) throw new KeyNotFoundException(type.Name); - return string.Format(REQUEST_FORMAT, redmineManager.Host, RedmineManager.Sufixes[type], id, - redmineManager.MimeFormat.ToString().ToLowerInvariant()); + return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, RedmineManager.Sufixes[type], id, + redmineManager.Format); } /// @@ -109,14 +110,14 @@ public static string GetUploadUrl(RedmineManager redmineManager, string id, T if (type == typeof(Version) || type == typeof(IssueCategory) || type == typeof(ProjectMembership)) { if (string.IsNullOrEmpty(ownerId)) throw new RedmineException("The owner id(project id) is mandatory!"); - return string.Format(ENTITY_WITH_PARENT_FORMAT, redmineManager.Host, RedmineKeys.PROJECTS, - ownerId, RedmineManager.Sufixes[type], redmineManager.MimeFormat.ToString().ToLowerInvariant()); + return string.Format(CultureInfo.InvariantCulture,ENTITY_WITH_PARENT_FORMAT, redmineManager.Host, RedmineKeys.PROJECTS, + ownerId, RedmineManager.Sufixes[type], redmineManager.Format); } if (type == typeof(IssueRelation)) { if (string.IsNullOrEmpty(ownerId)) throw new RedmineException("The owner id(issue id) is mandatory!"); - return string.Format(ENTITY_WITH_PARENT_FORMAT, redmineManager.Host, RedmineKeys.ISSUES, - ownerId, RedmineManager.Sufixes[type], redmineManager.MimeFormat.ToString().ToLowerInvariant()); + return string.Format(CultureInfo.InvariantCulture,ENTITY_WITH_PARENT_FORMAT, redmineManager.Host, RedmineKeys.ISSUES, + ownerId, RedmineManager.Sufixes[type], redmineManager.Format); } if (type == typeof(File)) @@ -125,11 +126,11 @@ public static string GetUploadUrl(RedmineManager redmineManager, string id, T { throw new RedmineException("The owner id(project id) is mandatory!"); } - return string.Format(FILE_URL_FORMAT, redmineManager.Host, ownerId, redmineManager.MimeFormat.ToString().ToLowerInvariant()); + return string.Format(CultureInfo.InvariantCulture,FILE_URL_FORMAT, redmineManager.Host, ownerId, redmineManager.Format); } - return string.Format(FORMAT, redmineManager.Host, RedmineManager.Sufixes[type], - redmineManager.MimeFormat.ToString().ToLowerInvariant()); + return string.Format(CultureInfo.InvariantCulture,FORMAT, redmineManager.Host, RedmineManager.Sufixes[type], + redmineManager.Format); } /// @@ -147,8 +148,8 @@ public static string GetUploadUrl(RedmineManager redmineManager, string id, T if (!RedmineManager.Sufixes.ContainsKey(type)) throw new KeyNotFoundException(type.Name); - return string.Format(REQUEST_FORMAT, redmineManager.Host, RedmineManager.Sufixes[type], id, - redmineManager.MimeFormat.ToString().ToLowerInvariant()); + return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, RedmineManager.Sufixes[type], id, + redmineManager.Format); } /// @@ -165,8 +166,8 @@ public static string GetUploadUrl(RedmineManager redmineManager, string id, T if (!RedmineManager.Sufixes.ContainsKey(type)) throw new KeyNotFoundException(type.Name); - return string.Format(REQUEST_FORMAT, redmineManager.Host, RedmineManager.Sufixes[type], id, - redmineManager.MimeFormat.ToString().ToLowerInvariant()); + return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, RedmineManager.Sufixes[type], id, + redmineManager.Format); } /// @@ -195,8 +196,8 @@ public static string GetListUrl(RedmineManager redmineManager, NameValueColle if (string.IsNullOrEmpty(projectId)) throw new RedmineException("The project id is mandatory! \nCheck if you have included the parameter project_id to parameters."); - return string.Format(ENTITY_WITH_PARENT_FORMAT, redmineManager.Host, RedmineKeys.PROJECTS, - projectId, RedmineManager.Sufixes[type], redmineManager.MimeFormat.ToString().ToLowerInvariant()); + return string.Format(CultureInfo.InvariantCulture,ENTITY_WITH_PARENT_FORMAT, redmineManager.Host, RedmineKeys.PROJECTS, + projectId, RedmineManager.Sufixes[type], redmineManager.Format); } if (type == typeof(IssueRelation)) { @@ -204,8 +205,8 @@ public static string GetListUrl(RedmineManager redmineManager, NameValueColle if (string.IsNullOrEmpty(issueId)) throw new RedmineException("The issue id is mandatory! \nCheck if you have included the parameter issue_id to parameters"); - return string.Format(ENTITY_WITH_PARENT_FORMAT, redmineManager.Host, RedmineKeys.ISSUES, - issueId, RedmineManager.Sufixes[type], redmineManager.MimeFormat.ToString().ToLowerInvariant()); + return string.Format(CultureInfo.InvariantCulture,ENTITY_WITH_PARENT_FORMAT, redmineManager.Host, RedmineKeys.ISSUES, + issueId, RedmineManager.Sufixes[type], redmineManager.Format); } if (type == typeof(File)) @@ -215,11 +216,11 @@ public static string GetListUrl(RedmineManager redmineManager, NameValueColle { throw new RedmineException("The project id is mandatory! \nCheck if you have included the parameter project_id to parameters."); } - return string.Format(FILE_URL_FORMAT, redmineManager.Host, projectId, redmineManager.MimeFormat.ToString().ToLowerInvariant()); + return string.Format(CultureInfo.InvariantCulture,FILE_URL_FORMAT, redmineManager.Host, projectId, redmineManager.Format); } - - return string.Format(FORMAT, redmineManager.Host, RedmineManager.Sufixes[type], - redmineManager.MimeFormat.ToString().ToLowerInvariant()); + + return string.Format(CultureInfo.InvariantCulture,FORMAT, redmineManager.Host, RedmineManager.Sufixes[type], + redmineManager.Format); } /// @@ -230,8 +231,8 @@ public static string GetListUrl(RedmineManager redmineManager, NameValueColle /// public static string GetWikisUrl(RedmineManager redmineManager, string projectId) { - return string.Format(WIKI_INDEX_FORMAT, redmineManager.Host, projectId, - redmineManager.MimeFormat.ToString().ToLowerInvariant()); + return string.Format(CultureInfo.InvariantCulture,WIKI_INDEX_FORMAT, redmineManager.Host, projectId, + redmineManager.Format); } /// @@ -247,10 +248,10 @@ public static string GetWikiPageUrl(RedmineManager redmineManager, string projec NameValueCollection parameters, string pageName, uint version = 0) { var uri = version == 0 - ? string.Format(WIKI_PAGE_FORMAT, redmineManager.Host, projectId, pageName, - redmineManager.MimeFormat.ToString().ToLowerInvariant()) - : string.Format(WIKI_VERSION_FORMAT, redmineManager.Host, projectId, pageName, version, - redmineManager.MimeFormat.ToString().ToLowerInvariant()); + ? string.Format(CultureInfo.InvariantCulture,WIKI_PAGE_FORMAT, redmineManager.Host, projectId, pageName, + redmineManager.Format) + : string.Format(CultureInfo.InvariantCulture,WIKI_VERSION_FORMAT, redmineManager.Host, projectId, pageName, version.ToString(CultureInfo.InvariantCulture), + redmineManager.Format); return uri; } @@ -262,9 +263,9 @@ public static string GetWikiPageUrl(RedmineManager redmineManager, string projec /// public static string GetAddUserToGroupUrl(RedmineManager redmineManager, int groupId) { - return string.Format(REQUEST_FORMAT, redmineManager.Host, + return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, RedmineManager.Sufixes[typeof(Group)], - $"{groupId}/users", redmineManager.MimeFormat.ToString().ToLowerInvariant()); + $"{groupId.ToString(CultureInfo.InvariantCulture)}/users", redmineManager.Format); } /// @@ -276,9 +277,9 @@ public static string GetAddUserToGroupUrl(RedmineManager redmineManager, int gro /// public static string GetRemoveUserFromGroupUrl(RedmineManager redmineManager, int groupId, int userId) { - return string.Format(REQUEST_FORMAT, redmineManager.Host, + return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, RedmineManager.Sufixes[typeof(Group)], - $"{groupId}/users/{userId}", redmineManager.MimeFormat.ToString().ToLowerInvariant()); + $"{groupId.ToString(CultureInfo.InvariantCulture)}/users/{userId.ToString(CultureInfo.InvariantCulture)}", redmineManager.Format); } /// @@ -288,8 +289,8 @@ public static string GetRemoveUserFromGroupUrl(RedmineManager redmineManager, in /// public static string GetUploadFileUrl(RedmineManager redmineManager) { - return string.Format(FORMAT, redmineManager.Host, RedmineKeys.UPLOADS, - redmineManager.MimeFormat.ToString().ToLowerInvariant()); + return string.Format(CultureInfo.InvariantCulture,FORMAT, redmineManager.Host, RedmineKeys.UPLOADS, + redmineManager.Format); } /// @@ -299,9 +300,9 @@ public static string GetUploadFileUrl(RedmineManager redmineManager) /// public static string GetCurrentUserUrl(RedmineManager redmineManager) { - return string.Format(REQUEST_FORMAT, redmineManager.Host, + return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, RedmineManager.Sufixes[typeof(User)], CURRENT_USER_URI, - redmineManager.MimeFormat.ToString().ToLowerInvariant()); + redmineManager.Format); } /// @@ -313,8 +314,8 @@ public static string GetCurrentUserUrl(RedmineManager redmineManager) /// public static string GetWikiCreateOrUpdaterUrl(RedmineManager redmineManager, string projectId, string pageName) { - return string.Format(WIKI_PAGE_FORMAT, redmineManager.Host, projectId, pageName, - redmineManager.MimeFormat.ToString().ToLowerInvariant()); + return string.Format(CultureInfo.InvariantCulture,WIKI_PAGE_FORMAT, redmineManager.Host, projectId, pageName, + redmineManager.Format); } /// @@ -326,8 +327,8 @@ public static string GetWikiCreateOrUpdaterUrl(RedmineManager redmineManager, st /// public static string GetDeleteWikirUrl(RedmineManager redmineManager, string projectId, string pageName) { - return string.Format(WIKI_PAGE_FORMAT, redmineManager.Host, projectId, pageName, - redmineManager.MimeFormat.ToString().ToLowerInvariant()); + return string.Format(CultureInfo.InvariantCulture,WIKI_PAGE_FORMAT, redmineManager.Host, projectId, pageName, + redmineManager.Format); } /// @@ -339,9 +340,9 @@ public static string GetDeleteWikirUrl(RedmineManager redmineManager, string pro /// public static string GetAddWatcherUrl(RedmineManager redmineManager, int issueId, int userId) { - return string.Format(REQUEST_FORMAT, redmineManager.Host, - RedmineManager.Sufixes[typeof(Issue)], $"{issueId}/watchers", - redmineManager.MimeFormat.ToString().ToLowerInvariant()); + return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, + RedmineManager.Sufixes[typeof(Issue)], $"{issueId.ToString(CultureInfo.InvariantCulture)}/watchers", + redmineManager.Format); } /// @@ -353,9 +354,9 @@ public static string GetAddWatcherUrl(RedmineManager redmineManager, int issueId /// public static string GetRemoveWatcherUrl(RedmineManager redmineManager, int issueId, int userId) { - return string.Format(REQUEST_FORMAT, redmineManager.Host, - RedmineManager.Sufixes[typeof(Issue)], $"{issueId}/watchers/{userId}", - redmineManager.MimeFormat.ToString().ToLowerInvariant()); + return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, + RedmineManager.Sufixes[typeof(Issue)], $"{issueId.ToString(CultureInfo.InvariantCulture)}/watchers/{userId.ToString(CultureInfo.InvariantCulture)}", + redmineManager.Format); } /// @@ -366,8 +367,11 @@ public static string GetRemoveWatcherUrl(RedmineManager redmineManager, int issu /// public static string GetAttachmentUpdateUrl(RedmineManager redmineManager, int issueId) { - return string.Format(ATTACHMENT_UPDATE_FORMAT, redmineManager.Host, issueId, - redmineManager.MimeFormat.ToString().ToLowerInvariant()); + return string.Format(CultureInfo.InvariantCulture, + ATTACHMENT_UPDATE_FORMAT, + redmineManager.Host, + issueId.ToString(CultureInfo.InvariantCulture), + redmineManager.Format); } } } \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/VersionConverter.cs b/src/redmine-net-api/JSonConverters/VersionConverter.cs index b3f7bf34..54e568e6 100755 --- a/src/redmine-net-api/JSonConverters/VersionConverter.cs +++ b/src/redmine-net-api/JSonConverters/VersionConverter.cs @@ -13,6 +13,8 @@ You may obtain a copy of the License at See the License for the specific language governing permissions and limitations under the License. */ + +using System.Globalization; #if !NET20 using System; using System.Collections.Generic; @@ -80,8 +82,8 @@ public override IDictionary Serialize(object obj, JavaScriptSeri if (entity != null) { result.Add(RedmineKeys.NAME, entity.Name); - result.Add(RedmineKeys.STATUS, entity.Status.ToString().ToLowerInvariant()); - result.Add(RedmineKeys.SHARING, entity.Sharing.ToString().ToLowerInvariant()); + result.Add(RedmineKeys.STATUS, entity.Status.ToString("G").ToLowerInv()); + result.Add(RedmineKeys.SHARING, entity.Sharing.ToString("G").ToLowerInv()); result.Add(RedmineKeys.DESCRIPTION, entity.Description); var root = new Dictionary(); diff --git a/src/redmine-net-api/JSonConverters/WatcherConverter.cs b/src/redmine-net-api/JSonConverters/WatcherConverter.cs index 3c3a1d31..b7f93e2d 100755 --- a/src/redmine-net-api/JSonConverters/WatcherConverter.cs +++ b/src/redmine-net-api/JSonConverters/WatcherConverter.cs @@ -13,6 +13,8 @@ You may obtain a copy of the License at See the License for the specific language governing permissions and limitations under the License. */ + +using System.Globalization; #if !NET20 using System; using System.Collections.Generic; @@ -75,7 +77,7 @@ public override IDictionary Serialize(object obj, JavaScriptSeri if (entity != null) { - result.Add(RedmineKeys.ID, entity.Id); + result.Add(RedmineKeys.ID, entity.Id.ToString(CultureInfo.InvariantCulture)); } return result; diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index 5f645a1e..82b5649c 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -173,8 +173,10 @@ public RedmineManager(string host, string login, string password, MimeFormat mim { cache = new CredentialCache { { new Uri(host), "Basic", new NetworkCredential(login, password) } }; - var token = Convert.ToBase64String(Encoding.UTF8.GetBytes(string.Format("{0}:{1}", login, password))); - basicAuthorization = string.Format("Basic {0}", token); + var token = Convert.ToBase64String(Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture,"{0}:{1}", login, password))); + basicAuthorization = string.Format(CultureInfo.InvariantCulture,"Basic {0}", token); + + } /// diff --git a/src/redmine-net-api/Types/CustomFieldValue.cs b/src/redmine-net-api/Types/CustomFieldValue.cs index 31fa884e..8d407c36 100755 --- a/src/redmine-net-api/Types/CustomFieldValue.cs +++ b/src/redmine-net-api/Types/CustomFieldValue.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -39,7 +39,7 @@ public class CustomFieldValue : IEquatable, ICloneable /// public bool Equals(CustomFieldValue other) { - return Info.Equals(other.Info); + return other != null && Info.Equals(other.Info, StringComparison.OrdinalIgnoreCase); } /// diff --git a/src/redmine-net-api/Types/IssueChild.cs b/src/redmine-net-api/Types/IssueChild.cs index 7dd10276..49fddc84 100755 --- a/src/redmine-net-api/Types/IssueChild.cs +++ b/src/redmine-net-api/Types/IssueChild.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,6 +15,7 @@ limitations under the License. */ using System; +using System.Globalization; using System.Xml; using System.Xml.Schema; using System.Xml.Serialization; @@ -54,7 +55,7 @@ public class IssueChild : Identifiable, IXmlSerializable, IEquatable /// public void ReadXml(XmlReader reader) { - Id = Convert.ToInt32(reader.GetAttribute(RedmineKeys.ID)); + Id = Convert.ToInt32(reader.GetAttribute(RedmineKeys.ID), CultureInfo.InvariantCulture); reader.Read(); while (!reader.EOF) diff --git a/src/redmine-net-api/Types/Version.cs b/src/redmine-net-api/Types/Version.cs index 64d3d66f..7b341dd4 100755 --- a/src/redmine-net-api/Types/Version.cs +++ b/src/redmine-net-api/Types/Version.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,7 @@ limitations under the License. using System; using System.Collections.Generic; +using System.Globalization; using System.Xml; using System.Xml.Serialization; using Redmine.Net.Api.Extensions; @@ -135,8 +136,8 @@ public override void ReadXml(XmlReader reader) public override void WriteXml(XmlWriter writer) { writer.WriteElementString(RedmineKeys.NAME, Name); - writer.WriteElementString(RedmineKeys.STATUS, Status.ToString().ToLowerInvariant()); - writer.WriteElementString(RedmineKeys.SHARING, Sharing.ToString().ToLowerInvariant()); + writer.WriteElementString(RedmineKeys.STATUS, Status.ToString("G").ToLowerInv()); + writer.WriteElementString(RedmineKeys.SHARING, Sharing.ToString("G").ToLowerInv()); writer.WriteDateOrEmpty(DueDate, RedmineKeys.DUE_DATE); writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); From f25c4d27f4db1c188dddf1bc1b39aa27722b0df4 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Thu, 21 Nov 2019 14:56:07 +0200 Subject: [PATCH 026/549] Fix CA1200 --- src/redmine-net-api/RedmineWebClient.cs | 6 +++--- src/redmine-net-api/Types/Group.cs | 6 +++--- src/redmine-net-api/Types/Project.cs | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/redmine-net-api/RedmineWebClient.cs b/src/redmine-net-api/RedmineWebClient.cs index 6150486b..c3e7b7f3 100644 --- a/src/redmine-net-api/RedmineWebClient.cs +++ b/src/redmine-net-api/RedmineWebClient.cs @@ -89,11 +89,11 @@ public RedmineWebClient() public bool KeepAlive { get; set; } /// - /// Returns a object for the specified resource. + /// Returns a object for the specified resource. /// - /// A that identifies the resource to request. + /// A that identifies the resource to request. /// - /// A new object for the specified resource. + /// A new object for the specified resource. /// protected override WebRequest GetWebRequest(Uri address) { diff --git a/src/redmine-net-api/Types/Group.cs b/src/redmine-net-api/Types/Group.cs index fe4e6900..ff95afaf 100755 --- a/src/redmine-net-api/Types/Group.cs +++ b/src/redmine-net-api/Types/Group.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -57,7 +57,7 @@ public class Group : IdentifiableName, IEquatable /// /// Generates an object from its XML representation. /// - /// The stream from which the object is deserialized. + /// The stream from which the object is deserialized. public override void ReadXml(XmlReader reader) { reader.Read(); @@ -89,7 +89,7 @@ public override void ReadXml(XmlReader reader) /// /// Converts an object into its XML representation. /// - /// The stream to which the object is serialized. + /// The stream to which the object is serialized. public override void WriteXml(XmlWriter writer) { writer.WriteElementString(RedmineKeys.NAME, Name); diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs index dbc171aa..98cb7f8f 100644 --- a/src/redmine-net-api/Types/Project.cs +++ b/src/redmine-net-api/Types/Project.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -150,7 +150,7 @@ public class Project : IdentifiableName, IEquatable /// /// Generates an object from its XML representation. /// - /// The stream from which the object is deserialized. + /// The stream from which the object is deserialized. public override void ReadXml(XmlReader reader) { if (reader == null) throw new ArgumentNullException(nameof(reader)); From 5e65b69189582cbb52aee61a8e910f494ab5d5c8 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Thu, 21 Nov 2019 14:56:35 +0200 Subject: [PATCH 027/549] Remove packages.config file --- tests/redmine-net-api.Tests/packages.config | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 tests/redmine-net-api.Tests/packages.config diff --git a/tests/redmine-net-api.Tests/packages.config b/tests/redmine-net-api.Tests/packages.config deleted file mode 100644 index e492b60e..00000000 --- a/tests/redmine-net-api.Tests/packages.config +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file From d3ce5ef5e936c10628f6a51f281ce2d91a193db1 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Thu, 21 Nov 2019 14:58:28 +0200 Subject: [PATCH 028/549] Add csproj NoWarn tag --- src/redmine-net-api/redmine-net-api.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 30f8bea1..330d4109 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -11,7 +11,7 @@ TRACE Debug;Release PackageReference - + NU5105;CA1303;CA1056;CA1062;CA1707;CA1720;CA1806;CA1716;CA1724;CA2227 From ea2566a0830ccaa3ce2df3ed08cc0eb08d0e0289 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Thu, 21 Nov 2019 15:48:36 +0200 Subject: [PATCH 029/549] Update appveyor --- appveyor.yml | 130 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 85 insertions(+), 45 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 8fd746e7..fd7734a0 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,61 +1,101 @@ -os: Visual Studio 2017 -version: 2.0.{build} -# environment: -# COVERALLS_REPO_TOKEN: -# secure: 8JYxwCWszeAaWBr41pD17LB925K7Sk7utvKsIb1qz44i2anf9uLmvh2q0ilMQTBO +version: '{build}' +image: + - Ubuntu + - macos + - Visual Studio 2019 + pull_requests: - do_not_increment_build_number: false + do_not_increment_build_number: true + branches: only: - - master -#configuration: Release -#platform: Any CPU -assembly_info: + - master + +configuration: Release + +init: + # Good practise, because Windows line endings are different from Unix/Linux ones + - cmd: git config --global core.autocrlf true + + # Set "build version number" to "short-commit-hash" or when tagged to "tag name" (Travis style) + - ps: >- + if ($env:APPVEYOR_REPO_TAG -eq "true") + { + Update-AppveyorBuild -Version "$($env:APPVEYOR_REPO_TAG_NAME.TrimStart("v"))" + } + else + { + Update-AppveyorBuild -Version "dev-$($env:APPVEYOR_REPO_COMMIT.substring(0,7))" + } + +nuget: + disable_publish_on_pr: true + +environment: + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + APPVEYOR_YML_DISABLE_PS_LINUX: true + +dotnet_csproj: patch: true - file: '**\AssemblyInfo.*' + file: '**\*.csproj' + version: '{version}' + version_prefix: '{version}' + package_version: '{version}' assembly_version: '{version}' - assembly_file_version: '{version}' - assembly_informational_version: '{version}' - -build_script: - - Msbuild.exe src/redmine-net20-api/redmine-net20-api.csproj /verbosity:minimal /p:BuildNetFX20=true - - Msbuild.exe src/redmine-net40-api/redmine-net40-api.csproj /verbosity:minimal /p:BuildNetFX40=true - - Msbuild.exe src/redmine-net40-api-signed/redmine-net40-api-signed.csproj /verbosity:minimal /p:BuildNetFX40=true - - Msbuild.exe src/redmine-net45-api/redmine-net45-api.csproj /verbosity:minimal /p:BuildNetFX45=true - - Msbuild.exe src/redmine-net45-api-signed/redmine-net45-api-signed.csproj /verbosity:minimal /p:BuildNetFX45=true - - Msbuild.exe src/redmine-net451-api/redmine-net451-api.csproj /verbosity:minimal /p:BuildNetFX451=true - - Msbuild.exe src/redmine-net451-api-signed/redmine-net451-api-signed.csproj /verbosity:minimal /p:BuildNetFX451=true - - Msbuild.exe src/redmine-net452-api/redmine-net452-api.csproj /verbosity:minimal /p:BuildNetFX452=true - - Msbuild.exe src/redmine-net452-api-signed/redmine-net452-api-signed.csproj /verbosity:minimal /p:BuildNetFX452=true + file_version: '{version}' + informational_version: '{version}' + +install: + - cmd: dotnet restore redmine-net-api.sln before_build: -- nuget restore + - cmd: dotnet --version build: project: redmine-net-api.sln - publish_nuget: true - verbosity: detailed + verbosity: minimal after_build: -- ps: nuget pack build/redmine-net-api.nuspec -Version $env:appveyor_build_version -- ps: nuget pack build/redmine-net-api-signed.nuspec -Version $env:appveyor_build_version - -nuget: - account_feed: true - project_feed: true + - cmd: dotnet pack src\redmine-net-api\redmine-net-api.csproj --output artifacts test: off artifacts: -- path: '*.nupkg' - -# preserve "packages" directory in the root of build folder but will reset it if packages.config is modified -cache: - - '%USERPROFILE%\.nuget\packages -> **\project.json' # project.json cache - -deploy: -- provider: NuGet - api_key: - secure: aOykHyBK5mqqlzZwbLgZnkB9qwmidaTFaLbc2ZKM2sSwBEuMV0VRF5OgKuoRehY0 - artifact: /.*\.nupkg/ - skip_symbols: true + - name: NuGet Packages + path: ./artifacts/**/*.nupkg + - name: NuGet Symbol Packages + path: ./artifacts/**/*.snupkg + +skip_commits: + files: + - '**/*.md' + - '**/*.gif' + - '**/*.png' + - '**/*.yml' + - LICENSE + - tests/* + +for: + - + matrix: + only: + - image: Ubuntu + + deploy: + - provider: NuGet + name: dev + api_key: + secure: fo+5VNPIRQ98jFPBZSd4SsOVyXEsTxnQ52VWTrg3sgH1GwGXhi70Q561eimlmRhy + skip_symbols: true + on: + branch: master + + - provider: NuGet + name: production + api_key: + secure: fo+5VNPIRQ98jFPBZSd4SsOVyXEsTxnQ52VWTrg3sgH1GwGXhi70Q561eimlmRhy + skip_symbols: false + on: + branch: master + APPVEYOR_REPO_TAG: true \ No newline at end of file From f29fa683d9ab79a8d65752ac3a035d91ce464575 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Thu, 21 Nov 2019 15:54:49 +0200 Subject: [PATCH 030/549] Fix project id & add version suffix build number condition --- src/redmine-net-api/redmine-net-api.csproj | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 330d4109..8f45e517 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -17,21 +17,14 @@ 1.0.0.0 - Adrian Popescu - Redmine Api is a .NET rest client for Redmine. - p.adi - Adrian Popescu, 2011-2020 - 1.0.0.0 - 1.0.0 - en-US - redmine-net-api + redmine-api https://github.com/zapadi/redmine-net-api/blob/master/logo.png https://github.com/zapadi/redmine-net-api/blob/master/LICENSE https://github.com/zapadi/redmine-net-api @@ -39,18 +32,15 @@ Changed to new csproj format. Redmine; REST; API; Client; .NET; Adrian Popescu; 1.0.0 - Redmine .NET API Client - git - https://github.com/zapadi/redmine-net-api - ... Redmine .NET API Client 1.0.0 2.0.0 - $(VersionSuffix) + pre + pre-$(BuildNumber) From d6df4a2274e4341280ae4a115ed8164776128392 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Thu, 21 Nov 2019 17:06:14 +0200 Subject: [PATCH 031/549] Update version tags --- src/redmine-net-api/redmine-net-api.csproj | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 8f45e517..5c50b21a 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -1,4 +1,4 @@ - + @@ -16,12 +16,12 @@ - 1.0.0.0 + Adrian Popescu Redmine Api is a .NET rest client for Redmine. p.adi Adrian Popescu, 2011-2020 - 1.0.0.0 + 1.0.0 en-US redmine-api @@ -31,14 +31,14 @@ true Changed to new csproj format. Redmine; REST; API; Client; .NET; Adrian Popescu; - 1.0.0 + Redmine .NET API Client git https://github.com/zapadi/redmine-net-api ... Redmine .NET API Client - 1.0.0 - 2.0.0 + + 2.0.43 pre pre-$(BuildNumber) From 9920a882927e2d467bfbde75624651bb7c7c3654 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Thu, 21 Nov 2019 17:06:40 +0200 Subject: [PATCH 032/549] Add Deterministic tag false --- src/redmine-net-api/redmine-net-api.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 5c50b21a..33315da2 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -1,4 +1,4 @@ - + @@ -7,6 +7,7 @@ false Redmine.Net.Api redmine-net-api + False true TRACE Debug;Release From 6314adad0f953ef745796315bf471d2bab5613b6 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Thu, 21 Nov 2019 17:07:51 +0200 Subject: [PATCH 033/549] Fix appveyor full version value --- appveyor.yml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index fd7734a0..12bd41f1 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -19,14 +19,21 @@ init: # Set "build version number" to "short-commit-hash" or when tagged to "tag name" (Travis style) - ps: >- + if ($env:APPVEYOR_REPO_TAG -eq "true") { - Update-AppveyorBuild -Version "$($env:APPVEYOR_REPO_TAG_NAME.TrimStart("v"))" + Update-AppveyorBuild -Version "$($env:APPVEYOR_REPO_TAG_NAME.TrimStart("v"))"; } else { - Update-AppveyorBuild -Version "dev-$($env:APPVEYOR_REPO_COMMIT.substring(0,7))" + $props = [xml](Get-Content Directory.Build.props) + $prefix = $props.Project.PropertyGroup.VersionPrefix + $suffix = "-$($env:APPVEYOR_REPO_COMMIT.substring(0,7))"; + + echo "Build: Full version is $prefix-$suffix"; } + + nuget: disable_publish_on_pr: true @@ -39,12 +46,8 @@ environment: dotnet_csproj: patch: true file: '**\*.csproj' - version: '{version}' + version_suffix: '${suffix}' version_prefix: '{version}' - package_version: '{version}' - assembly_version: '{version}' - file_version: '{version}' - informational_version: '{version}' install: - cmd: dotnet restore redmine-net-api.sln From 161afbb31cf71cef4aff47319657d0dbbebbceef Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Thu, 21 Nov 2019 17:14:59 +0200 Subject: [PATCH 034/549] Fix appveyor --- appveyor.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 12bd41f1..ecfda301 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -20,17 +20,19 @@ init: # Set "build version number" to "short-commit-hash" or when tagged to "tag name" (Travis style) - ps: >- + + $prefix = ""; if ($env:APPVEYOR_REPO_TAG -eq "true") { - Update-AppveyorBuild -Version "$($env:APPVEYOR_REPO_TAG_NAME.TrimStart("v"))"; + $prefix = $env:APPVEYOR_REPO_TAG_NAME.TrimStart("v"); + Update-AppveyorBuild -Version "$prefix"; } else { - $props = [xml](Get-Content Directory.Build.props) - $prefix = $props.Project.PropertyGroup.VersionPrefix - $suffix = "-$($env:APPVEYOR_REPO_COMMIT.substring(0,7))"; + $props = [xml](Get-Content Directory.Build.props); + $prefix = $props.Project.PropertyGroup.VersionPrefix + "-$($env:APPVEYOR_REPO_COMMIT.substring(0,7))"; - echo "Build: Full version is $prefix-$suffix"; + echo "Build: Full version is $prefix"; } @@ -46,8 +48,7 @@ environment: dotnet_csproj: patch: true file: '**\*.csproj' - version_suffix: '${suffix}' - version_prefix: '{version}' + version_prefix: '${prefix}' install: - cmd: dotnet restore redmine-net-api.sln From 749fee74a37af517aa810fb1e8a03ada8652856c Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Thu, 21 Nov 2019 18:22:31 +0200 Subject: [PATCH 035/549] Add Version tag --- src/redmine-net-api/redmine-net-api.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 33315da2..91f761fb 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -39,7 +39,7 @@ ... Redmine .NET API Client - 2.0.43 + 2.0.43 pre pre-$(BuildNumber) From 9b2885eb17cc67a50e5100df60cbe118acb99b91 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Fri, 22 Nov 2019 00:00:06 +0200 Subject: [PATCH 036/549] Update appveyor --- appveyor.yml | 47 +++++++++++++++-------------------------------- 1 file changed, 15 insertions(+), 32 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index ecfda301..5495055f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -15,27 +15,16 @@ configuration: Release init: # Good practise, because Windows line endings are different from Unix/Linux ones - - cmd: git config --global core.autocrlf true + - ps: git config --global core.autocrlf true # Set "build version number" to "short-commit-hash" or when tagged to "tag name" (Travis style) - ps: >- - - - $prefix = ""; - if ($env:APPVEYOR_REPO_TAG -eq "true") - { - $prefix = $env:APPVEYOR_REPO_TAG_NAME.TrimStart("v"); - Update-AppveyorBuild -Version "$prefix"; - } - else - { - $props = [xml](Get-Content Directory.Build.props); - $prefix = $props.Project.PropertyGroup.VersionPrefix + "-$($env:APPVEYOR_REPO_COMMIT.substring(0,7))"; - - echo "Build: Full version is $prefix"; - } - - + $branch = @{ $true = $env:APPVEYOR_REPO_BRANCH; $false = $(git symbolic-ref --short -q HEAD) }[$env:APPVEYOR_REPO_BRANCH -ne $NULL]; + $revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10); $false = "local" }[$env:APPVEYOR_BUILD_NUMBER -ne $NULL]; + $suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "master" -and $revision -ne "local"] + $commitHash = $(git rev-parse --short HEAD) + $buildSuffix = @{ $true = "$($suffix)-$($commitHash)"; $false = "$($branch)-$($commitHash)" }[$suffix -ne ""] + $versionSuffix = @{ $true = "--version-suffix=$($suffix)"; $false = ""}[$suffix -ne ""] nuget: disable_publish_on_pr: true @@ -45,31 +34,25 @@ environment: DOTNET_CLI_TELEMETRY_OPTOUT: true APPVEYOR_YML_DISABLE_PS_LINUX: true -dotnet_csproj: - patch: true - file: '**\*.csproj' - version_prefix: '${prefix}' - install: - - cmd: dotnet restore redmine-net-api.sln + - ps: dotnet restore redmine-net-api.sln before_build: - - cmd: dotnet --version + - ps: dotnet --version -build: - project: redmine-net-api.sln - verbosity: minimal +build_script: + - ps: dotnet build redmine-net-api.sln -c Release --version-suffix=$buildSuffix after_build: - - cmd: dotnet pack src\redmine-net-api\redmine-net-api.csproj --output artifacts + - ps: dotnet pack src\redmine-net-api\redmine-net-api.csproj -c Release --output .\artifacts --include-symbols --no-build $versionSuffix test: off artifacts: - name: NuGet Packages - path: ./artifacts/**/*.nupkg + path: .\artifacts\**\*.nupkg - name: NuGet Symbol Packages - path: ./artifacts/**/*.snupkg + path: .\artifacts\**\*.snupkg skip_commits: files: @@ -99,7 +82,7 @@ for: name: production api_key: secure: fo+5VNPIRQ98jFPBZSd4SsOVyXEsTxnQ52VWTrg3sgH1GwGXhi70Q561eimlmRhy - skip_symbols: false + skip_symbols: true on: branch: master APPVEYOR_REPO_TAG: true \ No newline at end of file From c559a212b91dd9277df798e9e6f9899bda565048 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Fri, 22 Nov 2019 00:00:22 +0200 Subject: [PATCH 037/549] Change VersionPrefix --- src/redmine-net-api/redmine-net-api.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 91f761fb..e57fe9e2 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -39,7 +39,7 @@ ... Redmine .NET API Client - 2.0.43 + 2.0.46 pre pre-$(BuildNumber) From f97bb5ff37bc0d43137ba9aed38cff8959e9879a Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Fri, 22 Nov 2019 00:17:12 +0200 Subject: [PATCH 038/549] Fix ++ --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 5495055f..7cd9500e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -22,7 +22,7 @@ init: $branch = @{ $true = $env:APPVEYOR_REPO_BRANCH; $false = $(git symbolic-ref --short -q HEAD) }[$env:APPVEYOR_REPO_BRANCH -ne $NULL]; $revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10); $false = "local" }[$env:APPVEYOR_BUILD_NUMBER -ne $NULL]; $suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "master" -and $revision -ne "local"] - $commitHash = $(git rev-parse --short HEAD) + $commitHash = $($env:APPVEYOR_REPO_COMMIT.substring(0,7)) $buildSuffix = @{ $true = "$($suffix)-$($commitHash)"; $false = "$($branch)-$($commitHash)" }[$suffix -ne ""] $versionSuffix = @{ $true = "--version-suffix=$($suffix)"; $false = ""}[$suffix -ne ""] From 3f3617fffc06259b1ef33c77cf5e38a7a2191006 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Fri, 22 Nov 2019 00:30:01 +0200 Subject: [PATCH 039/549] Fix ++ --- appveyor.yml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 7cd9500e..a3d634f4 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,8 +1,6 @@ version: '{build}' image: - - Ubuntu - - macos - - Visual Studio 2019 + - Visual Studio 2019 pull_requests: do_not_increment_build_number: true @@ -21,10 +19,10 @@ init: - ps: >- $branch = @{ $true = $env:APPVEYOR_REPO_BRANCH; $false = $(git symbolic-ref --short -q HEAD) }[$env:APPVEYOR_REPO_BRANCH -ne $NULL]; $revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10); $false = "local" }[$env:APPVEYOR_BUILD_NUMBER -ne $NULL]; - $suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "master" -and $revision -ne "local"] - $commitHash = $($env:APPVEYOR_REPO_COMMIT.substring(0,7)) - $buildSuffix = @{ $true = "$($suffix)-$($commitHash)"; $false = "$($branch)-$($commitHash)" }[$suffix -ne ""] - $versionSuffix = @{ $true = "--version-suffix=$($suffix)"; $false = ""}[$suffix -ne ""] + $suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "master" -and $revision -ne "local"]; + $commitHash = $($env:APPVEYOR_REPO_COMMIT.substring(0,7)); + $buildSuffix = @{ $true = "$($suffix)-$($commitHash)"; $false = "$($branch)-$($commitHash)" }[$suffix -ne ""]; + $versionSuffix = @{ $true = "--version-suffix=$($suffix)"; $false = ""}[$suffix -ne ""]; nuget: disable_publish_on_pr: true From 0b84a5d9b132750f88ff59d7246c5908c255c644 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Fri, 22 Nov 2019 01:37:39 +0200 Subject: [PATCH 040/549] Fix ++ --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index a3d634f4..5335897a 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -17,7 +17,7 @@ init: # Set "build version number" to "short-commit-hash" or when tagged to "tag name" (Travis style) - ps: >- - $branch = @{ $true = $env:APPVEYOR_REPO_BRANCH; $false = $(git symbolic-ref --short -q HEAD) }[$env:APPVEYOR_REPO_BRANCH -ne $NULL]; + $branch = $env:APPVEYOR_REPO_BRANCH; $revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10); $false = "local" }[$env:APPVEYOR_BUILD_NUMBER -ne $NULL]; $suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "master" -and $revision -ne "local"]; $commitHash = $($env:APPVEYOR_REPO_COMMIT.substring(0,7)); From 9fd65c3c1edbfb34e6d44a9dc20b31c00623afa2 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Fri, 22 Nov 2019 08:44:30 +0200 Subject: [PATCH 041/549] Add BUILD_SUFFIX & VERSION_SUFFIX env variables --- appveyor.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 5335897a..6d470994 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -15,14 +15,13 @@ init: # Good practise, because Windows line endings are different from Unix/Linux ones - ps: git config --global core.autocrlf true - # Set "build version number" to "short-commit-hash" or when tagged to "tag name" (Travis style) - ps: >- $branch = $env:APPVEYOR_REPO_BRANCH; - $revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10); $false = "local" }[$env:APPVEYOR_BUILD_NUMBER -ne $NULL]; - $suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "master" -and $revision -ne "local"]; + $revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10); $false = "" }[$env:APPVEYOR_REPO_TAG -eq "false"]; + $suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "master" -and $revision -ne ""]; $commitHash = $($env:APPVEYOR_REPO_COMMIT.substring(0,7)); - $buildSuffix = @{ $true = "$($suffix)-$($commitHash)"; $false = "$($branch)-$($commitHash)" }[$suffix -ne ""]; - $versionSuffix = @{ $true = "--version-suffix=$($suffix)"; $false = ""}[$suffix -ne ""]; + $env:BUILD_SUFFIX = @{ $true = "$($suffix)-$($commitHash)"; $false = "$($branch)-$($commitHash)" }[$suffix -ne ""]; + $env:VERSION_SUFFIX = @{ $true = "--version-suffix=$($suffix)"; $false = ""}[$suffix -ne ""]; nuget: disable_publish_on_pr: true @@ -31,6 +30,8 @@ environment: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true DOTNET_CLI_TELEMETRY_OPTOUT: true APPVEYOR_YML_DISABLE_PS_LINUX: true + BUILD_SUFFIX: "" + VERSION_SUFFIX: "" install: - ps: dotnet restore redmine-net-api.sln @@ -39,10 +40,10 @@ before_build: - ps: dotnet --version build_script: - - ps: dotnet build redmine-net-api.sln -c Release --version-suffix=$buildSuffix + - ps: dotnet build redmine-net-api.sln -c Release --version-suffix=$env:BUILD_SUFFIX after_build: - - ps: dotnet pack src\redmine-net-api\redmine-net-api.csproj -c Release --output .\artifacts --include-symbols --no-build $versionSuffix + - ps: dotnet pack src\redmine-net-api\redmine-net-api.csproj -c Release --output .\artifacts --include-symbols --no-build $env:VERSION_SUFFIX test: off From a23a1e6f8acf5111599f9b89dcbc6e4fa1546306 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Fri, 22 Nov 2019 09:02:51 +0200 Subject: [PATCH 042/549] Upda appveyor.yml --- appveyor.yml | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 6d470994..89deda36 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -2,36 +2,36 @@ version: '{build}' image: - Visual Studio 2019 +environment: + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + APPVEYOR_YML_DISABLE_PS_LINUX: false + BUILD_SUFFIX: "" + VERSION_SUFFIX: "" + +configuration: Release + pull_requests: do_not_increment_build_number: true + +nuget: + disable_publish_on_pr: true branches: only: - master -configuration: Release - init: # Good practise, because Windows line endings are different from Unix/Linux ones - ps: git config --global core.autocrlf true - - ps: >- - $branch = $env:APPVEYOR_REPO_BRANCH; - $revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10); $false = "" }[$env:APPVEYOR_REPO_TAG -eq "false"]; - $suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "master" -and $revision -ne ""]; - $commitHash = $($env:APPVEYOR_REPO_COMMIT.substring(0,7)); - $env:BUILD_SUFFIX = @{ $true = "$($suffix)-$($commitHash)"; $false = "$($branch)-$($commitHash)" }[$suffix -ne ""]; - $env:VERSION_SUFFIX = @{ $true = "--version-suffix=$($suffix)"; $false = ""}[$suffix -ne ""]; + - ps: $branch = $env:APPVEYOR_REPO_BRANCH; + - ps: $revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10); $false = "" }[$env:APPVEYOR_REPO_TAG -eq "false"]; + - ps: $suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "master" -and $revision -ne ""]; + - ps: $commitHash = $($env:APPVEYOR_REPO_COMMIT.substring(0,7)); + - ps: $env:BUILD_SUFFIX = @{ $true = "$($suffix)-$($commitHash)"; $false = "$($branch)-$($commitHash)" }[$suffix -ne ""]; + - ps: $env:VERSION_SUFFIX = @{ $true = "--version-suffix=$($suffix)"; $false = ""}[$suffix -ne ""]; -nuget: - disable_publish_on_pr: true - -environment: - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true - DOTNET_CLI_TELEMETRY_OPTOUT: true - APPVEYOR_YML_DISABLE_PS_LINUX: true - BUILD_SUFFIX: "" - VERSION_SUFFIX: "" install: - ps: dotnet restore redmine-net-api.sln From 653966621b676176dc17b9c089038fe5a47250fd Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Fri, 22 Nov 2019 09:17:01 +0200 Subject: [PATCH 043/549] Update ++ --- appveyor.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 89deda36..ad140d01 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,8 +6,8 @@ environment: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true DOTNET_CLI_TELEMETRY_OPTOUT: true APPVEYOR_YML_DISABLE_PS_LINUX: false - BUILD_SUFFIX: "" - VERSION_SUFFIX: "" + BUILD_SUFFIX: " " + VERSION_SUFFIX: " " configuration: Release @@ -26,7 +26,7 @@ init: - ps: git config --global core.autocrlf true - ps: $branch = $env:APPVEYOR_REPO_BRANCH; - - ps: $revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10); $false = "" }[$env:APPVEYOR_REPO_TAG -eq "false"]; + - ps: $revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10); $false = "" }[$env:APPVEYOR_REPO_TAG -ne "true"]; - ps: $suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "master" -and $revision -ne ""]; - ps: $commitHash = $($env:APPVEYOR_REPO_COMMIT.substring(0,7)); - ps: $env:BUILD_SUFFIX = @{ $true = "$($suffix)-$($commitHash)"; $false = "$($branch)-$($commitHash)" }[$suffix -ne ""]; @@ -37,12 +37,16 @@ install: - ps: dotnet restore redmine-net-api.sln before_build: + - ps: "echo 'Build suffix: $env:BUILD_SUFFIX'" + - ps: "echo 'Version suffix: $env:VERSION_SUFFIX'" - ps: dotnet --version build_script: - ps: dotnet build redmine-net-api.sln -c Release --version-suffix=$env:BUILD_SUFFIX after_build: + - ps: echo Build suffix= $env:BUILD_SUFFIX + - ps: echo Version suffix= $env:VERSION_SUFFIX - ps: dotnet pack src\redmine-net-api\redmine-net-api.csproj -c Release --output .\artifacts --include-symbols --no-build $env:VERSION_SUFFIX test: off From c7feb0ed5fe01388bf1870870ec2721c6097abdd Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Fri, 22 Nov 2019 09:23:14 +0200 Subject: [PATCH 044/549] Update ++ --- appveyor.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index ad140d01..ee919dbb 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -37,16 +37,16 @@ install: - ps: dotnet restore redmine-net-api.sln before_build: - - ps: "echo 'Build suffix: $env:BUILD_SUFFIX'" - - ps: "echo 'Version suffix: $env:VERSION_SUFFIX'" + - ps: echo "Build suffix: $env:BUILD_SUFFIX" + - ps: echo "Version suffix: $env:VERSION_SUFFIX" - ps: dotnet --version build_script: - ps: dotnet build redmine-net-api.sln -c Release --version-suffix=$env:BUILD_SUFFIX after_build: - - ps: echo Build suffix= $env:BUILD_SUFFIX - - ps: echo Version suffix= $env:VERSION_SUFFIX + - ps: echo "Build suffix= $env:BUILD_SUFFIX" + - ps: echo "Version suffix= $env:VERSION_SUFFIX" - ps: dotnet pack src\redmine-net-api\redmine-net-api.csproj -c Release --output .\artifacts --include-symbols --no-build $env:VERSION_SUFFIX test: off From e94ba2cde1e8e8d2db062a92fd4979e6cfcbc598 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Fri, 22 Nov 2019 09:33:56 +0200 Subject: [PATCH 045/549] Update ++ --- appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index ee919dbb..a02a621d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -37,8 +37,8 @@ install: - ps: dotnet restore redmine-net-api.sln before_build: - - ps: echo "Build suffix: $env:BUILD_SUFFIX" - - ps: echo "Version suffix: $env:VERSION_SUFFIX" + - ps: write-host "Build Suffix=$env:BUILD_SUFFIX" -foregroundcolor Green + - ps: write-host "Version suffix=$env:VERSION_SUFFIX" -foregroundcolor Orange - ps: dotnet --version build_script: From 1dfe4d827e9b98ac690dcff53dc070e63d55b75f Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Fri, 22 Nov 2019 09:38:41 +0200 Subject: [PATCH 046/549] Fix output color identifier --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index a02a621d..2fac82d0 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -38,7 +38,7 @@ install: before_build: - ps: write-host "Build Suffix=$env:BUILD_SUFFIX" -foregroundcolor Green - - ps: write-host "Version suffix=$env:VERSION_SUFFIX" -foregroundcolor Orange + - ps: write-host "Version suffix=$env:VERSION_SUFFIX" -foregroundcolor Magenta - ps: dotnet --version build_script: From 06b79d78a4334f4e98cd30f3b13ce8e7dd8f33e8 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Fri, 22 Nov 2019 09:45:33 +0200 Subject: [PATCH 047/549] Fix conditions --- appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 2fac82d0..bc096588 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -26,8 +26,8 @@ init: - ps: git config --global core.autocrlf true - ps: $branch = $env:APPVEYOR_REPO_BRANCH; - - ps: $revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10); $false = "" }[$env:APPVEYOR_REPO_TAG -ne "true"]; - - ps: $suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "master" -and $revision -ne ""]; + - ps: $revision = @{ $true = ""; $false = "{0:00000}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10) }[$env:APPVEYOR_REPO_TAG -eq "true"]; + - ps: $suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "master" -and $revision -eq ""]; - ps: $commitHash = $($env:APPVEYOR_REPO_COMMIT.substring(0,7)); - ps: $env:BUILD_SUFFIX = @{ $true = "$($suffix)-$($commitHash)"; $false = "$($branch)-$($commitHash)" }[$suffix -ne ""]; - ps: $env:VERSION_SUFFIX = @{ $true = "--version-suffix=$($suffix)"; $false = ""}[$suffix -ne ""]; From 319ea799014915f0fd9fda41b38dec9d1c3e9505 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Fri, 22 Nov 2019 09:46:00 +0200 Subject: [PATCH 048/549] Remove VersionSuffix build number condition --- src/redmine-net-api/redmine-net-api.csproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index e57fe9e2..196c8cdc 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -40,8 +40,6 @@ Redmine .NET API Client 2.0.46 - pre - pre-$(BuildNumber) From ca94245ae2ba4a54a9de1ae46413851ff9de34c9 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Fri, 22 Nov 2019 10:00:46 +0200 Subject: [PATCH 049/549] Add macos & Ubuntu images --- appveyor.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index bc096588..c65b89b4 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,6 +1,8 @@ version: '{build}' image: - Visual Studio 2019 + - macos + - Ubuntu environment: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true From 49d75ef1a2229bc258e95e19785ee89a3d6610f6 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Fri, 22 Nov 2019 10:01:42 +0200 Subject: [PATCH 050/549] Remove dev deploy option --- appveyor.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index c65b89b4..6ada651a 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -75,14 +75,6 @@ for: - image: Ubuntu deploy: - - provider: NuGet - name: dev - api_key: - secure: fo+5VNPIRQ98jFPBZSd4SsOVyXEsTxnQ52VWTrg3sgH1GwGXhi70Q561eimlmRhy - skip_symbols: true - on: - branch: master - - provider: NuGet name: production api_key: From a31923b0e6d9da578863d35ca4f74d95d6036247 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Fri, 22 Nov 2019 10:24:25 +0200 Subject: [PATCH 051/549] Fix appveyor --- appveyor.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 6ada651a..881ad826 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,15 +1,14 @@ version: '{build}' image: - Visual Studio 2019 - - macos - Ubuntu environment: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true DOTNET_CLI_TELEMETRY_OPTOUT: true - APPVEYOR_YML_DISABLE_PS_LINUX: false - BUILD_SUFFIX: " " - VERSION_SUFFIX: " " + APPVEYOR_YML_DISABLE_PS_LINUX: true + BUILD_SUFFIX: "" + VERSION_SUFFIX: "" configuration: Release @@ -37,7 +36,8 @@ init: install: - ps: dotnet restore redmine-net-api.sln - + - sh: dotnet restore redmine-net-api.sln + before_build: - ps: write-host "Build Suffix=$env:BUILD_SUFFIX" -foregroundcolor Green - ps: write-host "Version suffix=$env:VERSION_SUFFIX" -foregroundcolor Magenta @@ -45,7 +45,8 @@ before_build: build_script: - ps: dotnet build redmine-net-api.sln -c Release --version-suffix=$env:BUILD_SUFFIX - + - sh: dotnet build redmine-net-api.sln -c Release --version-suffix=$env:BUILD_SUFFIX + after_build: - ps: echo "Build suffix= $env:BUILD_SUFFIX" - ps: echo "Version suffix= $env:VERSION_SUFFIX" From f68c4bd846920cb1938ecb52ec62ccd2f7bac24a Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Fri, 22 Nov 2019 10:48:55 +0200 Subject: [PATCH 052/549] Enable PS on Linux --- appveyor.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 881ad826..02cf0dd7 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,7 +6,7 @@ image: environment: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true DOTNET_CLI_TELEMETRY_OPTOUT: true - APPVEYOR_YML_DISABLE_PS_LINUX: true + APPVEYOR_YML_DISABLE_PS_LINUX: false BUILD_SUFFIX: "" VERSION_SUFFIX: "" @@ -32,11 +32,9 @@ init: - ps: $commitHash = $($env:APPVEYOR_REPO_COMMIT.substring(0,7)); - ps: $env:BUILD_SUFFIX = @{ $true = "$($suffix)-$($commitHash)"; $false = "$($branch)-$($commitHash)" }[$suffix -ne ""]; - ps: $env:VERSION_SUFFIX = @{ $true = "--version-suffix=$($suffix)"; $false = ""}[$suffix -ne ""]; - install: - ps: dotnet restore redmine-net-api.sln - - sh: dotnet restore redmine-net-api.sln before_build: - ps: write-host "Build Suffix=$env:BUILD_SUFFIX" -foregroundcolor Green @@ -45,7 +43,6 @@ before_build: build_script: - ps: dotnet build redmine-net-api.sln -c Release --version-suffix=$env:BUILD_SUFFIX - - sh: dotnet build redmine-net-api.sln -c Release --version-suffix=$env:BUILD_SUFFIX after_build: - ps: echo "Build suffix= $env:BUILD_SUFFIX" From c380d8b6687cf61dcc82d40bfa48435d445f152d Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Fri, 22 Nov 2019 11:31:13 +0200 Subject: [PATCH 053/549] Add regex to allow tags to be build --- appveyor.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/appveyor.yml b/appveyor.yml index 02cf0dd7..9899e0a0 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -21,6 +21,7 @@ nuget: branches: only: - master + - /\d\.\d\.\d/ init: # Good practise, because Windows line endings are different from Unix/Linux ones From 39a1a22bfe80f607d7aead81235339643da6595f Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Fri, 22 Nov 2019 11:32:58 +0200 Subject: [PATCH 054/549] Change version --- src/redmine-net-api/redmine-net-api.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 196c8cdc..93e5e287 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -39,7 +39,7 @@ ... Redmine .NET API Client - 2.0.46 + 2.0.47 From d9f5b14d70c13003178cc04093499168ee4ddb0b Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Fri, 22 Nov 2019 11:54:01 +0200 Subject: [PATCH 055/549] Add SymbolPackageFormat property to create a .snupkg file in addition to the .nupkg file --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 9899e0a0..38833ec7 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -48,7 +48,7 @@ build_script: after_build: - ps: echo "Build suffix= $env:BUILD_SUFFIX" - ps: echo "Version suffix= $env:VERSION_SUFFIX" - - ps: dotnet pack src\redmine-net-api\redmine-net-api.csproj -c Release --output .\artifacts --include-symbols --no-build $env:VERSION_SUFFIX + - ps: dotnet pack src\redmine-net-api\redmine-net-api.csproj -c Release --output .\artifacts --include-symbols -p:SymbolPackageFormat=snupkg --no-build $env:VERSION_SUFFIX test: off From bdcabb6a230ea4287de568ec5cbf2fd6c7549a71 Mon Sep 17 00:00:00 2001 From: Padi Date: Fri, 22 Nov 2019 12:33:41 +0200 Subject: [PATCH 056/549] Update README.md --- README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 0c540592..419816ca 100755 --- a/README.md +++ b/README.md @@ -1,11 +1,11 @@ ![Nuget](https://img.shields.io/nuget/dt/redmine-net-api) -![alt text](https://ci.appveyor.com/api/projects/status/github/zapadi/redmine-net-api?branch=master&svg=true) +![Appveyor last build status](https://ci.appveyor.com/api/projects/status/github/zapadi/redmine-net-api?branch=master&svg=true&passingText=master%20-%20OK&failingText=ups...) [![NuGet package](https://img.shields.io/nuget/v/redmine-api.svg)](https://www.nuget.org/packages/redmine-api) Buy Me A Coffee -# redmine-net-api ![](https://github.com/zapadi/redmine-net-api/blob/master/logo.png) +# redmine-net-api ![redmine-net-api logo](https://github.com/zapadi/redmine-net-api/blob/master/logo.png) redmine-net-api is a library for communicating with a Redmine project management application. @@ -14,11 +14,12 @@ redmine-net-api is a library for communicating with a Redmine project management * Supports GZipped responses from servers. * This API provides access and basic CRUD operations (create, read, update, delete) for the resources described below: -Resource | Read | Create | Update | Delete ----------|------|--------|--------|------- +|Resource | Read | Create | Update | Delete | +|:---------|:------:|:--------:|:--------:|:-------:| Attachments|x|x|-|- Custom Fields|x|-|-|- Enumerations |x|-|-|- + Files |x|x|-|- Groups|x|x|x|x Issues |x|x|x|x Issue Categories|x|x|x|x @@ -34,11 +35,10 @@ Resource | Read | Create | Update | Delete Users |x|x|x|x Versions |x|x|x|x Wiki Pages |x|x|x|x - Files |x|x|-|- ## WIKI -Please review the wiki pages on how to use **redmine-net-api**. +Please review the ![wiki](https://github.com/zapadi/redmine-net-api/wiki) pages on how to use **redmine-net-api**. ## Contributing Contributions are really appreciated! @@ -47,7 +47,7 @@ A good way to get started (flow): 1. Fork the redmine-net-api repository. 2. Create a new branch in your current repos from the 'master' branch. -3. 'Check out' the code with *Git*, *GitHub Desktop* or *SourceTree*. +3. 'Check out' the code with *Git*, *GitHub Desktop*, *SourceTree*, *GitKraken*, *etc*. 4. Push commits and create a Pull Request (PR) to redmine-net-api. ## License From 6822a54715051d4d7981da166df32830bf8fa52a Mon Sep 17 00:00:00 2001 From: Alexey MAGician Date: Sat, 23 Nov 2019 14:52:53 +0300 Subject: [PATCH 057/549] Add check on null "Trackers" and "EnabledModules" in class "Project" while writing XML. --- src/redmine-net-api/Types/Project.cs | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs index 98cb7f8f..eb18f1bb 100644 --- a/src/redmine-net-api/Types/Project.cs +++ b/src/redmine-net-api/Types/Project.cs @@ -216,20 +216,27 @@ public override void WriteXml(XmlWriter writer) writer.WriteIdOrEmpty(Parent, RedmineKeys.PARENT_ID); writer.WriteElementString(RedmineKeys.HOMEPAGE, HomePage); - var trackers = new List(); - foreach (var tracker in Trackers) + if (Trackers != null) { - trackers.Add(tracker as IValue); + var trackers = new List(); + foreach (var tracker in Trackers) + { + trackers.Add(tracker as IValue); + } + writer.WriteListElements(trackers, RedmineKeys.TRACKER_IDS); } - var enabledModules = new List(); - foreach (var enabledModule in EnabledModules) + + if (EnabledModules != null) { - enabledModules.Add(enabledModule as IValue); - } + var enabledModules = new List(); + foreach (var enabledModule in EnabledModules) + { + enabledModules.Add(enabledModule as IValue); + } - writer.WriteListElements(trackers, RedmineKeys.TRACKER_IDS); - writer.WriteListElements(enabledModules, RedmineKeys.ENABLED_MODULE_NAMES); + writer.WriteListElements(enabledModules, RedmineKeys.ENABLED_MODULE_NAMES); + } if (Id == 0) return; From 012c4f20cdad03c8e7607f22cea523617f21d87d Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Sat, 23 Nov 2019 14:43:53 +0200 Subject: [PATCH 058/549] Fix CA1720: Identifiers should not contain type names --- .../Async/RedmineManagerAsync.cs | 18 +++++++-------- .../Async/RedmineManagerAsync40.cs | 20 ++++++++--------- .../Async/RedmineManagerAsync45.cs | 22 +++++++++---------- src/redmine-net-api/IRedmineManager.cs | 16 +++++++------- 4 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/redmine-net-api/Async/RedmineManagerAsync.cs b/src/redmine-net-api/Async/RedmineManagerAsync.cs index 887ef118..68468dea 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync.cs @@ -156,11 +156,11 @@ public static Task GetObjectAsync(this RedmineManager redmineManager, stri /// /// /// The redmine manager. - /// The object. + /// The object. /// - public static Task CreateObjectAsync(this RedmineManager redmineManager, T obj) where T : class, new() + public static Task CreateObjectAsync(this RedmineManager redmineManager, T entity) where T : class, new() { - return CreateObjectAsync(redmineManager, obj, null); + return CreateObjectAsync(redmineManager, entity, null); } /// @@ -168,13 +168,13 @@ public static Task GetObjectAsync(this RedmineManager redmineManager, stri /// /// /// The redmine manager. - /// The object. + /// The object. /// The owner identifier. /// - public static Task CreateObjectAsync(this RedmineManager redmineManager, T obj, string ownerId) + public static Task CreateObjectAsync(this RedmineManager redmineManager, T entity, string ownerId) where T : class, new() { - return delegate { return redmineManager.CreateObject(obj, ownerId); }; + return delegate { return redmineManager.CreateObject(entity, ownerId); }; } /// @@ -209,13 +209,13 @@ public static Task> GetObjectsAsync(this RedmineManager redmineManage /// /// The redmine manager. /// The identifier. - /// The object. + /// The object. /// The project identifier. /// - public static Task UpdateObjectAsync(this RedmineManager redmineManager, string id, T obj, + public static Task UpdateObjectAsync(this RedmineManager redmineManager, string id, T entity, string projectId = null) where T : class, new() { - return delegate { redmineManager.UpdateObject(id, obj, projectId); }; + return delegate { redmineManager.UpdateObject(id, entity, projectId); }; } /// diff --git a/src/redmine-net-api/Async/RedmineManagerAsync40.cs b/src/redmine-net-api/Async/RedmineManagerAsync40.cs index 2b8a1f39..d2b45f28 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync40.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync40.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -156,11 +156,11 @@ public static Task RemoveWatcherFromIssueAsync(this RedmineManager redmineManage /// /// /// The redmine manager. - /// The object. + /// The object. /// - public static Task CreateObjectAsync(this RedmineManager redmineManager, T obj) where T : class, new() + public static Task CreateObjectAsync(this RedmineManager redmineManager, T entity) where T : class, new() { - return CreateObjectAsync(redmineManager, obj, null); + return CreateObjectAsync(redmineManager, entity, null); } @@ -193,12 +193,12 @@ public static Task RemoveWatcherFromIssueAsync(this RedmineManager redmineManage /// /// /// The redmine manager. - /// The object. + /// The object. /// The owner identifier. /// - public static Task CreateObjectAsync(this RedmineManager redmineManager, T obj, string ownerId) where T : class, new() + public static Task CreateObjectAsync(this RedmineManager redmineManager, T entity, string ownerId) where T : class, new() { - return Task.Factory.StartNew(() => redmineManager.CreateObject(obj, ownerId), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); + return Task.Factory.StartNew(() => redmineManager.CreateObject(entity, ownerId), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); } /// @@ -231,12 +231,12 @@ public static Task RemoveWatcherFromIssueAsync(this RedmineManager redmineManage /// /// The redmine manager. /// The identifier. - /// The object. + /// The object. /// The project identifier. /// - public static Task UpdateObjectAsync(this RedmineManager redmineManager, string id, T obj, string projectId = null) where T : class, new() + public static Task UpdateObjectAsync(this RedmineManager redmineManager, string id, T entity, string projectId = null) where T : class, new() { - return Task.Factory.StartNew(() => redmineManager.UpdateObject(id, obj, projectId), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); + return Task.Factory.StartNew(() => redmineManager.UpdateObject(id, entity, projectId), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); } /// diff --git a/src/redmine-net-api/Async/RedmineManagerAsync45.cs b/src/redmine-net-api/Async/RedmineManagerAsync45.cs index e2cae408..77a7257b 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync45.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync45.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -330,12 +330,12 @@ public static async Task GetObjectAsync(this RedmineManager redmineManager /// /// The type of object to create. /// The redmine manager. - /// The object to create. + /// The object to create. /// - public static async Task CreateObjectAsync(this RedmineManager redmineManager, T obj) + public static async Task CreateObjectAsync(this RedmineManager redmineManager, T entity) where T : class, new() { - return await CreateObjectAsync(redmineManager, obj, null).ConfigureAwait(false); + return await CreateObjectAsync(redmineManager, entity, null).ConfigureAwait(false); } /// @@ -343,14 +343,14 @@ public static async Task CreateObjectAsync(this RedmineManager redmineMana /// /// The type of object to create. /// The redmine manager. - /// The object to create. + /// The object to create. /// The owner identifier. /// - public static async Task CreateObjectAsync(this RedmineManager redmineManager, T obj, string ownerId) + public static async Task CreateObjectAsync(this RedmineManager redmineManager, T entity, string ownerId) where T : class, new() { var uri = UrlHelper.GetCreateUrl(redmineManager, ownerId); - var data = RedmineSerializer.Serialize(obj, redmineManager.MimeFormat); + var data = RedmineSerializer.Serialize(entity, redmineManager.MimeFormat); var response = await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.POST, data, "CreateObjectAsync").ConfigureAwait(false); return RedmineSerializer.Deserialize(response, redmineManager.MimeFormat); @@ -362,14 +362,14 @@ public static async Task CreateObjectAsync(this RedmineManager redmineMana /// /// The redmine manager. /// The identifier. - /// The object. + /// The object. /// The project identifier. /// - public static async Task UpdateObjectAsync(this RedmineManager redmineManager, string id, T obj, string projectId = null) + public static async Task UpdateObjectAsync(this RedmineManager redmineManager, string id, T entity) where T : class, new() { - var uri = UrlHelper.GetUploadUrl(redmineManager, id, obj, projectId); - var data = RedmineSerializer.Serialize(obj, redmineManager.MimeFormat); + var uri = UrlHelper.GetUploadUrl(redmineManager, id); + var data = RedmineSerializer.Serialize(entity, redmineManager.MimeFormat); data = Regex.Replace(data, @"\r\n|\r|\n", "\r\n"); await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.PUT, data, "UpdateObjectAsync").ConfigureAwait(false); diff --git a/src/redmine-net-api/IRedmineManager.cs b/src/redmine-net-api/IRedmineManager.cs index 4a39c132..71885597 100644 --- a/src/redmine-net-api/IRedmineManager.cs +++ b/src/redmine-net-api/IRedmineManager.cs @@ -198,34 +198,34 @@ public interface IRedmineManager /// /// /// - /// + /// /// /// - T CreateObject(T obj) where T : class, new(); + T CreateObject(T entity) where T : class, new(); /// /// /// - /// + /// /// /// /// - T CreateObject(T obj, string ownerId) where T : class, new(); + T CreateObject(T entity, string ownerId) where T : class, new(); /// /// /// /// - /// + /// /// - void UpdateObject(string id, T obj) where T : class, new(); + void UpdateObject(string id, T entity) where T : class, new(); /// /// /// /// - /// + /// /// /// - void UpdateObject(string id, T obj, string projectId) where T : class, new(); + void UpdateObject(string id, T entity, string projectId) where T : class, new(); /// /// From 46140f5c188ae3a35bf901fe56d27e9eb7959456 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Sat, 23 Nov 2019 15:08:03 +0200 Subject: [PATCH 059/549] Cleanup --- .../Extensions/CollectionExtensions.cs | 2 +- .../Extensions/StringExtensions.cs | 2 +- src/redmine-net-api/Extensions/WebExtensions.cs | 2 +- .../Internals/RedmineSerializer.cs | 16 ++++++++-------- src/redmine-net-api/RedmineManager.cs | 2 +- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/redmine-net-api/Extensions/CollectionExtensions.cs b/src/redmine-net-api/Extensions/CollectionExtensions.cs index 187e6f1a..07d3b16b 100755 --- a/src/redmine-net-api/Extensions/CollectionExtensions.cs +++ b/src/redmine-net-api/Extensions/CollectionExtensions.cs @@ -85,7 +85,7 @@ public static string Dump(this IEnumerable collection) where TIn : cla return null; } - StringBuilder sb = new StringBuilder(); + var sb = new StringBuilder(); foreach (var item in collection) { sb.Append(",").Append(item); diff --git a/src/redmine-net-api/Extensions/StringExtensions.cs b/src/redmine-net-api/Extensions/StringExtensions.cs index 5f563fa4..b0219ed9 100644 --- a/src/redmine-net-api/Extensions/StringExtensions.cs +++ b/src/redmine-net-api/Extensions/StringExtensions.cs @@ -18,7 +18,7 @@ public static bool IsNullOrWhiteSpace(this string value) return true; } - for (int index = 0; index < value.Length; ++index) + for (var index = 0; index < value.Length; ++index) { if (!char.IsWhiteSpace(value[index])) { diff --git a/src/redmine-net-api/Extensions/WebExtensions.cs b/src/redmine-net-api/Extensions/WebExtensions.cs index 30971246..0a041489 100755 --- a/src/redmine-net-api/Extensions/WebExtensions.cs +++ b/src/redmine-net-api/Extensions/WebExtensions.cs @@ -77,7 +77,7 @@ public static void HandleWebException(this WebException exception, string method case 422: var errors = GetRedmineExceptions(exception.Response, mimeFormat); - string message = string.Empty; + var message = string.Empty; if (errors != null) { for (var index = 0; index < errors.Count; index++) diff --git a/src/redmine-net-api/Internals/RedmineSerializer.cs b/src/redmine-net-api/Internals/RedmineSerializer.cs index 8045bac5..795d08fe 100755 --- a/src/redmine-net-api/Internals/RedmineSerializer.cs +++ b/src/redmine-net-api/Internals/RedmineSerializer.cs @@ -87,12 +87,13 @@ private static T FromXML(string xml) where T : class { try { -#if !NET20 if (mimeFormat == MimeFormat.Json) { +#if !NET20 return JsonSerializer(obj); - } #endif + } + return ToXML(obj); } catch (Exception ex) @@ -121,9 +122,9 @@ private static T FromXML(string xml) where T : class if (string.IsNullOrEmpty(response)) throw new RedmineException("Could not deserialize null!"); try { -#if !NET20 if (mimeFormat == MimeFormat.Json) { +#if !NET20 var type = typeof (T); var jsonRoot = (string) null; if (type == typeof (IssueCategory)) jsonRoot = RedmineKeys.ISSUE_CATEGORY; @@ -132,8 +133,8 @@ private static T FromXML(string xml) where T : class if (type == typeof (ProjectMembership)) jsonRoot = RedmineKeys.MEMBERSHIP; if (type == typeof (WikiPage)) jsonRoot = RedmineKeys.WIKI_PAGE; return JsonDeserialize(response, jsonRoot); - } #endif + } return FromXML(response); } catch (Exception ex) @@ -160,12 +161,12 @@ public static PaginatedObjects DeserializeList(string response, MimeFormat try { if (response.IsNullOrWhiteSpace()) throw new RedmineException("Could not deserialize null!"); -#if !NET20 if (mimeFormat == MimeFormat.Json) { +#if !NET20 return JSonDeserializeList(response); - } #endif + } return XmlDeserializeList(response); } @@ -194,7 +195,7 @@ public static PaginatedObjects DeserializeList(string response, MimeFormat if (string.IsNullOrEmpty(jsonRoot)) jsonRoot = RedmineManager.Sufixes[type]; - var result = JsonDeserializeToList(response, jsonRoot, out int totalItems, out int offset); + var result = JsonDeserializeToList(response, jsonRoot, out var totalItems, out var offset); return new PaginatedObjects() { @@ -216,7 +217,6 @@ public static PaginatedObjects DeserializeList(string response, MimeFormat { using (var xmlReader = XmlTextReaderBuilder.Create(stringReader)) { - xmlReader.Read(); xmlReader.Read(); diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index 82b5649c..406758c0 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -723,7 +723,7 @@ public void DeleteWikiPage(string projectId, string pageName) /// public Upload UploadFile(byte[] data) { - string url = UrlHelper.GetUploadFileUrl(this); + var url = UrlHelper.GetUploadFileUrl(this); return WebApiHelper.ExecuteUploadFile(this, url, data, "UploadFile"); } From 805b443693bdb5be21fd177e104c61360d517055 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Sat, 23 Nov 2019 15:08:18 +0200 Subject: [PATCH 060/549] Remove unused parameters --- src/redmine-net-api/Async/RedmineManagerAsync45.cs | 4 ++-- src/redmine-net-api/Internals/UrlHelper.cs | 10 +++------- src/redmine-net-api/RedmineManager.cs | 8 ++++---- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/src/redmine-net-api/Async/RedmineManagerAsync45.cs b/src/redmine-net-api/Async/RedmineManagerAsync45.cs index 77a7257b..273c3cfd 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync45.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync45.cs @@ -113,7 +113,7 @@ public static async Task DownloadFileAsync(this RedmineManager redmineMa public static async Task GetWikiPageAsync(this RedmineManager redmineManager, string projectId, NameValueCollection parameters, string pageName, uint version = 0) { - var uri = UrlHelper.GetWikiPageUrl(redmineManager, projectId, parameters, pageName, version); + var uri = UrlHelper.GetWikiPageUrl(redmineManager, projectId, pageName, version); return await WebApiAsyncHelper.ExecuteDownload(redmineManager, uri, "GetWikiPageAsync", parameters).ConfigureAwait(false); } @@ -170,7 +170,7 @@ public static async Task RemoveUserFromGroupAsync(this RedmineManager redmineMan public static async Task AddWatcherToIssueAsync(this RedmineManager redmineManager, int issueId, int userId) { var data = DataHelper.UserData(userId, redmineManager.MimeFormat); - var uri = UrlHelper.GetAddWatcherUrl(redmineManager, issueId, userId); + var uri = UrlHelper.GetAddWatcherUrl(redmineManager, issueId); await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.POST, data, "AddWatcherAsync").ConfigureAwait(false); } diff --git a/src/redmine-net-api/Internals/UrlHelper.cs b/src/redmine-net-api/Internals/UrlHelper.cs index c4c9b221..8bc02e5d 100644 --- a/src/redmine-net-api/Internals/UrlHelper.cs +++ b/src/redmine-net-api/Internals/UrlHelper.cs @@ -73,11 +73,9 @@ internal static class UrlHelper /// /// The redmine manager. /// The identifier. - /// The object. - /// The project identifier. /// /// - public static string GetUploadUrl(RedmineManager redmineManager, string id, T obj, string projectId = null) + public static string GetUploadUrl(RedmineManager redmineManager, string id) where T : class, new() { var type = typeof(T); @@ -240,12 +238,11 @@ public static string GetWikisUrl(RedmineManager redmineManager, string projectId /// /// The redmine manager. /// The project identifier. - /// The parameters. /// Name of the page. /// The version. /// public static string GetWikiPageUrl(RedmineManager redmineManager, string projectId, - NameValueCollection parameters, string pageName, uint version = 0) + string pageName, uint version = 0) { var uri = version == 0 ? string.Format(CultureInfo.InvariantCulture,WIKI_PAGE_FORMAT, redmineManager.Host, projectId, pageName, @@ -336,9 +333,8 @@ public static string GetDeleteWikirUrl(RedmineManager redmineManager, string pro /// /// The redmine manager. /// The issue identifier. - /// The user identifier. /// - public static string GetAddWatcherUrl(RedmineManager redmineManager, int issueId, int userId) + public static string GetAddWatcherUrl(RedmineManager redmineManager, int issueId) { return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, RedmineManager.Sufixes[typeof(Issue)], $"{issueId.ToString(CultureInfo.InvariantCulture)}/watchers", diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index 406758c0..792919b9 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -296,7 +296,7 @@ public User GetCurrentUser(NameValueCollection parameters = null) /// The user identifier. public void AddWatcherToIssue(int issueId, int userId) { - var url = UrlHelper.GetAddWatcherUrl(this, issueId, userId); + var url = UrlHelper.GetAddWatcherUrl(this, issueId); WebApiHelper.ExecuteUpload(this, url, HttpVerbs.POST, DataHelper.UserData(userId, MimeFormat), "AddWatcher"); } @@ -359,7 +359,7 @@ public WikiPage CreateOrUpdateWikiPage(string projectId, string pageName, WikiPa /// public WikiPage GetWikiPage(string projectId, NameValueCollection parameters, string pageName, uint version = 0) { - var url = UrlHelper.GetWikiPageUrl(this, projectId, parameters, pageName, version); + var url = UrlHelper.GetWikiPageUrl(this, projectId, pageName, version); return WebApiHelper.ExecuteDownload(this, url, "GetWikiPage", parameters); } @@ -674,7 +674,7 @@ public void DeleteWikiPage(string projectId, string pageName) /// public void UpdateObject(string id, T obj, string projectId) where T : class, new() { - var url = UrlHelper.GetUploadUrl(this, id, obj, projectId); + var url = UrlHelper.GetUploadUrl(this, id); var data = RedmineSerializer.Serialize(obj, MimeFormat); data = Regex.Replace(data, @"\r\n|\r|\n", "\r\n"); WebApiHelper.ExecuteUpload(this, url, HttpVerbs.PUT, data, "UpdateObject"); From d5520b452824f130e45af8660964884a08b50832 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Sat, 23 Nov 2019 15:04:18 +0200 Subject: [PATCH 061/549] Fix [CA1825] Avoid unnecessary zero-length array allocations. --- src/redmine-net-api/Extensions/XmlWriterExtensions.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/redmine-net-api/Extensions/XmlWriterExtensions.cs b/src/redmine-net-api/Extensions/XmlWriterExtensions.cs index 29701be8..9266a4ea 100755 --- a/src/redmine-net-api/Extensions/XmlWriterExtensions.cs +++ b/src/redmine-net-api/Extensions/XmlWriterExtensions.cs @@ -31,7 +31,11 @@ namespace Redmine.Net.Api.Extensions public static partial class XmlExtensions { + // #if !(NET20 || NET40 || NET45 || NET451 || NET452) + // private static readonly Type[] emptyTypeArray = Array.Empty(); + // #else private static readonly Type[] emptyTypeArray = new Type[0]; + // #endif private static readonly XmlAttributeOverrides xmlAttributeOverrides = new XmlAttributeOverrides(); /// From bf5a084a9e6f6fe979b712a187f2625b4bf4f115 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Sat, 23 Nov 2019 15:05:21 +0200 Subject: [PATCH 062/549] Suppress CA1308: Normalize strings to uppercase --- src/redmine-net-api/Extensions/StringExtensions.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/redmine-net-api/Extensions/StringExtensions.cs b/src/redmine-net-api/Extensions/StringExtensions.cs index b0219ed9..ea2d011e 100644 --- a/src/redmine-net-api/Extensions/StringExtensions.cs +++ b/src/redmine-net-api/Extensions/StringExtensions.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; + namespace Redmine.Net.Api.Extensions { /// @@ -55,6 +57,7 @@ public static string Truncate(this string text, int maximumLength) /// /// /// + [SuppressMessage("ReSharper", "CA1308")] public static string ToLowerInv(this string text) { return text.IsNullOrWhiteSpace() ? text : text.ToLowerInvariant(); From ef6255dd2bce079f155225565514334d26572b66 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Sat, 23 Nov 2019 15:18:55 +0200 Subject: [PATCH 063/549] Override Equals --- src/redmine-net-api/Types/Attachment.cs | 6 ++++++ src/redmine-net-api/Types/IdentifiableName.cs | 6 ++++++ src/redmine-net-api/Types/Issue.cs | 6 ++++++ src/redmine-net-api/Types/IssueCategory.cs | 6 ++++++ src/redmine-net-api/Types/IssueChild.cs | 6 ++++++ src/redmine-net-api/Types/IssueCustomField.cs | 6 ++++++ src/redmine-net-api/Types/IssueRelation.cs | 6 ++++++ src/redmine-net-api/Types/IssueStatus.cs | 6 ++++++ src/redmine-net-api/Types/Membership.cs | 6 ++++++ src/redmine-net-api/Types/MembershipRole.cs | 6 ++++++ src/redmine-net-api/Types/News.cs | 6 ++++++ src/redmine-net-api/Types/Project.cs | 6 ++++++ src/redmine-net-api/Types/ProjectMembership.cs | 6 ++++++ src/redmine-net-api/Types/Query.cs | 6 ++++++ src/redmine-net-api/Types/TimeEntry.cs | 6 ++++++ src/redmine-net-api/Types/User.cs | 6 ++++++ src/redmine-net-api/Types/Version.cs | 6 ++++++ src/redmine-net-api/Types/WikiPage.cs | 6 ++++++ 18 files changed, 108 insertions(+) diff --git a/src/redmine-net-api/Types/Attachment.cs b/src/redmine-net-api/Types/Attachment.cs index 4a125135..7db7dd35 100755 --- a/src/redmine-net-api/Types/Attachment.cs +++ b/src/redmine-net-api/Types/Attachment.cs @@ -176,5 +176,11 @@ public override string ToString() return $"[Attachment: {base.ToString()}, FileName={FileName}, FileSize={FileSize}, ContentType={ContentType}, Description={Description}, ContentUrl={ContentUrl}, Author={Author}, CreatedOn={CreatedOn}]"; } + + /// + public override bool Equals(object obj) + { + return Equals(obj as Attachment); + } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/IdentifiableName.cs b/src/redmine-net-api/Types/IdentifiableName.cs index 5f585dfe..1eb9e882 100755 --- a/src/redmine-net-api/Types/IdentifiableName.cs +++ b/src/redmine-net-api/Types/IdentifiableName.cs @@ -115,5 +115,11 @@ public override int GetHashCode() return hashCode; } } + + /// + public override bool Equals(object obj) + { + return Equals(obj as IdentifiableName); + } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Issue.cs b/src/redmine-net-api/Types/Issue.cs index 1889566e..9dd7b0ef 100644 --- a/src/redmine-net-api/Types/Issue.cs +++ b/src/redmine-net-api/Types/Issue.cs @@ -614,5 +614,11 @@ public override int GetHashCode() return hashCode; } + + /// + public override bool Equals(object obj) + { + return Equals(obj as Issue); + } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/IssueCategory.cs b/src/redmine-net-api/Types/IssueCategory.cs index bc0a26ad..870cb0c9 100755 --- a/src/redmine-net-api/Types/IssueCategory.cs +++ b/src/redmine-net-api/Types/IssueCategory.cs @@ -139,5 +139,11 @@ public override string ToString() { return $"[IssueCategory: {base.ToString()}, Project={Project}, AsignTo={AsignTo}, Name={Name}]"; } + + /// + public override bool Equals(object obj) + { + return Equals(obj as IssueCategory); + } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/IssueChild.cs b/src/redmine-net-api/Types/IssueChild.cs index 49fddc84..9700b43c 100755 --- a/src/redmine-net-api/Types/IssueChild.cs +++ b/src/redmine-net-api/Types/IssueChild.cs @@ -128,5 +128,11 @@ public override string ToString() { return $"[IssueChild: {base.ToString()}, Tracker={Tracker}, Subject={Subject}]"; } + + /// + public override bool Equals(object obj) + { + return Equals(obj as IssueChild); + } } } diff --git a/src/redmine-net-api/Types/IssueCustomField.cs b/src/redmine-net-api/Types/IssueCustomField.cs index fb779dea..8e2c94b5 100755 --- a/src/redmine-net-api/Types/IssueCustomField.cs +++ b/src/redmine-net-api/Types/IssueCustomField.cs @@ -144,5 +144,11 @@ public static string GetValue(object item) if (item == null) throw new ArgumentNullException(nameof(item)); return ((CustomFieldValue)item).Info; } + + /// + public override bool Equals(object obj) + { + return Equals(obj as IssueCustomField); + } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/IssueRelation.cs b/src/redmine-net-api/Types/IssueRelation.cs index fa8a533a..1b59e086 100755 --- a/src/redmine-net-api/Types/IssueRelation.cs +++ b/src/redmine-net-api/Types/IssueRelation.cs @@ -172,5 +172,11 @@ public override string ToString() return $"[IssueRelation: {base.ToString()}, IssueId={IssueId}, IssueToId={IssueToId}, Type={Type}, Delay={Delay}]"; } + + /// + public override bool Equals(object obj) + { + return Equals(obj as IssueRelation); + } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/IssueStatus.cs b/src/redmine-net-api/Types/IssueStatus.cs index 63a2bd69..f551e886 100755 --- a/src/redmine-net-api/Types/IssueStatus.cs +++ b/src/redmine-net-api/Types/IssueStatus.cs @@ -116,5 +116,11 @@ public override string ToString() { return $"[IssueStatus: {base.ToString()}, IsDefault={IsDefault}, IsClosed={IsClosed}]"; } + + /// + public override bool Equals(object obj) + { + return Equals(obj as IssueStatus); + } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Membership.cs b/src/redmine-net-api/Types/Membership.cs index 44e22724..dcc112d0 100755 --- a/src/redmine-net-api/Types/Membership.cs +++ b/src/redmine-net-api/Types/Membership.cs @@ -123,5 +123,11 @@ public override string ToString() { return $"[Membership: {base.ToString()}, Project={Project}, Roles={Roles}]"; } + + /// + public override bool Equals(object obj) + { + return Equals(obj as Membership); + } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/MembershipRole.cs b/src/redmine-net-api/Types/MembershipRole.cs index edfe3bae..18228734 100755 --- a/src/redmine-net-api/Types/MembershipRole.cs +++ b/src/redmine-net-api/Types/MembershipRole.cs @@ -96,5 +96,11 @@ public override string ToString() { return $"[MembershipRole: {base.ToString()}, Inherited={Inherited}]"; } + + /// + public override bool Equals(object obj) + { + return Equals(obj as MembershipRole); + } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/News.cs b/src/redmine-net-api/Types/News.cs index 80bd3664..dbdda7ef 100755 --- a/src/redmine-net-api/Types/News.cs +++ b/src/redmine-net-api/Types/News.cs @@ -165,5 +165,11 @@ public override string ToString() return $"[News: {base.ToString()}, Project={Project}, Author={Author}, Title={Title}, Summary={Summary}, Description={Description}, CreatedOn={CreatedOn}]"; } + + /// + public override bool Equals(object obj) + { + return Equals(obj as News); + } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs index eb18f1bb..69147165 100644 --- a/src/redmine-net-api/Types/Project.cs +++ b/src/redmine-net-api/Types/Project.cs @@ -305,5 +305,11 @@ public override string ToString() return $"[Project: {base.ToString()}, Identifier={Identifier}, Description={Description}, Parent={Parent}, HomePage={HomePage}, CreatedOn={CreatedOn}, UpdatedOn={UpdatedOn}, Status={Status}, IsPublic={IsPublic}, InheritMembers={InheritMembers}, Trackers={Trackers}, CustomFields={CustomFields}, IssueCategories={IssueCategories}, EnabledModules={EnabledModules}]"; } + + /// + public override bool Equals(object obj) + { + return Equals(obj as Project); + } } } diff --git a/src/redmine-net-api/Types/ProjectMembership.cs b/src/redmine-net-api/Types/ProjectMembership.cs index 6175d16c..55520d2e 100755 --- a/src/redmine-net-api/Types/ProjectMembership.cs +++ b/src/redmine-net-api/Types/ProjectMembership.cs @@ -157,5 +157,11 @@ public override string ToString() return $"[ProjectMembership: {base.ToString()}, Project={Project}, User={User}, Group={Group}, Roles={Roles}]"; } + + /// + public override bool Equals(object obj) + { + return Equals(obj as ProjectMembership); + } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Query.cs b/src/redmine-net-api/Types/Query.cs index 160deacf..cfb93812 100755 --- a/src/redmine-net-api/Types/Query.cs +++ b/src/redmine-net-api/Types/Query.cs @@ -115,5 +115,11 @@ public override string ToString() { return $"[Query: {base.ToString()}, IsPublic={IsPublic}, ProjectId={ProjectId}]"; } + + /// + public override bool Equals(object obj) + { + return Equals(obj as Query); + } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/TimeEntry.cs b/src/redmine-net-api/Types/TimeEntry.cs index 68f67281..71e81fe0 100644 --- a/src/redmine-net-api/Types/TimeEntry.cs +++ b/src/redmine-net-api/Types/TimeEntry.cs @@ -249,5 +249,11 @@ public override string ToString() return $"[TimeEntry: {base.ToString()}, Issue={Issue}, Project={Project}, SpentOn={SpentOn}, Hours={Hours}, Activity={Activity}, User={User}, Comments={Comments}, CreatedOn={CreatedOn}, UpdatedOn={UpdatedOn}, CustomFields={CustomFields}]"; } + + /// + public override bool Equals(object obj) + { + return Equals(obj as TimeEntry); + } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/User.cs b/src/redmine-net-api/Types/User.cs index b901c77c..b3e6a52c 100644 --- a/src/redmine-net-api/Types/User.cs +++ b/src/redmine-net-api/Types/User.cs @@ -303,5 +303,11 @@ public override string ToString() return $"[User: {Groups}, Login={Login}, Password={Password}, FirstName={FirstName}, LastName={LastName}, Email={Email}, EmailNotification={MailNotification}, AuthenticationModeId={AuthenticationModeId}, CreatedOn={CreatedOn}, LastLoginOn={LastLoginOn}, ApiKey={ApiKey}, Status={Status}, MustChangePassword={MustChangePassword}, CustomFields={CustomFields}, Memberships={Memberships}, Groups={Groups}]"; } + + /// + public override bool Equals(object obj) + { + return Equals(obj as User); + } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Version.cs b/src/redmine-net-api/Types/Version.cs index 7b341dd4..f49c8d07 100755 --- a/src/redmine-net-api/Types/Version.cs +++ b/src/redmine-net-api/Types/Version.cs @@ -192,6 +192,12 @@ public override string ToString() return $"[Version: {base.ToString()}, Project={Project}, Description={Description}, Status={Status}, DueDate={DueDate}, Sharing={Sharing}, CreatedOn={CreatedOn}, UpdatedOn={UpdatedOn}, CustomFields={CustomFields}]"; } + + /// + public override bool Equals(object obj) + { + return Equals(obj as Version); + } } /// diff --git a/src/redmine-net-api/Types/WikiPage.cs b/src/redmine-net-api/Types/WikiPage.cs index 9adfbff3..d98a66ec 100755 --- a/src/redmine-net-api/Types/WikiPage.cs +++ b/src/redmine-net-api/Types/WikiPage.cs @@ -209,6 +209,12 @@ public override string ToString() $"[WikiPage: {base.ToString()}, Title={Title}, Text={Text}, Comments={Comments}, Version={Version}, Author={Author}, CreatedOn={CreatedOn}, UpdatedOn={UpdatedOn}, Attachments={Attachments}]"; } + /// + public override bool Equals(object obj) + { + return Equals(obj as WikiPage); + } + #endregion } } \ No newline at end of file From bf062e88d3b1255948fb74422645627d698f1c04 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Sat, 23 Nov 2019 15:30:18 +0200 Subject: [PATCH 064/549] Modify collections setters accessor to internal --- .../Extensions/XmlWriterExtensions.cs | 8 ++++---- src/redmine-net-api/IRedmineWebClient.cs | 2 +- src/redmine-net-api/Types/CustomField.cs | 6 +++--- src/redmine-net-api/Types/Group.cs | 6 +++--- src/redmine-net-api/Types/Issue.cs | 16 ++++++++-------- src/redmine-net-api/Types/IssueCustomField.cs | 2 +- src/redmine-net-api/Types/Journal.cs | 2 +- src/redmine-net-api/Types/Membership.cs | 2 +- src/redmine-net-api/Types/PaginatedObjects.cs | 2 +- src/redmine-net-api/Types/Project.cs | 10 +++++----- src/redmine-net-api/Types/ProjectMembership.cs | 2 +- src/redmine-net-api/Types/Role.cs | 2 +- src/redmine-net-api/Types/TimeEntry.cs | 2 +- src/redmine-net-api/Types/Upload.cs | 2 +- src/redmine-net-api/Types/User.cs | 6 +++--- src/redmine-net-api/Types/Version.cs | 2 +- src/redmine-net-api/Types/WikiPage.cs | 4 ++-- 17 files changed, 38 insertions(+), 38 deletions(-) diff --git a/src/redmine-net-api/Extensions/XmlWriterExtensions.cs b/src/redmine-net-api/Extensions/XmlWriterExtensions.cs index 9266a4ea..7018f3f5 100755 --- a/src/redmine-net-api/Extensions/XmlWriterExtensions.cs +++ b/src/redmine-net-api/Extensions/XmlWriterExtensions.cs @@ -31,11 +31,11 @@ namespace Redmine.Net.Api.Extensions public static partial class XmlExtensions { - // #if !(NET20 || NET40 || NET45 || NET451 || NET452) - // private static readonly Type[] emptyTypeArray = Array.Empty(); - // #else + #if !(NET20 || NET40 || NET45 || NET451 || NET452) + private static readonly Type[] emptyTypeArray = Array.Empty(); + #else private static readonly Type[] emptyTypeArray = new Type[0]; - // #endif + #endif private static readonly XmlAttributeOverrides xmlAttributeOverrides = new XmlAttributeOverrides(); /// diff --git a/src/redmine-net-api/IRedmineWebClient.cs b/src/redmine-net-api/IRedmineWebClient.cs index 8e3bcfd2..f33e5088 100644 --- a/src/redmine-net-api/IRedmineWebClient.cs +++ b/src/redmine-net-api/IRedmineWebClient.cs @@ -64,7 +64,7 @@ public interface IRedmineWebClient /// /// /// - NameValueCollection QueryString { get; set; } + NameValueCollection QueryString { get; } /// /// diff --git a/src/redmine-net-api/Types/CustomField.cs b/src/redmine-net-api/Types/CustomField.cs index f197cdbb..09b01050 100755 --- a/src/redmine-net-api/Types/CustomField.cs +++ b/src/redmine-net-api/Types/CustomField.cs @@ -100,21 +100,21 @@ public class CustomField : IdentifiableName, IEquatable /// [XmlArray(RedmineKeys.POSSIBLE_VALUES)] [XmlArrayItem(RedmineKeys.POSSIBLE_VALUE)] - public IList PossibleValues { get; set; } + public IList PossibleValues { get; internal set; } /// /// /// [XmlArray(RedmineKeys.TRACKERS)] [XmlArrayItem(RedmineKeys.TRACKER)] - public IList Trackers { get; set; } + public IList Trackers { get; internal set; } /// /// /// [XmlArray(RedmineKeys.ROLES)] [XmlArrayItem(RedmineKeys.ROLE)] - public IList Roles { get; set; } + public IList Roles { get; internal set; } /// /// diff --git a/src/redmine-net-api/Types/Group.cs b/src/redmine-net-api/Types/Group.cs index ff95afaf..0380269a 100755 --- a/src/redmine-net-api/Types/Group.cs +++ b/src/redmine-net-api/Types/Group.cs @@ -34,7 +34,7 @@ public class Group : IdentifiableName, IEquatable /// [XmlArray(RedmineKeys.USERS)] [XmlArrayItem(RedmineKeys.USER)] - public List Users { get; set; } + public List Users { get; internal set; } /// /// Gets or sets the custom fields. @@ -42,7 +42,7 @@ public class Group : IdentifiableName, IEquatable /// The custom fields. [XmlArray(RedmineKeys.CUSTOM_FIELDS)] [XmlArrayItem(RedmineKeys.CUSTOM_FIELD)] - public IList CustomFields { get; set; } + public IList CustomFields { get; internal set; } /// /// Gets or sets the custom fields. @@ -50,7 +50,7 @@ public class Group : IdentifiableName, IEquatable /// The custom fields. [XmlArray(RedmineKeys.MEMBERSHIPS)] [XmlArrayItem(RedmineKeys.MEMBERSHIP)] - public IList Memberships { get; set; } + public IList Memberships { get; internal set; } #region Implementation of IXmlSerializable diff --git a/src/redmine-net-api/Types/Issue.cs b/src/redmine-net-api/Types/Issue.cs index 9dd7b0ef..cc38daca 100644 --- a/src/redmine-net-api/Types/Issue.cs +++ b/src/redmine-net-api/Types/Issue.cs @@ -140,7 +140,7 @@ public class Issue : Identifiable, IXmlSerializable, IEquatable, I /// The custom fields. [XmlArray(RedmineKeys.CUSTOM_FIELDS)] [XmlArrayItem(RedmineKeys.CUSTOM_FIELD)] - public IList CustomFields { get; set; } + public IList CustomFields { get; internal set; } /// /// Gets or sets the created on. @@ -227,7 +227,7 @@ public class Issue : Identifiable, IXmlSerializable, IEquatable, I /// [XmlArray(RedmineKeys.JOURNALS)] [XmlArrayItem(RedmineKeys.JOURNAL)] - public IList Journals { get; set; } + public IList Journals { get; internal set; } /// /// Gets or sets the changesets. @@ -237,7 +237,7 @@ public class Issue : Identifiable, IXmlSerializable, IEquatable, I /// [XmlArray(RedmineKeys.CHANGESETS)] [XmlArrayItem(RedmineKeys.CHANGESET)] - public IList Changesets { get; set; } + public IList Changesets { get; internal set; } /// /// Gets or sets the attachments. @@ -247,7 +247,7 @@ public class Issue : Identifiable, IXmlSerializable, IEquatable, I /// [XmlArray(RedmineKeys.ATTACHMENTS)] [XmlArrayItem(RedmineKeys.ATTACHMENT)] - public IList Attachments { get; set; } + public IList Attachments { get; internal set; } /// /// Gets or sets the issue relations. @@ -257,7 +257,7 @@ public class Issue : Identifiable, IXmlSerializable, IEquatable, I /// [XmlArray(RedmineKeys.RELATIONS)] [XmlArrayItem(RedmineKeys.RELATION)] - public IList Relations { get; set; } + public IList Relations { get; internal set; } /// /// Gets or sets the issue children. @@ -268,7 +268,7 @@ public class Issue : Identifiable, IXmlSerializable, IEquatable, I /// [XmlArray(RedmineKeys.CHILDREN)] [XmlArrayItem(RedmineKeys.ISSUE)] - public IList Children { get; set; } + public IList Children { get; internal set; } /// /// Gets or sets the attachments. @@ -278,14 +278,14 @@ public class Issue : Identifiable, IXmlSerializable, IEquatable, I /// [XmlArray(RedmineKeys.UPLOADS)] [XmlArrayItem(RedmineKeys.UPLOAD)] - public IList Uploads { get; set; } + public IList Uploads { get; internal set; } /// /// /// [XmlArray(RedmineKeys.WATCHERS)] [XmlArrayItem(RedmineKeys.WATCHER)] - public IList Watchers { get; set; } + public IList Watchers { get; internal set; } /// /// diff --git a/src/redmine-net-api/Types/IssueCustomField.cs b/src/redmine-net-api/Types/IssueCustomField.cs index 8e2c94b5..686aeb9a 100755 --- a/src/redmine-net-api/Types/IssueCustomField.cs +++ b/src/redmine-net-api/Types/IssueCustomField.cs @@ -36,7 +36,7 @@ public class IssueCustomField : IdentifiableName, IEquatable, /// The value. [XmlArray(RedmineKeys.VALUE)] [XmlArrayItem(RedmineKeys.VALUE)] - public IList Values { get; set; } + public IList Values { get; internal set; } /// /// diff --git a/src/redmine-net-api/Types/Journal.cs b/src/redmine-net-api/Types/Journal.cs index 66cdb5d7..58ca0565 100644 --- a/src/redmine-net-api/Types/Journal.cs +++ b/src/redmine-net-api/Types/Journal.cs @@ -71,7 +71,7 @@ public class Journal : Identifiable, IEquatable, IXmlSerializa /// [XmlArray(RedmineKeys.DETAILS)] [XmlArrayItem(RedmineKeys.DETAIL)] - public IList Details { get; set; } + public IList Details { get; internal set; } /// /// diff --git a/src/redmine-net-api/Types/Membership.cs b/src/redmine-net-api/Types/Membership.cs index dcc112d0..6baf5aa2 100755 --- a/src/redmine-net-api/Types/Membership.cs +++ b/src/redmine-net-api/Types/Membership.cs @@ -43,7 +43,7 @@ public class Membership : Identifiable, IEquatable, IXml /// The type. [XmlArray(RedmineKeys.ROLES)] [XmlArrayItem(RedmineKeys.ROLE)] - public List Roles { get; set; } + public List Roles { get; internal set; } /// /// diff --git a/src/redmine-net-api/Types/PaginatedObjects.cs b/src/redmine-net-api/Types/PaginatedObjects.cs index 13250355..2498e3a5 100755 --- a/src/redmine-net-api/Types/PaginatedObjects.cs +++ b/src/redmine-net-api/Types/PaginatedObjects.cs @@ -27,7 +27,7 @@ public class PaginatedObjects /// /// /// - public List Objects { get; set; } + public List Objects { get; internal set; } /// /// /// diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs index 69147165..53bc1542 100644 --- a/src/redmine-net-api/Types/Project.cs +++ b/src/redmine-net-api/Types/Project.cs @@ -108,7 +108,7 @@ public class Project : IdentifiableName, IEquatable /// [XmlArray(RedmineKeys.TRACKERS)] [XmlArrayItem(RedmineKeys.TRACKER)] - public IList Trackers { get; set; } + public IList Trackers { get; internal set; } /// /// Gets or sets the custom fields. @@ -118,7 +118,7 @@ public class Project : IdentifiableName, IEquatable /// [XmlArray(RedmineKeys.CUSTOM_FIELDS)] [XmlArrayItem(RedmineKeys.CUSTOM_FIELD)] - public IList CustomFields { get; set; } + public IList CustomFields { get; internal set; } /// /// Gets or sets the issue categories. @@ -128,7 +128,7 @@ public class Project : IdentifiableName, IEquatable /// [XmlArray(RedmineKeys.ISSUE_CATEGORIES)] [XmlArrayItem(RedmineKeys.ISSUE_CATEGORY)] - public IList IssueCategories { get; set; } + public IList IssueCategories { get; internal set; } /// /// since 2.6.0 @@ -138,14 +138,14 @@ public class Project : IdentifiableName, IEquatable /// [XmlArray(RedmineKeys.ENABLED_MODULES)] [XmlArrayItem(RedmineKeys.ENABLED_MODULE)] - public IList EnabledModules { get; set; } + public IList EnabledModules { get; internal set; } /// /// /// [XmlArray(RedmineKeys.TIME_ENTRY_ACTIVITIES)] [XmlArrayItem(RedmineKeys.TIME_ENTRY_ACTIVITY)] - public IList TimeEntryActivities { get; set; } + public IList TimeEntryActivities { get; internal set; } /// /// Generates an object from its XML representation. diff --git a/src/redmine-net-api/Types/ProjectMembership.cs b/src/redmine-net-api/Types/ProjectMembership.cs index 55520d2e..698259d8 100755 --- a/src/redmine-net-api/Types/ProjectMembership.cs +++ b/src/redmine-net-api/Types/ProjectMembership.cs @@ -65,7 +65,7 @@ public class ProjectMembership : Identifiable, IEquatableThe type. [XmlArray(RedmineKeys.ROLES)] [XmlArrayItem(RedmineKeys.ROLE)] - public List Roles { get; set; } + public List Roles { get; internal set; } /// /// diff --git a/src/redmine-net-api/Types/Role.cs b/src/redmine-net-api/Types/Role.cs index fcf6fa93..1030089e 100755 --- a/src/redmine-net-api/Types/Role.cs +++ b/src/redmine-net-api/Types/Role.cs @@ -37,7 +37,7 @@ public class Role : IdentifiableName, IEquatable /// [XmlArray(RedmineKeys.PERMISSIONS)] [XmlArrayItem(RedmineKeys.PERMISSION)] - public IList Permissions { get; set; } + public IList Permissions { get; internal set; } /// /// diff --git a/src/redmine-net-api/Types/TimeEntry.cs b/src/redmine-net-api/Types/TimeEntry.cs index 71e81fe0..59ce33b4 100644 --- a/src/redmine-net-api/Types/TimeEntry.cs +++ b/src/redmine-net-api/Types/TimeEntry.cs @@ -108,7 +108,7 @@ public string Comments /// The custom fields. [XmlArray(RedmineKeys.CUSTOM_FIELDS)] [XmlArrayItem(RedmineKeys.CUSTOM_FIELD)] - public IList CustomFields { get; set; } + public IList CustomFields { get; internal set; } /// /// diff --git a/src/redmine-net-api/Types/Upload.cs b/src/redmine-net-api/Types/Upload.cs index cef6b530..59540fb7 100755 --- a/src/redmine-net-api/Types/Upload.cs +++ b/src/redmine-net-api/Types/Upload.cs @@ -60,7 +60,7 @@ public class Upload : IEquatable /// /// /// - public XmlSchema GetSchema() { return null; } + public static XmlSchema GetSchema() { return null; } /// /// Indicates whether the current object is equal to another object of the same type. diff --git a/src/redmine-net-api/Types/User.cs b/src/redmine-net-api/Types/User.cs index b3e6a52c..65d753f8 100644 --- a/src/redmine-net-api/Types/User.cs +++ b/src/redmine-net-api/Types/User.cs @@ -113,7 +113,7 @@ public class User : Identifiable, IXmlSerializable, IEquatable /// The custom fields. [XmlArray(RedmineKeys.CUSTOM_FIELDS)] [XmlArrayItem(RedmineKeys.CUSTOM_FIELD)] - public List CustomFields { get; set; } + public List CustomFields { get; internal set; } /// /// Gets or sets the memberships. @@ -123,7 +123,7 @@ public class User : Identifiable, IXmlSerializable, IEquatable /// [XmlArray(RedmineKeys.MEMBERSHIPS)] [XmlArrayItem(RedmineKeys.MEMBERSHIP)] - public List Memberships { get; set; } + public List Memberships { get; internal set; } /// /// Gets or sets the user's groups. @@ -133,7 +133,7 @@ public class User : Identifiable, IXmlSerializable, IEquatable /// [XmlArray(RedmineKeys.GROUPS)] [XmlArrayItem(RedmineKeys.GROUP)] - public List Groups { get; set; } + public List Groups { get; internal set; } /// /// Gets or sets the user's mail_notification. diff --git a/src/redmine-net-api/Types/Version.cs b/src/redmine-net-api/Types/Version.cs index f49c8d07..238a6214 100755 --- a/src/redmine-net-api/Types/Version.cs +++ b/src/redmine-net-api/Types/Version.cs @@ -85,7 +85,7 @@ public class Version : IdentifiableName, IEquatable /// The custom fields. [XmlArray(RedmineKeys.CUSTOM_FIELDS)] [XmlArrayItem(RedmineKeys.CUSTOM_FIELD)] - public IList CustomFields { get; set; } + public IList CustomFields { get; internal set; } /// /// diff --git a/src/redmine-net-api/Types/WikiPage.cs b/src/redmine-net-api/Types/WikiPage.cs index d98a66ec..4398d968 100755 --- a/src/redmine-net-api/Types/WikiPage.cs +++ b/src/redmine-net-api/Types/WikiPage.cs @@ -82,7 +82,7 @@ public class WikiPage : Identifiable, IXmlSerializable, IEquatable [XmlArray(RedmineKeys.ATTACHMENTS)] [XmlArrayItem(RedmineKeys.ATTACHMENT)] - public IList Attachments { get; set; } + public IList Attachments { get; internal set; } /// /// Sets the uploads. @@ -93,7 +93,7 @@ public class WikiPage : Identifiable, IXmlSerializable, IEquatableAvailability starting with redmine version 3.3 [XmlArray(RedmineKeys.UPLOADS)] [XmlArrayItem(RedmineKeys.UPLOAD)] - public IList Uploads { get; set; } + public IList Uploads { get; internal set; } #region Implementation of IXmlSerializable From 2850d3281dec05410f4ebb65b3913f051d669be2 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Sat, 23 Nov 2019 15:32:08 +0200 Subject: [PATCH 065/549] Remove unnecessary parameters --- src/redmine-net-api/Async/RedmineManagerAsync.cs | 8 ++------ src/redmine-net-api/Async/RedmineManagerAsync40.cs | 5 ++--- src/redmine-net-api/Async/RedmineManagerAsync45.cs | 6 ++---- src/redmine-net-api/RedmineManager.cs | 2 +- tests/redmine-net-api.Tests/Tests/Async/UserAsyncTests.cs | 2 +- 5 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/redmine-net-api/Async/RedmineManagerAsync.cs b/src/redmine-net-api/Async/RedmineManagerAsync.cs index 68468dea..b7ee7ff7 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync.cs @@ -80,11 +80,9 @@ public static Task GetWikiPageAsync(this RedmineManager redmineManager /// Gets all wiki pages asynchronous. /// /// The redmine manager. - /// The parameters. /// The project identifier. /// - public static Task> GetAllWikiPagesAsync(this RedmineManager redmineManager, - NameValueCollection parameters, string projectId) + public static Task> GetAllWikiPagesAsync(this RedmineManager redmineManager, string projectId) { return delegate { return redmineManager.GetAllWikiPages(projectId); }; } @@ -224,10 +222,8 @@ public static Task UpdateObjectAsync(this RedmineManager redmineManager, stri /// /// The redmine manager. /// The identifier. - /// The parameters. /// - public static Task DeleteObjectAsync(this RedmineManager redmineManager, string id, - NameValueCollection parameters) where T : class, new() + public static Task DeleteObjectAsync(this RedmineManager redmineManager, string id) where T : class, new() { return delegate { redmineManager.DeleteObject(id); }; } diff --git a/src/redmine-net-api/Async/RedmineManagerAsync40.cs b/src/redmine-net-api/Async/RedmineManagerAsync40.cs index d2b45f28..18506462 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync40.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync40.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -245,9 +245,8 @@ public static Task RemoveWatcherFromIssueAsync(this RedmineManager redmineManage /// /// The redmine manager. /// The identifier. - /// The parameters. /// - public static Task DeleteObjectAsync(this RedmineManager redmineManager, string id, NameValueCollection parameters) where T : class, new() + public static Task DeleteObjectAsync(this RedmineManager redmineManager, string id) where T : class, new() { return Task.Factory.StartNew(() => redmineManager.DeleteObject(id), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); } diff --git a/src/redmine-net-api/Async/RedmineManagerAsync45.cs b/src/redmine-net-api/Async/RedmineManagerAsync45.cs index 273c3cfd..be209248 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync45.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync45.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -363,7 +363,6 @@ public static async Task CreateObjectAsync(this RedmineManager redmineMana /// The redmine manager. /// The identifier. /// The object. - /// The project identifier. /// public static async Task UpdateObjectAsync(this RedmineManager redmineManager, string id, T entity) where T : class, new() @@ -381,9 +380,8 @@ public static async Task UpdateObjectAsync(this RedmineManager redmineManager /// The type of objects to delete. /// The redmine manager. /// The id of the object to delete - /// Optional filters and/or optional fetched data. /// - public static async Task DeleteObjectAsync(this RedmineManager redmineManager, string id, NameValueCollection parameters) + public static async Task DeleteObjectAsync(this RedmineManager redmineManager, string id) where T : class, new() { var uri = UrlHelper.GetDeleteUrl(redmineManager, id); diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index 792919b9..d0008040 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/redmine-net-api.Tests/Tests/Async/UserAsyncTests.cs b/tests/redmine-net-api.Tests/Tests/Async/UserAsyncTests.cs index d37a2f45..30075e8e 100644 --- a/tests/redmine-net-api.Tests/Tests/Async/UserAsyncTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Async/UserAsyncTests.cs @@ -200,7 +200,7 @@ public async Task Should_Update_User() public async Task Should_Delete_User() { var userId = 62.ToString(); - await fixture.RedmineManager.DeleteObjectAsync(userId, null); + await fixture.RedmineManager.DeleteObjectAsync(userId); await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetObjectAsync(userId, null)); } From 46ddaa6321fd17c2fe61c15595ad67c4324ecf31 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Sat, 23 Nov 2019 15:35:12 +0200 Subject: [PATCH 066/549] Remove CA1720 & CA2227 --- src/redmine-net-api/redmine-net-api.csproj | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 93e5e287..883eebeb 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -1,4 +1,4 @@ - + @@ -12,7 +12,16 @@ TRACE Debug;Release PackageReference - NU5105;CA1303;CA1056;CA1062;CA1707;CA1720;CA1806;CA1716;CA1724;CA2227 + + NU5105; + CA1303; + CA1056; + CA1062; + CA1707; + CA1716; + CA1724; + CA1806; + From 91497b2a44f464b6e6eaff9a68dd3ca8cdf61d07 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Sat, 23 Nov 2019 15:35:40 +0200 Subject: [PATCH 067/549] Update VersionPrefix --- src/redmine-net-api/redmine-net-api.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 883eebeb..0a0adbcf 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -1,4 +1,4 @@ - + @@ -48,7 +48,7 @@ ... Redmine .NET API Client - 2.0.47 + 3.0.0 From d270fac53dfd0be1f0e00749b1827dca54363e16 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Fri, 6 Dec 2019 13:13:41 +0200 Subject: [PATCH 068/549] Cleanup --- src/redmine-net-api/Async/RedmineManagerAsync45.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/redmine-net-api/Async/RedmineManagerAsync45.cs b/src/redmine-net-api/Async/RedmineManagerAsync45.cs index 273c3cfd..816f515c 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync45.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync45.cs @@ -363,7 +363,6 @@ public static async Task CreateObjectAsync(this RedmineManager redmineMana /// The redmine manager. /// The identifier. /// The object. - /// The project identifier. /// public static async Task UpdateObjectAsync(this RedmineManager redmineManager, string id, T entity) where T : class, new() From 6c111cc57c12738756cfa3d0cf3397be686e6466 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Fri, 6 Dec 2019 13:42:54 +0200 Subject: [PATCH 069/549] Add redmine-net-api.snk --- redmine-net-api.sln | 5 +++++ redmine-net-api.snk | Bin 0 -> 596 bytes src/redmine-net-api/redmine-net-api.csproj | 17 ++++++++++++++--- 3 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 redmine-net-api.snk diff --git a/redmine-net-api.sln b/redmine-net-api.sln index 6a6da6ee..e06a01cf 100644 --- a/redmine-net-api.sln +++ b/redmine-net-api.sln @@ -15,6 +15,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionFolder", "SolutionF ProjectSection(SolutionItems) = preProject appveyor.yml = appveyor.yml docker-compose.yml = docker-compose.yml + CONTRIBUTING.md = CONTRIBUTING.md + LICENSE = LICENSE + logo.png = logo.png + README.md = README.md + redmine-net-api.snk = redmine-net-api.snk EndProjectSection EndProject Global diff --git a/redmine-net-api.snk b/redmine-net-api.snk new file mode 100644 index 0000000000000000000000000000000000000000..9bc3c18fa396f7f21f34cb39af87c94da753e446 GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50097JhH$|Om5knNbcroe2Z&qJt2a_rxL?`8Y-_p1Hlja}r`6m`Q;mf7#QGzQrhxCY?fqYn+e7$m`H3ETq#B z1s1D$y)N0w9JpDEbC=#6?mi@J2nYoygYW_H!bbX*5r&;GFR!J_^`<6BjvQS3V?b38 zq*6Bf{0ztRc-{Q>b334}+O}G!``r64EWLEDki1+)5shPCi4n%h$NEV^%+?*>r8PLz zFEpgWH_*vcio!WQZUU4)Z$8`1-9y%4|IJ>k3-mmFRlIOC!@s_1Fx5maAEh4oIo{7^ zV(S_-Vmi!uca%g&p=vjbD}(bYwH-5e<+IZ6+n+%rplsIH8_c-tXy2ttp+y5QjX>te zv>U)+-XHrE#bR+&6o%v^UPJVF;w?7!8V56@4)R#HI^&noK7%iYTy&bN=}L_iN-?Pe zs$<+Z99tzgv zYddaxg8|Lc(61YXk{<7fo?4w^c1cKzJJZmSZP!M=s+Byd-gq9|l@WmYu0zJ`j5E@l iRu(`pg{ek4Nqo%YyKq>e%VaH*xx1o|qf9Bj(5AdBKp}1.0.0 en-US - redmine-api - https://github.com/zapadi/redmine-net-api/blob/master/logo.png + + redmine-api + redmine-api-signed + https://raw.githubusercontent.com/zapadi/redmine-net-api/master/logo.png https://github.com/zapadi/redmine-net-api/blob/master/LICENSE https://github.com/zapadi/redmine-net-api true @@ -42,6 +44,11 @@ 2.0.47 + + + true + ..\..\redmine-net-api.snk + NET20;NETFULL @@ -170,6 +177,9 @@ + + redmine-net-api.snk + @@ -199,7 +209,8 @@ - <_Parameter1>$(MSBuildProjectName).Tests + <_Parameter1>"..\..\tests\$(MSBuildProjectName).Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100678670c10a958cde6b74892d5207885bd2ab375255b85fd7794d60ff01ba1cf81aaff13f54d8a08a8f8c7816ef4fc0138de7941031e47b5b0c5d51f58cbfe6c5652e11cfa0865e2d0a860f47f73b701e6758e3e381665f7664f938462c9eb9bdc17312621e984981227fd9d38dbec5288e269d42836b9c8fc4c8ebd0282ca4d3" + From d090ea148bbb51b332e4431f6cb98bbb03afb338 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Fri, 6 Dec 2019 13:38:05 +0200 Subject: [PATCH 070/549] Update appveyor to generate signed package --- appveyor.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/appveyor.yml b/appveyor.yml index 38833ec7..57ba2a35 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -49,6 +49,7 @@ after_build: - ps: echo "Build suffix= $env:BUILD_SUFFIX" - ps: echo "Version suffix= $env:VERSION_SUFFIX" - ps: dotnet pack src\redmine-net-api\redmine-net-api.csproj -c Release --output .\artifacts --include-symbols -p:SymbolPackageFormat=snupkg --no-build $env:VERSION_SUFFIX + - ps: dotnet pack src\redmine-net-api\redmine-net-api.csproj -c Release --output .\artifacts --include-symbols -p:SymbolPackageFormat=snupkg --no-build $env:VERSION_SUFFIX -p:Sign=true test: off From ffc873516d2d792aef1844d93e6fcf317b075864 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Fri, 6 Dec 2019 13:38:41 +0200 Subject: [PATCH 071/549] Exclude artifacts folder --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 27110cda..1008d646 100755 --- a/.gitignore +++ b/.gitignore @@ -260,3 +260,4 @@ paket-files/ ### VisualStudioCode ### .vscode +.artifacts From 9b6e25dd3e35f100fc765c29e427299006727cd4 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Fri, 6 Dec 2019 13:42:39 +0200 Subject: [PATCH 072/549] Changed csproj package, icon & copyright --- src/redmine-net-api/redmine-net-api.csproj | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index f51e1ec4..9820e922 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -21,14 +21,15 @@ Adrian Popescu Redmine Api is a .NET rest client for Redmine. p.adi - Adrian Popescu, 2011-2020 - + Adrian Popescu, 2011 - $([System.DateTime]::Now.Year.ToString()) 1.0.0 en-US - redmine-api redmine-api-signed https://raw.githubusercontent.com/zapadi/redmine-net-api/master/logo.png + logo.png + Apache-2.0 + LICENSE https://github.com/zapadi/redmine-net-api/blob/master/LICENSE https://github.com/zapadi/redmine-net-api true @@ -180,6 +181,8 @@ redmine-net-api.snk + + From eac0abed3a383082ae054e4f9e58342452ef4a03 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Fri, 6 Dec 2019 15:40:24 +0200 Subject: [PATCH 073/549] Fix #242 --- .../Internals/XmlTextReaderBuilder.cs | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs b/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs index b5af4dd1..6a44a297 100644 --- a/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs +++ b/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs @@ -3,7 +3,7 @@ namespace Redmine.Net.Api.Internals { - internal static class XmlTextReaderBuilder + public static class XmlTextReaderBuilder { #if NET20 public static XmlReader Create(StringReader stringReader) @@ -18,16 +18,18 @@ public static XmlReader Create(StringReader stringReader) } - public static XmlReader Create(string stringReader) + public static XmlReader Create(string xml) { - return XmlReader.Create(stringReader, new XmlReaderSettings() + using (var stringReader = new StringReader(xml)) { - ProhibitDtd = true, - XmlResolver = null, - IgnoreComments = true, - IgnoreWhitespace = true, - }); - + return XmlReader.Create(stringReader, new XmlReaderSettings() + { + ProhibitDtd = true, + XmlResolver = null, + IgnoreComments = true, + IgnoreWhitespace = true, + }); + } } #else public static XmlTextReader Create(StringReader stringReader) @@ -40,14 +42,17 @@ public static XmlTextReader Create(StringReader stringReader) }; } - public static XmlTextReader Create(string stringReader) + public static XmlTextReader Create(string xml) { - return new XmlTextReader(stringReader) + using (var stringReader = new StringReader(xml)) { - DtdProcessing = DtdProcessing.Prohibit, - XmlResolver = null, - WhitespaceHandling = WhitespaceHandling.None - }; + return new XmlTextReader(stringReader) + { + DtdProcessing = DtdProcessing.Prohibit, + XmlResolver = null, + WhitespaceHandling = WhitespaceHandling.None + }; + } } #endif } From d2ea752768f4a58d1c20f93e8ec0e5699cbb42fc Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Fri, 6 Dec 2019 15:49:46 +0200 Subject: [PATCH 074/549] Update PackageRelease & bump version --- src/redmine-net-api/redmine-net-api.csproj | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 9820e922..7e5a87fc 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -1,8 +1,7 @@  - + - net20;net40;net45;net451;net452;net46;net461;net462;net47;net471;net472;net48; false Redmine.Net.Api @@ -16,8 +15,6 @@ - - Adrian Popescu Redmine Api is a .NET rest client for Redmine. p.adi @@ -33,17 +30,18 @@ https://github.com/zapadi/redmine-net-api/blob/master/LICENSE https://github.com/zapadi/redmine-net-api true - Changed to new csproj format. + + Add redmine-net-api.snk + Fix #242 - Invalid URI: The Uri scheme is too long + Fix package icon url. + Redmine; REST; API; Client; .NET; Adrian Popescu; - Redmine .NET API Client git https://github.com/zapadi/redmine-net-api ... Redmine .NET API Client - - 2.0.47 - + 2.0.48 @@ -83,7 +81,6 @@ NET462;NETFULL - NET47;NETFULL @@ -109,7 +106,6 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - @@ -184,14 +180,12 @@ - full true - full true @@ -209,7 +203,6 @@ true - <_Parameter1>"..\..\tests\$(MSBuildProjectName).Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100678670c10a958cde6b74892d5207885bd2ab375255b85fd7794d60ff01ba1cf81aaff13f54d8a08a8f8c7816ef4fc0138de7941031e47b5b0c5d51f58cbfe6c5652e11cfa0865e2d0a860f47f73b701e6758e3e381665f7664f938462c9eb9bdc17312621e984981227fd9d38dbec5288e269d42836b9c8fc4c8ebd0282ca4d3" From 062e42cf4760219649b853baa9f57221f93d1476 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Fri, 6 Dec 2019 16:27:56 +0200 Subject: [PATCH 075/549] Fix build --- .../Internals/XmlTextReaderBuilder.cs | 24 +++++++++++++++++++ src/redmine-net-api/redmine-net-api.csproj | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs b/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs index 6a44a297..404fc270 100644 --- a/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs +++ b/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs @@ -3,9 +3,17 @@ namespace Redmine.Net.Api.Internals { + /// + /// + /// public static class XmlTextReaderBuilder { #if NET20 + /// + /// + /// + /// + /// public static XmlReader Create(StringReader stringReader) { return XmlReader.Create(stringReader, new XmlReaderSettings() @@ -18,6 +26,11 @@ public static XmlReader Create(StringReader stringReader) } + /// + /// + /// + /// + /// public static XmlReader Create(string xml) { using (var stringReader = new StringReader(xml)) @@ -32,6 +45,11 @@ public static XmlReader Create(string xml) } } #else + /// + /// + /// + /// + /// public static XmlTextReader Create(StringReader stringReader) { return new XmlTextReader(stringReader) @@ -42,6 +60,12 @@ public static XmlTextReader Create(StringReader stringReader) }; } + + /// + /// + /// + /// + /// public static XmlTextReader Create(string xml) { using (var stringReader = new StringReader(xml)) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 7e5a87fc..54da17e6 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -205,7 +205,7 @@ - <_Parameter1>"..\..\tests\$(MSBuildProjectName).Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100678670c10a958cde6b74892d5207885bd2ab375255b85fd7794d60ff01ba1cf81aaff13f54d8a08a8f8c7816ef4fc0138de7941031e47b5b0c5d51f58cbfe6c5652e11cfa0865e2d0a860f47f73b701e6758e3e381665f7664f938462c9eb9bdc17312621e984981227fd9d38dbec5288e269d42836b9c8fc4c8ebd0282ca4d3" + <_Parameter1>$(MSBuildProjectName).Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100678670c10a958cde6b74892d5207885bd2ab375255b85fd7794d60ff01ba1cf81aaff13f54d8a08a8f8c7816ef4fc0138de7941031e47b5b0c5d51f58cbfe6c5652e11cfa0865e2d0a860f47f73b701e6758e3e381665f7664f938462c9eb9bdc17312621e984981227fd9d38dbec5288e269d42836b9c8fc4c8ebd0282ca4d3 From f9ed9a6e97bb72c24777a214246f2a5ced6742ed Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Fri, 6 Dec 2019 16:58:46 +0200 Subject: [PATCH 076/549] Update packages & fix test build --- src/redmine-net-api/redmine-net-api.csproj | 11 +++++----- .../redmine-net-api.Tests.csproj | 21 ++++++++++++++++--- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index d6272139..645bc13c 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -107,11 +107,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -186,8 +186,8 @@ redmine-net-api.snk - - + + @@ -214,7 +214,8 @@ - <_Parameter1>$(MSBuildProjectName).Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100678670c10a958cde6b74892d5207885bd2ab375255b85fd7794d60ff01ba1cf81aaff13f54d8a08a8f8c7816ef4fc0138de7941031e47b5b0c5d51f58cbfe6c5652e11cfa0865e2d0a860f47f73b701e6758e3e381665f7664f938462c9eb9bdc17312621e984981227fd9d38dbec5288e269d42836b9c8fc4c8ebd0282ca4d3 + <_Parameter1 Condition="'$(Sign)' == '' OR '$(Sign)' == 'false'">$(MSBuildProjectName).Tests + <_Parameter1 Condition="'$(Sign)' == 'true'">$(MSBuildProjectName).Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100678670c10a958cde6b74892d5207885bd2ab375255b85fd7794d60ff01ba1cf81aaff13f54d8a08a8f8c7816ef4fc0138de7941031e47b5b0c5d51f58cbfe6c5652e11cfa0865e2d0a860f47f73b701e6758e3e381665f7664f938462c9eb9bdc17312621e984981227fd9d38dbec5288e269d42836b9c8fc4c8ebd0282ca4d3 diff --git a/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj index efdec128..a11a5962 100644 --- a/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj +++ b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj @@ -6,12 +6,16 @@ false net48 net45;net451;net452;net46;net461;net462;net47;net471;net472;net48; - true + false redmine.net.api.Tests redmine-net-api.Tests - + + true + ..\..\redmine-net-api.snk + + NET20;NETFULL @@ -63,7 +67,12 @@ - + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -89,5 +98,11 @@ + + + redmine-net-api.snk + + + \ No newline at end of file From 25bb0e87daf384257ffe2e03265b62836d4c3da4 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Fri, 6 Dec 2019 17:01:34 +0200 Subject: [PATCH 077/549] Fix NU5035 The PackageLicenseUrl is being deprecated and cannot be used in conjunction with the PackageLicenseFile or PackageLicenseExpression. --- src/redmine-net-api/redmine-net-api.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 645bc13c..75fa4b8a 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -36,7 +36,6 @@ logo.png Apache-2.0 LICENSE - https://github.com/zapadi/redmine-net-api/blob/master/LICENSE https://github.com/zapadi/redmine-net-api true From d44510c079d585fdbc05bad3087d92891ff96268 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Fri, 6 Dec 2019 17:05:41 +0200 Subject: [PATCH 078/549] Fix NU5033 --- src/redmine-net-api/redmine-net-api.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 75fa4b8a..20281c4d 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -34,7 +34,6 @@ redmine-api-signed https://raw.githubusercontent.com/zapadi/redmine-net-api/master/logo.png logo.png - Apache-2.0 LICENSE https://github.com/zapadi/redmine-net-api true From 5b75f792546ac25768fc4e58931a584b8b6b03bd Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Fri, 6 Dec 2019 17:09:31 +0200 Subject: [PATCH 079/549] Bump version --- src/redmine-net-api/redmine-net-api.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 20281c4d..15a7f6ff 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -48,7 +48,7 @@ https://github.com/zapadi/redmine-net-api ... Redmine .NET API Client - 3.0.0 + 3.0.1 From 045d6c86b5b3eba2419f4ffaa94dc70b18f2341c Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Tue, 10 Dec 2019 15:39:49 +0200 Subject: [PATCH 080/549] Update appveyor --- appveyor.yml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 57ba2a35..a6ab1732 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -27,12 +27,14 @@ init: # Good practise, because Windows line endings are different from Unix/Linux ones - ps: git config --global core.autocrlf true - - ps: $branch = $env:APPVEYOR_REPO_BRANCH; - - ps: $revision = @{ $true = ""; $false = "{0:00000}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10) }[$env:APPVEYOR_REPO_TAG -eq "true"]; - - ps: $suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "master" -and $revision -eq ""]; - ps: $commitHash = $($env:APPVEYOR_REPO_COMMIT.substring(0,7)); - - ps: $env:BUILD_SUFFIX = @{ $true = "$($suffix)-$($commitHash)"; $false = "$($branch)-$($commitHash)" }[$suffix -ne ""]; - - ps: $env:VERSION_SUFFIX = @{ $true = "--version-suffix=$($suffix)"; $false = ""}[$suffix -ne ""]; + - ps: $branch = $env:APPVEYOR_REPO_BRANCH; + - ps: $buildNumber = $env:APPVEYOR_BUILD_NUMBER; + - ps: $isRepoTag = $env:APPVEYOR_REPO_TAG; + - ps: $revision = @{ $true = ""; $false = "{0:00000}" -f [convert]::ToInt32("0" + $buildNumber, 10) }[$isRepoTag -eq [bool]::true]; + - ps: $suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "master" -and [string]::IsNullOrEmpty($revision)]; + - ps: $env:BUILD_SUFFIX = @{ $true = "$($suffix)-$($commitHash)"; $false = "$($branch)-$($commitHash)" }[ -not ([string]::IsNullOrEmpty($suffix))]; + - ps: $env:VERSION_SUFFIX = @{ $true = "--version-suffix=$($suffix)"; $false = ""}[ -not ([string]::IsNullOrEmpty($suffix))]; install: - ps: dotnet restore redmine-net-api.sln @@ -44,8 +46,11 @@ before_build: build_script: - ps: dotnet build redmine-net-api.sln -c Release --version-suffix=$env:BUILD_SUFFIX + - ps: dotnet build redmine-net-api.sln -c Release --version-suffix=$env:BUILD_SUFFIX -p:Sign=true after_build: + - ps: echo "Build number= $buildNumber" + - ps: echo "Is repository tag= $isRepoTag" - ps: echo "Build suffix= $env:BUILD_SUFFIX" - ps: echo "Version suffix= $env:VERSION_SUFFIX" - ps: dotnet pack src\redmine-net-api\redmine-net-api.csproj -c Release --output .\artifacts --include-symbols -p:SymbolPackageFormat=snupkg --no-build $env:VERSION_SUFFIX From e0c23994efcbc2a2df0f9ba479a7850b4f58afa5 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Tue, 10 Dec 2019 15:55:29 +0200 Subject: [PATCH 081/549] Bump version --- src/redmine-net-api/redmine-net-api.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 15a7f6ff..ec10fd63 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -48,7 +48,7 @@ https://github.com/zapadi/redmine-net-api ... Redmine .NET API Client - 3.0.1 + 3.0.2 From 55f680bb2527fa2e9e757ef96299a1cbf76508eb Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Tue, 10 Dec 2019 16:17:10 +0200 Subject: [PATCH 082/549] Update appveyor --- appveyor.yml | 16 ++++++++-------- src/redmine-net-api/redmine-net-api.csproj | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index a6ab1732..d78b0825 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -31,8 +31,8 @@ init: - ps: $branch = $env:APPVEYOR_REPO_BRANCH; - ps: $buildNumber = $env:APPVEYOR_BUILD_NUMBER; - ps: $isRepoTag = $env:APPVEYOR_REPO_TAG; - - ps: $revision = @{ $true = ""; $false = "{0:00000}" -f [convert]::ToInt32("0" + $buildNumber, 10) }[$isRepoTag -eq [bool]::true]; - - ps: $suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "master" -and [string]::IsNullOrEmpty($revision)]; + - ps: $revision = @{ $true = [string]::Empty; $false = "{0:00000}" -f [convert]::ToInt32("0" + $buildNumber, 10) }[[bool]::$isRepoTag -eq [bool]::true]; + - ps: $suffix = @{ $true = [string]::Empty; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "master" -and [string]::IsNullOrEmpty($revision)]; - ps: $env:BUILD_SUFFIX = @{ $true = "$($suffix)-$($commitHash)"; $false = "$($branch)-$($commitHash)" }[ -not ([string]::IsNullOrEmpty($suffix))]; - ps: $env:VERSION_SUFFIX = @{ $true = "--version-suffix=$($suffix)"; $false = ""}[ -not ([string]::IsNullOrEmpty($suffix))]; @@ -40,8 +40,12 @@ install: - ps: dotnet restore redmine-net-api.sln before_build: - - ps: write-host "Build Suffix=$env:BUILD_SUFFIX" -foregroundcolor Green - - ps: write-host "Version suffix=$env:VERSION_SUFFIX" -foregroundcolor Magenta + - ps: write-host "Build Suffix = $env:BUILD_SUFFIX" -foregroundcolor Green + - ps: write-host "Version suffix = $env:VERSION_SUFFIX" -foregroundcolor Magenta + - ps: write-host "Branch = $branch" -foregroundcolor Cyan + - ps: write-host "Revision = $revision" -foregroundcolor Orange + - ps: write-host "Build suffix = $env:BUILD_SUFFIX" -foregroundcolor Yellow + - ps: write-host "Version suffix = $env:VERSION_SUFFIX" -foregroundcolor Red - ps: dotnet --version build_script: @@ -49,10 +53,6 @@ build_script: - ps: dotnet build redmine-net-api.sln -c Release --version-suffix=$env:BUILD_SUFFIX -p:Sign=true after_build: - - ps: echo "Build number= $buildNumber" - - ps: echo "Is repository tag= $isRepoTag" - - ps: echo "Build suffix= $env:BUILD_SUFFIX" - - ps: echo "Version suffix= $env:VERSION_SUFFIX" - ps: dotnet pack src\redmine-net-api\redmine-net-api.csproj -c Release --output .\artifacts --include-symbols -p:SymbolPackageFormat=snupkg --no-build $env:VERSION_SUFFIX - ps: dotnet pack src\redmine-net-api\redmine-net-api.csproj -c Release --output .\artifacts --include-symbols -p:SymbolPackageFormat=snupkg --no-build $env:VERSION_SUFFIX -p:Sign=true diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index ec10fd63..0acb8e27 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -48,7 +48,7 @@ https://github.com/zapadi/redmine-net-api ... Redmine .NET API Client - 3.0.2 + 3.0.4 From 558cdb8b92906e4456ae187ac798087cb89494ce Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Tue, 10 Dec 2019 16:24:20 +0200 Subject: [PATCH 083/549] Fix color --- appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index d78b0825..ae130714 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -42,8 +42,8 @@ install: before_build: - ps: write-host "Build Suffix = $env:BUILD_SUFFIX" -foregroundcolor Green - ps: write-host "Version suffix = $env:VERSION_SUFFIX" -foregroundcolor Magenta - - ps: write-host "Branch = $branch" -foregroundcolor Cyan - - ps: write-host "Revision = $revision" -foregroundcolor Orange + - ps: write-host "Branch = $branch" -foregroundcolor DarkYellow + - ps: write-host "Revision = $revision" -foregroundcolor Cyan - ps: write-host "Build suffix = $env:BUILD_SUFFIX" -foregroundcolor Yellow - ps: write-host "Version suffix = $env:VERSION_SUFFIX" -foregroundcolor Red - ps: dotnet --version From 3f56941da1005b28ac9275e6db64db3d6566618d Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Tue, 10 Dec 2019 16:40:51 +0200 Subject: [PATCH 084/549] Update appveyor --- appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index ae130714..113451aa 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -40,8 +40,8 @@ install: - ps: dotnet restore redmine-net-api.sln before_build: - - ps: write-host "Build Suffix = $env:BUILD_SUFFIX" -foregroundcolor Green - - ps: write-host "Version suffix = $env:VERSION_SUFFIX" -foregroundcolor Magenta + - ps: write-host "Is repo tag = $isRepoTag" -foregroundcolor Green + - ps: write-host "Build number = $buildNumber" -foregroundcolor Magenta - ps: write-host "Branch = $branch" -foregroundcolor DarkYellow - ps: write-host "Revision = $revision" -foregroundcolor Cyan - ps: write-host "Build suffix = $env:BUILD_SUFFIX" -foregroundcolor Yellow From 2940fd6e5972398f9f36a0462952d32b814c8bf7 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Tue, 10 Dec 2019 16:54:51 +0200 Subject: [PATCH 085/549] Fix tag condition --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 113451aa..c81ca4d5 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -31,7 +31,7 @@ init: - ps: $branch = $env:APPVEYOR_REPO_BRANCH; - ps: $buildNumber = $env:APPVEYOR_BUILD_NUMBER; - ps: $isRepoTag = $env:APPVEYOR_REPO_TAG; - - ps: $revision = @{ $true = [string]::Empty; $false = "{0:00000}" -f [convert]::ToInt32("0" + $buildNumber, 10) }[[bool]::$isRepoTag -eq [bool]::true]; + - ps: $revision = @{ $true = [string]::Empty; $false = "{0:00000}" -f [convert]::ToInt32("0" + $buildNumber, 10) }[$isRepoTag -eq "true"]; - ps: $suffix = @{ $true = [string]::Empty; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "master" -and [string]::IsNullOrEmpty($revision)]; - ps: $env:BUILD_SUFFIX = @{ $true = "$($suffix)-$($commitHash)"; $false = "$($branch)-$($commitHash)" }[ -not ([string]::IsNullOrEmpty($suffix))]; - ps: $env:VERSION_SUFFIX = @{ $true = "--version-suffix=$($suffix)"; $false = ""}[ -not ([string]::IsNullOrEmpty($suffix))]; From c80ad0011b0140a1d8b2f494e6d599e09a18a340 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Tue, 10 Dec 2019 17:12:10 +0200 Subject: [PATCH 086/549] Update appveyor tag version regex --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index c81ca4d5..618c75f3 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -21,7 +21,7 @@ nuget: branches: only: - master - - /\d\.\d\.\d/ + - /v\d*\.\d*\.\d*/ init: # Good practise, because Windows line endings are different from Unix/Linux ones From 05db46f07d5433b7a1f70fa733d82d61dd15624c Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Tue, 10 Dec 2019 17:31:54 +0200 Subject: [PATCH 087/549] Update appveyor skip files --- appveyor.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 618c75f3..a8e24db6 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -69,7 +69,6 @@ skip_commits: - '**/*.md' - '**/*.gif' - '**/*.png' - - '**/*.yml' - LICENSE - tests/* From c79a769fab3aab9d48286e4c666e2d3209f66f67 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Wed, 11 Dec 2019 18:38:21 +0200 Subject: [PATCH 088/549] Fix #243 --- src/redmine-net-api/Types/Group.cs | 2 +- src/redmine-net-api/Types/Issue.cs | 4 ++-- src/redmine-net-api/Types/IssueCustomField.cs | 2 +- src/redmine-net-api/Types/Project.cs | 6 +++--- src/redmine-net-api/Types/ProjectMembership.cs | 2 +- src/redmine-net-api/Types/User.cs | 2 +- src/redmine-net-api/Types/WikiPage.cs | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/redmine-net-api/Types/Group.cs b/src/redmine-net-api/Types/Group.cs index 0380269a..325ee1e9 100755 --- a/src/redmine-net-api/Types/Group.cs +++ b/src/redmine-net-api/Types/Group.cs @@ -34,7 +34,7 @@ public class Group : IdentifiableName, IEquatable /// [XmlArray(RedmineKeys.USERS)] [XmlArrayItem(RedmineKeys.USER)] - public List Users { get; internal set; } + public List Users { get; set; } /// /// Gets or sets the custom fields. diff --git a/src/redmine-net-api/Types/Issue.cs b/src/redmine-net-api/Types/Issue.cs index cc38daca..f3874baf 100644 --- a/src/redmine-net-api/Types/Issue.cs +++ b/src/redmine-net-api/Types/Issue.cs @@ -140,7 +140,7 @@ public class Issue : Identifiable, IXmlSerializable, IEquatable, I /// The custom fields. [XmlArray(RedmineKeys.CUSTOM_FIELDS)] [XmlArrayItem(RedmineKeys.CUSTOM_FIELD)] - public IList CustomFields { get; internal set; } + public IList CustomFields { get; set; } /// /// Gets or sets the created on. @@ -278,7 +278,7 @@ public class Issue : Identifiable, IXmlSerializable, IEquatable, I /// [XmlArray(RedmineKeys.UPLOADS)] [XmlArrayItem(RedmineKeys.UPLOAD)] - public IList Uploads { get; internal set; } + public IList Uploads { get; set; } /// /// diff --git a/src/redmine-net-api/Types/IssueCustomField.cs b/src/redmine-net-api/Types/IssueCustomField.cs index 686aeb9a..8e2c94b5 100755 --- a/src/redmine-net-api/Types/IssueCustomField.cs +++ b/src/redmine-net-api/Types/IssueCustomField.cs @@ -36,7 +36,7 @@ public class IssueCustomField : IdentifiableName, IEquatable, /// The value. [XmlArray(RedmineKeys.VALUE)] [XmlArrayItem(RedmineKeys.VALUE)] - public IList Values { get; internal set; } + public IList Values { get; set; } /// /// diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs index 53bc1542..02da4267 100644 --- a/src/redmine-net-api/Types/Project.cs +++ b/src/redmine-net-api/Types/Project.cs @@ -108,7 +108,7 @@ public class Project : IdentifiableName, IEquatable /// [XmlArray(RedmineKeys.TRACKERS)] [XmlArrayItem(RedmineKeys.TRACKER)] - public IList Trackers { get; internal set; } + public IList Trackers { get; set; } /// /// Gets or sets the custom fields. @@ -118,7 +118,7 @@ public class Project : IdentifiableName, IEquatable /// [XmlArray(RedmineKeys.CUSTOM_FIELDS)] [XmlArrayItem(RedmineKeys.CUSTOM_FIELD)] - public IList CustomFields { get; internal set; } + public IList CustomFields { get; set; } /// /// Gets or sets the issue categories. @@ -138,7 +138,7 @@ public class Project : IdentifiableName, IEquatable /// [XmlArray(RedmineKeys.ENABLED_MODULES)] [XmlArrayItem(RedmineKeys.ENABLED_MODULE)] - public IList EnabledModules { get; internal set; } + public IList EnabledModules { get; set; } /// /// diff --git a/src/redmine-net-api/Types/ProjectMembership.cs b/src/redmine-net-api/Types/ProjectMembership.cs index 698259d8..ba41fe57 100755 --- a/src/redmine-net-api/Types/ProjectMembership.cs +++ b/src/redmine-net-api/Types/ProjectMembership.cs @@ -65,7 +65,7 @@ public class ProjectMembership : Identifiable, IEquatableThe type. [XmlArray(RedmineKeys.ROLES)] [XmlArrayItem(RedmineKeys.ROLE)] - public List Roles { get; internal set; } + public List Roles { get; set; } /// /// diff --git a/src/redmine-net-api/Types/User.cs b/src/redmine-net-api/Types/User.cs index 65d753f8..5471206c 100644 --- a/src/redmine-net-api/Types/User.cs +++ b/src/redmine-net-api/Types/User.cs @@ -113,7 +113,7 @@ public class User : Identifiable, IXmlSerializable, IEquatable /// The custom fields. [XmlArray(RedmineKeys.CUSTOM_FIELDS)] [XmlArrayItem(RedmineKeys.CUSTOM_FIELD)] - public List CustomFields { get; internal set; } + public List CustomFields { get; set; } /// /// Gets or sets the memberships. diff --git a/src/redmine-net-api/Types/WikiPage.cs b/src/redmine-net-api/Types/WikiPage.cs index 4398d968..4a1124dd 100755 --- a/src/redmine-net-api/Types/WikiPage.cs +++ b/src/redmine-net-api/Types/WikiPage.cs @@ -93,7 +93,7 @@ public class WikiPage : Identifiable, IXmlSerializable, IEquatableAvailability starting with redmine version 3.3 [XmlArray(RedmineKeys.UPLOADS)] [XmlArrayItem(RedmineKeys.UPLOAD)] - public IList Uploads { get; internal set; } + public IList Uploads { get; set; } #region Implementation of IXmlSerializable From a20cb142d51ccbe116a56a5301646238af1b8ce6 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Wed, 11 Dec 2019 18:39:21 +0200 Subject: [PATCH 089/549] Update version --- src/redmine-net-api/redmine-net-api.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 0acb8e27..c44497db 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -48,7 +48,7 @@ https://github.com/zapadi/redmine-net-api ... Redmine .NET API Client - 3.0.4 + 3.0.6 From f6eb52e8946383586371c14c312a868c0a251854 Mon Sep 17 00:00:00 2001 From: zapadi Date: Fri, 10 Jan 2020 18:54:10 +0200 Subject: [PATCH 090/549] Temporarily remove json serialization --- .../Extensions/JsonExtensions.cs | 232 ----------------- .../Internals/RedmineSerializer.cs | 74 +++--- .../Internals/RedmineSerializerJson.cs | 245 ------------------ .../JSonConverters/AttachmentConverter.cs | 103 -------- .../JSonConverters/AttachmentsConverter.cs | 85 ------ .../JSonConverters/ChangeSetConverter.cs | 82 ------ .../JSonConverters/CustomFieldConverter.cs | 93 ------- .../CustomFieldPossibleValueConverter.cs | 77 ------ .../CustomFieldRoleConverter.cs | 77 ------ .../JSonConverters/DetailConverter.cs | 83 ------ .../JSonConverters/ErrorConverter.cs | 77 ------ .../JSonConverters/FileConverter.cs | 112 -------- .../JSonConverters/GroupConverter.cs | 98 ------- .../JSonConverters/GroupUserConverter.cs | 78 ------ .../IdentifiableNameConverter.cs | 92 ------- .../JSonConverters/IssueCategoryConverter.cs | 98 ------- .../JSonConverters/IssueChildConverter.cs | 76 ------ .../JSonConverters/IssueConverter.cs | 156 ----------- .../IssueCustomFieldConverter.cs | 122 --------- .../JSonConverters/IssuePriorityConverter.cs | 78 ------ .../JSonConverters/IssueRelationConverter.cs | 102 -------- .../JSonConverters/IssueStatusConverter.cs | 82 ------ .../JSonConverters/JournalConverter.cs | 85 ------ .../JSonConverters/MembershipConverter.cs | 82 ------ .../JSonConverters/MembershipRoleConverter.cs | 82 ------ .../JSonConverters/NewsConverter.cs | 85 ------ .../JSonConverters/PermissionConverter.cs | 73 ------ .../JSonConverters/ProjectConverter.cs | 119 --------- .../ProjectEnabledModuleConverter.cs | 78 ------ .../ProjectIssueCategoryConverter.cs | 90 ------- .../ProjectMembershipConverter.cs | 96 ------- .../JSonConverters/ProjectTrackerConverter.cs | 78 ------ .../JSonConverters/QueryConverter.cs | 83 ------ .../JSonConverters/RoleConverter.cs | 92 ------- .../TimeEntryActivityConverter.cs | 78 ------ .../JSonConverters/TimeEntryConverter.cs | 121 --------- .../JSonConverters/TrackerConverter.cs | 81 ------ .../TrackerCustomFieldConverter.cs | 68 ----- .../JSonConverters/UploadConverter.cs | 93 ------- .../JSonConverters/UserConverter.cs | 127 --------- .../JSonConverters/UserGroupConverter.cs | 68 ----- .../JSonConverters/VersionConverter.cs | 109 -------- .../JSonConverters/WatcherConverter.cs | 87 ------- .../JSonConverters/WikiPageConverter.cs | 99 ------- 44 files changed, 37 insertions(+), 4259 deletions(-) delete mode 100644 src/redmine-net-api/Extensions/JsonExtensions.cs mode change 100755 => 100644 src/redmine-net-api/Internals/RedmineSerializer.cs delete mode 100755 src/redmine-net-api/Internals/RedmineSerializerJson.cs delete mode 100755 src/redmine-net-api/JSonConverters/AttachmentConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/AttachmentsConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/ChangeSetConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/CustomFieldConverter.cs delete mode 100644 src/redmine-net-api/JSonConverters/CustomFieldPossibleValueConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/CustomFieldRoleConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/DetailConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/ErrorConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/FileConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/GroupConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/GroupUserConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/IdentifiableNameConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/IssueCategoryConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/IssueChildConverter.cs delete mode 100644 src/redmine-net-api/JSonConverters/IssueConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/IssueCustomFieldConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/IssuePriorityConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/IssueRelationConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/IssueStatusConverter.cs delete mode 100644 src/redmine-net-api/JSonConverters/JournalConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/MembershipConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/MembershipRoleConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/NewsConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/PermissionConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/ProjectConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/ProjectEnabledModuleConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/ProjectIssueCategoryConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/ProjectMembershipConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/ProjectTrackerConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/QueryConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/RoleConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/TimeEntryActivityConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/TimeEntryConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/TrackerConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/TrackerCustomFieldConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/UploadConverter.cs delete mode 100644 src/redmine-net-api/JSonConverters/UserConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/UserGroupConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/VersionConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/WatcherConverter.cs delete mode 100755 src/redmine-net-api/JSonConverters/WikiPageConverter.cs diff --git a/src/redmine-net-api/Extensions/JsonExtensions.cs b/src/redmine-net-api/Extensions/JsonExtensions.cs deleted file mode 100644 index 34a8d00a..00000000 --- a/src/redmine-net-api/Extensions/JsonExtensions.cs +++ /dev/null @@ -1,232 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -#if !NET20 -using System; -using System.Collections; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Internals; -using Redmine.Net.Api.JSonConverters; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.Extensions -{ - /// - /// - /// - public static class JsonExtensions - { - /// - /// Writes the identifier if not null. - /// - /// The dictionary. - /// The ident. - /// The key. - public static void WriteIdIfNotNull(this Dictionary dictionary, IdentifiableName ident, string key) - { - if (ident != null) dictionary.Add(key, ident.Id.ToString(CultureInfo.InvariantCulture)); - } - - /// - /// Writes the identifier or empty. - /// - /// The dictionary. - /// The ident. - /// The key. - /// The empty value. - public static void WriteIdOrEmpty(this Dictionary dictionary, IdentifiableName ident, string key, string emptyValue = null) - { - if (ident != null) dictionary.Add(key, ident.Id.ToString(CultureInfo.InvariantCulture)); - else dictionary.Add(key, emptyValue); - } - - /// - /// Writes the array. - /// - /// - /// The dictionary. - /// The key. - /// The col. - /// The converter. - /// The serializer. - public static void WriteArray(this Dictionary dictionary, string key, IEnumerable col, - JavaScriptConverter converter, JavaScriptSerializer serializer) - { - if (col != null) - { - serializer.RegisterConverters(new[] { converter }); - dictionary.Add(key, col.ToArray()); - } - } - - /// - /// Writes the ids array. - /// - /// The dictionary. - /// The key. - /// The coll. - public static void WriteIdsArray(this Dictionary dictionary, string key, - IEnumerable coll) - { - if (coll != null) - dictionary.Add(key, coll.Select(x => x.Id).ToArray()); - } - - /// - /// Writes the names array. - /// - /// The dictionary. - /// The key. - /// The coll. - public static void WriteNamesArray(this Dictionary dictionary, string key, - IEnumerable coll) - { - if (coll != null) - dictionary.Add(key, coll.Select(x => x.Name).ToArray()); - } - - /// - /// Writes the date or empty. - /// - /// The dictionary. - /// The value. - /// The tag. - public static void WriteDateOrEmpty(this Dictionary dictionary, DateTime? val, string tag) - { - if (!val.HasValue || val.Value.Equals(default(DateTime))) - dictionary.Add(tag, string.Empty); - else - dictionary.Add(tag, string.Format(NumberFormatInfo.InvariantInfo, "{0}", val.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture))); - } - - /// - /// Writes the value or empty. - /// - /// - /// The dictionary. - /// The value. - /// The tag. - public static void WriteValueOrEmpty(this Dictionary dictionary, T? val, string tag) where T : struct - { - if (!val.HasValue || EqualityComparer.Default.Equals(val.Value, default(T))) - dictionary.Add(tag, string.Empty); - else - dictionary.Add(tag, val.Value.ToString()); - } - - /// - /// Writes the value or default. - /// - /// - /// The dictionary. - /// The value. - /// The tag. - public static void WriteValueOrDefault(this Dictionary dictionary, T? val, string tag) where T : struct - { - dictionary.Add(tag, val.GetValueOrDefault().ToString()); - } - - /// - /// Gets the value. - /// - /// - /// The dictionary. - /// The key. - /// - public static T GetValue(this IDictionary dictionary, string key) - { - var dict = dictionary; - var type = typeof(T); - if (!dict.TryGetValue(key, out var val)) return default(T); - - if (val == null) return default(T); - - if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) - { - type = Nullable.GetUnderlyingType(type); - } - - if (val.GetType() == typeof(ArrayList)) return (T)val; - - if (type.IsEnum) val = Enum.Parse(type, val.ToString(), true); - - return (T)Convert.ChangeType(val, type, CultureInfo.InvariantCulture); - } - - /// - /// Gets the name of the value as identifiable. - /// - /// The dictionary. - /// The key. - /// - public static IdentifiableName GetValueAsIdentifiableName(this IDictionary dictionary, string key) - { - if (!dictionary.TryGetValue(key, out var val)) return null; - - var ser = new JavaScriptSerializer(); - ser.RegisterConverters(new[] { new IdentifiableNameConverter() }); - - var result = ser.ConvertToType(val); - return result; - } - - /// - /// For Json - /// - /// - /// The dictionary. - /// The key. - /// - public static List GetValueAsCollection(this IDictionary dictionary, string key) where T : new() - { - if (!dictionary.TryGetValue(key, out var val)) return null; - - var ser = new JavaScriptSerializer(); - ser.RegisterConverters(new[] { RedmineSerializer.JsonConverters[typeof(T)] }); - - var list = new List(); - - var arrayList = val as ArrayList; - if (arrayList != null) - { - list.AddRange(from object item in arrayList select ser.ConvertToType(item)); - } - else - { - var dict = val as Dictionary; - if (dict != null) - { - list.AddRange(dict.Select(pair => ser.ConvertToType(pair.Value))); - } - } - return list; - } - - /// - /// - /// - /// - /// - public static string ToLowerInv(this bool value) - { - return !value ? "false" : "true"; - } - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/Internals/RedmineSerializer.cs b/src/redmine-net-api/Internals/RedmineSerializer.cs old mode 100755 new mode 100644 index 795d08fe..188fe8ad --- a/src/redmine-net-api/Internals/RedmineSerializer.cs +++ b/src/redmine-net-api/Internals/RedmineSerializer.cs @@ -90,7 +90,7 @@ private static T FromXML(string xml) where T : class if (mimeFormat == MimeFormat.Json) { #if !NET20 - return JsonSerializer(obj); + // return JsonSerializer(obj); #endif } @@ -125,14 +125,14 @@ private static T FromXML(string xml) where T : class if (mimeFormat == MimeFormat.Json) { #if !NET20 - var type = typeof (T); - var jsonRoot = (string) null; - if (type == typeof (IssueCategory)) jsonRoot = RedmineKeys.ISSUE_CATEGORY; - if (type == typeof (IssueRelation)) jsonRoot = RedmineKeys.RELATION; - if (type == typeof (TimeEntry)) jsonRoot = RedmineKeys.TIME_ENTRY; - if (type == typeof (ProjectMembership)) jsonRoot = RedmineKeys.MEMBERSHIP; - if (type == typeof (WikiPage)) jsonRoot = RedmineKeys.WIKI_PAGE; - return JsonDeserialize(response, jsonRoot); + //var type = typeof (T); + //var jsonRoot = (string) null; + //if (type == typeof (IssueCategory)) jsonRoot = RedmineKeys.ISSUE_CATEGORY; + //if (type == typeof (IssueRelation)) jsonRoot = RedmineKeys.RELATION; + //if (type == typeof (TimeEntry)) jsonRoot = RedmineKeys.TIME_ENTRY; + //if (type == typeof (ProjectMembership)) jsonRoot = RedmineKeys.MEMBERSHIP; + //if (type == typeof (WikiPage)) jsonRoot = RedmineKeys.WIKI_PAGE; + //return JsonDeserialize(response, jsonRoot); #endif } return FromXML(response); @@ -164,7 +164,7 @@ public static PaginatedObjects DeserializeList(string response, MimeFormat if (mimeFormat == MimeFormat.Json) { #if !NET20 - return JSonDeserializeList(response); + // return JSonDeserializeList(response); #endif } return XmlDeserializeList(response); @@ -177,33 +177,33 @@ public static PaginatedObjects DeserializeList(string response, MimeFormat } #if !NET20 - /// - /// js the son deserialize list. - /// - /// - /// The response. - /// - private static PaginatedObjects JSonDeserializeList(string response) where T : class, new() - { - var type = typeof(T); - var jsonRoot = (string)null; - if (type == typeof(Error)) jsonRoot = RedmineKeys.ERRORS; - if (type == typeof(WikiPage)) jsonRoot = RedmineKeys.WIKI_PAGES; - if (type == typeof(IssuePriority)) jsonRoot = RedmineKeys.ISSUE_PRIORITIES; - if (type == typeof(TimeEntryActivity)) jsonRoot = RedmineKeys.TIME_ENTRY_ACTIVITIES; - - if (string.IsNullOrEmpty(jsonRoot)) - jsonRoot = RedmineManager.Sufixes[type]; - - var result = JsonDeserializeToList(response, jsonRoot, out var totalItems, out var offset); - - return new PaginatedObjects() - { - TotalCount = totalItems, - Offset = offset, - Objects = result.ToList() - }; - } + ///// + ///// js the son deserialize list. + ///// + ///// + ///// The response. + ///// + //private static PaginatedObjects JSonDeserializeList(string response) where T : class, new() + //{ + // var type = typeof(T); + // var jsonRoot = (string)null; + // if (type == typeof(Error)) jsonRoot = RedmineKeys.ERRORS; + // if (type == typeof(WikiPage)) jsonRoot = RedmineKeys.WIKI_PAGES; + // if (type == typeof(IssuePriority)) jsonRoot = RedmineKeys.ISSUE_PRIORITIES; + // if (type == typeof(TimeEntryActivity)) jsonRoot = RedmineKeys.TIME_ENTRY_ACTIVITIES; + + // if (string.IsNullOrEmpty(jsonRoot)) + // jsonRoot = RedmineManager.Sufixes[type]; + + // var result = JsonDeserializeToList(response, jsonRoot, out var totalItems, out var offset); + + // return new PaginatedObjects() + // { + // TotalCount = totalItems, + // Offset = offset, + // Objects = result.ToList() + // }; + //} #endif /// /// XMLs the deserialize list. diff --git a/src/redmine-net-api/Internals/RedmineSerializerJson.cs b/src/redmine-net-api/Internals/RedmineSerializerJson.cs deleted file mode 100755 index 8dff8bb8..00000000 --- a/src/redmine-net-api/Internals/RedmineSerializerJson.cs +++ /dev/null @@ -1,245 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using System.Collections; -using System.Linq; -using Redmine.Net.Api.JSonConverters; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; -using Version = Redmine.Net.Api.Types.Version; - -namespace Redmine.Net.Api.Internals -{ - /// - /// - /// - internal static partial class RedmineSerializer - { - private static readonly Dictionary jsonConverters = new Dictionary - { - {typeof (Issue), new IssueConverter()}, - {typeof (Project), new ProjectConverter()}, - {typeof (User), new UserConverter()}, - {typeof (UserGroup), new UserGroupConverter()}, - {typeof (News), new NewsConverter()}, - {typeof (Query), new QueryConverter()}, - {typeof (Version), new VersionConverter()}, - {typeof (Attachment), new AttachmentConverter()}, - {typeof (Attachments), new AttachmentsConverter()}, - {typeof (IssueRelation), new IssueRelationConverter()}, - {typeof (TimeEntry), new TimeEntryConverter()}, - {typeof (IssueStatus),new IssueStatusConverter()}, - {typeof (Tracker),new TrackerConverter()}, - {typeof (TrackerCustomField),new TrackerCustomFieldConverter()}, - {typeof (IssueCategory), new IssueCategoryConverter()}, - {typeof (Role), new RoleConverter()}, - {typeof (ProjectMembership), new ProjectMembershipConverter()}, - {typeof (Group), new GroupConverter()}, - {typeof (GroupUser), new GroupUserConverter()}, - {typeof (Error), new ErrorConverter()}, - {typeof (IssueCustomField), new IssueCustomFieldConverter()}, - {typeof (ProjectTracker), new ProjectTrackerConverter()}, - {typeof (Journal), new JournalConverter()}, - {typeof (TimeEntryActivity), new TimeEntryActivityConverter()}, - {typeof (IssuePriority), new IssuePriorityConverter()}, - {typeof (WikiPage), new WikiPageConverter()}, - {typeof (Detail), new DetailConverter()}, - {typeof (ChangeSet), new ChangeSetConverter()}, - {typeof (Membership), new MembershipConverter()}, - {typeof (MembershipRole), new MembershipRoleConverter()}, - {typeof (IdentifiableName), new IdentifiableNameConverter()}, - {typeof (Permission), new PermissionConverter()}, - {typeof (IssueChild), new IssueChildConverter()}, - {typeof (ProjectIssueCategory), new ProjectIssueCategoryConverter()}, - {typeof (Watcher), new WatcherConverter()}, - {typeof (Upload), new UploadConverter()}, - {typeof (ProjectEnabledModule), new ProjectEnabledModuleConverter()}, - {typeof (CustomField), new CustomFieldConverter()}, - {typeof (CustomFieldRole), new CustomFieldRoleConverter()}, - {typeof (CustomFieldPossibleValue), new CustomFieldPossibleValueConverter()}, - {typeof (File), new FileConverter() } - }; - - /// - /// Available json converters. - /// - public static Dictionary JsonConverters { get { return jsonConverters; } } - - /// - /// Jsons the serializer. - /// - /// - /// The type. - /// - public static string JsonSerializer(T type) where T : new() - { - var serializer = new JavaScriptSerializer() { MaxJsonLength = int.MaxValue }; - serializer.RegisterConverters(new[] { jsonConverters[typeof(T)] }); - return serializer.Serialize(type); - } - - /// - /// JSON Deserialization - /// - /// - /// The json string. - /// The root. - /// - public static List JsonDeserializeToList(string jsonString, string root) where T : class, new() - { - int totalCount; - int offset; - return JsonDeserializeToList(jsonString, root, out totalCount, out offset); - } - - /// - /// JSON Deserialization - /// - /// - /// The json string. - /// The root. - /// The total count. - /// The offset. - /// - public static List JsonDeserializeToList(string jsonString, string root, out int totalCount, out int offset) where T : class,new() - { - var result = JsonDeserializeToList(jsonString, root, typeof(T), out totalCount, out offset); - return ((ArrayList)result).OfType().ToList(); - } - - /// - /// Jsons the deserialize. - /// - /// - /// The json string. - /// The root. - /// - public static T JsonDeserialize(string jsonString, string root) where T : new() - { - var type = typeof(T); - var result = JsonDeserialize(jsonString, type, root); - return result == null ? default(T) : (T) result; - } - - /// - /// Jsons the deserialize. - /// - /// The json string. - /// The type. - /// The root. - /// - /// jsonString - /// - /// - /// - public static object JsonDeserialize(string jsonString, Type type, string root) - { - if (string.IsNullOrEmpty(jsonString)) throw new ArgumentNullException(nameof(jsonString)); - - var serializer = new JavaScriptSerializer(); - serializer.RegisterConverters(new[] { jsonConverters[type] }); - - var dictionary = serializer.Deserialize>(jsonString); - if (dictionary == null) return null; - - object obj; - return !dictionary.TryGetValue(root ?? type.Name.ToLowerInv(), out obj) ? null : serializer.ConvertToType(obj, type); - } - - /// - /// Adds to list. - /// - /// The serializer. - /// The list. - /// The type. - /// The array list. - private static void AddToList(JavaScriptSerializer serializer, IList list, Type type, object arrayList) - { - foreach (var obj in (ArrayList)arrayList) - { - if (obj is ArrayList) - { - AddToList(serializer, list, type, obj); - } - else - { - var convertedType = serializer.ConvertToType(obj, type); - list.Add(convertedType); - } - } - } - - /// - /// Jsons the deserialize to list. - /// - /// The json string. - /// The root. - /// The type. - /// The total count. - /// The offset. - /// - /// jsonString - private static object JsonDeserializeToList(string jsonString, string root, Type type, out int totalCount, out int offset) - { - totalCount = 0; - offset = 0; - if (string.IsNullOrEmpty(jsonString)) throw new ArgumentNullException(nameof(jsonString)); - - var serializer = new JavaScriptSerializer(); - serializer.RegisterConverters(new[] { jsonConverters[type] }); - var dictionary = serializer.Deserialize>(jsonString); - if (dictionary == null) return null; - - object obj, tc, off; - - if (dictionary.TryGetValue(RedmineKeys.TOTAL_COUNT, out tc)) totalCount = (int)tc; - - if (dictionary.TryGetValue(RedmineKeys.OFFSET, out off)) offset = (int)off; - - if (!dictionary.TryGetValue(root.ToLowerInv(), out obj)) return null; - - var arrayList = new ArrayList(); - if (type == typeof(Error)) - { - string info = null; - foreach (var item in (ArrayList)obj) - { - var innerArrayList = item as ArrayList; - if (innerArrayList != null) - { - info = innerArrayList.Cast() - .Aggregate(info, (current, item2) => current + (item2 as string + " ")); - } - else - { - info += $"{item as string} "; - } - } - var err = new Error { Info = info }; - arrayList.Add(err); - } - else - { - AddToList(serializer, arrayList, type, obj); - } - return arrayList; - } - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/AttachmentConverter.cs b/src/redmine-net-api/JSonConverters/AttachmentConverter.cs deleted file mode 100755 index aab99206..00000000 --- a/src/redmine-net-api/JSonConverters/AttachmentConverter.cs +++ /dev/null @@ -1,103 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 - - -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - - -namespace Redmine.Net.Api.JSonConverters -{ - /// - /// - /// - /// - internal class AttachmentConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// An instance of property data stored as name/value pairs. - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var attachment = new Attachment(); - - attachment.Id = dictionary.GetValue(RedmineKeys.ID); - attachment.Description = dictionary.GetValue(RedmineKeys.DESCRIPTION); - attachment.Author = dictionary.GetValueAsIdentifiableName(RedmineKeys.AUTHOR); - attachment.ContentType = dictionary.GetValue(RedmineKeys.CONTENT_TYPE); - attachment.ContentUrl = dictionary.GetValue(RedmineKeys.CONTENT_URL); - attachment.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); - attachment.FileName = dictionary.GetValue(RedmineKeys.FILENAME); - attachment.FileSize = dictionary.GetValue(RedmineKeys.FILESIZE); - - return attachment; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as Attachment; - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.FILENAME, entity.FileName); - result.Add(RedmineKeys.DESCRIPTION, entity.Description); - } - - var root = new Dictionary(); - root[RedmineKeys.ATTACHMENT] = result; - - return root; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(Attachment)}; } - } - - #endregion - } -} - -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/AttachmentsConverter.cs b/src/redmine-net-api/JSonConverters/AttachmentsConverter.cs deleted file mode 100755 index bc95d87e..00000000 --- a/src/redmine-net-api/JSonConverters/AttachmentsConverter.cs +++ /dev/null @@ -1,85 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System.Globalization; -#if !NET20 -using System; -using System.Collections.Generic; -using System.Linq; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class AttachmentsConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// An instance of property data stored as name/value pairs. - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object’s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as Attachments; - var result = new Dictionary(); - - if (entity != null) - { - foreach (var entry in entity) - { - var attachment = new AttachmentConverter().Serialize(entry.Value, serializer); - result.Add(entry.Key.ToString(CultureInfo.InvariantCulture), attachment.First().Value); - } - } - - var root = new Dictionary(); - root[RedmineKeys.ATTACHMENTS] = result; - - return root; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(Attachments)}; } - } - - #endregion - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/ChangeSetConverter.cs b/src/redmine-net-api/JSonConverters/ChangeSetConverter.cs deleted file mode 100755 index 56b6de4f..00000000 --- a/src/redmine-net-api/JSonConverters/ChangeSetConverter.cs +++ /dev/null @@ -1,82 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class ChangeSetConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// An instance of property data stored as name/value pairs. - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var changeSet = new ChangeSet - { - Revision = dictionary.GetValue(RedmineKeys.REVISION), - Comments = dictionary.GetValue(RedmineKeys.COMMENTS), - User = dictionary.GetValueAsIdentifiableName(RedmineKeys.USER), - CommittedOn = dictionary.GetValue(RedmineKeys.COMMITTED_ON) - }; - - - return changeSet; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(ChangeSet)}; } - } - - #endregion - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/CustomFieldConverter.cs b/src/redmine-net-api/JSonConverters/CustomFieldConverter.cs deleted file mode 100755 index 98bcbf6f..00000000 --- a/src/redmine-net-api/JSonConverters/CustomFieldConverter.cs +++ /dev/null @@ -1,93 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class CustomFieldConverter : IdentifiableNameConverter - { - #region Overrides of JavaScriptConverter - - /// - /// Deserializes the specified dictionary. - /// - /// The dictionary. - /// The type. - /// The serializer. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var customField = new CustomField(); - - customField.Id = dictionary.GetValue(RedmineKeys.ID); - customField.Name = dictionary.GetValue(RedmineKeys.NAME); - customField.CustomizedType = dictionary.GetValue(RedmineKeys.CUSTOMIZED_TYPE); - customField.FieldFormat = dictionary.GetValue(RedmineKeys.FIELD_FORMAT); - customField.Regexp = dictionary.GetValue(RedmineKeys.REGEXP); - customField.MinLength = dictionary.GetValue(RedmineKeys.MIN_LENGTH); - customField.MaxLength = dictionary.GetValue(RedmineKeys.MAX_LENGTH); - customField.IsRequired = dictionary.GetValue(RedmineKeys.IS_REQUIRED); - customField.IsFilter = dictionary.GetValue(RedmineKeys.IS_FILTER); - customField.Searchable = dictionary.GetValue(RedmineKeys.SEARCHABLE); - customField.Multiple = dictionary.GetValue(RedmineKeys.MULTIPLE); - customField.DefaultValue = dictionary.GetValue(RedmineKeys.DEFAULT_VALUE); - customField.Visible = dictionary.GetValue(RedmineKeys.VISIBLE); - customField.PossibleValues = - dictionary.GetValueAsCollection(RedmineKeys.POSSIBLE_VALUES); - customField.Trackers = dictionary.GetValueAsCollection(RedmineKeys.TRACKERS); - customField.Roles = dictionary.GetValueAsCollection(RedmineKeys.ROLES); - - - return customField; - } - - return null; - } - - /// - /// Serializes the specified object. - /// - /// The object. - /// The serializer. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// Gets the supported types. - /// - /// - /// The supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(CustomField)}; } - } - - #endregion - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/CustomFieldPossibleValueConverter.cs b/src/redmine-net-api/JSonConverters/CustomFieldPossibleValueConverter.cs deleted file mode 100644 index 434ae939..00000000 --- a/src/redmine-net-api/JSonConverters/CustomFieldPossibleValueConverter.cs +++ /dev/null @@ -1,77 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class CustomFieldPossibleValueConverter : IdentifiableNameConverter - { - #region Overrides of JavaScriptConverter - - /// - /// Deserializes the specified dictionary. - /// - /// The dictionary. - /// The type. - /// The serializer. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var entity = new CustomFieldPossibleValue(); - - entity.Value = dictionary.GetValue(RedmineKeys.VALUE); - entity.Label = dictionary.GetValue(RedmineKeys.LABEL); - - return entity; - } - - return null; - } - - /// - /// Serializes the specified object. - /// - /// The object. - /// The serializer. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// Gets the supported types. - /// - /// - /// The supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(CustomFieldPossibleValue)}; } - } - - #endregion - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/CustomFieldRoleConverter.cs b/src/redmine-net-api/JSonConverters/CustomFieldRoleConverter.cs deleted file mode 100755 index dc18c821..00000000 --- a/src/redmine-net-api/JSonConverters/CustomFieldRoleConverter.cs +++ /dev/null @@ -1,77 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class CustomFieldRoleConverter : IdentifiableNameConverter - { - #region Overrides of JavaScriptConverter - - /// - /// Deserializes the specified dictionary. - /// - /// The dictionary. - /// The type. - /// The serializer. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var entity = new CustomFieldRole(); - - entity.Id = dictionary.GetValue(RedmineKeys.ID); - entity.Name = dictionary.GetValue(RedmineKeys.NAME); - - return entity; - } - - return null; - } - - /// - /// Serializes the specified object. - /// - /// The object. - /// The serializer. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// Gets the supported types. - /// - /// - /// The supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(CustomFieldRole)}; } - } - - #endregion - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/DetailConverter.cs b/src/redmine-net-api/JSonConverters/DetailConverter.cs deleted file mode 100755 index 85aec9dd..00000000 --- a/src/redmine-net-api/JSonConverters/DetailConverter.cs +++ /dev/null @@ -1,83 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class DetailConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var detail = new Detail(); - - detail.NewValue = dictionary.GetValue(RedmineKeys.NEW_VALUE); - detail.OldValue = dictionary.GetValue(RedmineKeys.OLD_VALUE); - detail.Property = dictionary.GetValue(RedmineKeys.PROPERTY); - detail.Name = dictionary.GetValue(RedmineKeys.NAME); - - return detail; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(Detail)}; } - } - - #endregion - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/ErrorConverter.cs b/src/redmine-net-api/JSonConverters/ErrorConverter.cs deleted file mode 100755 index 9e388f18..00000000 --- a/src/redmine-net-api/JSonConverters/ErrorConverter.cs +++ /dev/null @@ -1,77 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class ErrorConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var error = new Error {Info = dictionary.GetValue(RedmineKeys.ERROR)}; - return error; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(Error)}; } - } - - #endregion - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/FileConverter.cs b/src/redmine-net-api/JSonConverters/FileConverter.cs deleted file mode 100755 index 1fe97b53..00000000 --- a/src/redmine-net-api/JSonConverters/FileConverter.cs +++ /dev/null @@ -1,112 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class FileConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var file = new File { }; - - file.Author = dictionary.GetValueAsIdentifiableName(RedmineKeys.AUTHOR); - file.ContentType = dictionary.GetValue(RedmineKeys.CONTENT_TYPE); - file.ContentUrl = dictionary.GetValue(RedmineKeys.CONTENT_URL); - file.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); - file.Description = dictionary.GetValue(RedmineKeys.DESCRIPTION); - file.Digest = dictionary.GetValue(RedmineKeys.DIGEST); - file.Downloads = dictionary.GetValue(RedmineKeys.DOWNLOADS); - file.Filename = dictionary.GetValue(RedmineKeys.FILENAME); - file.Filesize = dictionary.GetValue(RedmineKeys.FILESIZE); - file.Id = dictionary.GetValue(RedmineKeys.ID); - file.Token = dictionary.GetValue(RedmineKeys.TOKEN); - var versionId = dictionary.GetValue(RedmineKeys.VERSION_ID); - if (versionId.HasValue) - { - file.Version = new IdentifiableName { Id = versionId.Value }; - } - else - { - file.Version = dictionary.GetValueAsIdentifiableName(RedmineKeys.VERSION); - } - return file; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object’s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as File; - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.TOKEN, entity.Token); - result.WriteIdIfNotNull(entity.Version, RedmineKeys.VERSION_ID); - result.Add(RedmineKeys.FILENAME, entity.Filename); - result.Add(RedmineKeys.DESCRIPTION, entity.Description); - - var root = new Dictionary(); - root[RedmineKeys.FILE] = result; - return root; - } - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] { typeof(File) }; } - } - - #endregion - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/GroupConverter.cs b/src/redmine-net-api/JSonConverters/GroupConverter.cs deleted file mode 100755 index 893888de..00000000 --- a/src/redmine-net-api/JSonConverters/GroupConverter.cs +++ /dev/null @@ -1,98 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class GroupConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var group = new Group(); - - group.Id = dictionary.GetValue(RedmineKeys.ID); - group.Name = dictionary.GetValue(RedmineKeys.NAME); - group.Users = dictionary.GetValueAsCollection(RedmineKeys.USERS); - group.CustomFields = dictionary.GetValueAsCollection(RedmineKeys.CUSTOM_FIELDS); - group.Memberships = dictionary.GetValueAsCollection(RedmineKeys.MEMBERSHIPS); - - return group; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as Group; - - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.NAME, entity.Name); - result.WriteIdsArray(RedmineKeys.USER_IDS, entity.Users); - - var root = new Dictionary(); - root[RedmineKeys.GROUP] = result; - return root; - } - - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(Group)}; } - } - - #endregion - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/GroupUserConverter.cs b/src/redmine-net-api/JSonConverters/GroupUserConverter.cs deleted file mode 100755 index 2ac9d56f..00000000 --- a/src/redmine-net-api/JSonConverters/GroupUserConverter.cs +++ /dev/null @@ -1,78 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - - -namespace Redmine.Net.Api.JSonConverters -{ - internal class GroupUserConverter : IdentifiableNameConverter - { - #region Overrides of JavaScriptConverter - - /// - /// Deserializes the specified dictionary. - /// - /// The dictionary. - /// The type. - /// The serializer. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var userGroup = new GroupUser(); - - userGroup.Id = dictionary.GetValue(RedmineKeys.ID); - userGroup.Name = dictionary.GetValue(RedmineKeys.NAME); - - return userGroup; - } - - return null; - } - - /// - /// Serializes the specified object. - /// - /// The object. - /// The serializer. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// Gets the supported types. - /// - /// - /// The supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(GroupUser)}; } - } - - #endregion - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/IdentifiableNameConverter.cs b/src/redmine-net-api/JSonConverters/IdentifiableNameConverter.cs deleted file mode 100755 index 4ea1bc91..00000000 --- a/src/redmine-net-api/JSonConverters/IdentifiableNameConverter.cs +++ /dev/null @@ -1,92 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class IdentifiableNameConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var entity = new IdentifiableName(); - - entity.Id = dictionary.GetValue(RedmineKeys.ID); - entity.Name = dictionary.GetValue(RedmineKeys.NAME); - - return entity; - } - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as IdentifiableName; - var result = new Dictionary(); - - if (entity != null) - { - result.WriteIdIfNotNull(entity, RedmineKeys.ID); - - if (!string.IsNullOrEmpty(entity.Name)) - result.Add(RedmineKeys.NAME, entity.Name); - return result; - } - - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(IdentifiableName)}; } - } - - #endregion - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/IssueCategoryConverter.cs b/src/redmine-net-api/JSonConverters/IssueCategoryConverter.cs deleted file mode 100755 index 8a0f34f5..00000000 --- a/src/redmine-net-api/JSonConverters/IssueCategoryConverter.cs +++ /dev/null @@ -1,98 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class IssueCategoryConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var issueCategory = new IssueCategory(); - - issueCategory.Id = dictionary.GetValue(RedmineKeys.ID); - issueCategory.Project = dictionary.GetValueAsIdentifiableName(RedmineKeys.PROJECT); - issueCategory.AsignTo = dictionary.GetValueAsIdentifiableName(RedmineKeys.ASSIGNED_TO); - issueCategory.Name = dictionary.GetValue(RedmineKeys.NAME); - - return issueCategory; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as IssueCategory; - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.NAME, entity.Name); - result.WriteIdIfNotNull(entity.Project, RedmineKeys.PROJECT_ID); - result.WriteIdIfNotNull(entity.AsignTo, RedmineKeys.ASSIGNED_TO_ID); - - var root = new Dictionary(); - - root[RedmineKeys.ISSUE_CATEGORY] = result; - return root; - } - - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(IssueCategory)}; } - } - - #endregion - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/IssueChildConverter.cs b/src/redmine-net-api/JSonConverters/IssueChildConverter.cs deleted file mode 100755 index bfd7d233..00000000 --- a/src/redmine-net-api/JSonConverters/IssueChildConverter.cs +++ /dev/null @@ -1,76 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class IssueChildConverter : JavaScriptConverter - { - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(IssueChild)}; } - } - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// An instance of property data stored as name/value pairs. - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var issueChild = new IssueChild - { - Id = dictionary.GetValue(RedmineKeys.ID), - Tracker = dictionary.GetValueAsIdentifiableName(RedmineKeys.TRACKER), - Subject = dictionary.GetValue(RedmineKeys.SUBJECT) - }; - - return issueChild; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/IssueConverter.cs b/src/redmine-net-api/JSonConverters/IssueConverter.cs deleted file mode 100644 index a67167dc..00000000 --- a/src/redmine-net-api/JSonConverters/IssueConverter.cs +++ /dev/null @@ -1,156 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - - -namespace Redmine.Net.Api.JSonConverters -{ - internal class IssueConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var issue = new Issue(); - - issue.Id = dictionary.GetValue(RedmineKeys.ID); - issue.Description = dictionary.GetValue(RedmineKeys.DESCRIPTION); - issue.Project = dictionary.GetValueAsIdentifiableName(RedmineKeys.PROJECT); - issue.Tracker = dictionary.GetValueAsIdentifiableName(RedmineKeys.TRACKER); - issue.Status = dictionary.GetValueAsIdentifiableName(RedmineKeys.STATUS); - issue.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); - issue.UpdatedOn = dictionary.GetValue(RedmineKeys.UPDATED_ON); - issue.ClosedOn = dictionary.GetValue(RedmineKeys.CLOSED_ON); - issue.Priority = dictionary.GetValueAsIdentifiableName(RedmineKeys.PRIORITY); - issue.Author = dictionary.GetValueAsIdentifiableName(RedmineKeys.AUTHOR); - issue.AssignedTo = dictionary.GetValueAsIdentifiableName(RedmineKeys.ASSIGNED_TO); - issue.Category = dictionary.GetValueAsIdentifiableName(RedmineKeys.CATEGORY); - issue.FixedVersion = dictionary.GetValueAsIdentifiableName(RedmineKeys.FIXED_VERSION); - issue.Subject = dictionary.GetValue(RedmineKeys.SUBJECT); - issue.Notes = dictionary.GetValue(RedmineKeys.NOTES); - issue.IsPrivate = dictionary.GetValue(RedmineKeys.IS_PRIVATE); - issue.StartDate = dictionary.GetValue(RedmineKeys.START_DATE); - issue.DueDate = dictionary.GetValue(RedmineKeys.DUE_DATE); - issue.SpentHours = dictionary.GetValue(RedmineKeys.SPENT_HOURS); - issue.TotalSpentHours = dictionary.GetValue(RedmineKeys.TOTAL_SPENT_HOURS); - issue.DoneRatio = dictionary.GetValue(RedmineKeys.DONE_RATIO); - issue.EstimatedHours = dictionary.GetValue(RedmineKeys.ESTIMATED_HOURS); - issue.TotalEstimatedHours = dictionary.GetValue(RedmineKeys.TOTAL_ESTIMATED_HOURS); - issue.ParentIssue = dictionary.GetValueAsIdentifiableName(RedmineKeys.PARENT); - - issue.CustomFields = dictionary.GetValueAsCollection(RedmineKeys.CUSTOM_FIELDS); - issue.Attachments = dictionary.GetValueAsCollection(RedmineKeys.ATTACHMENTS); - issue.Relations = dictionary.GetValueAsCollection(RedmineKeys.RELATIONS); - issue.Journals = dictionary.GetValueAsCollection(RedmineKeys.JOURNALS); - issue.Changesets = dictionary.GetValueAsCollection(RedmineKeys.CHANGESETS); - issue.Watchers = dictionary.GetValueAsCollection(RedmineKeys.WATCHERS); - issue.Children = dictionary.GetValueAsCollection(RedmineKeys.CHILDREN); - return issue; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as Issue; - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.SUBJECT, entity.Subject); - result.Add(RedmineKeys.DESCRIPTION, entity.Description); - result.Add(RedmineKeys.NOTES, entity.Notes); - if (entity.Id != 0) - { - result.Add(RedmineKeys.PRIVATE_NOTES, entity.PrivateNotes.ToLowerInv()); - } - result.Add(RedmineKeys.IS_PRIVATE, entity.IsPrivate.ToLowerInv()); - result.WriteIdIfNotNull(entity.Project, RedmineKeys.PROJECT_ID); - result.WriteIdIfNotNull(entity.Priority, RedmineKeys.PRIORITY_ID); - result.WriteIdIfNotNull(entity.Status, RedmineKeys.STATUS_ID); - result.WriteIdIfNotNull(entity.Category, RedmineKeys.CATEGORY_ID); - result.WriteIdIfNotNull(entity.Tracker, RedmineKeys.TRACKER_ID); - result.WriteIdIfNotNull(entity.AssignedTo, RedmineKeys.ASSIGNED_TO_ID); - result.WriteIdIfNotNull(entity.FixedVersion, RedmineKeys.FIXED_VERSION_ID); - result.WriteValueOrEmpty(entity.EstimatedHours, RedmineKeys.ESTIMATED_HOURS); - - result.WriteIdOrEmpty(entity.ParentIssue, RedmineKeys.PARENT_ISSUE_ID); - result.WriteDateOrEmpty(entity.StartDate, RedmineKeys.START_DATE); - result.WriteDateOrEmpty(entity.DueDate, RedmineKeys.DUE_DATE); - result.WriteDateOrEmpty(entity.UpdatedOn, RedmineKeys.UPDATED_ON); - - if (entity.DoneRatio != null) - result.Add(RedmineKeys.DONE_RATIO, entity.DoneRatio.Value.ToString(CultureInfo.InvariantCulture)); - - if (entity.SpentHours != null) - result.Add(RedmineKeys.SPENT_HOURS, entity.SpentHours.Value.ToString(CultureInfo.InvariantCulture)); - - result.WriteArray(RedmineKeys.UPLOADS, entity.Uploads, new UploadConverter(), serializer); - result.WriteArray(RedmineKeys.CUSTOM_FIELDS, entity.CustomFields, new IssueCustomFieldConverter(), - serializer); - - result.WriteIdsArray(RedmineKeys.WATCHER_USER_IDS, entity.Watchers); - - var root = new Dictionary(); - root[RedmineKeys.ISSUE] = result; - return root; - } - - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(Issue)}; } - } - - #endregion - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/IssueCustomFieldConverter.cs b/src/redmine-net-api/JSonConverters/IssueCustomFieldConverter.cs deleted file mode 100755 index f788e242..00000000 --- a/src/redmine-net-api/JSonConverters/IssueCustomFieldConverter.cs +++ /dev/null @@ -1,122 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System.Globalization; -#if !NET20 -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class IssueCustomFieldConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var customField = new IssueCustomField(); - - customField.Id = dictionary.GetValue(RedmineKeys.ID); - customField.Name = dictionary.GetValue(RedmineKeys.NAME); - customField.Multiple = dictionary.GetValue(RedmineKeys.MULTIPLE); - - var val = dictionary.GetValue(RedmineKeys.VALUE); - - if (val != null) - { - if (customField.Values == null) customField.Values = new List(); - var list = val as ArrayList; - if (list != null) - { - foreach (var value in list) - { - customField.Values.Add(new CustomFieldValue {Info = Convert.ToString(value, CultureInfo.InvariantCulture)}); - } - } - else - { - customField.Values.Add(new CustomFieldValue {Info = Convert.ToString(val, CultureInfo.InvariantCulture)}); - } - } - return customField; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as IssueCustomField; - - var result = new Dictionary(); - - if (entity == null) return result; - if (entity.Values == null) return null; - var itemsCount = entity.Values.Count; - - result.Add(RedmineKeys.ID, entity.Id.ToString(CultureInfo.InvariantCulture)); - if (itemsCount > 1) - { - result.Add(RedmineKeys.VALUE, entity.Values.Select(x => x.Info).ToArray()); - } - else - { - result.Add(RedmineKeys.VALUE, itemsCount > 0 ? entity.Values[0].Info : null); - } - - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(IssueCustomField)}; } - } - - #endregion - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/IssuePriorityConverter.cs b/src/redmine-net-api/JSonConverters/IssuePriorityConverter.cs deleted file mode 100755 index bb41d295..00000000 --- a/src/redmine-net-api/JSonConverters/IssuePriorityConverter.cs +++ /dev/null @@ -1,78 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class IssuePriorityConverter : JavaScriptConverter - { - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(IssuePriority)}; } - } - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var issuePriority = new IssuePriority(); - - issuePriority.Id = dictionary.GetValue(RedmineKeys.ID); - issuePriority.Name = dictionary.GetValue(RedmineKeys.NAME); - issuePriority.IsDefault = dictionary.GetValue(RedmineKeys.IS_DEFAULT); - - return issuePriority; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/IssueRelationConverter.cs b/src/redmine-net-api/JSonConverters/IssueRelationConverter.cs deleted file mode 100755 index c5b0ce4a..00000000 --- a/src/redmine-net-api/JSonConverters/IssueRelationConverter.cs +++ /dev/null @@ -1,102 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System.Globalization; -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class IssueRelationConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var issueRelation = new IssueRelation(); - - issueRelation.Id = dictionary.GetValue(RedmineKeys.ID); - issueRelation.IssueId = dictionary.GetValue(RedmineKeys.ISSUE_ID); - issueRelation.IssueToId = dictionary.GetValue(RedmineKeys.ISSUE_TO_ID); - issueRelation.Type = dictionary.GetValue(RedmineKeys.RELATION_TYPE); - issueRelation.Delay = dictionary.GetValue(RedmineKeys.DELAY); - - return issueRelation; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as IssueRelation; - - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.ISSUE_TO_ID, entity.IssueToId.ToString(CultureInfo.InvariantCulture)); - result.Add(RedmineKeys.RELATION_TYPE, entity.Type.ToString()); - if (entity.Type == IssueRelationType.precedes || entity.Type == IssueRelationType.follows) - result.WriteValueOrEmpty(entity.Delay, RedmineKeys.DELAY); - - var root = new Dictionary(); - root[RedmineKeys.RELATION] = result; - return root; - } - - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(IssueRelation)}; } - } - - #endregion - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/IssueStatusConverter.cs b/src/redmine-net-api/JSonConverters/IssueStatusConverter.cs deleted file mode 100755 index f3c6815f..00000000 --- a/src/redmine-net-api/JSonConverters/IssueStatusConverter.cs +++ /dev/null @@ -1,82 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class IssueStatusConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var issueStatus = new IssueStatus(); - - issueStatus.Id = dictionary.GetValue(RedmineKeys.ID); - issueStatus.Name = dictionary.GetValue(RedmineKeys.NAME); - issueStatus.IsClosed = dictionary.GetValue(RedmineKeys.IS_CLOSED); - issueStatus.IsDefault = dictionary.GetValue(RedmineKeys.IS_DEFAULT); - return issueStatus; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(IssueStatus)}; } - } - - #endregion - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/JournalConverter.cs b/src/redmine-net-api/JSonConverters/JournalConverter.cs deleted file mode 100644 index f6a3ba5d..00000000 --- a/src/redmine-net-api/JSonConverters/JournalConverter.cs +++ /dev/null @@ -1,85 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class JournalConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var journal = new Journal(); - - journal.Id = dictionary.GetValue(RedmineKeys.ID); - journal.Notes = dictionary.GetValue(RedmineKeys.NOTES); - journal.User = dictionary.GetValueAsIdentifiableName(RedmineKeys.USER); - journal.PrivateNotes = dictionary.GetValue(RedmineKeys.PRIVATE_NOTES); - journal.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); - journal.Details = dictionary.GetValueAsCollection(RedmineKeys.DETAILS); - - return journal; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(Journal)}; } - } - - #endregion - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/MembershipConverter.cs b/src/redmine-net-api/JSonConverters/MembershipConverter.cs deleted file mode 100755 index 8ec11a74..00000000 --- a/src/redmine-net-api/JSonConverters/MembershipConverter.cs +++ /dev/null @@ -1,82 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class MembershipConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var membership = new Membership(); - - membership.Id = dictionary.GetValue(RedmineKeys.ID); - membership.Project = dictionary.GetValueAsIdentifiableName(RedmineKeys.PROJECT); - membership.Roles = dictionary.GetValueAsCollection(RedmineKeys.ROLES); - - return membership; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(Membership)}; } - } - - #endregion - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/MembershipRoleConverter.cs b/src/redmine-net-api/JSonConverters/MembershipRoleConverter.cs deleted file mode 100755 index c1548591..00000000 --- a/src/redmine-net-api/JSonConverters/MembershipRoleConverter.cs +++ /dev/null @@ -1,82 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class MembershipRoleConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var membershipRole = new MembershipRole(); - - membershipRole.Id = dictionary.GetValue(RedmineKeys.ID); - membershipRole.Inherited = dictionary.GetValue(RedmineKeys.INHERITED); - membershipRole.Name = dictionary.GetValue(RedmineKeys.NAME); - - return membershipRole; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(MembershipRole)}; } - } - - #endregion - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/NewsConverter.cs b/src/redmine-net-api/JSonConverters/NewsConverter.cs deleted file mode 100755 index 71420ba4..00000000 --- a/src/redmine-net-api/JSonConverters/NewsConverter.cs +++ /dev/null @@ -1,85 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class NewsConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var news = new News(); - - news.Id = dictionary.GetValue(RedmineKeys.ID); - news.Author = dictionary.GetValueAsIdentifiableName(RedmineKeys.AUTHOR); - news.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); - news.Description = dictionary.GetValue(RedmineKeys.DESCRIPTION); - news.Project = dictionary.GetValueAsIdentifiableName(RedmineKeys.PROJECT); - news.Summary = dictionary.GetValue(RedmineKeys.SUMMARY); - news.Title = dictionary.GetValue(RedmineKeys.TITLE); - - return news; - } - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(News)}; } - } - - #endregion - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/PermissionConverter.cs b/src/redmine-net-api/JSonConverters/PermissionConverter.cs deleted file mode 100755 index 7e2895d6..00000000 --- a/src/redmine-net-api/JSonConverters/PermissionConverter.cs +++ /dev/null @@ -1,73 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class PermissionConverter : JavaScriptConverter - { - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(Permission)}; } - } - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var permission = new Permission {Info = dictionary.GetValue(RedmineKeys.PERMISSION)}; - return permission; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/ProjectConverter.cs b/src/redmine-net-api/JSonConverters/ProjectConverter.cs deleted file mode 100755 index 7ba99aa5..00000000 --- a/src/redmine-net-api/JSonConverters/ProjectConverter.cs +++ /dev/null @@ -1,119 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System.Globalization; -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class ProjectConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var project = new Project(); - - project.Id = dictionary.GetValue(RedmineKeys.ID); - project.Description = dictionary.GetValue(RedmineKeys.DESCRIPTION); - project.HomePage = dictionary.GetValue(RedmineKeys.HOMEPAGE); - project.Name = dictionary.GetValue(RedmineKeys.NAME); - project.Identifier = dictionary.GetValue(RedmineKeys.IDENTIFIER); - project.Status = dictionary.GetValue(RedmineKeys.STATUS); - project.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); - project.UpdatedOn = dictionary.GetValue(RedmineKeys.UPDATED_ON); - project.Trackers = dictionary.GetValueAsCollection(RedmineKeys.TRACKERS); - project.CustomFields = dictionary.GetValueAsCollection(RedmineKeys.CUSTOM_FIELDS); - project.IsPublic = dictionary.GetValue(RedmineKeys.IS_PUBLIC); - project.Parent = dictionary.GetValueAsIdentifiableName(RedmineKeys.PARENT); - project.IssueCategories = dictionary.GetValueAsCollection(RedmineKeys.ISSUE_CATEGORIES); - project.EnabledModules = dictionary.GetValueAsCollection(RedmineKeys.ENABLED_MODULES); - project.TimeEntryActivities = dictionary.GetValueAsCollection(RedmineKeys.TIME_ENTRY_ACTIVITIES); - return project; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object’s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as Project; - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.NAME, entity.Name); - result.Add(RedmineKeys.IDENTIFIER, entity.Identifier); - result.Add(RedmineKeys.DESCRIPTION, entity.Description); - result.Add(RedmineKeys.HOMEPAGE, entity.HomePage); - //result.Add(RedmineKeys.INHERIT_MEMBERS, entity.InheritMembers.ToLowerInv()); - result.Add(RedmineKeys.IS_PUBLIC, entity.IsPublic.ToLowerInv()); - result.WriteIdOrEmpty(entity.Parent, RedmineKeys.PARENT_ID, string.Empty); - result.WriteIdsArray(RedmineKeys.TRACKER_IDS, entity.Trackers); - result.WriteNamesArray(RedmineKeys.ENABLED_MODULE_NAMES, entity.EnabledModules); - if (entity.Id > 0) - { - result.WriteArray(RedmineKeys.CUSTOM_FIELDS, entity.CustomFields, new IssueCustomFieldConverter(), - serializer); - } - var root = new Dictionary(); - root[RedmineKeys.PROJECT] = result; - return root; - } - - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] { typeof(Project) }; } - } - - #endregion - } -} -#endif diff --git a/src/redmine-net-api/JSonConverters/ProjectEnabledModuleConverter.cs b/src/redmine-net-api/JSonConverters/ProjectEnabledModuleConverter.cs deleted file mode 100755 index 089d9f4b..00000000 --- a/src/redmine-net-api/JSonConverters/ProjectEnabledModuleConverter.cs +++ /dev/null @@ -1,78 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class ProjectEnabledModuleConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var projectEnableModule = new ProjectEnabledModule(); - projectEnableModule.Id = dictionary.GetValue(RedmineKeys.ID); - projectEnableModule.Name = dictionary.GetValue(RedmineKeys.NAME); - return projectEnableModule; - } - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(ProjectEnabledModule)}; } - } - - #endregion - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/ProjectIssueCategoryConverter.cs b/src/redmine-net-api/JSonConverters/ProjectIssueCategoryConverter.cs deleted file mode 100755 index 0319ccbc..00000000 --- a/src/redmine-net-api/JSonConverters/ProjectIssueCategoryConverter.cs +++ /dev/null @@ -1,90 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class ProjectIssueCategoryConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var projectTracker = new ProjectIssueCategory(); - projectTracker.Id = dictionary.GetValue(RedmineKeys.ID); - projectTracker.Name = dictionary.GetValue(RedmineKeys.NAME); - return projectTracker; - } - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as ProjectIssueCategory; - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.ID, entity.Id); - result.Add(RedmineKeys.NAME, entity.Name); - - var root = new Dictionary(); - root[RedmineKeys.ISSUE_CATEGORY] = result; - return root; - } - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(ProjectIssueCategory)}; } - } - - #endregion - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/ProjectMembershipConverter.cs b/src/redmine-net-api/JSonConverters/ProjectMembershipConverter.cs deleted file mode 100755 index 595f31fb..00000000 --- a/src/redmine-net-api/JSonConverters/ProjectMembershipConverter.cs +++ /dev/null @@ -1,96 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class ProjectMembershipConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var projectMembership = new ProjectMembership(); - - projectMembership.Id = dictionary.GetValue(RedmineKeys.ID); - projectMembership.Group = dictionary.GetValueAsIdentifiableName(RedmineKeys.GROUP); - projectMembership.Project = dictionary.GetValueAsIdentifiableName(RedmineKeys.PROJECT); - projectMembership.Roles = dictionary.GetValueAsCollection(RedmineKeys.ROLES); - projectMembership.User = dictionary.GetValueAsIdentifiableName(RedmineKeys.USER); - - return projectMembership; - } - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as ProjectMembership; - var result = new Dictionary(); - - if (entity != null) - { - result.WriteIdIfNotNull(entity.User, RedmineKeys.USER_ID); - result.WriteIdsArray(RedmineKeys.ROLE_IDS, entity.Roles); - - var root = new Dictionary(); - root[RedmineKeys.MEMBERSHIP] = result; - return root; - } - - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(ProjectMembership)}; } - } - - #endregion - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/ProjectTrackerConverter.cs b/src/redmine-net-api/JSonConverters/ProjectTrackerConverter.cs deleted file mode 100755 index 5697d888..00000000 --- a/src/redmine-net-api/JSonConverters/ProjectTrackerConverter.cs +++ /dev/null @@ -1,78 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class ProjectTrackerConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var projectTracker = new ProjectTracker(); - projectTracker.Id = dictionary.GetValue(RedmineKeys.ID); - projectTracker.Name = dictionary.GetValue(RedmineKeys.NAME); - return projectTracker; - } - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(ProjectTracker)}; } - } - - #endregion - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/QueryConverter.cs b/src/redmine-net-api/JSonConverters/QueryConverter.cs deleted file mode 100755 index 88f8fc62..00000000 --- a/src/redmine-net-api/JSonConverters/QueryConverter.cs +++ /dev/null @@ -1,83 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class QueryConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var query = new Query(); - - query.Id = dictionary.GetValue(RedmineKeys.ID); - query.IsPublic = dictionary.GetValue(RedmineKeys.IS_PUBLIC); - query.ProjectId = dictionary.GetValue(RedmineKeys.PROJECT_ID); - query.Name = dictionary.GetValue(RedmineKeys.NAME); - - return query; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(Query)}; } - } - - #endregion - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/RoleConverter.cs b/src/redmine-net-api/JSonConverters/RoleConverter.cs deleted file mode 100755 index b14af70a..00000000 --- a/src/redmine-net-api/JSonConverters/RoleConverter.cs +++ /dev/null @@ -1,92 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class RoleConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var role = new Role(); - - role.Id = dictionary.GetValue(RedmineKeys.ID); - role.Name = dictionary.GetValue(RedmineKeys.NAME); - - var permissions = dictionary.GetValue(RedmineKeys.PERMISSIONS); - if (permissions != null) - { - role.Permissions = new List(); - foreach (var permission in permissions) - { - var perms = new Permission {Info = permission.ToString()}; - role.Permissions.Add(perms); - } - } - - return role; - } - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(Role)}; } - } - - #endregion - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/TimeEntryActivityConverter.cs b/src/redmine-net-api/JSonConverters/TimeEntryActivityConverter.cs deleted file mode 100755 index 435b29ef..00000000 --- a/src/redmine-net-api/JSonConverters/TimeEntryActivityConverter.cs +++ /dev/null @@ -1,78 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class TimeEntryActivityConverter : JavaScriptConverter - { - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(TimeEntryActivity)}; } - } - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var timeEntryActivity = new TimeEntryActivity - { - Id = dictionary.GetValue(RedmineKeys.ID), - Name = dictionary.GetValue(RedmineKeys.NAME), - IsDefault = dictionary.GetValue(RedmineKeys.IS_DEFAULT) - }; - return timeEntryActivity; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/TimeEntryConverter.cs b/src/redmine-net-api/JSonConverters/TimeEntryConverter.cs deleted file mode 100755 index 3e001bd0..00000000 --- a/src/redmine-net-api/JSonConverters/TimeEntryConverter.cs +++ /dev/null @@ -1,121 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Extensions; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class TimeEntryConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var timeEntry = new TimeEntry(); - - timeEntry.Id = dictionary.GetValue(RedmineKeys.ID); - timeEntry.Activity = - dictionary.GetValueAsIdentifiableName(dictionary.ContainsKey(RedmineKeys.ACTIVITY) - ? RedmineKeys.ACTIVITY - : RedmineKeys.ACTIVITY_ID); - timeEntry.Comments = dictionary.GetValue(RedmineKeys.COMMENTS); - timeEntry.Hours = dictionary.GetValue(RedmineKeys.HOURS); - timeEntry.Issue = - dictionary.GetValueAsIdentifiableName(dictionary.ContainsKey(RedmineKeys.ISSUE) - ? RedmineKeys.ISSUE - : RedmineKeys.ISSUE_ID); - timeEntry.Project = - dictionary.GetValueAsIdentifiableName(dictionary.ContainsKey(RedmineKeys.PROJECT) - ? RedmineKeys.PROJECT - : RedmineKeys.PROJECT_ID); - timeEntry.SpentOn = dictionary.GetValue(RedmineKeys.SPENT_ON); - timeEntry.User = dictionary.GetValueAsIdentifiableName(RedmineKeys.USER); - timeEntry.CustomFields = dictionary.GetValueAsCollection(RedmineKeys.CUSTOM_FIELDS); - timeEntry.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); - timeEntry.UpdatedOn = dictionary.GetValue(RedmineKeys.UPDATED_ON); - - return timeEntry; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as TimeEntry; - var result = new Dictionary(); - - if (entity != null) - { - result.WriteIdIfNotNull(entity.Issue, RedmineKeys.ISSUE_ID); - result.WriteIdIfNotNull(entity.Project, RedmineKeys.PROJECT_ID); - result.WriteIdIfNotNull(entity.Activity, RedmineKeys.ACTIVITY_ID); - - if (!entity.SpentOn.HasValue) entity.SpentOn = DateTime.Now; - - result.WriteDateOrEmpty(entity.SpentOn, RedmineKeys.SPENT_ON); - result.Add(RedmineKeys.HOURS, entity.Hours); - result.Add(RedmineKeys.COMMENTS, entity.Comments); - result.WriteArray(RedmineKeys.CUSTOM_FIELDS, entity.CustomFields, new IssueCustomFieldConverter(), - serializer); - - var root = new Dictionary(); - root[RedmineKeys.TIME_ENTRY] = result; - return root; - } - - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(TimeEntry)}; } - } - - #endregion - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/TrackerConverter.cs b/src/redmine-net-api/JSonConverters/TrackerConverter.cs deleted file mode 100755 index 221dbae4..00000000 --- a/src/redmine-net-api/JSonConverters/TrackerConverter.cs +++ /dev/null @@ -1,81 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class TrackerConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var tracker = new Tracker - { - Id = dictionary.GetValue(RedmineKeys.ID), - Name = dictionary.GetValue(RedmineKeys.NAME) - }; - return tracker; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(Tracker)}; } - } - - #endregion - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/TrackerCustomFieldConverter.cs b/src/redmine-net-api/JSonConverters/TrackerCustomFieldConverter.cs deleted file mode 100755 index 415480b3..00000000 --- a/src/redmine-net-api/JSonConverters/TrackerCustomFieldConverter.cs +++ /dev/null @@ -1,68 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class TrackerCustomFieldConverter : IdentifiableNameConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var entity = new TrackerCustomField(); - - entity.Id = dictionary.GetValue(RedmineKeys.ID); - entity.Name = dictionary.GetValue(RedmineKeys.NAME); - - return entity; - } - - return null; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(TrackerCustomField)}; } - } - - #endregion - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/UploadConverter.cs b/src/redmine-net-api/JSonConverters/UploadConverter.cs deleted file mode 100755 index 1df7d6f1..00000000 --- a/src/redmine-net-api/JSonConverters/UploadConverter.cs +++ /dev/null @@ -1,93 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class UploadConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var upload = new Upload(); - - upload.ContentType = dictionary.GetValue(RedmineKeys.CONTENT_TYPE); - upload.FileName = dictionary.GetValue(RedmineKeys.FILENAME); - upload.Token = dictionary.GetValue(RedmineKeys.TOKEN); - upload.Description = dictionary.GetValue(RedmineKeys.DESCRIPTION); - return upload; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as Upload; - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.CONTENT_TYPE, entity.ContentType); - result.Add(RedmineKeys.FILENAME, entity.FileName); - result.Add(RedmineKeys.TOKEN, entity.Token); - result.Add(RedmineKeys.DESCRIPTION, entity.Description); - } - - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(Upload)}; } - } - - #endregion - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/UserConverter.cs b/src/redmine-net-api/JSonConverters/UserConverter.cs deleted file mode 100644 index d5055a9c..00000000 --- a/src/redmine-net-api/JSonConverters/UserConverter.cs +++ /dev/null @@ -1,127 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class UserConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var user = new User(); - user.Login = dictionary.GetValue(RedmineKeys.LOGIN); - user.Id = dictionary.GetValue(RedmineKeys.ID); - user.FirstName = dictionary.GetValue(RedmineKeys.FIRSTNAME); - user.LastName = dictionary.GetValue(RedmineKeys.LASTNAME); - user.Email = dictionary.GetValue(RedmineKeys.MAIL); - user.MailNotification = dictionary.GetValue(RedmineKeys.MAIL_NOTIFICATION); - user.AuthenticationModeId = dictionary.GetValue(RedmineKeys.AUTH_SOURCE_ID); - user.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); - user.LastLoginOn = dictionary.GetValue(RedmineKeys.LAST_LOGIN_ON); - user.ApiKey = dictionary.GetValue(RedmineKeys.API_KEY); - user.Status = dictionary.GetValue(RedmineKeys.STATUS); - user.MustChangePassword = dictionary.GetValue(RedmineKeys.MUST_CHANGE_PASSWD); - user.CustomFields = dictionary.GetValueAsCollection(RedmineKeys.CUSTOM_FIELDS); - user.Memberships = dictionary.GetValueAsCollection(RedmineKeys.MEMBERSHIPS); - user.Groups = dictionary.GetValueAsCollection(RedmineKeys.GROUPS); - - return user; - } - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as User; - - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.LOGIN, entity.Login); - result.Add(RedmineKeys.FIRSTNAME, entity.FirstName); - result.Add(RedmineKeys.LASTNAME, entity.LastName); - result.Add(RedmineKeys.MAIL, entity.Email); - if(!string.IsNullOrWhiteSpace(entity.MailNotification)) - { - result.Add(RedmineKeys.MAIL_NOTIFICATION, entity.MailNotification); - } - - if(!string.IsNullOrWhiteSpace(entity.Password)) - { - result.Add(RedmineKeys.PASSWORD, entity.Password); - } - - result.Add(RedmineKeys.MUST_CHANGE_PASSWD, entity.MustChangePassword.ToLowerInv()); - result.Add(RedmineKeys.STATUS, ((int)entity.Status).ToString(CultureInfo.InvariantCulture)); - - if(entity.AuthenticationModeId.HasValue) - { - result.WriteValueOrEmpty(entity.AuthenticationModeId, RedmineKeys.AUTH_SOURCE_ID); - } - result.WriteArray(RedmineKeys.CUSTOM_FIELDS, entity.CustomFields, new IssueCustomFieldConverter(), - serializer); - - var root = new Dictionary(); - root[RedmineKeys.USER] = result; - return root; - } - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(User)}; } - } - - #endregion - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/UserGroupConverter.cs b/src/redmine-net-api/JSonConverters/UserGroupConverter.cs deleted file mode 100755 index 2137bb10..00000000 --- a/src/redmine-net-api/JSonConverters/UserGroupConverter.cs +++ /dev/null @@ -1,68 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class UserGroupConverter : IdentifiableNameConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(UserGroup)}; } - } - - #endregion - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var userGroup = new UserGroup(); - - userGroup.Id = dictionary.GetValue(RedmineKeys.ID); - userGroup.Name = dictionary.GetValue(RedmineKeys.NAME); - - return userGroup; - } - - return null; - } - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/VersionConverter.cs b/src/redmine-net-api/JSonConverters/VersionConverter.cs deleted file mode 100755 index 54e568e6..00000000 --- a/src/redmine-net-api/JSonConverters/VersionConverter.cs +++ /dev/null @@ -1,109 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System.Globalization; -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; -using Version = Redmine.Net.Api.Types.Version; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class VersionConverter : JavaScriptConverter - { - #region Overrides of JavaScriptConverter - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var version = new Version(); - - version.Id = dictionary.GetValue(RedmineKeys.ID); - version.Description = dictionary.GetValue(RedmineKeys.DESCRIPTION); - version.Name = dictionary.GetValue(RedmineKeys.NAME); - version.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); - version.UpdatedOn = dictionary.GetValue(RedmineKeys.UPDATED_ON); - version.DueDate = dictionary.GetValue(RedmineKeys.DUE_DATE); - version.Project = dictionary.GetValueAsIdentifiableName(RedmineKeys.PROJECT); - version.Sharing = dictionary.GetValue(RedmineKeys.SHARING); - version.Status = dictionary.GetValue(RedmineKeys.STATUS); - version.CustomFields = dictionary.GetValueAsCollection(RedmineKeys.CUSTOM_FIELDS); - - return version; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as Version; - - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.NAME, entity.Name); - result.Add(RedmineKeys.STATUS, entity.Status.ToString("G").ToLowerInv()); - result.Add(RedmineKeys.SHARING, entity.Sharing.ToString("G").ToLowerInv()); - result.Add(RedmineKeys.DESCRIPTION, entity.Description); - - var root = new Dictionary(); - result.WriteDateOrEmpty(entity.DueDate, RedmineKeys.DUE_DATE); - root[RedmineKeys.VERSION] = result; - return root; - } - - return result; - } - - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(Version)}; } - } - - #endregion - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/WatcherConverter.cs b/src/redmine-net-api/JSonConverters/WatcherConverter.cs deleted file mode 100755 index b7f93e2d..00000000 --- a/src/redmine-net-api/JSonConverters/WatcherConverter.cs +++ /dev/null @@ -1,87 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System.Globalization; -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class WatcherConverter : JavaScriptConverter - { - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] {typeof(Watcher)}; } - } - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var watcher = new Watcher(); - - watcher.Id = dictionary.GetValue(RedmineKeys.ID); - watcher.Name = dictionary.GetValue(RedmineKeys.NAME); - - return watcher; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as Watcher; - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.ID, entity.Id.ToString(CultureInfo.InvariantCulture)); - } - - return result; - } - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/JSonConverters/WikiPageConverter.cs b/src/redmine-net-api/JSonConverters/WikiPageConverter.cs deleted file mode 100755 index c5c6af0a..00000000 --- a/src/redmine-net-api/JSonConverters/WikiPageConverter.cs +++ /dev/null @@ -1,99 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !NET20 -using System; -using System.Collections.Generic; -using System.Web.Script.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.JSonConverters -{ - internal class WikiPageConverter : JavaScriptConverter - { - /// - /// When overridden in a derived class, gets a collection of the supported types. - /// - public override IEnumerable SupportedTypes - { - get { return new[] { typeof(WikiPage) }; } - } - - /// - /// When overridden in a derived class, converts the provided dictionary into an object of the specified type. - /// - /// - /// An instance of property data stored - /// as name/value pairs. - /// - /// The type of the resulting object. - /// The instance. - /// - /// The deserialized object. - /// - public override object Deserialize(IDictionary dictionary, Type type, - JavaScriptSerializer serializer) - { - if (dictionary != null) - { - var tracker = new WikiPage(); - - tracker.Id = dictionary.GetValue(RedmineKeys.ID); - tracker.Author = dictionary.GetValueAsIdentifiableName(RedmineKeys.AUTHOR); - tracker.Comments = dictionary.GetValue(RedmineKeys.COMMENTS); - tracker.CreatedOn = dictionary.GetValue(RedmineKeys.CREATED_ON); - tracker.Text = dictionary.GetValue(RedmineKeys.TEXT); - tracker.Title = dictionary.GetValue(RedmineKeys.TITLE); - tracker.UpdatedOn = dictionary.GetValue(RedmineKeys.UPDATED_ON); - tracker.Version = dictionary.GetValue(RedmineKeys.VERSION); - tracker.Attachments = dictionary.GetValueAsCollection(RedmineKeys.ATTACHMENTS); - - return tracker; - } - - return null; - } - - /// - /// When overridden in a derived class, builds a dictionary of name/value pairs. - /// - /// The object to serialize. - /// The object that is responsible for the serialization. - /// - /// An object that contains key/value pairs that represent the object�s data. - /// - public override IDictionary Serialize(object obj, JavaScriptSerializer serializer) - { - var entity = obj as WikiPage; - var result = new Dictionary(); - - if (entity != null) - { - result.Add(RedmineKeys.TEXT, entity.Text); - result.Add(RedmineKeys.COMMENTS, entity.Comments); - result.WriteValueOrEmpty(entity.Version, RedmineKeys.VERSION); - result.WriteArray(RedmineKeys.UPLOADS, entity.Uploads, new UploadConverter(), serializer); - - var root = new Dictionary(); - root[RedmineKeys.WIKI_PAGE] = result; - return root; - } - - return result; - } - } -} -#endif \ No newline at end of file From be31def798252e0e9844c4972c33b13a239fdfe3 Mon Sep 17 00:00:00 2001 From: zapadi Date: Fri, 10 Jan 2020 18:55:43 +0200 Subject: [PATCH 091/549] Refactor xml serialization --- .../Extensions/XmlReaderExtensions.cs | 22 +- .../Extensions/XmlWriterExtensions.cs | 153 +++++-- src/redmine-net-api/RedmineKeys.cs | 51 ++- .../Serialization/ISerialization.cs | 11 + .../Serialization/PagedResults.cs | 62 +++ .../Serialization/XmlRedmineSerializer.cs | 179 ++++++++ src/redmine-net-api/Types/Attachment.cs | 97 ++-- src/redmine-net-api/Types/Attachments.cs | 2 +- src/redmine-net-api/Types/ChangeSet.cs | 42 +- src/redmine-net-api/Types/CustomField.cs | 162 +++---- .../Types/CustomFieldPossibleValue.cs | 93 +++- src/redmine-net-api/Types/CustomFieldRole.cs | 16 +- src/redmine-net-api/Types/CustomFieldValue.cs | 102 ++++- src/redmine-net-api/Types/Detail.cs | 65 ++- src/redmine-net-api/Types/Error.cs | 55 ++- src/redmine-net-api/Types/File.cs | 180 +++----- src/redmine-net-api/Types/Group.cs | 61 +-- src/redmine-net-api/Types/GroupUser.cs | 18 +- src/redmine-net-api/Types/Identifiable.cs | 59 ++- src/redmine-net-api/Types/IdentifiableName.cs | 61 ++- src/redmine-net-api/Types/Issue.cs | 433 +++++++----------- src/redmine-net-api/Types/IssueCategory.cs | 75 ++- src/redmine-net-api/Types/IssueChild.cs | 66 ++- src/redmine-net-api/Types/IssueCustomField.cs | 117 +++-- src/redmine-net-api/Types/IssuePriority.cs | 34 +- src/redmine-net-api/Types/IssueRelation.cs | 70 ++- .../Types/IssueRelationType.cs | 14 +- src/redmine-net-api/Types/IssueStatus.cs | 55 +-- src/redmine-net-api/Types/Journal.cs | 75 +-- src/redmine-net-api/Types/Membership.cs | 58 +-- src/redmine-net-api/Types/MembershipRole.cs | 51 ++- src/redmine-net-api/Types/News.cs | 75 +-- src/redmine-net-api/Types/Permission.cs | 60 ++- src/redmine-net-api/Types/Project.cs | 235 +++++----- .../Types/ProjectEnabledModule.cs | 40 +- .../Types/ProjectIssueCategory.cs | 21 +- .../Types/ProjectMembership.cs | 94 ++-- .../Types/ProjectTimeEntryActivity.cs | 31 ++ src/redmine-net-api/Types/ProjectTracker.cs | 41 +- src/redmine-net-api/Types/Query.cs | 43 +- src/redmine-net-api/Types/Role.cs | 30 +- src/redmine-net-api/Types/TimeEntry.cs | 157 +++---- .../Types/TimeEntryActivity.cs | 36 +- src/redmine-net-api/Types/Tracker.cs | 23 +- .../Types/TrackerCustomField.cs | 14 +- src/redmine-net-api/Types/Upload.cs | 72 ++- src/redmine-net-api/Types/User.cs | 155 +++---- src/redmine-net-api/Types/UserGroup.cs | 10 +- src/redmine-net-api/Types/UserStatus.cs | 0 src/redmine-net-api/Types/Version.cs | 141 ++---- src/redmine-net-api/Types/VersionSharing.cs | 29 ++ src/redmine-net-api/Types/VersionStatus.cs | 21 + src/redmine-net-api/Types/Watcher.cs | 28 +- 53 files changed, 2153 insertions(+), 1742 deletions(-) mode change 100755 => 100644 src/redmine-net-api/Extensions/XmlReaderExtensions.cs mode change 100755 => 100644 src/redmine-net-api/Extensions/XmlWriterExtensions.cs mode change 100755 => 100644 src/redmine-net-api/RedmineKeys.cs create mode 100644 src/redmine-net-api/Serialization/ISerialization.cs create mode 100644 src/redmine-net-api/Serialization/PagedResults.cs create mode 100644 src/redmine-net-api/Serialization/XmlRedmineSerializer.cs mode change 100755 => 100644 src/redmine-net-api/Types/Attachment.cs mode change 100755 => 100644 src/redmine-net-api/Types/Attachments.cs mode change 100755 => 100644 src/redmine-net-api/Types/ChangeSet.cs mode change 100755 => 100644 src/redmine-net-api/Types/CustomField.cs mode change 100755 => 100644 src/redmine-net-api/Types/CustomFieldPossibleValue.cs mode change 100755 => 100644 src/redmine-net-api/Types/CustomFieldRole.cs mode change 100755 => 100644 src/redmine-net-api/Types/CustomFieldValue.cs mode change 100755 => 100644 src/redmine-net-api/Types/Detail.cs mode change 100755 => 100644 src/redmine-net-api/Types/Error.cs mode change 100755 => 100644 src/redmine-net-api/Types/Group.cs mode change 100755 => 100644 src/redmine-net-api/Types/GroupUser.cs mode change 100755 => 100644 src/redmine-net-api/Types/Identifiable.cs mode change 100755 => 100644 src/redmine-net-api/Types/IdentifiableName.cs mode change 100755 => 100644 src/redmine-net-api/Types/IssueCategory.cs mode change 100755 => 100644 src/redmine-net-api/Types/IssueChild.cs mode change 100755 => 100644 src/redmine-net-api/Types/IssueCustomField.cs mode change 100755 => 100644 src/redmine-net-api/Types/IssuePriority.cs mode change 100755 => 100644 src/redmine-net-api/Types/IssueRelation.cs mode change 100755 => 100644 src/redmine-net-api/Types/IssueRelationType.cs mode change 100755 => 100644 src/redmine-net-api/Types/IssueStatus.cs mode change 100755 => 100644 src/redmine-net-api/Types/Membership.cs mode change 100755 => 100644 src/redmine-net-api/Types/MembershipRole.cs mode change 100755 => 100644 src/redmine-net-api/Types/News.cs mode change 100755 => 100644 src/redmine-net-api/Types/Permission.cs mode change 100755 => 100644 src/redmine-net-api/Types/ProjectEnabledModule.cs mode change 100755 => 100644 src/redmine-net-api/Types/ProjectIssueCategory.cs mode change 100755 => 100644 src/redmine-net-api/Types/ProjectMembership.cs create mode 100644 src/redmine-net-api/Types/ProjectTimeEntryActivity.cs mode change 100755 => 100644 src/redmine-net-api/Types/ProjectTracker.cs mode change 100755 => 100644 src/redmine-net-api/Types/Query.cs mode change 100755 => 100644 src/redmine-net-api/Types/Role.cs mode change 100755 => 100644 src/redmine-net-api/Types/TimeEntryActivity.cs mode change 100755 => 100644 src/redmine-net-api/Types/Tracker.cs mode change 100755 => 100644 src/redmine-net-api/Types/TrackerCustomField.cs mode change 100755 => 100644 src/redmine-net-api/Types/Upload.cs mode change 100755 => 100644 src/redmine-net-api/Types/UserGroup.cs mode change 100755 => 100644 src/redmine-net-api/Types/UserStatus.cs mode change 100755 => 100644 src/redmine-net-api/Types/Version.cs create mode 100644 src/redmine-net-api/Types/VersionSharing.cs create mode 100644 src/redmine-net-api/Types/VersionStatus.cs mode change 100755 => 100644 src/redmine-net-api/Types/Watcher.cs diff --git a/src/redmine-net-api/Extensions/XmlReaderExtensions.cs b/src/redmine-net-api/Extensions/XmlReaderExtensions.cs old mode 100755 new mode 100644 index 00f9a7e5..7f1b121b --- a/src/redmine-net-api/Extensions/XmlReaderExtensions.cs +++ b/src/redmine-net-api/Extensions/XmlReaderExtensions.cs @@ -84,7 +84,7 @@ public static bool ReadAttributeAsBoolean(this XmlReader reader, string attribut { return false; } - + return result; } @@ -116,6 +116,7 @@ public static bool ReadAttributeAsBoolean(this XmlReader reader, string attribut public static float? ReadElementContentAsNullableFloat(this XmlReader reader) { var content = reader.ReadElementContentAsString(); + if (content.IsNullOrWhiteSpace() || !float.TryParse(content, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out var result)) { return null; @@ -167,12 +168,12 @@ public static bool ReadAttributeAsBoolean(this XmlReader reader, string attribut public static List ReadElementContentAsCollection(this XmlReader reader) where T : class { List result = null; - XmlSerializer serializer = null; + var serializer = new XmlSerializer(typeof(T)); var outerXml = reader.ReadOuterXml(); using (var stringReader = new StringReader(outerXml)) { - using (var xmlTextReader = XmlTextReaderBuilder.Create(stringReader)) + using (var xmlTextReader = XmlTextReaderBuilder.Create(stringReader)) { xmlTextReader.ReadStartElement(); while (!xmlTextReader.EOF) @@ -185,11 +186,6 @@ public static List ReadElementContentAsCollection(this XmlReader reader) w T entity; - if (serializer == null) - { - serializer = new XmlSerializer(typeof(T)); - } - if (xmlTextReader.IsEmptyElement && xmlTextReader.HasAttributes) { entity = serializer.Deserialize(xmlTextReader) as T; @@ -234,7 +230,7 @@ public static List ReadElementContentAsCollection(this XmlReader reader) w /// public static IEnumerable ReadElementContentAsEnumerable(this XmlReader reader) where T : class { - XmlSerializer serializer = null; + var serializer = new XmlSerializer(typeof(T)); var outerXml = reader.ReadOuterXml(); using (var stringReader = new StringReader(outerXml)) { @@ -250,11 +246,7 @@ public static IEnumerable ReadElementContentAsEnumerable(this XmlReader re } T entity; - if (serializer == null) - { - serializer = new XmlSerializer(typeof(T)); - } - + if (xmlTextReader.IsEmptyElement && xmlTextReader.HasAttributes) { entity = serializer.Deserialize(xmlTextReader) as T; @@ -266,7 +258,7 @@ public static IEnumerable ReadElementContentAsEnumerable(this XmlReader re } if (entity != null) { - yield return entity; + yield return entity; } if (!xmlTextReader.IsEmptyElement) diff --git a/src/redmine-net-api/Extensions/XmlWriterExtensions.cs b/src/redmine-net-api/Extensions/XmlWriterExtensions.cs old mode 100755 new mode 100644 index 7018f3f5..408af16f --- a/src/redmine-net-api/Extensions/XmlWriterExtensions.cs +++ b/src/redmine-net-api/Extensions/XmlWriterExtensions.cs @@ -37,16 +37,19 @@ public static partial class XmlExtensions private static readonly Type[] emptyTypeArray = new Type[0]; #endif private static readonly XmlAttributeOverrides xmlAttributeOverrides = new XmlAttributeOverrides(); - + /// /// Writes the id if not null. /// /// The writer. - /// The ident. - /// The tag. - public static void WriteIdIfNotNull(this XmlWriter writer, IdentifiableName ident, string tag) + /// + /// + public static void WriteIdIfNotNull(this XmlWriter writer, string elementName, IdentifiableName identifiableName) { - if (ident != null) writer.WriteElementString(tag, ident.Id.ToString(CultureInfo.InvariantCulture)); + if (identifiableName != null) + { + writer.WriteElementString(elementName, identifiableName.Id.ToString(CultureInfo.InvariantCulture)); + } } /// @@ -55,7 +58,7 @@ public static void WriteIdIfNotNull(this XmlWriter writer, IdentifiableName iden /// The writer. /// The collection. /// Name of the element. - public static void WriteArray(this XmlWriter writer, IEnumerable collection, string elementName) + public static void WriteArray(this XmlWriter writer, string elementName, IEnumerable collection) { if (collection == null) return; writer.WriteStartElement(elementName); @@ -69,6 +72,32 @@ public static void WriteArray(this XmlWriter writer, IEnumerable collection, str writer.WriteEndElement(); } + /// + /// Writes the array. + /// + /// The writer. + /// The collection. + /// Name of the element. + public static void WriteArray(this XmlWriter writer, string elementName, IEnumerable collection) + { + if (collection == null) + { + return; + } + + writer.WriteStartElement(elementName); + writer.WriteAttributeString("type", "array"); + + var serializer = new XmlSerializer(typeof(T)); + + foreach (var item in collection) + { + serializer.Serialize(writer, item); + } + + writer.WriteEndElement(); + } + /// /// Writes the array ids. /// @@ -77,7 +106,7 @@ public static void WriteArray(this XmlWriter writer, IEnumerable collection, str /// Name of the element. /// The type. /// The f. - public static void WriteArrayIds(this XmlWriter writer, IEnumerable collection, string elementName, Type type, Func f) + public static void WriteArrayIds(this XmlWriter writer, string elementName, IEnumerable collection, Type type, Func f) { if (collection == null || f == null) return; @@ -103,7 +132,7 @@ public static void WriteArrayIds(this XmlWriter writer, IEnumerable collection, /// The type. /// The root. /// The default namespace. - public static void WriteArray(this XmlWriter writer, IEnumerable collection, string elementName, Type type, string root, string defaultNamespace = null) + public static void WriteArray(this XmlWriter writer, string elementName, IEnumerable collection, Type type, string root, string defaultNamespace = null) { if (collection == null) return; @@ -124,34 +153,36 @@ public static void WriteArray(this XmlWriter writer, IEnumerable collection, str } /// - /// Writes the array string element. + /// Writes the list elements. /// - /// The writer. + /// The XML writer. /// The collection. /// Name of the element. - /// The func to invoke. - public static void WriteArrayStringElement(this XmlWriter writer, IEnumerable collection, string elementName, Func f) + public static void WriteListElements(this XmlWriter xmlWriter, string elementName, IEnumerable collection) { - if (collection == null || f == null) return; - writer.WriteStartElement(elementName); - writer.WriteAttributeString("type", "array"); + if (collection == null) + { + return; + } foreach (var item in collection) { - writer.WriteElementString(elementName, f.Invoke(item)); + xmlWriter.WriteElementString(elementName, item.Value); } - writer.WriteEndElement(); } /// - /// Writes the list elements. + /// /// - /// The XML writer. - /// The collection. - /// Name of the element. - public static void WriteListElements(this XmlWriter xmlWriter, IEnumerable collection, string elementName) + /// + /// + /// + public static void WriteRepeatableElement(this XmlWriter xmlWriter, string elementName, IEnumerable collection) { - if (collection == null) return; + if (collection == null) + { + return; + } foreach (var item in collection) { @@ -159,15 +190,33 @@ public static void WriteListElements(this XmlWriter xmlWriter, IEnumerable f) + { + if (collection == null) + { + return; + } + + writer.WriteStartElement(elementName); + writer.WriteAttributeString("type", "array"); + + foreach (var item in collection) + { + writer.WriteElementString(elementName, f.Invoke(item)); + } + + writer.WriteEndElement(); + } + /// - /// Writes the identifier or empty. + /// /// - /// The writer. - /// The ident. - /// The tag. - public static void WriteIdOrEmpty(this XmlWriter writer, IdentifiableName ident, string tag) + /// + /// + /// + public static void WriteIdOrEmpty(this XmlWriter writer, string elementName, IdentifiableName ident) { - writer.WriteElementString(tag, ident != null ? ident.Id.ToString(CultureInfo.InvariantCulture) : string.Empty); + writer.WriteElementString(elementName, ident != null ? ident.Id.ToString(CultureInfo.InvariantCulture) : string.Empty); } /// @@ -175,13 +224,22 @@ public static void WriteIdOrEmpty(this XmlWriter writer, IdentifiableName ident, /// /// /// The writer. - /// The value. - /// The tag. - public static void WriteIfNotDefaultOrNull(this XmlWriter writer, T? val, string tag) where T : struct + /// The value. + /// The tag. + public static void WriteIfNotDefaultOrNull(this XmlWriter writer, string elementName, T value) { - if (!val.HasValue) return; - if (!EqualityComparer.Default.Equals(val.Value, default(T))) - writer.WriteElementString(tag, string.Format(NumberFormatInfo.InvariantInfo, "{0}", val.Value.ToString())); + if (EqualityComparer.Default.Equals(value, default(T))) + { + return; + } + + if (value is bool) + { + writer.WriteElementString(elementName, value.ToString().ToLowerInvariant()); + return; + } + + writer.WriteElementString(elementName, string.Format(CultureInfo.InvariantCulture, "{0}", value.ToString())); } /// @@ -190,27 +248,36 @@ public static void WriteIfNotDefaultOrNull(this XmlWriter writer, T? val, str /// /// The writer. /// The value. - /// The tag. - public static void WriteValueOrEmpty(this XmlWriter writer, T? val, string tag) where T : struct + /// The tag. + public static void WriteValueOrEmpty(this XmlWriter writer, string elementName, T? val) where T : struct { if (!val.HasValue || EqualityComparer.Default.Equals(val.Value, default(T))) - writer.WriteElementString(tag, string.Empty); + { + writer.WriteElementString(elementName, string.Empty); + } else - writer.WriteElementString(tag, string.Format(NumberFormatInfo.InvariantInfo, "{0}", val.Value.ToString())); + { + writer.WriteElementString(elementName, string.Format(NumberFormatInfo.InvariantInfo, "{0}", val.Value)); + } } /// /// Writes the date or empty. /// /// The writer. + /// The tag. /// The value. - /// The tag. - public static void WriteDateOrEmpty(this XmlWriter writer, DateTime? val, string tag) + /// + public static void WriteDateOrEmpty(this XmlWriter writer, string elementName, DateTime? val, string dateTimeFormat = "yyyy-MM-dd") { if (!val.HasValue || val.Value.Equals(default(DateTime))) - writer.WriteElementString(tag, string.Empty); + { + writer.WriteElementString(elementName, string.Empty); + } else - writer.WriteElementString(tag, string.Format(NumberFormatInfo.InvariantInfo, "{0}", val.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture))); + { + writer.WriteElementString(elementName, val.Value.ToString(dateTimeFormat, CultureInfo.InvariantCulture)); + } } } } \ No newline at end of file diff --git a/src/redmine-net-api/RedmineKeys.cs b/src/redmine-net-api/RedmineKeys.cs old mode 100755 new mode 100644 index f31258c3..69c18200 --- a/src/redmine-net-api/RedmineKeys.cs +++ b/src/redmine-net-api/RedmineKeys.cs @@ -71,11 +71,11 @@ public static class RedmineKeys /// /// /// - public const string CHANGESET = "changeset"; + public const string CHANGE_SET = "changeset"; /// /// /// - public const string CHANGESETS = "changesets"; + public const string CHANGE_SETS = "changesets"; /// /// /// @@ -104,6 +104,12 @@ public static class RedmineKeys /// /// public const string CREATED_ON = "created_on"; + + /// + /// + /// + public const string CURRENT_USER = "current_user"; + /// /// /// @@ -191,7 +197,7 @@ public static class RedmineKeys /// /// /// - public const string FILESIZE = "filesize"; + public const string FILE_SIZE = "filesize"; /// /// /// @@ -260,6 +266,11 @@ public static class RedmineKeys /// /// public const string ISSUE_CATEGORY = "issue_category"; + + /// + /// + /// + public const string ISSUE_CUSTOM_FIELD_IDS = "issue_custom_field_ids"; /// /// /// @@ -319,6 +330,10 @@ public static class RedmineKeys /// /// /// + public const string LABEL = "label"; + /// + /// + /// public const string LASTNAME = "lastname"; /// /// @@ -363,7 +378,7 @@ public static class RedmineKeys /// /// /// - public const string MUST_CHANGE_PASSWD = "must_change_passwd"; + public const string MUST_CHANGE_PASSWORD = "must_change_passwd"; /// /// /// @@ -453,6 +468,10 @@ public static class RedmineKeys /// public const string QUERY = "query"; /// + /// + /// + public const string REASSIGN_TO_ID = "reassign_to_id"; + /// /// /// public const string REGEXP = "regexp"; @@ -527,7 +546,7 @@ public static class RedmineKeys /// /// /// - public const string SUBPROJECT_ID = "subproject_id"; + public const string SUB_PROJECT_ID = "subproject_id"; /// /// /// @@ -555,6 +574,10 @@ public static class RedmineKeys /// /// /// + public const string THUMBNAIL_URL = "thumbnail_url"; + /// + /// + /// public const string TOKEN = "token"; /// /// @@ -564,12 +587,10 @@ public static class RedmineKeys /// /// public const string TOTAL_ESTIMATED_HOURS = "total_estimated_hours"; - /// /// /// public const string TOTAL_SPENT_HOURS = "total_spent_hours"; - /// /// /// @@ -618,14 +639,10 @@ public static class RedmineKeys /// /// public const string VALUE = "value"; - /// - /// - /// - public const string LABEL = "label"; - /// - /// - /// - public const string VERSION = "version"; + /// + /// + /// + public const string VERSION = "version"; /// /// /// @@ -654,9 +671,5 @@ public static class RedmineKeys /// /// public const string WIKI_PAGES = "wiki_pages"; - /// - /// - /// - public const string REASSIGN_TO_ID = "reassign_to_id"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Serialization/ISerialization.cs b/src/redmine-net-api/Serialization/ISerialization.cs new file mode 100644 index 00000000..172fb95c --- /dev/null +++ b/src/redmine-net-api/Serialization/ISerialization.cs @@ -0,0 +1,11 @@ +namespace Redmine.Net.Api.Serialization +{ + internal interface IRedmineSerializer + { + string Type { get; } + + string Serialize(T obj) where T : class; + + T Deserialize(string response) where T : new(); + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Serialization/PagedResults.cs b/src/redmine-net-api/Serialization/PagedResults.cs new file mode 100644 index 00000000..46031d83 --- /dev/null +++ b/src/redmine-net-api/Serialization/PagedResults.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; + +namespace Redmine.Net.Api.Serialization +{ + /// + /// + /// + public sealed class PagedResults where TOut: class + { + /// + /// + /// + /// + /// + /// + /// + public PagedResults(IEnumerable items, int total, int offset, int pageSize) + { + Items = items; + TotalItems = total; + Offset = offset; + PageSize = pageSize; + + if (pageSize > 0) + { + CurrentPage = offset / pageSize + 1; + + TotalPages = total / pageSize + 1; + } + } + + /// + /// + /// + public int PageSize { get; } + + /// + /// + /// + public int Offset { get; } + + /// + /// + /// + public int CurrentPage { get; } + + /// + /// + /// + public int TotalItems { get; } + + /// + /// + /// + public int TotalPages { get; } + + /// + /// + /// + public IEnumerable Items { get; } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Serialization/XmlRedmineSerializer.cs b/src/redmine-net-api/Serialization/XmlRedmineSerializer.cs new file mode 100644 index 00000000..74978872 --- /dev/null +++ b/src/redmine-net-api/Serialization/XmlRedmineSerializer.cs @@ -0,0 +1,179 @@ +using System; +using System.IO; +using System.Xml; +using System.Xml.Serialization; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; + +namespace Redmine.Net.Api.Serialization +{ + internal sealed class XmlRedmineSerializer : IRedmineSerializer + { + + public XmlRedmineSerializer() + { + XMLWriterSettings = new XmlWriterSettings + { + OmitXmlDeclaration = true + }; + } + + public XmlRedmineSerializer(XmlWriterSettings xmlWriterSettings) + { + XMLWriterSettings = xmlWriterSettings; + } + + private readonly XmlWriterSettings XMLWriterSettings; + + public T Deserialize(string response) where T : new() + { + try + { + return XmlDeserializeEntity(response); + } + catch (Exception ex) + { + throw new RedmineException(ex.Message, ex); + } + } + + public PagedResults DeserializeToPagedResults(string response) where T : class, new() + { + try + { + return XmlDeserializeList(response, false); + } + catch (Exception ex) + { + throw new RedmineException(ex.Message, ex); + } + } + + public int Count(string xmlResponse) where T : class, new() + { + try + { + var pagedResults = XmlDeserializeList(xmlResponse, true); + return pagedResults.TotalItems; + } + catch (Exception ex) + { + throw new RedmineException(ex.Message, ex); + } + } + + public string Type { get; } = "xml"; + + public string Serialize(T entity) where T : class + { + try + { + return ToXML(entity); + } + catch (Exception ex) + { + throw new RedmineException(ex.Message, ex); + } + } + + /// + /// XMLs the deserialize list. + /// + /// + /// The response. + /// + /// + private static PagedResults XmlDeserializeList(string xmlResponse, bool onlyCount) where T : class, new() + { + if (xmlResponse.IsNullOrWhiteSpace()) + { + throw new ArgumentNullException(nameof(xmlResponse), $"Could not deserialize null or empty input for type '{typeof(T).Name}'."); + } + + using (TextReader stringReader = new StringReader(xmlResponse)) + { + using (var xmlReader = XmlReader.Create(stringReader)) + { + xmlReader.Read(); + xmlReader.Read(); + + var totalItems = xmlReader.ReadAttributeAsInt(RedmineKeys.TOTAL_COUNT); + if (onlyCount) + { + return new PagedResults(null, totalItems, 0, 0); + } + var offset = xmlReader.ReadAttributeAsInt(RedmineKeys.OFFSET); + var limit = xmlReader.ReadAttributeAsInt(RedmineKeys.LIMIT); + var result = xmlReader.ReadElementContentAsCollection(); + + return new PagedResults(result, totalItems, offset, limit); + } + } + } + + /// + /// Serializes the specified System.Object and writes the XML document to a string. + /// + /// The type of objects to serialize. + /// The object to serialize. + /// + /// The System.String that contains the XML document. + /// + /// + // ReSharper disable once InconsistentNaming + private string ToXML(T entity) where T : class + { + if (entity == default(T)) + { + throw new ArgumentNullException(nameof(entity), $"Could not serialize null of type {typeof(T).Name}"); + } + + using (var stringWriter = new StringWriter()) + { + using (var xmlWriter = XmlWriter.Create(stringWriter, XMLWriterSettings)) + { + var serializer = new XmlSerializer(typeof(T)); + + serializer.Serialize(xmlWriter, entity); + + return stringWriter.ToString(); + } + } + } + + /// + /// Deserializes the XML document contained by the specific System.String. + /// + /// The type of objects to deserialize. + /// The System.String that contains the XML document to deserialize. + /// + /// The T object being deserialized. + /// + /// + /// An error occurred during deserialization. The original exception is available + /// using the System.Exception.InnerException property. + /// + // ReSharper disable once InconsistentNaming + private TOut XmlDeserializeEntity(string xml) where TOut : new() + { + if (xml.IsNullOrWhiteSpace()) + { + throw new ArgumentNullException(nameof(xml), $"Could not deserialize null or empty input for type '{typeof(TOut).Name}'."); + } + + using (var textReader = new StringReader(xml)) + { + var serializer = new XmlSerializer(typeof(TOut)); + + var entity = serializer.Deserialize(textReader); + + if (entity is TOut t) + { + return t; + } + + return default; + } + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/Attachment.cs b/src/redmine-net-api/Types/Attachment.cs old mode 100755 new mode 100644 index 7db7dd35..66fa2c0b --- a/src/redmine-net-api/Types/Attachment.cs +++ b/src/redmine-net-api/Types/Attachment.cs @@ -15,8 +15,9 @@ limitations under the License. */ using System; +using System.Diagnostics; +using System.Globalization; using System.Xml; -using System.Xml.Schema; using System.Xml.Serialization; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; @@ -26,69 +27,66 @@ namespace Redmine.Net.Api.Types /// /// Availability 1.3 /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.ATTACHMENT)] - public class Attachment : Identifiable, IXmlSerializable, IEquatable + public sealed class Attachment : Identifiable { + #region Properties /// /// Gets or sets the name of the file. /// /// The name of the file. - [XmlElement(RedmineKeys.FILENAME)] public string FileName { get; set; } /// - /// Gets or sets the size of the file. + /// Gets the size of the file. /// /// The size of the file. - [XmlElement(RedmineKeys.FILESIZE)] - public int FileSize { get; set; } + public int FileSize { get; internal set; } /// - /// Gets or sets the type of the content. + /// Gets the type of the content. /// /// The type of the content. - [XmlElement(RedmineKeys.CONTENT_TYPE)] - public string ContentType { get; set; } + public string ContentType { get; internal set; } /// /// Gets or sets the description. /// /// The description. - [XmlElement(RedmineKeys.DESCRIPTION)] public string Description { get; set; } /// - /// Gets or sets the content URL. + /// Gets the content URL. /// /// The content URL. - [XmlElement(RedmineKeys.CONTENT_URL)] - public string ContentUrl { get; set; } + public string ContentUrl { get; internal set; } /// - /// Gets or sets the author. + /// Gets the author. /// /// The author. - [XmlElement(RedmineKeys.AUTHOR)] - public IdentifiableName Author { get; set; } + public IdentifiableName Author { get; internal set; } /// - /// Gets or sets the created on. + /// Gets the created on. /// /// The created on. - [XmlElement(RedmineKeys.CREATED_ON)] - public DateTime? CreatedOn { get; set; } + public DateTime? CreatedOn { get; internal set; } /// - /// + /// Gets the thumbnail url. /// - /// - public XmlSchema GetSchema() { return null; } + public string ThumbnailUrl { get; internal set; } + #endregion + + #region Implementation of IXmlSerializable /// /// /// /// - public void ReadXml(XmlReader reader) + public override void ReadXml(XmlReader reader) { reader.Read(); while (!reader.EOF) @@ -102,21 +100,14 @@ public void ReadXml(XmlReader reader) switch (reader.Name) { case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.FILENAME: FileName = reader.ReadElementContentAsString(); break; - - case RedmineKeys.FILESIZE: FileSize = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.CONTENT_TYPE: ContentType = reader.ReadElementContentAsString(); break; - case RedmineKeys.AUTHOR: Author = new IdentifiableName(reader); break; - + case RedmineKeys.CONTENT_TYPE: ContentType = reader.ReadElementContentAsString(); break; + case RedmineKeys.CONTENT_URL: ContentUrl = reader.ReadElementContentAsString(); break; case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; - case RedmineKeys.DESCRIPTION: Description = reader.ReadElementContentAsString(); break; - - case RedmineKeys.CONTENT_URL: ContentUrl = reader.ReadElementContentAsString(); break; - + case RedmineKeys.FILENAME: FileName = reader.ReadElementContentAsString(); break; + case RedmineKeys.FILE_SIZE: FileSize = reader.ReadElementContentAsInt(); break; + case RedmineKeys.THUMBNAIL_URL: ThumbnailUrl = reader.ReadElementContentAsString(); break; default: reader.Read(); break; } } @@ -126,14 +117,23 @@ public void ReadXml(XmlReader reader) /// /// /// - public void WriteXml(XmlWriter writer) { } + public override void WriteXml(XmlWriter writer) + { + writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); + writer.WriteElementString(RedmineKeys.FILENAME, FileName); + } + #endregion + + + + #region Implementation of IEquatable /// /// /// /// /// - public bool Equals(Attachment other) + public override bool Equals(Attachment other) { if (other == null) return false; return (Id == other.Id @@ -141,6 +141,7 @@ public bool Equals(Attachment other) && FileSize == other.FileSize && ContentType == other.ContentType && Author == other.Author + && ThumbnailUrl == other.ThumbnailUrl && CreatedOn == other.CreatedOn && Description == other.Description && ContentUrl == other.ContentUrl); @@ -162,25 +163,27 @@ public override int GetHashCode() hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); hashCode = HashCodeHelper.GetHashCode(Description, hashCode); hashCode = HashCodeHelper.GetHashCode(ContentUrl, hashCode); + hashCode = HashCodeHelper.GetHashCode(ThumbnailUrl, hashCode); return hashCode; } } + #endregion /// /// /// /// - public override string ToString() - { - return - $"[Attachment: {base.ToString()}, FileName={FileName}, FileSize={FileSize}, ContentType={ContentType}, Description={Description}, ContentUrl={ContentUrl}, Author={Author}, CreatedOn={CreatedOn}]"; - } + private string DebuggerDisplay => + $@"[{nameof(Attachment)}: +{ToString()}, +FileName={FileName}, +FileSize={FileSize.ToString(CultureInfo.InvariantCulture)}, +ContentType={ContentType}, +Description={Description}, +ContentUrl={ContentUrl}, +Author={Author}, +CreatedOn={CreatedOn?.ToString("u", CultureInfo.InvariantCulture)}]"; - /// - public override bool Equals(object obj) - { - return Equals(obj as Attachment); - } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Attachments.cs b/src/redmine-net-api/Types/Attachments.cs old mode 100755 new mode 100644 index e2594976..174b74e7 --- a/src/redmine-net-api/Types/Attachments.cs +++ b/src/redmine-net-api/Types/Attachments.cs @@ -23,6 +23,6 @@ namespace Redmine.Net.Api.Types /// internal class Attachments : Dictionary { - + } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/ChangeSet.cs b/src/redmine-net-api/Types/ChangeSet.cs old mode 100755 new mode 100644 index ec41ad24..011c918c --- a/src/redmine-net-api/Types/ChangeSet.cs +++ b/src/redmine-net-api/Types/ChangeSet.cs @@ -15,6 +15,8 @@ limitations under the License. */ using System; +using System.Diagnostics; +using System.Globalization; using System.Xml; using System.Xml.Schema; using System.Xml.Serialization; @@ -26,33 +28,33 @@ namespace Redmine.Net.Api.Types /// /// /// - [XmlRoot(RedmineKeys.CHANGESET)] - public class ChangeSet : IXmlSerializable, IEquatable + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [XmlRoot(RedmineKeys.CHANGE_SET)] + public sealed class ChangeSet : IXmlSerializable, IEquatable { + #region Properties /// /// /// - [XmlAttribute(RedmineKeys.REVISION)] - public int Revision { get; set; } + public int Revision { get; internal set; } /// /// /// - [XmlElement(RedmineKeys.USER)] - public IdentifiableName User { get; set; } + public IdentifiableName User { get; internal set; } /// /// /// - [XmlElement(RedmineKeys.COMMENTS)] - public string Comments { get; set; } + public string Comments { get; internal set; } /// /// /// - [XmlElement(RedmineKeys.COMMITTED_ON, IsNullable = true)] - public DateTime? CommittedOn { get; set; } + public DateTime? CommittedOn { get; internal set; } + #endregion + #region Implementation of IXmlSerializable /// /// /// @@ -78,12 +80,12 @@ public void ReadXml(XmlReader reader) switch (reader.Name) { - case RedmineKeys.USER: User = new IdentifiableName(reader); break; - case RedmineKeys.COMMENTS: Comments = reader.ReadElementContentAsString(); break; case RedmineKeys.COMMITTED_ON: CommittedOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.USER: User = new IdentifiableName(reader); break; + default: reader.Read(); break; } } @@ -94,7 +96,11 @@ public void ReadXml(XmlReader reader) /// /// public void WriteXml(XmlWriter writer) { } + #endregion + + + #region Implementation of IEquatable /// /// /// @@ -139,14 +145,18 @@ public override int GetHashCode() return hashCode; } } + #endregion /// /// /// /// - public override string ToString() - { - return $"Revision: {Revision}, User: '{User}', CommitedOn: {CommittedOn}, Comments: '{Comments}'"; - } + private string DebuggerDisplay => + $@"[{nameof(ChangeSet)}: +Revision={Revision.ToString(CultureInfo.InvariantCulture)}, +User='{User}', +CommittedOn={CommittedOn?.ToString("u", CultureInfo.InvariantCulture)}, +Comments='{Comments}']"; + } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/CustomField.cs b/src/redmine-net-api/Types/CustomField.cs old mode 100755 new mode 100644 index 09b01050..4fbb32b6 --- a/src/redmine-net-api/Types/CustomField.cs +++ b/src/redmine-net-api/Types/CustomField.cs @@ -16,6 +16,8 @@ limitations under the License. using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; using System.Xml; using System.Xml.Serialization; using Redmine.Net.Api.Extensions; @@ -26,96 +28,83 @@ namespace Redmine.Net.Api.Types /// /// /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.CUSTOM_FIELD)] - public class CustomField : IdentifiableName, IEquatable + public sealed class CustomField : IdentifiableName, IEquatable { + #region Properties /// /// /// - [XmlElement(RedmineKeys.CUSTOMIZED_TYPE)] - public string CustomizedType { get; set; } + public string CustomizedType { get; internal set; } /// /// /// - [XmlElement(RedmineKeys.FIELD_FORMAT)] - public string FieldFormat { get; set; } + public string FieldFormat { get; internal set; } /// /// /// - [XmlElement(RedmineKeys.REGEXP)] - public string Regexp { get; set; } + public string Regexp { get; internal set; } /// /// /// - [XmlElement(RedmineKeys.MIN_LENGTH)] - public int? MinLength { get; set; } + public int? MinLength { get; internal set; } /// /// /// - [XmlElement(RedmineKeys.MAX_LENGTH)] - public int? MaxLength { get; set; } + public int? MaxLength { get; internal set; } /// /// /// - [XmlElement(RedmineKeys.IS_REQUIRED)] - public bool IsRequired { get; set; } + public bool IsRequired { get; internal set; } /// /// /// - [XmlElement(RedmineKeys.IS_FILTER)] - public bool IsFilter { get; set; } + public bool IsFilter { get; internal set; } /// /// /// - [XmlElement(RedmineKeys.SEARCHABLE)] - public bool Searchable { get; set; } + public bool Searchable { get; internal set; } /// /// /// - [XmlElement(RedmineKeys.MULTIPLE)] - public bool Multiple { get; set; } + public bool Multiple { get; internal set; } /// /// /// - [XmlElement(RedmineKeys.DEFAULT_VALUE)] - public string DefaultValue { get; set; } + public string DefaultValue { get; internal set; } /// /// /// - [XmlElement(RedmineKeys.VISIBLE)] - public bool Visible { get; set; } + public bool Visible { get; internal set; } /// /// /// - [XmlArray(RedmineKeys.POSSIBLE_VALUES)] - [XmlArrayItem(RedmineKeys.POSSIBLE_VALUE)] public IList PossibleValues { get; internal set; } /// /// /// - [XmlArray(RedmineKeys.TRACKERS)] - [XmlArrayItem(RedmineKeys.TRACKER)] public IList Trackers { get; internal set; } /// /// /// - [XmlArray(RedmineKeys.ROLES)] - [XmlArrayItem(RedmineKeys.ROLE)] public IList Roles { get; internal set; } + #endregion + #region Implementation of IXmlSerializable /// /// /// @@ -134,48 +123,31 @@ public override void ReadXml(XmlReader reader) switch (reader.Name) { case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; - case RedmineKeys.CUSTOMIZED_TYPE: CustomizedType = reader.ReadElementContentAsString(); break; - + case RedmineKeys.DEFAULT_VALUE: DefaultValue = reader.ReadElementContentAsString(); break; case RedmineKeys.FIELD_FORMAT: FieldFormat = reader.ReadElementContentAsString(); break; - - case RedmineKeys.REGEXP: Regexp = reader.ReadElementContentAsString(); break; - - case RedmineKeys.MIN_LENGTH: MinLength = reader.ReadElementContentAsNullableInt(); break; - - case RedmineKeys.MAX_LENGTH: MaxLength = reader.ReadElementContentAsNullableInt(); break; - - case RedmineKeys.IS_REQUIRED: IsRequired = reader.ReadElementContentAsBoolean(); break; - case RedmineKeys.IS_FILTER: IsFilter = reader.ReadElementContentAsBoolean(); break; - - case RedmineKeys.SEARCHABLE: Searchable = reader.ReadElementContentAsBoolean(); break; - - case RedmineKeys.VISIBLE: Visible = reader.ReadElementContentAsBoolean(); break; - - case RedmineKeys.DEFAULT_VALUE: DefaultValue = reader.ReadElementContentAsString(); break; - + case RedmineKeys.IS_REQUIRED: IsRequired = reader.ReadElementContentAsBoolean(); break; + case RedmineKeys.MAX_LENGTH: MaxLength = reader.ReadElementContentAsNullableInt(); break; + case RedmineKeys.MIN_LENGTH: MinLength = reader.ReadElementContentAsNullableInt(); break; case RedmineKeys.MULTIPLE: Multiple = reader.ReadElementContentAsBoolean(); break; - - case RedmineKeys.TRACKERS: Trackers = reader.ReadElementContentAsCollection(); break; - - case RedmineKeys.ROLES: Roles = reader.ReadElementContentAsCollection(); break; - + case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; case RedmineKeys.POSSIBLE_VALUES: PossibleValues = reader.ReadElementContentAsCollection(); break; - + case RedmineKeys.REGEXP: Regexp = reader.ReadElementContentAsString(); break; + case RedmineKeys.ROLES: Roles = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.SEARCHABLE: Searchable = reader.ReadElementContentAsBoolean(); break; + case RedmineKeys.TRACKERS: Trackers = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.VISIBLE: Visible = reader.ReadElementContentAsBoolean(); break; default: reader.Read(); break; } } } - /// - /// - /// - /// - public override void WriteXml(XmlWriter writer) { } + #endregion + + + #region Implementation of IEquatable /// /// /// @@ -191,13 +163,13 @@ public bool Equals(CustomField other) && Multiple == other.Multiple && Searchable == other.Searchable && Visible == other.Visible - && CustomizedType.Equals(other.CustomizedType, StringComparison.OrdinalIgnoreCase) - && DefaultValue.Equals(other.DefaultValue, StringComparison.OrdinalIgnoreCase) - && FieldFormat.Equals(other.FieldFormat, StringComparison.OrdinalIgnoreCase) + && CustomizedType.Equals(other.CustomizedType) + && DefaultValue.Equals(other.DefaultValue) + && FieldFormat.Equals(other.FieldFormat) && MaxLength == other.MaxLength && MinLength == other.MinLength - && Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase) - && Regexp.Equals(other.Regexp, StringComparison.OrdinalIgnoreCase) + && Name.Equals(other.Name) + && Regexp.Equals(other.Regexp) && PossibleValues.Equals(other.PossibleValues) && Roles.Equals(other.Roles) && Trackers.Equals(other.Trackers); @@ -225,34 +197,42 @@ public override int GetHashCode() unchecked { var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id,hashCode); - hashCode = HashCodeHelper.GetHashCode(IsFilter,hashCode); - hashCode = HashCodeHelper.GetHashCode(IsRequired,hashCode); - hashCode = HashCodeHelper.GetHashCode(Multiple,hashCode); - hashCode = HashCodeHelper.GetHashCode(Searchable,hashCode); - hashCode = HashCodeHelper.GetHashCode(Visible,hashCode); - hashCode = HashCodeHelper.GetHashCode(CustomizedType,hashCode); - hashCode = HashCodeHelper.GetHashCode(DefaultValue,hashCode); - hashCode = HashCodeHelper.GetHashCode(FieldFormat,hashCode); - hashCode = HashCodeHelper.GetHashCode(MaxLength,hashCode); - hashCode = HashCodeHelper.GetHashCode(MinLength,hashCode); - hashCode = HashCodeHelper.GetHashCode(Name,hashCode); - hashCode = HashCodeHelper.GetHashCode(Regexp,hashCode); - hashCode = HashCodeHelper.GetHashCode(PossibleValues,hashCode); - hashCode = HashCodeHelper.GetHashCode(Roles,hashCode); - hashCode = HashCodeHelper.GetHashCode(Trackers,hashCode); + hashCode = HashCodeHelper.GetHashCode(Id, hashCode); + hashCode = HashCodeHelper.GetHashCode(IsFilter, hashCode); + hashCode = HashCodeHelper.GetHashCode(IsRequired, hashCode); + hashCode = HashCodeHelper.GetHashCode(Multiple, hashCode); + hashCode = HashCodeHelper.GetHashCode(Searchable, hashCode); + hashCode = HashCodeHelper.GetHashCode(Visible, hashCode); + hashCode = HashCodeHelper.GetHashCode(CustomizedType, hashCode); + hashCode = HashCodeHelper.GetHashCode(DefaultValue, hashCode); + hashCode = HashCodeHelper.GetHashCode(FieldFormat, hashCode); + hashCode = HashCodeHelper.GetHashCode(MaxLength, hashCode); + hashCode = HashCodeHelper.GetHashCode(MinLength, hashCode); + hashCode = HashCodeHelper.GetHashCode(Name, hashCode); + hashCode = HashCodeHelper.GetHashCode(Regexp, hashCode); + hashCode = HashCodeHelper.GetHashCode(PossibleValues, hashCode); + hashCode = HashCodeHelper.GetHashCode(Roles, hashCode); + hashCode = HashCodeHelper.GetHashCode(Trackers, hashCode); return hashCode; } } - - /// - /// - /// - /// - public override string ToString () - { - return - $"[CustomField: Id={Id}, Name={Name}, CustomizedType={CustomizedType}, FieldFormat={FieldFormat}, Regexp={Regexp}, MinLength={MinLength}, MaxLength={MaxLength}, IsRequired={IsRequired}, IsFilter={IsFilter}, Searchable={Searchable}, Multiple={Multiple}, DefaultValue={DefaultValue}, Visible={Visible}, PossibleValues={PossibleValues}, Trackers={Trackers}, Roles={Roles}]"; - } + #endregion + + private string DebuggerDisplay => + $@"[{nameof(CustomField)}: {ToString()} +, CustomizedType={CustomizedType} +, FieldFormat={FieldFormat} +, Regexp={Regexp} +, MinLength={MinLength?.ToString(CultureInfo.InvariantCulture)} +, MaxLength={MaxLength?.ToString(CultureInfo.InvariantCulture)} +, IsRequired={IsRequired.ToString(CultureInfo.InvariantCulture)} +, IsFilter={IsFilter.ToString(CultureInfo.InvariantCulture)} +, Searchable={Searchable.ToString(CultureInfo.InvariantCulture)} +, Multiple={Multiple.ToString(CultureInfo.InvariantCulture)} +, DefaultValue={DefaultValue} +, Visible={Visible.ToString(CultureInfo.InvariantCulture)} +, PossibleValues={PossibleValues.Dump()} +, Trackers={Trackers.Dump()} +, Roles={Roles.Dump()}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/CustomFieldPossibleValue.cs b/src/redmine-net-api/Types/CustomFieldPossibleValue.cs old mode 100755 new mode 100644 index ff138076..3078da08 --- a/src/redmine-net-api/Types/CustomFieldPossibleValue.cs +++ b/src/redmine-net-api/Types/CustomFieldPossibleValue.cs @@ -15,6 +15,9 @@ limitations under the License. */ using System; +using System.Diagnostics; +using System.Xml; +using System.Xml.Schema; using System.Xml.Serialization; using Redmine.Net.Api.Internals; @@ -23,27 +26,76 @@ namespace Redmine.Net.Api.Types /// /// /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.POSSIBLE_VALUE)] - public class CustomFieldPossibleValue : IEquatable + public sealed class CustomFieldPossibleValue : IXmlSerializable, IEquatable { + #region Properties /// /// /// - [XmlElement(RedmineKeys.VALUE)] - public string Value { get; set; } - - /// - /// - /// - [XmlElement( RedmineKeys.LABEL )] - public string Label { get; set; } - - /// - /// - /// - /// - /// - public bool Equals(CustomFieldPossibleValue other) + public string Value { get; internal set; } + + /// + /// + /// + public string Label { get; internal set; } + #endregion + + #region Implementation of IXmlSerializable + + /// + /// + /// + /// + public XmlSchema GetSchema() + { + return null; + } + + /// + /// + /// + /// + public void ReadXml(XmlReader reader) + { + reader.Read(); + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.LABEL: Label = reader.ReadElementContentAsString(); break; + + case RedmineKeys.VALUE: Value = reader.ReadElementContentAsString(); break; + + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public void WriteXml(XmlWriter writer) { } + + #endregion + + + + #region Implementation of IEquatable + /// + /// + /// + /// + /// + public bool Equals(CustomFieldPossibleValue other) { if (other == null) return false; return (Value == other.Value); @@ -71,18 +123,17 @@ public override int GetHashCode() unchecked { var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Value,hashCode); + hashCode = HashCodeHelper.GetHashCode(Value, hashCode); return hashCode; } } + #endregion /// /// /// /// - public override string ToString () - { - return $"[CustomFieldPossibleValue: {base.ToString()}]"; - } + private string DebuggerDisplay => $"[{nameof(CustomFieldPossibleValue)}: Label:{Label}, Value:{Value}]"; + } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/CustomFieldRole.cs b/src/redmine-net-api/Types/CustomFieldRole.cs old mode 100755 new mode 100644 index 213b5948..9aa7020f --- a/src/redmine-net-api/Types/CustomFieldRole.cs +++ b/src/redmine-net-api/Types/CustomFieldRole.cs @@ -14,6 +14,7 @@ You may obtain a copy of the License at limitations under the License. */ +using System.Diagnostics; using System.Xml.Serialization; namespace Redmine.Net.Api.Types @@ -21,16 +22,21 @@ namespace Redmine.Net.Api.Types /// /// /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.ROLE)] - public class CustomFieldRole : IdentifiableName + public sealed class CustomFieldRole : IdentifiableName { + internal CustomFieldRole(int id, string name) + { + Id = id; + Name = name; + } + /// /// /// /// - public override string ToString () - { - return $"[CustomFieldRole: {base.ToString()}]"; - } + private string DebuggerDisplay => $"[{nameof(CustomFieldRole)}: {ToString()}]"; + } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/CustomFieldValue.cs b/src/redmine-net-api/Types/CustomFieldValue.cs old mode 100755 new mode 100644 index 8d407c36..094b24da --- a/src/redmine-net-api/Types/CustomFieldValue.cs +++ b/src/redmine-net-api/Types/CustomFieldValue.cs @@ -15,6 +15,9 @@ limitations under the License. */ using System; +using System.Diagnostics; +using System.Xml; +using System.Xml.Schema; using System.Xml.Serialization; using Redmine.Net.Api.Internals; @@ -23,14 +26,86 @@ namespace Redmine.Net.Api.Types /// /// /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.VALUE)] - public class CustomFieldValue : IEquatable, ICloneable + public class CustomFieldValue : IXmlSerializable, IEquatable, ICloneable { /// /// /// - [XmlText] - public string Info { get; set; } + public CustomFieldValue() + { + } + + /// + /// + /// + /// + public CustomFieldValue(string value) + { + Info = value; + } + + #region Properties + + /// + /// + /// + public string Info { get; internal set; } + + #endregion + + #region Implementation of IXmlSerializable + + /// + /// + /// + /// + public XmlSchema GetSchema() + { + return null; + } + + /// + /// + /// + /// + public void ReadXml(XmlReader reader) + { + while (!reader.EOF) + { + if (reader.IsEmptyElement) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.VALUE: + Info = reader.ReadElementContentAsString(); + break; + + default: + reader.Read(); + break; + } + } + } + + /// + /// + /// + /// + public void WriteXml(XmlWriter writer) + { + } + + #endregion + + + + #region Implementation of IEquatable /// /// @@ -39,7 +114,8 @@ public class CustomFieldValue : IEquatable, ICloneable /// public bool Equals(CustomFieldValue other) { - return other != null && Info.Equals(other.Info, StringComparison.OrdinalIgnoreCase); + if (other == null) return false; + return Info.Equals(other.Info); } /// @@ -69,23 +145,27 @@ public override int GetHashCode() } } + #endregion + + #region Implementation of IClonable + /// /// /// /// - public override string ToString() + public object Clone() { - return $"[CustomFieldValue: Info={Info}]"; + var customFieldValue = new CustomFieldValue {Info = Info}; + return customFieldValue; } + #endregion + /// /// /// /// - public object Clone() - { - var customFieldValue = new CustomFieldValue { Info = Info }; - return customFieldValue; - } + private string DebuggerDisplay => $"[{nameof(CustomFieldValue)}: {Info}]"; + } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Detail.cs b/src/redmine-net-api/Types/Detail.cs old mode 100755 new mode 100644 index c0943f6d..17219580 --- a/src/redmine-net-api/Types/Detail.cs +++ b/src/redmine-net-api/Types/Detail.cs @@ -15,6 +15,7 @@ limitations under the License. */ using System; +using System.Diagnostics; using System.Xml; using System.Xml.Schema; using System.Xml.Serialization; @@ -25,45 +26,58 @@ namespace Redmine.Net.Api.Types /// /// /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.DETAIL)] - public class Detail : IXmlSerializable, IEquatable + public sealed class Detail : IXmlSerializable, IEquatable { /// - /// Gets or sets the property. + /// + /// + public Detail() { } + + internal Detail(string name = null, string property = null, string oldValue = null, string newValue = null) + { + Name = name; + Property = property; + OldValue = oldValue; + NewValue = newValue; + } + + #region Properties + /// + /// Gets the property. /// /// /// The property. /// - [XmlAttribute(RedmineKeys.PROPERTY)] - public string Property { get; set; } + public string Property { get; internal set; } /// - /// Gets or sets the name. + /// Gets the name. /// /// /// The name. /// - [XmlAttribute(RedmineKeys.NAME)] - public string Name { get; set; } + public string Name { get; internal set; } /// - /// Gets or sets the old value. + /// Gets the old value. /// /// /// The old value. /// - [XmlElement(RedmineKeys.OLD_VALUE)] - public string OldValue { get; set; } + public string OldValue { get; internal set; } /// - /// Gets or sets the new value. + /// Gets the new value. /// /// /// The new value. /// - [XmlElement(RedmineKeys.NEW_VALUE)] - public string NewValue { get; set; } + public string NewValue { get; internal set; } + #endregion + #region Implementation of IXmlSerialization /// /// /// @@ -76,8 +90,8 @@ public class Detail : IXmlSerializable, IEquatable /// public void ReadXml(XmlReader reader) { - Property = reader.GetAttribute(RedmineKeys.PROPERTY); Name = reader.GetAttribute(RedmineKeys.NAME); + Property = reader.GetAttribute(RedmineKeys.PROPERTY); reader.Read(); @@ -91,10 +105,10 @@ public void ReadXml(XmlReader reader) switch (reader.Name) { - case RedmineKeys.OLD_VALUE: OldValue = reader.ReadElementContentAsString(); break; - case RedmineKeys.NEW_VALUE: NewValue = reader.ReadElementContentAsString(); break; + case RedmineKeys.OLD_VALUE: OldValue = reader.ReadElementContentAsString(); break; + default: reader.Read(); break; } } @@ -105,7 +119,11 @@ public void ReadXml(XmlReader reader) /// /// public void WriteXml(XmlWriter writer) { } + #endregion + + + #region Implementation of IEquatable /// /// /// @@ -114,10 +132,10 @@ public void WriteXml(XmlWriter writer) { } public bool Equals(Detail other) { if (other == null) return false; - return (Property?.Equals(other.Property, StringComparison.OrdinalIgnoreCase) ?? other.Property == null) - && (Name?.Equals(other.Name, StringComparison.OrdinalIgnoreCase) ?? other.Name == null) - && (OldValue?.Equals(other.OldValue, StringComparison.OrdinalIgnoreCase) ?? other.OldValue == null) - && (NewValue?.Equals(other.NewValue, StringComparison.OrdinalIgnoreCase) ?? other.NewValue == null); + return (Property != null ? string.Equals(Property,other.Property, StringComparison.InvariantCultureIgnoreCase) : other.Property == null) + && (Name != null ? string.Equals(Name,other.Name, StringComparison.InvariantCultureIgnoreCase) : other.Name == null) + && (OldValue != null ? string.Equals(OldValue,other.OldValue, StringComparison.InvariantCultureIgnoreCase) : other.OldValue == null) + && (NewValue != null ? string.Equals(NewValue,other.NewValue, StringComparison.InvariantCultureIgnoreCase) : other.NewValue == null); } /// @@ -150,14 +168,13 @@ public override int GetHashCode() return hashCode; } } + #endregion /// /// /// /// - public override string ToString() - { - return $"[Detail: Property={Property}, Name={Name}, OldValue={OldValue}, NewValue={NewValue}]"; - } + private string DebuggerDisplay => $"[{nameof(Detail)}: Property={Property}, Name={Name}, OldValue={OldValue}, NewValue={NewValue}]"; + } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Error.cs b/src/redmine-net-api/Types/Error.cs old mode 100755 new mode 100644 index 96fb26b7..70634087 --- a/src/redmine-net-api/Types/Error.cs +++ b/src/redmine-net-api/Types/Error.cs @@ -15,6 +15,7 @@ limitations under the License. */ using System; +using System.Diagnostics; using System.Xml; using System.Xml.Schema; using System.Xml.Serialization; @@ -25,36 +26,31 @@ namespace Redmine.Net.Api.Types /// /// /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.ERROR)] - public class Error : IXmlSerializable, IEquatable + public sealed class Error : IXmlSerializable, IEquatable { /// /// /// - [XmlText] - public string Info { get; set; } + public Error() { } /// /// /// - /// - /// - public bool Equals(Error other) + internal Error(string info) { - if (other == null) return false; - - return Info.Equals(other.Info, StringComparison.OrdinalIgnoreCase); + Info = info; } + #region Properties /// /// /// - /// - public override string ToString() - { - return $"[Error: Info={Info}]"; - } + public string Info { get; private set; } + #endregion + #region Implementation of IXmlSerialization /// /// /// @@ -69,12 +65,6 @@ public void ReadXml(XmlReader reader) { while (!reader.EOF) { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - switch (reader.Name) { case RedmineKeys.ERROR: Info = reader.ReadElementContentAsString(); break; @@ -89,6 +79,23 @@ public void ReadXml(XmlReader reader) /// /// public void WriteXml(XmlWriter writer) { } + #endregion + + + + #region Implementation of IEquatable + + /// + /// + /// + /// + /// + public bool Equals(Error other) + { + if (other == null) return false; + + return string.Equals(Info,other.Info, StringComparison.InvariantCultureIgnoreCase); + } /// /// @@ -116,5 +123,13 @@ public override int GetHashCode() return hashCode; } } + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[{nameof(Error)}: {Info}]"; + } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/File.cs b/src/redmine-net-api/Types/File.cs index f7ca5b81..294e9269 100644 --- a/src/redmine-net-api/Types/File.cs +++ b/src/redmine-net-api/Types/File.cs @@ -19,8 +19,8 @@ limitations under the License. using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; using System; +using System.Diagnostics; using System.Xml; -using System.Xml.Schema; using System.Xml.Serialization; namespace Redmine.Net.Api.Types @@ -28,91 +28,136 @@ namespace Redmine.Net.Api.Types /// /// /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.FILE)] - public class File : Identifiable, IEquatable, IXmlSerializable + public sealed class File : Identifiable { - + #region Properties /// /// /// - [XmlElement(RedmineKeys.FILENAME)] public string Filename { get; set; } /// /// /// - [XmlElement(RedmineKeys.FILESIZE)] - public int Filesize { get; set; } + public int FileSize { get; internal set; } /// /// /// - [XmlElement(RedmineKeys.CONTENT_TYPE)] - public string ContentType { get; set; } + public string ContentType { get; internal set; } /// /// /// - [XmlElement(RedmineKeys.DESCRIPTION)] public string Description { get; set; } /// /// /// - [XmlElement(RedmineKeys.CONTENT_URL)] - public string ContentUrl { get; set; } + public string ContentUrl { get; internal set; } /// /// /// - [XmlElement(RedmineKeys.AUTHOR)] - public IdentifiableName Author { get; set; } + public IdentifiableName Author { get; internal set; } /// /// /// - [XmlElement(RedmineKeys.CREATED_ON)] - public DateTime? CreatedOn { get; set; } + public DateTime? CreatedOn { get; internal set; } /// /// /// - [XmlElement(RedmineKeys.VERSION)] public IdentifiableName Version { get; set; } /// /// /// - [XmlElement(RedmineKeys.DIGEST)] - public string Digest { get; set; } + public string Digest { get; internal set; } /// /// /// - [XmlElement(RedmineKeys.DOWNLOADS)] - public int Downloads { get; set; } + public int Downloads { get; internal set; } /// /// /// - [XmlElement(RedmineKeys.TOKEN)] public string Token { get; set; } - + #endregion + + #region Implementation of IXmlSerializable + + /// + /// + /// + /// + public override void ReadXml(XmlReader reader) + { + reader.Read(); + + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.AUTHOR: Author = new IdentifiableName(reader); break; + case RedmineKeys.CONTENT_TYPE: ContentType = reader.ReadElementContentAsString(); break; + case RedmineKeys.CONTENT_URL: ContentUrl = reader.ReadElementContentAsString(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadElementContentAsString(); break; + case RedmineKeys.DIGEST: Digest = reader.ReadElementContentAsString(); break; + case RedmineKeys.DOWNLOADS: Downloads = reader.ReadElementContentAsInt(); break; + case RedmineKeys.FILENAME: Filename = reader.ReadElementContentAsString(); break; + case RedmineKeys.FILE_SIZE: FileSize = reader.ReadElementContentAsInt(); break; + case RedmineKeys.TOKEN: Token = reader.ReadElementContentAsString(); break; + case RedmineKeys.VERSION: Version = new IdentifiableName(reader); break; + case RedmineKeys.VERSION_ID: Version = new IdentifiableName() { Id = reader.ReadElementContentAsInt() }; break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteXml(XmlWriter writer) + { + writer.WriteElementString(RedmineKeys.TOKEN, Token); + writer.WriteIdIfNotNull(RedmineKeys.VERSION_ID, Version); + writer.WriteElementString(RedmineKeys.FILENAME, Filename); + writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); + } + #endregion + + + + #region Implementation of IEquatable /// /// /// /// /// - public bool Equals(File other) + public override bool Equals(File other) { if (other == null) return false; - return (Id == other.Id - && Filename == other.Filename - && Filesize == other.Filesize + return (Id == other.Id + && Filename == other.Filename + && FileSize == other.FileSize && Description == other.Description - && ContentType == other.ContentType + && ContentType == other.ContentType && ContentUrl == other.ContentUrl - && Author ==other.Author + && Author == other.Author && CreatedOn == other.CreatedOn && Version == other.Version && Digest == other.Digest @@ -130,7 +175,7 @@ public override int GetHashCode() var hashCode = base.GetHashCode(); hashCode = HashCodeHelper.GetHashCode(Filename, hashCode); - hashCode = HashCodeHelper.GetHashCode(Filesize, hashCode); + hashCode = HashCodeHelper.GetHashCode(FileSize, hashCode); hashCode = HashCodeHelper.GetHashCode(ContentType, hashCode); hashCode = HashCodeHelper.GetHashCode(Description, hashCode); hashCode = HashCodeHelper.GetHashCode(Author, hashCode); @@ -144,86 +189,13 @@ public override int GetHashCode() return hashCode; } + #endregion /// /// /// /// - public override string ToString() - { - return $"[File: Id={Id}, Name={Filename}]"; - } + private string DebuggerDisplay => $"[{nameof(File)}: {ToString()}, Name={Filename}]"; - /// - /// - /// - /// - /// - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - return Equals(obj as File); - } - - /// - /// - /// - /// - public XmlSchema GetSchema() - { - return null; - } - - /// - /// - /// - /// - public void ReadXml(XmlReader reader) - { - reader.Read(); - - while (!reader.EOF) - { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - - switch (reader.Name) - { - case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - case RedmineKeys.FILENAME: Filename = reader.ReadElementContentAsString(); break; - case RedmineKeys.FILESIZE: Filesize = reader.ReadElementContentAsInt(); break; - case RedmineKeys.CONTENT_TYPE: ContentType = reader.ReadElementContentAsString(); break; - case RedmineKeys.DESCRIPTION: Description = reader.ReadElementContentAsString(); break; - case RedmineKeys.CONTENT_URL: ContentUrl = reader.ReadElementContentAsString(); break; - case RedmineKeys.AUTHOR: Author = new IdentifiableName(reader); break; - case RedmineKeys.CREATED_ON:CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; - case RedmineKeys.VERSION: Version = new IdentifiableName(reader); break; - case RedmineKeys.VERSION_ID: Version = new IdentifiableName() {Id = reader.ReadElementContentAsInt()}; break; - case RedmineKeys.DIGEST: Digest = reader.ReadElementContentAsString(); break; - case RedmineKeys.DOWNLOADS: Downloads = reader.ReadElementContentAsInt(); break; - case RedmineKeys.TOKEN:Token = reader.ReadElementContentAsString(); break; - default: - reader.Read(); - break; - } - } - } - - /// - /// - /// - /// - public void WriteXml(XmlWriter writer) - { - writer.WriteElementString(RedmineKeys.TOKEN, Token); - writer.WriteIdIfNotNull(Version, RedmineKeys.VERSION_ID); - writer.WriteElementString(RedmineKeys.FILENAME, Filename); - writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); - } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Group.cs b/src/redmine-net-api/Types/Group.cs old mode 100755 new mode 100644 index 325ee1e9..7247aff6 --- a/src/redmine-net-api/Types/Group.cs +++ b/src/redmine-net-api/Types/Group.cs @@ -16,6 +16,7 @@ limitations under the License. using System; using System.Collections.Generic; +using System.Diagnostics; using System.Xml; using System.Xml.Serialization; using Redmine.Net.Api.Extensions; @@ -26,38 +27,49 @@ namespace Redmine.Net.Api.Types /// /// Availability 2.1 /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.GROUP)] - public class Group : IdentifiableName, IEquatable + public sealed class Group : IdentifiableName, IEquatable { + /// + /// + /// + public Group() { } + + /// + /// + /// + /// + public Group(string name) + { + Name = name; + } + + #region Properties /// /// Represents the group's users. /// - [XmlArray(RedmineKeys.USERS)] - [XmlArrayItem(RedmineKeys.USER)] - public List Users { get; set; } + public IList Users { get; internal set; } /// /// Gets or sets the custom fields. /// /// The custom fields. - [XmlArray(RedmineKeys.CUSTOM_FIELDS)] - [XmlArrayItem(RedmineKeys.CUSTOM_FIELD)] public IList CustomFields { get; internal set; } /// /// Gets or sets the custom fields. /// /// The custom fields. - [XmlArray(RedmineKeys.MEMBERSHIPS)] - [XmlArrayItem(RedmineKeys.MEMBERSHIP)] public IList Memberships { get; internal set; } + #endregion #region Implementation of IXmlSerializable /// /// Generates an object from its XML representation. /// - /// The stream from which the object is deserialized. + /// The stream from which the object is deserialized. public override void ReadXml(XmlReader reader) { reader.Read(); @@ -72,15 +84,10 @@ public override void ReadXml(XmlReader reader) switch (reader.Name) { case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; - - case RedmineKeys.USERS: Users = reader.ReadElementContentAsCollection(); break; - case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadElementContentAsCollection(); break; - case RedmineKeys.MEMBERSHIPS: Memberships = reader.ReadElementContentAsCollection(); break; - + case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; + case RedmineKeys.USERS: Users = reader.ReadElementContentAsCollection(); break; default: reader.Read(); break; } } @@ -89,15 +96,18 @@ public override void ReadXml(XmlReader reader) /// /// Converts an object into its XML representation. /// - /// The stream to which the object is serialized. + /// The stream to which the object is serialized. public override void WriteXml(XmlWriter writer) { writer.WriteElementString(RedmineKeys.NAME, Name); - writer.WriteArrayIds(Users, RedmineKeys.USER_IDS, typeof(int), GetGroupUserId); + //TODO: change to repeatable elements + writer.WriteArrayIds(RedmineKeys.USER_IDS, Users, typeof(int), GetGroupUserId); } #endregion + + #region Implementation of IEquatable /// @@ -112,9 +122,9 @@ public bool Equals(Group other) if (other == null) return false; return Id == other.Id && Name == other.Name - && (Users?.Equals(other.Users) ?? other.Users == null) - && (CustomFields?.Equals(other.CustomFields) ?? other.CustomFields == null) - && (Memberships?.Equals(other.Memberships) ?? other.Memberships == null); + && (Users != null ? Users.Equals(other.Users) : other.Users == null) + && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null) + && (Memberships != null ? Memberships.Equals(other.Memberships) : other.Memberships == null); } /// @@ -154,18 +164,15 @@ public override int GetHashCode() /// /// /// - public override string ToString() - { - return - $"[Group: Id={Id}, Name={Name}, Users={Users}, CustomFields={CustomFields}, Memberships={Memberships}]"; - } + private string DebuggerDisplay => $"[{nameof(Group)}: {ToString()}, Users={Users.Dump()}, CustomFields={CustomFields.Dump()}, Memberships={Memberships.Dump()}]"; + /// /// /// /// /// - public static int GetGroupUserId(object gu) + public int GetGroupUserId(object gu) { return ((GroupUser)gu).Id; } diff --git a/src/redmine-net-api/Types/GroupUser.cs b/src/redmine-net-api/Types/GroupUser.cs old mode 100755 new mode 100644 index e3f10928..cb2da9d9 --- a/src/redmine-net-api/Types/GroupUser.cs +++ b/src/redmine-net-api/Types/GroupUser.cs @@ -14,6 +14,8 @@ You may obtain a copy of the License at limitations under the License. */ +using System.Diagnostics; +using System.Globalization; using System.Xml.Serialization; namespace Redmine.Net.Api.Types @@ -21,16 +23,22 @@ namespace Redmine.Net.Api.Types /// /// /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.USER)] - public class GroupUser : IdentifiableName + public sealed class GroupUser : IdentifiableName, IValue { + #region Implementation of IValue + /// + /// + /// + public string Value => Id.ToString(CultureInfo.InvariantCulture); + #endregion + /// /// /// /// - public override string ToString () - { - return $"[GroupUser: {base.ToString()}]"; - } + private string DebuggerDisplay => $"[{nameof(GroupUser)}: {ToString()}]"; + } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Identifiable.cs b/src/redmine-net-api/Types/Identifiable.cs old mode 100755 new mode 100644 index d9934286..1f29a548 --- a/src/redmine-net-api/Types/Identifiable.cs +++ b/src/redmine-net-api/Types/Identifiable.cs @@ -15,6 +15,10 @@ limitations under the License. */ using System; +using System.Diagnostics; +using System.Globalization; +using System.Xml; +using System.Xml.Schema; using System.Xml.Serialization; using Redmine.Net.Api.Internals; @@ -24,16 +28,40 @@ namespace Redmine.Net.Api.Types /// /// /// - public abstract class Identifiable where T : Identifiable, IEquatable + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + public abstract class Identifiable : IXmlSerializable, IEquatable, IEquatable> where T : Identifiable { - + #region Properties /// - /// Gets or sets the id. + /// Gets the id. /// /// The id. - [XmlAttribute(RedmineKeys.ID)] - public int Id { get; set; } + public int Id { get; protected internal set; } + #endregion + + #region Implementation of IXmlSerialization + /// + /// + /// + /// + public XmlSchema GetSchema() { return null; } + + /// + /// + /// + /// + public virtual void ReadXml(XmlReader reader) { } + + /// + /// + /// + /// + public virtual void WriteXml(XmlWriter writer) { } + #endregion + + + #region Implementation of IEquatable> /// /// /// @@ -41,8 +69,20 @@ public abstract class Identifiable where T : Identifiable, IEquatable /// public bool Equals(Identifiable other) { - if (other == null) return false; + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + + return Id == other.Id; + } + /// + /// + /// + /// + /// + public virtual bool Equals(T other) + { + if (other == null) return false; return Id == other.Id; } @@ -94,14 +134,13 @@ public override int GetHashCode() { return !Equals(left, right); } + #endregion /// /// /// /// - public override string ToString() - { - return $"[Identifiable: Id={Id}]"; - } + private string DebuggerDisplay => $"Id={Id.ToString(CultureInfo.InvariantCulture)}"; + } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/IdentifiableName.cs b/src/redmine-net-api/Types/IdentifiableName.cs old mode 100755 new mode 100644 index 1eb9e882..b90d4fd1 --- a/src/redmine-net-api/Types/IdentifiableName.cs +++ b/src/redmine-net-api/Types/IdentifiableName.cs @@ -14,11 +14,10 @@ You may obtain a copy of the License at limitations under the License. */ -using System; +using System.Diagnostics; using System.Globalization; using System.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; +using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; namespace Redmine.Net.Api.Types @@ -26,14 +25,13 @@ namespace Redmine.Net.Api.Types /// /// /// - public class IdentifiableName : Identifiable, IXmlSerializable, IEquatable + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + public class IdentifiableName : Identifiable { /// /// Initializes a new instance of the class. /// - public IdentifiableName() - { - } + public IdentifiableName() { } /// /// Initializes a new instance of the class. @@ -44,31 +42,31 @@ public IdentifiableName(XmlReader reader) Initialize(reader); } + + private void Initialize(XmlReader reader) { ReadXml(reader); } + + + #region Properties /// /// Gets or sets the name. /// - /// The name. - [XmlAttribute(RedmineKeys.NAME)] - public string Name { get; set; } + public string Name { get; internal set; } + #endregion - /// - /// - /// - /// - public XmlSchema GetSchema() { return null; } + #region Implementation of IXmlSerializable /// /// /// /// - public virtual void ReadXml(XmlReader reader) + public override void ReadXml(XmlReader reader) { - Id = Convert.ToInt32(reader.GetAttribute(RedmineKeys.ID), CultureInfo.InvariantCulture); + Id = reader.ReadAttributeAsInt(RedmineKeys.ID); Name = reader.GetAttribute(RedmineKeys.NAME); reader.Read(); } @@ -77,26 +75,23 @@ public virtual void ReadXml(XmlReader reader) /// /// /// - public virtual void WriteXml(XmlWriter writer) + public override void WriteXml(XmlWriter writer) { writer.WriteAttributeString(RedmineKeys.ID, Id.ToString(CultureInfo.InvariantCulture)); writer.WriteAttributeString(RedmineKeys.NAME, Name); } - /// - /// - /// - /// - public override string ToString() - { - return string.Format(CultureInfo.InvariantCulture,"[IdentifiableName: Id={0}, Name={1}]", Id.ToString(CultureInfo.InvariantCulture), Name); - } + #endregion + + + + #region Implementation of IEquatable /// /// /// /// /// - public bool Equals(IdentifiableName other) + public override bool Equals(IdentifiableName other) { if (other == null) return false; return (Id == other.Id && Name == other.Name); @@ -115,11 +110,13 @@ public override int GetHashCode() return hashCode; } } + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[{nameof(IdentifiableName)}: {base.ToString()}, Name={Name}]"; - /// - public override bool Equals(object obj) - { - return Equals(obj as IdentifiableName); - } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Issue.cs b/src/redmine-net-api/Types/Issue.cs index f3874baf..dbf24337 100644 --- a/src/redmine-net-api/Types/Issue.cs +++ b/src/redmine-net-api/Types/Issue.cs @@ -16,9 +16,9 @@ limitations under the License. using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.Xml; -using System.Xml.Schema; using System.Xml.Serialization; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; @@ -26,89 +26,83 @@ limitations under the License. namespace Redmine.Net.Api.Types { /// + /// + /// + /// /// Available as of 1.1 : - ///include: fetch associated data (optional). - ///Possible values: children, attachments, relations, changesets and journals. To fetch multiple associations use comma (e.g ?include=relations,journals). + /// include: fetch associated data (optional). + /// Possible values: children, attachments, relations, changesets and journals. To fetch multiple associations use comma (e.g ?include=relations,journals). /// See Issue journals for more information. - /// + /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.ISSUE)] - public class Issue : Identifiable, IXmlSerializable, IEquatable, ICloneable + public sealed class Issue : Identifiable, ICloneable { + #region Properties /// /// Gets or sets the project. /// /// The project. - [XmlElement(RedmineKeys.PROJECT)] public IdentifiableName Project { get; set; } /// /// Gets or sets the tracker. /// /// The tracker. - [XmlElement(RedmineKeys.TRACKER)] public IdentifiableName Tracker { get; set; } /// /// Gets or sets the status.Possible values: open, closed, * to get open and closed issues, status id /// /// The status. - [XmlElement(RedmineKeys.STATUS)] public IdentifiableName Status { get; set; } /// /// Gets or sets the priority. /// /// The priority. - [XmlElement(RedmineKeys.PRIORITY)] public IdentifiableName Priority { get; set; } /// /// Gets or sets the author. /// /// The author. - [XmlElement(RedmineKeys.AUTHOR)] public IdentifiableName Author { get; set; } /// /// Gets or sets the category. /// /// The category. - [XmlElement(RedmineKeys.CATEGORY)] public IdentifiableName Category { get; set; } /// /// Gets or sets the subject. /// /// The subject. - [XmlElement(RedmineKeys.SUBJECT)] public string Subject { get; set; } /// /// Gets or sets the description. /// /// The description. - [XmlElement(RedmineKeys.DESCRIPTION)] public string Description { get; set; } /// /// Gets or sets the start date. /// /// The start date. - [XmlElement(RedmineKeys.START_DATE, IsNullable = true)] public DateTime? StartDate { get; set; } /// /// Gets or sets the due date. /// /// The due date. - [XmlElement(RedmineKeys.DUE_DATE, IsNullable = true)] public DateTime? DueDate { get; set; } /// /// Gets or sets the done ratio. /// /// The done ratio. - [XmlElement(RedmineKeys.DONE_RATIO, IsNullable = true)] public float? DoneRatio { get; set; } /// @@ -117,56 +111,47 @@ public class Issue : Identifiable, IXmlSerializable, IEquatable, I /// /// true if [private notes]; otherwise, false. /// - [XmlElement(RedmineKeys.PRIVATE_NOTES)] public bool PrivateNotes { get; set; } /// /// Gets or sets the estimated hours. /// /// The estimated hours. - [XmlElement(RedmineKeys.ESTIMATED_HOURS, IsNullable = true)] public float? EstimatedHours { get; set; } /// /// Gets or sets the hours spent on the issue. /// /// The hours spent on the issue. - [XmlElement(RedmineKeys.SPENT_HOURS, IsNullable = true)] public float? SpentHours { get; set; } /// /// Gets or sets the custom fields. /// /// The custom fields. - [XmlArray(RedmineKeys.CUSTOM_FIELDS)] - [XmlArrayItem(RedmineKeys.CUSTOM_FIELD)] - public IList CustomFields { get; set; } + public IList CustomFields { get; set; } /// /// Gets or sets the created on. /// /// The created on. - [XmlElement(RedmineKeys.CREATED_ON, IsNullable = true)] public DateTime? CreatedOn { get; set; } /// /// Gets or sets the updated on. /// /// The updated on. - [XmlElement(RedmineKeys.UPDATED_ON, IsNullable = true)] - public DateTime? UpdatedOn { get; set; } + public DateTime? UpdatedOn { get; internal set; } /// /// Gets or sets the closed on. /// /// The closed on. - [XmlElement(RedmineKeys.CLOSED_ON, IsNullable = true)] - public DateTime? ClosedOn { get; set; } + public DateTime? ClosedOn { get; internal set; } /// /// Gets or sets the notes. /// - [XmlElement(RedmineKeys.NOTES)] public string Notes { get; set; } /// @@ -175,7 +160,6 @@ public class Issue : Identifiable, IXmlSerializable, IEquatable, I /// /// The assigned to. /// - [XmlElement(RedmineKeys.ASSIGNED_TO)] public IdentifiableName AssignedTo { get; set; } /// @@ -184,7 +168,6 @@ public class Issue : Identifiable, IXmlSerializable, IEquatable, I /// /// The parent issue id. /// - [XmlElement(RedmineKeys.PARENT)] public IdentifiableName ParentIssue { get; set; } /// @@ -193,7 +176,6 @@ public class Issue : Identifiable, IXmlSerializable, IEquatable, I /// /// The fixed version. /// - [XmlElement(RedmineKeys.FIXED_VERSION)] public IdentifiableName FixedVersion { get; set; } /// @@ -202,21 +184,18 @@ public class Issue : Identifiable, IXmlSerializable, IEquatable, I /// /// true if this issue is private; otherwise, false. /// - [XmlElement(RedmineKeys.IS_PRIVATE)] public bool IsPrivate { get; set; } /// - /// Returns the sum of spent hours of the task and all the subtasks. + /// Returns the sum of spent hours of the task and all the sub tasks. /// /// Availability starting with redmine version 3.3 - [XmlElement(RedmineKeys.TOTAL_SPENT_HOURS)] public float? TotalSpentHours { get; set; } /// - /// Returns the sum of estimated hours of task and all the subtasks. + /// Returns the sum of estimated hours of task and all the sub tasks. /// /// Availability starting with redmine version 3.3 - [XmlElement(RedmineKeys.TOTAL_ESTIMATED_HOURS)] public float? TotalEstimatedHours { get; set; } /// @@ -225,19 +204,15 @@ public class Issue : Identifiable, IXmlSerializable, IEquatable, I /// /// The journals. /// - [XmlArray(RedmineKeys.JOURNALS)] - [XmlArrayItem(RedmineKeys.JOURNAL)] - public IList Journals { get; internal set; } + public IList Journals { get; set; } /// - /// Gets or sets the changesets. + /// Gets or sets the change sets. /// /// - /// The changesets. + /// The change sets. /// - [XmlArray(RedmineKeys.CHANGESETS)] - [XmlArrayItem(RedmineKeys.CHANGESET)] - public IList Changesets { get; internal set; } + public IList ChangeSets { get; set; } /// /// Gets or sets the attachments. @@ -245,9 +220,7 @@ public class Issue : Identifiable, IXmlSerializable, IEquatable, I /// /// The attachments. /// - [XmlArray(RedmineKeys.ATTACHMENTS)] - [XmlArrayItem(RedmineKeys.ATTACHMENT)] - public IList Attachments { get; internal set; } + public IList Attachments { get; set; } /// /// Gets or sets the issue relations. @@ -255,9 +228,7 @@ public class Issue : Identifiable, IXmlSerializable, IEquatable, I /// /// The issue relations. /// - [XmlArray(RedmineKeys.RELATIONS)] - [XmlArrayItem(RedmineKeys.RELATION)] - public IList Relations { get; internal set; } + public IList Relations { get; set; } /// /// Gets or sets the issue children. @@ -266,9 +237,7 @@ public class Issue : Identifiable, IXmlSerializable, IEquatable, I /// The issue children. /// NOTE: Only Id, tracker and subject are filled. /// - [XmlArray(RedmineKeys.CHILDREN)] - [XmlArrayItem(RedmineKeys.ISSUE)] - public IList Children { get; internal set; } + public IList Children { get; set; } /// /// Gets or sets the attachments. @@ -276,31 +245,20 @@ public class Issue : Identifiable, IXmlSerializable, IEquatable, I /// /// The attachment. /// - [XmlArray(RedmineKeys.UPLOADS)] - [XmlArrayItem(RedmineKeys.UPLOAD)] - public IList Uploads { get; set; } + public IList Uploads { get; set; } /// /// /// - [XmlArray(RedmineKeys.WATCHERS)] - [XmlArrayItem(RedmineKeys.WATCHER)] - public IList Watchers { get; internal set; } - - /// - /// - /// - /// - public XmlSchema GetSchema() - { - return null; - } + public IList Watchers { get; set; } + #endregion + #region Implementation of IXmlSerialization /// /// /// /// - public void ReadXml(XmlReader reader) + public override void ReadXml(XmlReader reader) { reader.Read(); @@ -314,137 +272,39 @@ public void ReadXml(XmlReader reader) switch (reader.Name) { - case RedmineKeys.ID: - Id = reader.ReadElementContentAsInt(); - break; - - case RedmineKeys.PROJECT: - Project = new IdentifiableName(reader); - break; - - case RedmineKeys.TRACKER: - Tracker = new IdentifiableName(reader); - break; - - case RedmineKeys.STATUS: - Status = new IdentifiableName(reader); - break; - - case RedmineKeys.PRIORITY: - Priority = new IdentifiableName(reader); - break; - - case RedmineKeys.AUTHOR: - Author = new IdentifiableName(reader); - break; - - case RedmineKeys.ASSIGNED_TO: - AssignedTo = new IdentifiableName(reader); - break; - - case RedmineKeys.CATEGORY: - Category = new IdentifiableName(reader); - break; - - case RedmineKeys.PARENT: - ParentIssue = new IdentifiableName(reader); - break; - - case RedmineKeys.FIXED_VERSION: - FixedVersion = new IdentifiableName(reader); - break; - - case RedmineKeys.PRIVATE_NOTES: - PrivateNotes = reader.ReadElementContentAsBoolean(); - break; - - case RedmineKeys.IS_PRIVATE: - IsPrivate = reader.ReadElementContentAsBoolean(); - break; - - case RedmineKeys.SUBJECT: - Subject = reader.ReadElementContentAsString(); - break; - - case RedmineKeys.NOTES: - Notes = reader.ReadElementContentAsString(); - break; - - case RedmineKeys.DESCRIPTION: - Description = reader.ReadElementContentAsString(); - break; - - case RedmineKeys.START_DATE: - StartDate = reader.ReadElementContentAsNullableDateTime(); - break; - - case RedmineKeys.DUE_DATE: - DueDate = reader.ReadElementContentAsNullableDateTime(); - break; - - case RedmineKeys.DONE_RATIO: - DoneRatio = reader.ReadElementContentAsNullableFloat(); - break; - - case RedmineKeys.ESTIMATED_HOURS: - EstimatedHours = reader.ReadElementContentAsNullableFloat(); - break; - - case RedmineKeys.TOTAL_ESTIMATED_HOURS: - TotalEstimatedHours = reader.ReadElementContentAsNullableFloat(); - break; - - case RedmineKeys.TOTAL_SPENT_HOURS: - TotalSpentHours = reader.ReadElementContentAsNullableFloat(); - break; - - case RedmineKeys.SPENT_HOURS: - SpentHours = reader.ReadElementContentAsNullableFloat(); - break; - - case RedmineKeys.CREATED_ON: - CreatedOn = reader.ReadElementContentAsNullableDateTime(); - break; - - case RedmineKeys.UPDATED_ON: - UpdatedOn = reader.ReadElementContentAsNullableDateTime(); - break; - - case RedmineKeys.CLOSED_ON: - ClosedOn = reader.ReadElementContentAsNullableDateTime(); - break; - - case RedmineKeys.CUSTOM_FIELDS: - CustomFields = reader.ReadElementContentAsCollection(); - break; - - case RedmineKeys.ATTACHMENTS: - Attachments = reader.ReadElementContentAsCollection(); - break; - - case RedmineKeys.RELATIONS: - Relations = reader.ReadElementContentAsCollection(); - break; - - case RedmineKeys.JOURNALS: - Journals = reader.ReadElementContentAsCollection(); - break; - - case RedmineKeys.CHANGESETS: - Changesets = reader.ReadElementContentAsCollection(); - break; - - case RedmineKeys.CHILDREN: - Children = reader.ReadElementContentAsCollection(); - break; - - case RedmineKeys.WATCHERS: - Watchers = reader.ReadElementContentAsCollection(); - break; - - default: - reader.Read(); - break; + case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.ASSIGNED_TO: AssignedTo = new IdentifiableName(reader); break; + case RedmineKeys.ATTACHMENTS: Attachments = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.AUTHOR: Author = new IdentifiableName(reader); break; + case RedmineKeys.CATEGORY: Category = new IdentifiableName(reader); break; + case RedmineKeys.CHANGE_SETS: ChangeSets = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.CHILDREN: Children = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.CLOSED_ON: ClosedOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadElementContentAsString(); break; + case RedmineKeys.DONE_RATIO: DoneRatio = reader.ReadElementContentAsNullableFloat(); break; + case RedmineKeys.DUE_DATE: DueDate = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.ESTIMATED_HOURS: EstimatedHours = reader.ReadElementContentAsNullableFloat(); break; + case RedmineKeys.FIXED_VERSION: FixedVersion = new IdentifiableName(reader); break; + case RedmineKeys.IS_PRIVATE: IsPrivate = reader.ReadElementContentAsBoolean(); break; + case RedmineKeys.JOURNALS: Journals = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.NOTES: Notes = reader.ReadElementContentAsString(); break; + case RedmineKeys.PARENT: ParentIssue = new IdentifiableName(reader); break; + case RedmineKeys.PRIORITY: Priority = new IdentifiableName(reader); break; + case RedmineKeys.PRIVATE_NOTES: PrivateNotes = reader.ReadElementContentAsBoolean(); break; + case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; + case RedmineKeys.RELATIONS: Relations = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.SPENT_HOURS: SpentHours = reader.ReadElementContentAsNullableFloat(); break; + case RedmineKeys.START_DATE: StartDate = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.STATUS: Status = new IdentifiableName(reader); break; + case RedmineKeys.SUBJECT: Subject = reader.ReadElementContentAsString(); break; + case RedmineKeys.TOTAL_ESTIMATED_HOURS: TotalEstimatedHours = reader.ReadElementContentAsNullableFloat(); break; + case RedmineKeys.TOTAL_SPENT_HOURS: TotalSpentHours = reader.ReadElementContentAsNullableFloat(); break; + case RedmineKeys.TRACKER: Tracker = new IdentifiableName(reader); break; + case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.WATCHERS: Watchers = reader.ReadElementContentAsCollection(); break; + default: reader.Read(); break; } } } @@ -453,78 +313,51 @@ public void ReadXml(XmlReader reader) /// /// /// - public void WriteXml(XmlWriter writer) + public override void WriteXml(XmlWriter writer) { writer.WriteElementString(RedmineKeys.SUBJECT, Subject); writer.WriteElementString(RedmineKeys.NOTES, Notes); if (Id != 0) { - writer.WriteElementString(RedmineKeys.PRIVATE_NOTES, XmlConvert.ToString(PrivateNotes)); + writer.WriteElementString(RedmineKeys.PRIVATE_NOTES, PrivateNotes.ToString().ToLowerInvariant()); } writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); - writer.WriteStartElement(RedmineKeys.IS_PRIVATE); - writer.WriteValue(XmlConvert.ToString(IsPrivate)); - writer.WriteEndElement(); - - writer.WriteIdIfNotNull(Project, RedmineKeys.PROJECT_ID); - writer.WriteIdIfNotNull(Priority, RedmineKeys.PRIORITY_ID); - writer.WriteIdIfNotNull(Status, RedmineKeys.STATUS_ID); - writer.WriteIdIfNotNull(Category, RedmineKeys.CATEGORY_ID); - writer.WriteIdIfNotNull(Tracker, RedmineKeys.TRACKER_ID); - writer.WriteIdIfNotNull(AssignedTo, RedmineKeys.ASSIGNED_TO_ID); - writer.WriteIdIfNotNull(ParentIssue, RedmineKeys.PARENT_ISSUE_ID); - writer.WriteIdIfNotNull(FixedVersion, RedmineKeys.FIXED_VERSION_ID); - - writer.WriteValueOrEmpty(EstimatedHours, RedmineKeys.ESTIMATED_HOURS); - writer.WriteValueOrEmpty(DoneRatio, RedmineKeys.DONE_RATIO); - writer.WriteDateOrEmpty(StartDate, RedmineKeys.START_DATE); - writer.WriteDateOrEmpty(DueDate, RedmineKeys.DUE_DATE); - writer.WriteDateOrEmpty(UpdatedOn, RedmineKeys.UPDATED_ON); - - writer.WriteArray(Uploads, RedmineKeys.UPLOADS); - writer.WriteArray(CustomFields, RedmineKeys.CUSTOM_FIELDS); - - writer.WriteListElements(Watchers as IList, RedmineKeys.WATCHER_USER_IDS); - } + writer.WriteElementString(RedmineKeys.IS_PRIVATE, IsPrivate.ToString().ToLowerInvariant()); - /// - /// - /// - /// - public object Clone() - { - var issue = new Issue - { - AssignedTo = AssignedTo, - Author = Author, - Category = Category, - CustomFields = CustomFields.Clone(), - Description = Description, - DoneRatio = DoneRatio, - DueDate = DueDate, - SpentHours = SpentHours, - EstimatedHours = EstimatedHours, - Priority = Priority, - StartDate = StartDate, - Status = Status, - Subject = Subject, - Tracker = Tracker, - Project = Project, - FixedVersion = FixedVersion, - Notes = Notes, - Watchers = Watchers.Clone() - }; - return issue; + writer.WriteIdIfNotNull(RedmineKeys.PROJECT_ID, Project); + writer.WriteIdIfNotNull(RedmineKeys.PRIORITY_ID, Priority); + writer.WriteIdIfNotNull(RedmineKeys.STATUS_ID, Status); + writer.WriteIdIfNotNull(RedmineKeys.CATEGORY_ID, Category); + writer.WriteIdIfNotNull(RedmineKeys.TRACKER_ID, Tracker); + writer.WriteIdIfNotNull(RedmineKeys.ASSIGNED_TO_ID, AssignedTo); + writer.WriteIdIfNotNull(RedmineKeys.PARENT_ISSUE_ID, ParentIssue); + writer.WriteIdIfNotNull(RedmineKeys.FIXED_VERSION_ID, FixedVersion); + + writer.WriteValueOrEmpty(RedmineKeys.ESTIMATED_HOURS, EstimatedHours); + writer.WriteIfNotDefaultOrNull(RedmineKeys.DONE_RATIO, DoneRatio); + + writer.WriteDateOrEmpty(RedmineKeys.START_DATE, StartDate); + writer.WriteDateOrEmpty(RedmineKeys.DUE_DATE, DueDate); + writer.WriteDateOrEmpty(RedmineKeys.UPDATED_ON, UpdatedOn); + + writer.WriteArray(RedmineKeys.UPLOADS, Uploads); + writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); + + writer.WriteListElements(RedmineKeys.WATCHER_USER_IDS, (IEnumerable)Watchers); } + #endregion + + + #region Implementation of IEquatable /// /// /// /// /// - public bool Equals(Issue other) + public override bool Equals(Issue other) { if (other == null) return false; return ( @@ -541,34 +374,24 @@ public bool Equals(Issue other) && DueDate == other.DueDate && DoneRatio == other.DoneRatio && EstimatedHours == other.EstimatedHours - && (CustomFields?.Equals(other.CustomFields) ?? other.CustomFields == null) + && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null) && CreatedOn == other.CreatedOn && UpdatedOn == other.UpdatedOn && AssignedTo == other.AssignedTo && FixedVersion == other.FixedVersion && Notes == other.Notes - && (Watchers?.Equals(other.Watchers) ?? other.Watchers == null) + && (Watchers != null ? Watchers.Equals(other.Watchers) : other.Watchers == null) && ClosedOn == other.ClosedOn && SpentHours == other.SpentHours && PrivateNotes == other.PrivateNotes - && (Attachments?.Equals(other.Attachments) ?? other.Attachments == null) - && (Changesets?.Equals(other.Changesets) ?? other.Changesets == null) - && (Children?.Equals(other.Children) ?? other.Children == null) - && (Journals?.Equals(other.Journals) ?? other.Journals == null) - && (Relations?.Equals(other.Relations) ?? other.Relations == null) + && (Attachments != null ? Attachments.Equals(other.Attachments) : other.Attachments == null) + && (ChangeSets != null ? ChangeSets.Equals(other.ChangeSets) : other.ChangeSets == null) + && (Children != null ? Children.Equals(other.Children) : other.Children == null) + && (Journals != null ? Journals.Equals(other.Journals) : other.Journals == null) + && (Relations != null ? Relations.Equals(other.Relations) : other.Relations == null) ); } - /// - /// - /// - /// - public override string ToString() - { - return - $"[Issue: {base.ToString()}, Project={Project}, Tracker={Tracker}, Status={Status}, Priority={Priority}, Author={Author}, Category={Category}, Subject={Subject}, Description={Description}, StartDate={StartDate}, DueDate={DueDate}, DoneRatio={DoneRatio}, PrivateNotes={PrivateNotes}, EstimatedHours={EstimatedHours}, SpentHours={SpentHours}, CustomFields={CustomFields}, CreatedOn={CreatedOn}, UpdatedOn={UpdatedOn}, ClosedOn={ClosedOn}, Notes={Notes}, AssignedTo={AssignedTo}, ParentIssue={ParentIssue}, FixedVersion={FixedVersion}, IsPrivate={IsPrivate}, Journals={Journals}, Changesets={Changesets}, Attachments={Attachments}, Relations={Relations}, Children={Children}, Uploads={Uploads}, Watchers={Watchers}]"; - } - /// /// /// @@ -605,7 +428,7 @@ public override int GetHashCode() hashCode = HashCodeHelper.GetHashCode(Journals, hashCode); hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); - hashCode = HashCodeHelper.GetHashCode(Changesets, hashCode); + hashCode = HashCodeHelper.GetHashCode(ChangeSets, hashCode); hashCode = HashCodeHelper.GetHashCode(Attachments, hashCode); hashCode = HashCodeHelper.GetHashCode(Relations, hashCode); hashCode = HashCodeHelper.GetHashCode(Children, hashCode); @@ -614,11 +437,69 @@ public override int GetHashCode() return hashCode; } + #endregion - /// - public override bool Equals(object obj) + #region Implementation of IClonable + /// + /// + /// + /// + public object Clone() { - return Equals(obj as Issue); + var issue = new Issue + { + AssignedTo = AssignedTo, + Author = Author, + Category = Category, + CustomFields = CustomFields.Clone(), + Description = Description, + DoneRatio = DoneRatio, + DueDate = DueDate, + SpentHours = SpentHours, + EstimatedHours = EstimatedHours, + Priority = Priority, + StartDate = StartDate, + Status = Status, + Subject = Subject, + Tracker = Tracker, + Project = Project, + FixedVersion = FixedVersion, + Notes = Notes, + Watchers = Watchers.Clone() + }; + return issue; } + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => + $@"[{nameof(Issue)}: {ToString()}, Project={Project}, Tracker={Tracker}, Status={Status}, +Priority={Priority}, Author={Author}, Category={Category}, Subject={Subject}, Description={Description}, +StartDate={StartDate?.ToString("u", CultureInfo.InvariantCulture)}, +DueDate={DueDate?.ToString("u", CultureInfo.InvariantCulture)}, +DoneRatio={DoneRatio?.ToString("F", CultureInfo.InvariantCulture)}, +PrivateNotes={PrivateNotes.ToString(CultureInfo.InvariantCulture)}, +EstimatedHours={EstimatedHours?.ToString("F", CultureInfo.InvariantCulture)}, +SpentHours={SpentHours?.ToString("F", CultureInfo.InvariantCulture)}, +CustomFields={CustomFields.Dump()}, +CreatedOn={CreatedOn?.ToString("u", CultureInfo.InvariantCulture)}, +UpdatedOn={UpdatedOn?.ToString("u", CultureInfo.InvariantCulture)}, +ClosedOn={ClosedOn?.ToString("u", CultureInfo.InvariantCulture)}, +Notes={Notes}, +AssignedTo={AssignedTo}, +ParentIssue={ParentIssue}, +FixedVersion={FixedVersion}, +IsPrivate={IsPrivate.ToString(CultureInfo.InvariantCulture)}, +Journals={Journals.Dump()}, +ChangeSets={ChangeSets.Dump()}, +Attachments={Attachments.Dump()}, +Relations={Relations.Dump()}, +Children={Children.Dump()}, +Uploads={Uploads.Dump()}, +Watchers={Watchers.Dump()}]"; + } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/IssueCategory.cs b/src/redmine-net-api/Types/IssueCategory.cs old mode 100755 new mode 100644 index 870cb0c9..57c11d92 --- a/src/redmine-net-api/Types/IssueCategory.cs +++ b/src/redmine-net-api/Types/IssueCategory.cs @@ -14,9 +14,8 @@ You may obtain a copy of the License at limitations under the License. */ -using System; +using System.Diagnostics; using System.Xml; -using System.Xml.Schema; using System.Xml.Serialization; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; @@ -26,16 +25,17 @@ namespace Redmine.Net.Api.Types /// /// Availability 1.3 /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.ISSUE_CATEGORY)] - public class IssueCategory : Identifiable, IEquatable, IXmlSerializable + public sealed class IssueCategory : Identifiable { + #region Properties /// /// Gets or sets the project. /// /// /// The project. /// - [XmlElement(RedmineKeys.PROJECT)] public IdentifiableName Project { get; set; } /// @@ -44,8 +44,7 @@ public class IssueCategory : Identifiable, IEquatable /// The asign to. /// - [XmlElement(RedmineKeys.ASSIGNED_TO)] - public IdentifiableName AsignTo { get; set; } + public IdentifiableName AssignTo { get; set; } /// /// Gets or sets the name. @@ -53,31 +52,15 @@ public class IssueCategory : Identifiable, IEquatable /// The name. /// - [XmlElement(RedmineKeys.NAME)] public string Name { get; set; } + #endregion - /// - /// - /// - /// - /// - public bool Equals(IssueCategory other) - { - if (other == null) return false; - return (Id == other.Id && Project == other.Project && AsignTo == other.AsignTo && Name == other.Name); - } - - /// - /// - /// - /// - public XmlSchema GetSchema() { return null; } - + #region Implementation of IXmlSerialization /// /// /// /// - public void ReadXml(XmlReader reader) + public override void ReadXml(XmlReader reader) { reader.Read(); while (!reader.EOF) @@ -91,13 +74,9 @@ public void ReadXml(XmlReader reader) switch (reader.Name) { case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; - - case RedmineKeys.ASSIGNED_TO: AsignTo = new IdentifiableName(reader); break; - + case RedmineKeys.ASSIGNED_TO: AssignTo = new IdentifiableName(reader); break; case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; - + case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; default: reader.Read(); break; } } @@ -107,11 +86,26 @@ public void ReadXml(XmlReader reader) /// /// /// - public void WriteXml(XmlWriter writer) + public override void WriteXml(XmlWriter writer) { - writer.WriteIdIfNotNull(Project, RedmineKeys.PROJECT_ID); + writer.WriteIdIfNotNull(RedmineKeys.PROJECT_ID, Project); writer.WriteElementString(RedmineKeys.NAME, Name); - writer.WriteIdIfNotNull(AsignTo, RedmineKeys.ASSIGNED_TO_ID); + writer.WriteIdIfNotNull(RedmineKeys.ASSIGNED_TO_ID, AssignTo); + } + #endregion + + + + #region Implementation of IEquatable + /// + /// + /// + /// + /// + public override bool Equals(IssueCategory other) + { + if (other == null) return false; + return (Id == other.Id && Project == other.Project && AssignTo == other.AssignTo && Name == other.Name); } /// @@ -125,25 +119,18 @@ public override int GetHashCode() var hashCode = 13; hashCode = HashCodeHelper.GetHashCode(Id, hashCode); hashCode = HashCodeHelper.GetHashCode(Project, hashCode); - hashCode = HashCodeHelper.GetHashCode(AsignTo, hashCode); + hashCode = HashCodeHelper.GetHashCode(AssignTo, hashCode); hashCode = HashCodeHelper.GetHashCode(Name, hashCode); return hashCode; } } + #endregion /// /// /// /// - public override string ToString() - { - return $"[IssueCategory: {base.ToString()}, Project={Project}, AsignTo={AsignTo}, Name={Name}]"; - } + private string DebuggerDisplay => $"[{nameof(IssueCategory)}: {ToString()}, Project={Project}, AssignTo={AssignTo}, Name={Name}]"; - /// - public override bool Equals(object obj) - { - return Equals(obj as IssueCategory); - } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/IssueChild.cs b/src/redmine-net-api/Types/IssueChild.cs old mode 100755 new mode 100644 index 9700b43c..b498c3a4 --- a/src/redmine-net-api/Types/IssueChild.cs +++ b/src/redmine-net-api/Types/IssueChild.cs @@ -15,9 +15,9 @@ limitations under the License. */ using System; +using System.Diagnostics; using System.Globalization; using System.Xml; -using System.Xml.Schema; using System.Xml.Serialization; using Redmine.Net.Api.Internals; @@ -26,34 +26,30 @@ namespace Redmine.Net.Api.Types /// /// /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.ISSUE)] - public class IssueChild : Identifiable, IXmlSerializable, IEquatable, ICloneable + public sealed class IssueChild : Identifiable, ICloneable { + #region Properties /// /// Gets or sets the tracker. /// /// The tracker. - [XmlElement(RedmineKeys.TRACKER)] - public IdentifiableName Tracker { get; set; } + public IdentifiableName Tracker { get; internal set; } /// /// Gets or sets the subject. /// /// The subject. - [XmlElement(RedmineKeys.SUBJECT)] - public string Subject { get; set; } - - /// - /// - /// - /// - public XmlSchema GetSchema() { return null; } + public string Subject { get; internal set; } + #endregion + #region Implementation of IXmlSerialization /// /// /// /// - public void ReadXml(XmlReader reader) + public override void ReadXml(XmlReader reader) { Id = Convert.ToInt32(reader.GetAttribute(RedmineKeys.ID), CultureInfo.InvariantCulture); reader.Read(); @@ -68,37 +64,23 @@ public void ReadXml(XmlReader reader) switch (reader.Name) { - case RedmineKeys.TRACKER: Tracker = new IdentifiableName(reader); break; - case RedmineKeys.SUBJECT: Subject = reader.ReadElementContentAsString(); break; - + case RedmineKeys.TRACKER: Tracker = new IdentifiableName(reader); break; default: reader.Read(); break; } } } + #endregion - /// - /// - /// - /// - public void WriteXml(XmlWriter writer) { } - - /// - /// - /// - /// - public object Clone() - { - var issueChild = new IssueChild { Subject = Subject, Tracker = Tracker }; - return issueChild; - } + + #region Implementation of IEquatable /// /// /// /// /// - public bool Equals(IssueChild other) + public override bool Equals(IssueChild other) { if (other == null) return false; return (Id == other.Id && Tracker == other.Tracker && Subject == other.Subject); @@ -119,20 +101,26 @@ public override int GetHashCode() return hashCode; } } + #endregion + + #region Implementation of IClonable /// /// /// /// - public override string ToString() + public object Clone() { - return $"[IssueChild: {base.ToString()}, Tracker={Tracker}, Subject={Subject}]"; + var issueChild = new IssueChild { Subject = Subject, Tracker = Tracker }; + return issueChild; } + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[{nameof(IssueChild)}: {ToString()}, Tracker={Tracker}, Subject={Subject}]"; - /// - public override bool Equals(object obj) - { - return Equals(obj as IssueChild); - } } } diff --git a/src/redmine-net-api/Types/IssueCustomField.cs b/src/redmine-net-api/Types/IssueCustomField.cs old mode 100755 new mode 100644 index 8e2c94b5..54ddec94 --- a/src/redmine-net-api/Types/IssueCustomField.cs +++ b/src/redmine-net-api/Types/IssueCustomField.cs @@ -16,6 +16,7 @@ limitations under the License. using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.Xml; using System.Xml.Serialization; @@ -27,22 +28,24 @@ namespace Redmine.Net.Api.Types /// /// /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.CUSTOM_FIELD)] - public class IssueCustomField : IdentifiableName, IEquatable, ICloneable + public sealed class IssueCustomField : IdentifiableName, IEquatable, ICloneable, IValue { + #region Properties /// /// Gets or sets the value. /// /// The value. - [XmlArray(RedmineKeys.VALUE)] - [XmlArrayItem(RedmineKeys.VALUE)] public IList Values { get; set; } /// /// /// - [XmlAttribute(RedmineKeys.MULTIPLE)] public bool Multiple { get; set; } + #endregion + + #region Implementation of IXmlSerializable /// /// @@ -52,18 +55,44 @@ public override void ReadXml(XmlReader reader) { Id = Convert.ToInt32(reader.GetAttribute(RedmineKeys.ID), CultureInfo.InvariantCulture); Name = reader.GetAttribute(RedmineKeys.NAME); - Multiple = reader.ReadAttributeAsBoolean(RedmineKeys.MULTIPLE); + reader.Read(); - if (string.IsNullOrEmpty(reader.GetAttribute("type"))) + if (reader.NodeType == XmlNodeType.Whitespace) + { + reader.Read(); + } + + if (reader.NodeType == XmlNodeType.Text) + { + Values = new List + { + new CustomFieldValue(reader.Value) + }; + + reader.Read(); + return; + } + + var attributeExists = !reader.GetAttribute("type").IsNullOrWhiteSpace(); + + if (!attributeExists) { - Values = new List { new CustomFieldValue { Info = reader.ReadElementContentAsString() } }; + if (reader.IsEmptyElement) + { + reader.Read(); + return; + } + + Values = new List + { + new CustomFieldValue(reader.ReadElementContentAsString()) + }; } else { - var result = reader.ReadElementContentAsCollection(); - Values = result; + Values = reader.ReadElementContentAsCollection(); } } @@ -73,20 +102,29 @@ public override void ReadXml(XmlReader reader) /// public override void WriteXml(XmlWriter writer) { - if (Values == null) return; + if (Values == null) + { + return; + } + var itemsCount = Values.Count; writer.WriteAttributeString(RedmineKeys.ID, Id.ToString(CultureInfo.InvariantCulture)); + if (itemsCount > 1) { - writer.WriteArrayStringElement(Values, RedmineKeys.VALUE, GetValue); + writer.WriteArrayStringElement(RedmineKeys.VALUE, Values, GetValue); } else { writer.WriteElementString(RedmineKeys.VALUE, itemsCount > 0 ? Values[0].Info : null); } } + #endregion + + + #region Implementation of IEquatable /// /// /// @@ -95,60 +133,65 @@ public override void WriteXml(XmlWriter writer) public bool Equals(IssueCustomField other) { if (other == null) return false; - return (Id == other.Id && Name == other.Name && Multiple == other.Multiple && Values.Equals(other.Values)); + return (Id == other.Id + && Name == other.Name + && Multiple == other.Multiple + && (Values != null ? Values.Equals(other.Values) : other.Values == null)); } /// /// /// /// - public object Clone() + public override int GetHashCode() { - var issueCustomField = new IssueCustomField { Multiple = Multiple, Values = Values.Clone() }; - return issueCustomField; + unchecked + { + var hashCode = 13; + hashCode = HashCodeHelper.GetHashCode(Id, hashCode); + hashCode = HashCodeHelper.GetHashCode(Name, hashCode); + hashCode = HashCodeHelper.GetHashCode(Values, hashCode); + hashCode = HashCodeHelper.GetHashCode(Multiple, hashCode); + return hashCode; + } } + #endregion + #region Implementation of IClonable /// /// /// /// - public override string ToString() + public object Clone() { - return $"[IssueCustomField: {base.ToString()} Values={Values}, Multiple={Multiple}]"; + var issueCustomField = new IssueCustomField { Multiple = Multiple, Values = Values.Clone() }; + return issueCustomField; } + #endregion + #region Implementation of IValue /// /// /// - /// - public override int GetHashCode() - { - unchecked - { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); - hashCode = HashCodeHelper.GetHashCode(Name, hashCode); - hashCode = HashCodeHelper.GetHashCode(Values, hashCode); - hashCode = HashCodeHelper.GetHashCode(Multiple, hashCode); - return hashCode; - } - } + public string Value => Id.ToString(CultureInfo.InvariantCulture); + + #endregion /// /// /// /// /// - public static string GetValue(object item) + public string GetValue(object item) { - if (item == null) throw new ArgumentNullException(nameof(item)); return ((CustomFieldValue)item).Info; } - /// - public override bool Equals(object obj) - { - return Equals(obj as IssueCustomField); - } + /// + /// + /// + /// + private string DebuggerDisplay => $"[{nameof(IssueCustomField)}: {ToString()} Values={Values.Dump()}, Multiple={Multiple.ToString(CultureInfo.InvariantCulture)}]"; + } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/IssuePriority.cs b/src/redmine-net-api/Types/IssuePriority.cs old mode 100755 new mode 100644 index 87754916..30a5846d --- a/src/redmine-net-api/Types/IssuePriority.cs +++ b/src/redmine-net-api/Types/IssuePriority.cs @@ -15,6 +15,8 @@ limitations under the License. */ using System; +using System.Diagnostics; +using System.Globalization; using System.Xml; using System.Xml.Serialization; using Redmine.Net.Api.Internals; @@ -24,24 +26,25 @@ namespace Redmine.Net.Api.Types /// /// Availability 2.2 /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.ISSUE_PRIORITY)] - public class IssuePriority : IdentifiableName, IEquatable + public sealed class IssuePriority : IdentifiableName, IEquatable { + #region Properties /// /// /// - [XmlElement(RedmineKeys.IS_DEFAULT)] - public bool IsDefault { get; set; } + public bool IsDefault { get; internal set; } + #endregion #region Implementation of IXmlSerializable /// /// Generates an object from its XML representation. /// - /// The stream from which the object is deserialized. + /// The stream from which the object is deserialized. public override void ReadXml(XmlReader reader) { - if (reader == null) throw new ArgumentNullException(nameof(reader)); reader.Read(); while (!reader.EOF) { @@ -54,24 +57,16 @@ public override void ReadXml(XmlReader reader) switch (reader.Name) { case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; - case RedmineKeys.IS_DEFAULT: IsDefault = reader.ReadElementContentAsBoolean(); break; - + case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; default: reader.Read(); break; } } } - - /// - /// - /// - /// - public override void WriteXml(XmlWriter writer) { } - #endregion + + #region Implementation of IEquatable /// /// @@ -113,16 +108,13 @@ public override int GetHashCode() return hashCode; } } + #endregion /// /// /// /// - public override string ToString() - { - return $"[IssuePriority: Id={Id}, Name={Name}, IsDefault={IsDefault}]"; - } + private string DebuggerDisplay => $"[IssuePriority: {ToString()}, IsDefault={IsDefault.ToString(CultureInfo.InvariantCulture)}]"; - #endregion } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/IssueRelation.cs b/src/redmine-net-api/Types/IssueRelation.cs old mode 100755 new mode 100644 index 1b59e086..653d0e97 --- a/src/redmine-net-api/Types/IssueRelation.cs +++ b/src/redmine-net-api/Types/IssueRelation.cs @@ -15,9 +15,9 @@ limitations under the License. */ using System; +using System.Diagnostics; using System.Globalization; using System.Xml; -using System.Xml.Schema; using System.Xml.Serialization; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; @@ -27,50 +27,43 @@ namespace Redmine.Net.Api.Types /// /// Availability 1.3 /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.RELATION)] - public class IssueRelation : Identifiable, IXmlSerializable, IEquatable + public sealed class IssueRelation : Identifiable { + #region Properties /// /// Gets or sets the issue id. /// /// The issue id. - [XmlElement(RedmineKeys.ISSUE_ID)] - public int IssueId { get; set; } + public int IssueId { get; internal set; } /// /// Gets or sets the related issue id. /// /// The issue to id. - [XmlElement(RedmineKeys.ISSUE_TO_ID)] public int IssueToId { get; set; } /// /// Gets or sets the type of relation. /// /// The type. - [XmlElement(RedmineKeys.RELATION_TYPE)] public IssueRelationType Type { get; set; } /// /// Gets or sets the delay for a "precedes" or "follows" relation. /// /// The delay. - [XmlElement(RedmineKeys.DELAY, IsNullable = true)] public int? Delay { get; set; } + #endregion - /// - /// - /// - /// - public XmlSchema GetSchema() { return null; } - + #region Implementation of IXmlSerialization /// /// /// /// - public void ReadXml(XmlReader reader) + public override void ReadXml(XmlReader reader) { - if (reader == null) throw new ArgumentNullException(nameof(reader)); if (!reader.IsEmptyElement) reader.Read(); while (!reader.EOF) { @@ -88,16 +81,16 @@ public void ReadXml(XmlReader reader) switch (reader.Name) { case RedmineKeys.ID: Id = reader.ReadAttributeAsInt(attributeName); break; + case RedmineKeys.DELAY: Delay = reader.ReadAttributeAsNullableInt(attributeName); break; case RedmineKeys.ISSUE_ID: IssueId = reader.ReadAttributeAsInt(attributeName); break; case RedmineKeys.ISSUE_TO_ID: IssueToId = reader.ReadAttributeAsInt(attributeName); break; case RedmineKeys.RELATION_TYPE: - var rt = reader.GetAttribute(attributeName); - if (!string.IsNullOrEmpty(rt)) + var issueRelationType = reader.GetAttribute(attributeName); + if (!issueRelationType.IsNullOrWhiteSpace()) { - Type = (IssueRelationType)Enum.Parse(typeof(IssueRelationType), rt, true); + Type = (IssueRelationType)Enum.Parse(typeof(IssueRelationType), issueRelationType, true); } break; - case RedmineKeys.DELAY: Delay = reader.ReadAttributeAsNullableInt(attributeName); break; } } return; @@ -106,16 +99,16 @@ public void ReadXml(XmlReader reader) switch (reader.Name) { case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.DELAY: Delay = reader.ReadElementContentAsNullableInt(); break; case RedmineKeys.ISSUE_ID: IssueId = reader.ReadElementContentAsInt(); break; case RedmineKeys.ISSUE_TO_ID: IssueToId = reader.ReadElementContentAsInt(); break; case RedmineKeys.RELATION_TYPE: - var rt = reader.ReadElementContentAsString(); - if (!string.IsNullOrEmpty(rt)) + var issueRelationType = reader.ReadElementContentAsString(); + if (!issueRelationType.IsNullOrWhiteSpace()) { - Type = (IssueRelationType)Enum.Parse(typeof(IssueRelationType), rt, true); + Type = (IssueRelationType)Enum.Parse(typeof(IssueRelationType), issueRelationType, true); } break; - case RedmineKeys.DELAY: Delay = reader.ReadElementContentAsNullableInt(); break; default: reader.Read(); break; } } @@ -125,21 +118,26 @@ public void ReadXml(XmlReader reader) /// /// /// - public void WriteXml(XmlWriter writer) + public override void WriteXml(XmlWriter writer) { - if (writer == null) throw new ArgumentNullException(nameof(writer)); writer.WriteElementString(RedmineKeys.ISSUE_TO_ID, IssueToId.ToString(CultureInfo.InvariantCulture)); writer.WriteElementString(RedmineKeys.RELATION_TYPE, Type.ToString()); - if (Type == IssueRelationType.precedes || Type == IssueRelationType.follows) - writer.WriteValueOrEmpty(Delay, RedmineKeys.DELAY); + if (Type == IssueRelationType.Precedes || Type == IssueRelationType.Follows) + { + writer.WriteValueOrEmpty(RedmineKeys.DELAY, Delay); + } } + #endregion + + + #region Implementation of IEquatable /// /// /// /// /// - public bool Equals(IssueRelation other) + public override bool Equals(IssueRelation other) { if (other == null) return false; return (Id == other.Id && IssueId == other.IssueId && IssueToId == other.IssueToId && Type == other.Type && Delay == other.Delay); @@ -162,21 +160,17 @@ public override int GetHashCode() return hashCode; } } + #endregion /// /// /// /// - public override string ToString() - { - return - $"[IssueRelation: {base.ToString()}, IssueId={IssueId}, IssueToId={IssueToId}, Type={Type}, Delay={Delay}]"; - } + private string DebuggerDisplay => $@"[{nameof(IssueRelation)}: {ToString()}, +IssueId={IssueId.ToString(CultureInfo.InvariantCulture)}, +IssueToId={IssueToId.ToString(CultureInfo.InvariantCulture)}, +Type={Type.ToString("G")}, +Delay={Delay?.ToString(CultureInfo.InvariantCulture)}]"; - /// - public override bool Equals(object obj) - { - return Equals(obj as IssueRelation); - } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/IssueRelationType.cs b/src/redmine-net-api/Types/IssueRelationType.cs old mode 100755 new mode 100644 index ebdf1c7c..8355bcee --- a/src/redmine-net-api/Types/IssueRelationType.cs +++ b/src/redmine-net-api/Types/IssueRelationType.cs @@ -24,31 +24,31 @@ public enum IssueRelationType /// /// /// - relates = 1, + Relates = 1, /// /// /// - duplicates, + Duplicates, /// /// /// - duplicated, + Duplicated, /// /// /// - blocks, + Blocks, /// /// /// - blocked, + Blocked, /// /// /// - precedes, + Precedes, /// /// /// - follows, + Follows, /// /// /// diff --git a/src/redmine-net-api/Types/IssueStatus.cs b/src/redmine-net-api/Types/IssueStatus.cs old mode 100755 new mode 100644 index f551e886..1ae4f930 --- a/src/redmine-net-api/Types/IssueStatus.cs +++ b/src/redmine-net-api/Types/IssueStatus.cs @@ -15,6 +15,8 @@ limitations under the License. */ using System; +using System.Diagnostics; +using System.Globalization; using System.Xml; using System.Xml.Serialization; using Redmine.Net.Api.Internals; @@ -24,32 +26,33 @@ namespace Redmine.Net.Api.Types /// /// Availability 1.3 /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.ISSUE_STATUS)] - public class IssueStatus : IdentifiableName, IEquatable + public sealed class IssueStatus : IdentifiableName, IEquatable { + #region Properties /// /// Gets or sets a value indicating whether IssueStatus is default. /// /// /// true if IssueStatus is default; otherwise, false. /// - [XmlElement(RedmineKeys.IS_DEFAULT)] - public bool IsDefault { get; set; } + public bool IsDefault { get; internal set; } /// /// Gets or sets a value indicating whether IssueStatus is closed. /// /// true if IssueStatus is closed; otherwise, false. - [XmlElement(RedmineKeys.IS_CLOSED)] - public bool IsClosed { get; set; } + public bool IsClosed { get; internal set; } + #endregion + #region Implementation of IXmlSerialization /// /// /// /// public override void ReadXml(XmlReader reader) { - if (reader == null) throw new ArgumentNullException(nameof(reader)); reader.Read(); while (!reader.EOF) { @@ -62,24 +65,18 @@ public override void ReadXml(XmlReader reader) switch (reader.Name) { case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; - - case RedmineKeys.IS_DEFAULT: IsDefault = reader.ReadElementContentAsBoolean(); break; - case RedmineKeys.IS_CLOSED: IsClosed = reader.ReadElementContentAsBoolean(); break; - + case RedmineKeys.IS_DEFAULT: IsDefault = reader.ReadElementContentAsBoolean(); break; + case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; default: reader.Read(); break; } } } + #endregion - /// - /// - /// - /// - public override void WriteXml(XmlWriter writer) { } + + #region Implementation of IEquatable /// /// /// @@ -91,6 +88,19 @@ public bool Equals(IssueStatus other) return (Id == other.Id && Name == other.Name && IsClosed == other.IsClosed && IsDefault == other.IsDefault); } + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as IssueStatus); + } + /// /// /// @@ -107,20 +117,13 @@ public override int GetHashCode() return hashCode; } } + #endregion /// /// /// /// - public override string ToString() - { - return $"[IssueStatus: {base.ToString()}, IsDefault={IsDefault}, IsClosed={IsClosed}]"; - } + private string DebuggerDisplay => $"[{nameof(IssueStatus)}: {ToString()}, IsDefault={IsDefault.ToString(CultureInfo.InvariantCulture)}, IsClosed={IsClosed.ToString(CultureInfo.InvariantCulture)}]"; - /// - public override bool Equals(object obj) - { - return Equals(obj as IssueStatus); - } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Journal.cs b/src/redmine-net-api/Types/Journal.cs index 58ca0565..047851c4 100644 --- a/src/redmine-net-api/Types/Journal.cs +++ b/src/redmine-net-api/Types/Journal.cs @@ -16,8 +16,9 @@ limitations under the License. using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; using System.Xml; -using System.Xml.Schema; using System.Xml.Serialization; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; @@ -27,17 +28,18 @@ namespace Redmine.Net.Api.Types /// /// /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.JOURNAL)] - public class Journal : Identifiable, IEquatable, IXmlSerializable + public sealed class Journal : Identifiable { + #region Properties /// /// Gets or sets the user. /// /// /// The user. /// - [XmlElement(RedmineKeys.USER)] - public IdentifiableName User { get; set; } + public IdentifiableName User { get; internal set; } /// /// Gets or sets the notes. @@ -45,8 +47,7 @@ public class Journal : Identifiable, IEquatable, IXmlSerializa /// /// The notes. /// - [XmlElement(RedmineKeys.NOTES)] - public string Notes { get; set; } + public string Notes { get; internal set; } /// /// Gets or sets the created on. @@ -54,14 +55,12 @@ public class Journal : Identifiable, IEquatable, IXmlSerializa /// /// The created on. /// - [XmlElement(RedmineKeys.CREATED_ON, IsNullable = true)] - public DateTime? CreatedOn { get; set; } + public DateTime? CreatedOn { get; internal set; } /// /// /// - [XmlElement(RedmineKeys.PRIVATE_NOTES)] - public bool PrivateNotes { get; set; } + public bool PrivateNotes { get; internal set; } /// /// Gets or sets the details. @@ -69,23 +68,16 @@ public class Journal : Identifiable, IEquatable, IXmlSerializa /// /// The details. /// - [XmlArray(RedmineKeys.DETAILS)] - [XmlArrayItem(RedmineKeys.DETAIL)] public IList Details { get; internal set; } + #endregion - /// - /// - /// - /// - public XmlSchema GetSchema() { return null; } - + #region Implementation of IXmlSerialization /// /// /// /// - public void ReadXml(XmlReader reader) + public override void ReadXml(XmlReader reader) { - if (reader == null) throw new ArgumentNullException(nameof(reader)); Id = reader.ReadAttributeAsInt(RedmineKeys.ID); reader.Read(); @@ -99,53 +91,33 @@ public void ReadXml(XmlReader reader) switch (reader.Name) { - case RedmineKeys.USER: User = new IdentifiableName(reader); break; - - case RedmineKeys.NOTES: Notes = reader.ReadElementContentAsString(); break; - case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; - - case RedmineKeys.PRIVATE_NOTES: PrivateNotes = reader.ReadElementContentAsBoolean(); break; - case RedmineKeys.DETAILS: Details = reader.ReadElementContentAsCollection(); break; - + case RedmineKeys.NOTES: Notes = reader.ReadElementContentAsString(); break; + case RedmineKeys.PRIVATE_NOTES: PrivateNotes = reader.ReadElementContentAsBoolean(); break; + case RedmineKeys.USER: User = new IdentifiableName(reader); break; default: reader.Read(); break; } } } + #endregion - /// - /// - /// - /// - public void WriteXml(XmlWriter writer) { } + + #region Implementation of IEquatable /// /// /// /// /// - public bool Equals(Journal other) + public override bool Equals(Journal other) { if (other == null) return false; return Id == other.Id && User == other.User && Notes == other.Notes && CreatedOn == other.CreatedOn - && (Details?.Equals(other.Details) ?? other.Details == null); - } - - /// - /// - /// - /// - /// - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) return false; - if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - return Equals(obj as Journal); + && (Details != null ? Details.Equals(other.Details) : other.Details == null); } /// @@ -165,14 +137,13 @@ public override int GetHashCode() return hashCode; } } + #endregion /// /// /// /// - public override string ToString() - { - return $"[Journal: Id={Id}, User={User}, Notes={Notes}, CreatedOn={CreatedOn}, Details={Details}]"; - } + private string DebuggerDisplay => $"[{nameof(Journal)}: {ToString()}, User={User}, Notes={Notes}, CreatedOn={CreatedOn?.ToString("u", CultureInfo.InvariantCulture)}, Details={Details.Dump()}]"; + } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Membership.cs b/src/redmine-net-api/Types/Membership.cs old mode 100755 new mode 100644 index 6baf5aa2..ea8551cd --- a/src/redmine-net-api/Types/Membership.cs +++ b/src/redmine-net-api/Types/Membership.cs @@ -14,10 +14,9 @@ You may obtain a copy of the License at limitations under the License. */ -using System; using System.Collections.Generic; +using System.Diagnostics; using System.Xml; -using System.Xml.Schema; using System.Xml.Serialization; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; @@ -27,37 +26,31 @@ namespace Redmine.Net.Api.Types /// /// Only the roles can be updated, the project and the user of a membership are read-only. /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.MEMBERSHIP)] - public class Membership : Identifiable, IEquatable, IXmlSerializable + public sealed class Membership : Identifiable { + #region Properties /// /// Gets or sets the project. /// /// The project. - [XmlElement(RedmineKeys.PROJECT)] - public IdentifiableName Project { get; set; } + public IdentifiableName Project { get; internal set; } /// /// Gets or sets the type. /// /// The type. - [XmlArray(RedmineKeys.ROLES)] - [XmlArrayItem(RedmineKeys.ROLE)] - public List Roles { get; internal set; } - - /// - /// - /// - /// - public XmlSchema GetSchema() { return null; } + public IList Roles { get; internal set; } + #endregion + #region Implementation of IXmlSerialization /// /// /// /// - public void ReadXml(XmlReader reader) + public override void ReadXml(XmlReader reader) { - if (reader == null) throw new ArgumentNullException(nameof(reader)); reader.Read(); while (!reader.EOF) { @@ -70,33 +63,29 @@ public void ReadXml(XmlReader reader) switch (reader.Name) { case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; - case RedmineKeys.ROLES: Roles = reader.ReadElementContentAsCollection(); break; - default: reader.Read(); break; } } } + #endregion - /// - /// - /// - /// - public void WriteXml(XmlWriter writer) { } + + #region Implementation of IEquatable /// /// /// /// /// - public bool Equals(Membership other) + public override bool Equals(Membership other) { if (other == null) return false; - return (Id == other.Id && - (Project != null ? Project.Equals(other.Project) : other.Project == null) && - (Roles?.Equals(other.Roles) ?? other.Roles == null)); + return ( + Id == other.Id && + Project != null ? Project.Equals(other.Project) : other.Project == null && + Roles != null ? Roles.Equals(other.Roles) : other.Roles == null); } /// @@ -110,24 +99,17 @@ public override int GetHashCode() var hashCode = 13; hashCode = HashCodeHelper.GetHashCode(Id, hashCode); hashCode = HashCodeHelper.GetHashCode(Project, hashCode); - //hashCode = Utils.GetHashCode(Roles, hashCode); + hashCode = HashCodeHelper.GetHashCode(Roles, hashCode); return hashCode; } } + #endregion /// /// /// /// - public override string ToString() - { - return $"[Membership: {base.ToString()}, Project={Project}, Roles={Roles}]"; - } + private string DebuggerDisplay => $"[{nameof(Membership)}: {ToString()}, Project={Project}, Roles={Roles.Dump()}]"; - /// - public override bool Equals(object obj) - { - return Equals(obj as Membership); - } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/MembershipRole.cs b/src/redmine-net-api/Types/MembershipRole.cs old mode 100755 new mode 100644 index 18228734..65586fab --- a/src/redmine-net-api/Types/MembershipRole.cs +++ b/src/redmine-net-api/Types/MembershipRole.cs @@ -15,6 +15,7 @@ limitations under the License. */ using System; +using System.Diagnostics; using System.Globalization; using System.Xml; using System.Xml.Serialization; @@ -26,28 +27,30 @@ namespace Redmine.Net.Api.Types /// /// /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.ROLE)] - public class MembershipRole : IdentifiableName, IEquatable + public sealed class MembershipRole : IdentifiableName, IEquatable, IValue { + #region Properties /// /// Gets or sets a value indicating whether this is inherited. /// /// /// true if inherited; otherwise, false. /// - [XmlAttribute(RedmineKeys.INHERITED)] - public bool Inherited { get; set; } + public bool Inherited { get; internal set; } + #endregion + #region Implementation of IXmlSerialization /// /// Reads the XML. /// /// The reader. public override void ReadXml(XmlReader reader) { - if (reader == null) throw new ArgumentNullException(nameof(reader)); - Id = Convert.ToInt32(reader.GetAttribute(RedmineKeys.ID), CultureInfo.InvariantCulture); - Name = reader.GetAttribute(RedmineKeys.NAME); + Id = reader.ReadAttributeAsInt(RedmineKeys.ID); Inherited = reader.ReadAttributeAsBoolean(RedmineKeys.INHERITED); + Name = reader.GetAttribute(RedmineKeys.NAME); reader.Read(); } @@ -57,10 +60,13 @@ public override void ReadXml(XmlReader reader) /// public override void WriteXml(XmlWriter writer) { - if (writer == null) throw new ArgumentNullException(nameof(writer)); writer.WriteValue(Id); } + #endregion + + + #region Implementation of IEquatable /// /// /// @@ -72,6 +78,19 @@ public bool Equals(MembershipRole other) return Id == other.Id && Name == other.Name && Inherited == other.Inherited; } + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as MembershipRole); + } + /// /// /// @@ -87,20 +106,20 @@ public override int GetHashCode() return hashCode; } } + #endregion + + #region Implementation of IClonable + /// + /// + /// + public string Value => Id.ToString(CultureInfo.InvariantCulture); + #endregion /// /// /// /// - public override string ToString() - { - return $"[MembershipRole: {base.ToString()}, Inherited={Inherited}]"; - } + private string DebuggerDisplay => $"[MembershipRole: {ToString()}, Inherited={Inherited.ToString(CultureInfo.InvariantCulture)}]"; - /// - public override bool Equals(object obj) - { - return Equals(obj as MembershipRole); - } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/News.cs b/src/redmine-net-api/Types/News.cs old mode 100755 new mode 100644 index dbdda7ef..abb4a7ec --- a/src/redmine-net-api/Types/News.cs +++ b/src/redmine-net-api/Types/News.cs @@ -15,8 +15,9 @@ limitations under the License. */ using System; +using System.Diagnostics; +using System.Globalization; using System.Xml; -using System.Xml.Schema; using System.Xml.Serialization; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; @@ -26,64 +27,55 @@ namespace Redmine.Net.Api.Types /// /// Availability 1.1 /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.NEWS)] - public class News : Identifiable, IEquatable, IXmlSerializable + public sealed class News : Identifiable { + #region Properties /// /// Gets or sets the project. /// /// The project. - [XmlElement(RedmineKeys.PROJECT)] - public IdentifiableName Project { get; set; } + public IdentifiableName Project { get; internal set; } /// /// Gets or sets the author. /// /// The author. - [XmlElement(RedmineKeys.AUTHOR)] - public IdentifiableName Author { get; set; } + public IdentifiableName Author { get; internal set; } /// /// Gets or sets the title. /// /// The title. - [XmlElement(RedmineKeys.TITLE)] - public string Title { get; set; } + public string Title { get; internal set; } /// /// Gets or sets the summary. /// /// The summary. - [XmlElement(RedmineKeys.SUMMARY)] - public string Summary { get; set; } + public string Summary { get; internal set; } /// /// Gets or sets the description. /// /// The description. - [XmlElement(RedmineKeys.DESCRIPTION)] - public string Description { get; set; } + public string Description { get; internal set; } /// /// Gets or sets the created on. /// /// The created on. - [XmlElement(RedmineKeys.CREATED_ON, IsNullable = true)] - public DateTime? CreatedOn { get; set; } - - /// - /// - /// - /// - public XmlSchema GetSchema() { return null; } + public DateTime? CreatedOn { get; internal set; } + #endregion + #region Implementation of IXmlSerialization /// /// /// /// - public void ReadXml(XmlReader reader) + public override void ReadXml(XmlReader reader) { - if (reader == null) throw new ArgumentNullException(nameof(reader)); reader.Read(); while (!reader.EOF) { @@ -96,36 +88,27 @@ public void ReadXml(XmlReader reader) switch (reader.Name) { case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; - case RedmineKeys.AUTHOR: Author = new IdentifiableName(reader); break; - - case RedmineKeys.TITLE: Title = reader.ReadElementContentAsString(); break; - - case RedmineKeys.SUMMARY: Summary = reader.ReadElementContentAsString(); break; - - case RedmineKeys.DESCRIPTION: Description = reader.ReadElementContentAsString(); break; - case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; - + case RedmineKeys.DESCRIPTION: Description = reader.ReadElementContentAsString(); break; + case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; + case RedmineKeys.SUMMARY: Summary = reader.ReadElementContentAsString(); break; + case RedmineKeys.TITLE: Title = reader.ReadElementContentAsString(); break; default: reader.Read(); break; } } } + #endregion - /// - /// - /// - /// - public void WriteXml(XmlWriter writer) { } + + #region Implementation of IEquatable /// /// /// /// /// - public bool Equals(News other) + public override bool Equals(News other) { if (other == null) return false; return (Id == other.Id @@ -136,7 +119,7 @@ public bool Equals(News other) && Description == other.Description && CreatedOn == other.CreatedOn); } - + /// /// /// @@ -155,21 +138,13 @@ public override int GetHashCode() return hashCode; } } + #endregion /// /// /// /// - public override string ToString() - { - return - $"[News: {base.ToString()}, Project={Project}, Author={Author}, Title={Title}, Summary={Summary}, Description={Description}, CreatedOn={CreatedOn}]"; - } + private string DebuggerDisplay => $"[{nameof(News)}: {ToString()}, Project={Project}, Author={Author}, Title={Title}, Summary={Summary}, Description={Description}, CreatedOn={CreatedOn?.ToString("u", CultureInfo.InvariantCulture)}]"; - /// - public override bool Equals(object obj) - { - return Equals(obj as News); - } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Permission.cs b/src/redmine-net-api/Types/Permission.cs old mode 100755 new mode 100644 index 9706629d..656b6daf --- a/src/redmine-net-api/Types/Permission.cs +++ b/src/redmine-net-api/Types/Permission.cs @@ -15,6 +15,9 @@ limitations under the License. */ using System; +using System.Diagnostics; +using System.Xml; +using System.Xml.Schema; using System.Xml.Serialization; using Redmine.Net.Api.Internals; @@ -23,15 +26,59 @@ namespace Redmine.Net.Api.Types /// /// /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.PERMISSION)] - public class Permission : IEquatable + public sealed class Permission : IXmlSerializable, IEquatable { + #region Properties /// /// /// - [XmlText] - public string Info { get; set; } + public string Info { get; internal set; } + #endregion + #region Implementation of IXmlSerializable + + /// + /// + /// + /// + public XmlSchema GetSchema() { return null; } + + /// + /// + /// + /// + public void ReadXml(XmlReader reader) + { + reader.Read(); + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.PERMISSION: Info = reader.ReadElementContentAsString(); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public void WriteXml(XmlWriter writer) { } + + #endregion + + + + #region Implementation of IEquatable /// /// /// @@ -68,14 +115,13 @@ public override int GetHashCode() return hashCode; } } + #endregion /// /// /// /// - public override string ToString() - { - return $"[Permission: Info={Info}]"; - } + private string DebuggerDisplay => $"[{nameof(Permission)}: {Info}]"; + } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs index 02da4267..6bf01248 100644 --- a/src/redmine-net-api/Types/Project.cs +++ b/src/redmine-net-api/Types/Project.cs @@ -16,6 +16,7 @@ limitations under the License. using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.Xml; using System.Xml.Serialization; @@ -27,59 +28,55 @@ namespace Redmine.Net.Api.Types /// /// Availability 1.0 /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.PROJECT)] - public class Project : IdentifiableName, IEquatable + public sealed class Project : IdentifiableName, IEquatable { + #region Properties /// - /// Gets or sets the identifier (Required). + /// Gets or sets the identifier. /// + /// Required for create /// The identifier. - [XmlElement(RedmineKeys.IDENTIFIER)] public string Identifier { get; set; } /// /// Gets or sets the description. /// /// The description. - [XmlElement(RedmineKeys.DESCRIPTION)] public string Description { get; set; } /// /// Gets or sets the parent. /// /// The parent. - [XmlElement(RedmineKeys.PARENT)] public IdentifiableName Parent { get; set; } /// /// Gets or sets the home page. /// /// The home page. - [XmlElement(RedmineKeys.HOMEPAGE)] public string HomePage { get; set; } /// - /// Gets or sets the created on. + /// Gets the created on. /// /// The created on. - [XmlElement(RedmineKeys.CREATED_ON, IsNullable = true)] - public DateTime? CreatedOn { get; set; } + public DateTime? CreatedOn { get; internal set; } /// - /// Gets or sets the updated on. + /// Gets the updated on. /// /// The updated on. - [XmlElement(RedmineKeys.UPDATED_ON, IsNullable = true)] - public DateTime? UpdatedOn { get; set; } + public DateTime? UpdatedOn { get; internal set; } /// - /// Gets or sets the status. + /// Gets the status. /// /// /// The status. /// - [XmlElement(RedmineKeys.STATUS)] - public ProjectStatus Status { get; set; } + public ProjectStatus Status { get; internal set; } /// /// Gets or sets a value indicating whether this project is public. @@ -87,8 +84,7 @@ public class Project : IdentifiableName, IEquatable /// /// true if this project is public; otherwise, false. /// - /// is exposed since 2.6.0 - [XmlElement(RedmineKeys.IS_PUBLIC)] + /// Available in Redmine starting with 2.6.0 version. public bool IsPublic { get; set; } /// @@ -97,7 +93,6 @@ public class Project : IdentifiableName, IEquatable /// /// true if [inherit members]; otherwise, false. /// - [XmlElement(RedmineKeys.INHERIT_MEMBERS)] public bool InheritMembers { get; set; } /// @@ -106,54 +101,49 @@ public class Project : IdentifiableName, IEquatable /// /// The trackers. /// - [XmlArray(RedmineKeys.TRACKERS)] - [XmlArrayItem(RedmineKeys.TRACKER)] + /// Available in Redmine starting with 2.6.0 version. public IList Trackers { get; set; } /// - /// Gets or sets the custom fields. + /// Gets or sets the enabled modules. /// /// - /// The custom fields. + /// The enabled modules. /// - [XmlArray(RedmineKeys.CUSTOM_FIELDS)] - [XmlArrayItem(RedmineKeys.CUSTOM_FIELD)] - public IList CustomFields { get; set; } + /// Available in Redmine starting with 2.6.0 version. + public IList EnabledModules { get; set; } /// - /// Gets or sets the issue categories. + /// Gets the custom fields. /// /// - /// The issue categories. + /// The custom fields. /// - [XmlArray(RedmineKeys.ISSUE_CATEGORIES)] - [XmlArrayItem(RedmineKeys.ISSUE_CATEGORY)] - public IList IssueCategories { get; internal set; } + public IList CustomFields { get; internal set; } /// - /// since 2.6.0 + /// Gets the issue categories. /// /// - /// The enabled modules. + /// The issue categories. /// - [XmlArray(RedmineKeys.ENABLED_MODULES)] - [XmlArrayItem(RedmineKeys.ENABLED_MODULE)] - public IList EnabledModules { get; set; } + /// Available in Redmine starting with 2.6.0 version. + public IList IssueCategories { get; internal set; } /// - /// + /// Gets the time entry activities. /// - [XmlArray(RedmineKeys.TIME_ENTRY_ACTIVITIES)] - [XmlArrayItem(RedmineKeys.TIME_ENTRY_ACTIVITY)] - public IList TimeEntryActivities { get; internal set; } + /// Available in Redmine starting with 3.4.0 version. + public IList TimeEntryActivities { get; internal set; } + #endregion + #region Implementation of IXmlSerializer /// /// Generates an object from its XML representation. /// - /// The stream from which the object is deserialized. + /// The stream from which the object is deserialized. public override void ReadXml(XmlReader reader) { - if (reader == null) throw new ArgumentNullException(nameof(reader)); reader.Read(); while (!reader.EOF) { @@ -166,37 +156,21 @@ public override void ReadXml(XmlReader reader) switch (reader.Name) { case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; - - case RedmineKeys.IDENTIFIER: Identifier = reader.ReadElementContentAsString(); break; - + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadElementContentAsCollection(); break; case RedmineKeys.DESCRIPTION: Description = reader.ReadElementContentAsString(); break; - - case RedmineKeys.STATUS: Status = (ProjectStatus)reader.ReadElementContentAsInt(); break; - - case RedmineKeys.PARENT: Parent = new IdentifiableName(reader); break; - + case RedmineKeys.ENABLED_MODULES: EnabledModules = reader.ReadElementContentAsCollection(); break; case RedmineKeys.HOMEPAGE: HomePage = reader.ReadElementContentAsString(); break; - - case RedmineKeys.IS_PUBLIC: IsPublic = reader.ReadElementContentAsBoolean(); break; - + case RedmineKeys.IDENTIFIER: Identifier = reader.ReadElementContentAsString(); break; case RedmineKeys.INHERIT_MEMBERS: InheritMembers = reader.ReadElementContentAsBoolean(); break; - - case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; - - case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadElementContentAsNullableDateTime(); break; - - case RedmineKeys.TRACKERS: Trackers = reader.ReadElementContentAsCollection(); break; - - case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadElementContentAsCollection(); break; - + case RedmineKeys.IS_PUBLIC: IsPublic = reader.ReadElementContentAsBoolean(); break; case RedmineKeys.ISSUE_CATEGORIES: IssueCategories = reader.ReadElementContentAsCollection(); break; - - case RedmineKeys.ENABLED_MODULES: EnabledModules = reader.ReadElementContentAsCollection(); break; - - case RedmineKeys.TIME_ENTRY_ACTIVITIES: TimeEntryActivities = reader.ReadElementContentAsCollection(); break; - + case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; + case RedmineKeys.PARENT: Parent = new IdentifiableName(reader); break; + case RedmineKeys.STATUS: Status = (ProjectStatus)reader.ReadElementContentAsInt(); break; + case RedmineKeys.TIME_ENTRY_ACTIVITIES: TimeEntryActivities = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.TRACKERS: Trackers = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadElementContentAsNullableDateTime(); break; default: reader.Read(); break; } } @@ -207,42 +181,32 @@ public override void ReadXml(XmlReader reader) /// public override void WriteXml(XmlWriter writer) { - if (writer == null) throw new ArgumentNullException(nameof(writer)); writer.WriteElementString(RedmineKeys.NAME, Name); writer.WriteElementString(RedmineKeys.IDENTIFIER, Identifier); - writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); - //writer.WriteElementString(RedmineKeys.INHERIT_MEMBERS, XmlConvert.ToString(InheritMembers)); - writer.WriteElementString(RedmineKeys.IS_PUBLIC, XmlConvert.ToString(IsPublic)); - writer.WriteIdOrEmpty(Parent, RedmineKeys.PARENT_ID); - writer.WriteElementString(RedmineKeys.HOMEPAGE, HomePage); - if (Trackers != null) - { - var trackers = new List(); - foreach (var tracker in Trackers) - { - trackers.Add(tracker as IValue); - } + writer.WriteIfNotDefaultOrNull(RedmineKeys.DESCRIPTION, Description); + writer.WriteIfNotDefaultOrNull(RedmineKeys.INHERIT_MEMBERS, InheritMembers); + writer.WriteIfNotDefaultOrNull(RedmineKeys.IS_PUBLIC, IsPublic); + writer.WriteIfNotDefaultOrNull(RedmineKeys.HOMEPAGE, HomePage); - writer.WriteListElements(trackers, RedmineKeys.TRACKER_IDS); - } + writer.WriteIdIfNotNull(RedmineKeys.PARENT_ID, Parent); - if (EnabledModules != null) - { - var enabledModules = new List(); - foreach (var enabledModule in EnabledModules) - { - enabledModules.Add(enabledModule as IValue); - } + writer.WriteRepeatableElement(RedmineKeys.TRACKER_IDS, (IEnumerable)Trackers); + writer.WriteRepeatableElement(RedmineKeys.ENABLED_MODULE_NAMES, (IEnumerable)EnabledModules); - writer.WriteListElements(enabledModules, RedmineKeys.ENABLED_MODULE_NAMES); + if (Id == 0) + { + writer.WriteRepeatableElement(RedmineKeys.ISSUE_CUSTOM_FIELD_IDS, (IEnumerable)CustomFields); + return; } - if (Id == 0) return; - - writer.WriteArray(CustomFields, RedmineKeys.CUSTOM_FIELDS); + writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); } + #endregion + + + #region Implementation of IEquatable /// /// /// @@ -250,22 +214,27 @@ public override void WriteXml(XmlWriter writer) /// public bool Equals(Project other) { - if (other == null) return false; + if (other == null) + { + return false; + } + return ( Id == other.Id - && Identifier.Equals(other.Identifier, StringComparison.OrdinalIgnoreCase) - && Description.Equals(other.Description, StringComparison.OrdinalIgnoreCase) + && string.Equals(Identifier,other.Identifier, StringComparison.OrdinalIgnoreCase) + && string.Equals(Description,other.Description, StringComparison.OrdinalIgnoreCase) && (Parent != null ? Parent.Equals(other.Parent) : other.Parent == null) - && (HomePage?.Equals(other.HomePage, StringComparison.OrdinalIgnoreCase) ?? other.HomePage == null) + && (HomePage != null ? string.Equals(HomePage,other.HomePage, StringComparison.OrdinalIgnoreCase) : other.HomePage == null) && CreatedOn == other.CreatedOn && UpdatedOn == other.UpdatedOn && Status == other.Status && IsPublic == other.IsPublic && InheritMembers == other.InheritMembers - && (Trackers?.Equals(other.Trackers) ?? other.Trackers == null) - && (CustomFields?.Equals(other.CustomFields) ?? other.CustomFields == null) - && (IssueCategories?.Equals(other.IssueCategories) ?? other.IssueCategories == null) - && (EnabledModules?.Equals(other.EnabledModules) ?? other.EnabledModules == null) + && (Trackers != null ? Trackers.Equals(other.Trackers) : other.Trackers == null) + && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null) + && (IssueCategories != null ? IssueCategories.Equals(other.IssueCategories) : other.IssueCategories == null) + && (EnabledModules != null ? EnabledModules.Equals(other.EnabledModules) : other.EnabledModules == null) + && (TimeEntryActivities != null ? TimeEntryActivities.Equals(other.TimeEntryActivities) : other.TimeEntryActivities == null) ); } @@ -275,41 +244,45 @@ public bool Equals(Project other) /// public override int GetHashCode() { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(Identifier, hashCode); - hashCode = HashCodeHelper.GetHashCode(Description, hashCode); - hashCode = HashCodeHelper.GetHashCode(Parent, hashCode); - hashCode = HashCodeHelper.GetHashCode(HomePage, hashCode); - hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(UpdatedOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(Status, hashCode); - hashCode = HashCodeHelper.GetHashCode(IsPublic, hashCode); - //hashCode = HashCodeHelper.GetHashCode(InheritMembers, hashCode); - hashCode = HashCodeHelper.GetHashCode(Trackers, hashCode); - hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); - hashCode = HashCodeHelper.GetHashCode(IssueCategories, hashCode); - hashCode = HashCodeHelper.GetHashCode(EnabledModules, hashCode); - - return hashCode; - } + unchecked + { + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Identifier, hashCode); + hashCode = HashCodeHelper.GetHashCode(Description, hashCode); + hashCode = HashCodeHelper.GetHashCode(Parent, hashCode); + hashCode = HashCodeHelper.GetHashCode(HomePage, hashCode); + hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(UpdatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(Status, hashCode); + hashCode = HashCodeHelper.GetHashCode(IsPublic, hashCode); + hashCode = HashCodeHelper.GetHashCode(InheritMembers, hashCode); + hashCode = HashCodeHelper.GetHashCode(Trackers, hashCode); + hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); + hashCode = HashCodeHelper.GetHashCode(IssueCategories, hashCode); + hashCode = HashCodeHelper.GetHashCode(EnabledModules, hashCode); + hashCode = HashCodeHelper.GetHashCode(TimeEntryActivities, hashCode); + + return hashCode; + } } + #endregion /// /// /// /// - public override string ToString() - { - return - $"[Project: {base.ToString()}, Identifier={Identifier}, Description={Description}, Parent={Parent}, HomePage={HomePage}, CreatedOn={CreatedOn}, UpdatedOn={UpdatedOn}, Status={Status}, IsPublic={IsPublic}, InheritMembers={InheritMembers}, Trackers={Trackers}, CustomFields={CustomFields}, IssueCategories={IssueCategories}, EnabledModules={EnabledModules}]"; - } + private string DebuggerDisplay => + $@"[Project: {ToString()}, Identifier={Identifier}, Description={Description}, Parent={Parent}, HomePage={HomePage}, +CreatedOn={CreatedOn?.ToString("u", CultureInfo.InvariantCulture)}, +UpdatedOn={UpdatedOn?.ToString("u", CultureInfo.InvariantCulture)}, +Status={Status.ToString("G")}, +IsPublic={IsPublic.ToString(CultureInfo.InvariantCulture)}, +InheritMembers={InheritMembers.ToString(CultureInfo.InvariantCulture)}, +Trackers={Trackers.Dump()}, +CustomFields={CustomFields.Dump()}, +IssueCategories={IssueCategories.Dump()}, +EnabledModules={EnabledModules.Dump()}, +TimeEntryActivities = {TimeEntryActivities.Dump()}]"; - /// - public override bool Equals(object obj) - { - return Equals(obj as Project); - } } } diff --git a/src/redmine-net-api/Types/ProjectEnabledModule.cs b/src/redmine-net-api/Types/ProjectEnabledModule.cs old mode 100755 new mode 100644 index 23e71027..a39d8122 --- a/src/redmine-net-api/Types/ProjectEnabledModule.cs +++ b/src/redmine-net-api/Types/ProjectEnabledModule.cs @@ -14,33 +14,55 @@ You may obtain a copy of the License at limitations under the License. */ +using System; +using System.Diagnostics; using System.Xml.Serialization; +using Redmine.Net.Api.Extensions; namespace Redmine.Net.Api.Types { /// - /// the module name: boards, calendar, documents, files, gantt, issue_tracking, news, repository, time_tracking, wiki. + /// the module name: boards, calendar, documents, files, gant, issue_tracking, news, repository, time_tracking, wiki. /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.ENABLED_MODULE)] - public class ProjectEnabledModule : IdentifiableName, IValue + public sealed class ProjectEnabledModule : IdentifiableName, IValue { - #region IValue implementation + #region Ctors /// /// /// - public string Value + public ProjectEnabledModule() { } + + /// + /// + /// + /// + public ProjectEnabledModule(string moduleName) { - get { return Name; } + if (moduleName.IsNullOrWhiteSpace()) + { + throw new ArgumentException(nameof(moduleName)); + } + + Name = moduleName; } #endregion + + #region Implementation of IValue + /// + /// + /// + public string Value => Name; + + #endregion + /// /// /// /// - public override string ToString() - { - return $"[ProjectEnabledModule: {base.ToString()}]"; - } + private string DebuggerDisplay => $"[{nameof(ProjectEnabledModule)}: {ToString()}]"; + } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/ProjectIssueCategory.cs b/src/redmine-net-api/Types/ProjectIssueCategory.cs old mode 100755 new mode 100644 index 4c4d58c3..b4c9aa0d --- a/src/redmine-net-api/Types/ProjectIssueCategory.cs +++ b/src/redmine-net-api/Types/ProjectIssueCategory.cs @@ -14,6 +14,7 @@ You may obtain a copy of the License at limitations under the License. */ +using System.Diagnostics; using System.Xml.Serialization; namespace Redmine.Net.Api.Types @@ -21,16 +22,26 @@ namespace Redmine.Net.Api.Types /// /// /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.ISSUE_CATEGORY)] - public class ProjectIssueCategory : IdentifiableName + public sealed class ProjectIssueCategory : IdentifiableName { + /// + /// + /// + public ProjectIssueCategory() { } + + internal ProjectIssueCategory(int id, string name) + { + Id = id; + Name = name; + } + /// /// /// /// - public override string ToString () - { - return $"[ProjectIssueCategory: {base.ToString()}]"; - } + private string DebuggerDisplay => $"[{nameof(ProjectIssueCategory)}: {ToString()}]"; + } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/ProjectMembership.cs b/src/redmine-net-api/Types/ProjectMembership.cs old mode 100755 new mode 100644 index ba41fe57..1cc92014 --- a/src/redmine-net-api/Types/ProjectMembership.cs +++ b/src/redmine-net-api/Types/ProjectMembership.cs @@ -14,10 +14,9 @@ You may obtain a copy of the License at limitations under the License. */ -using System; using System.Collections.Generic; +using System.Diagnostics; using System.Xml; -using System.Xml.Schema; using System.Xml.Serialization; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; @@ -26,20 +25,23 @@ namespace Redmine.Net.Api.Types { /// /// Availability 1.4 + /// + /// /// POST - Adds a project member. /// GET - Returns the membership of given :id. /// PUT - Updates the membership of given :id. Only the roles can be updated, the project and the user of a membership are read-only. /// DELETE - Deletes a memberships. Memberships inherited from a group membership can not be deleted. You must delete the group membership. - /// + /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.MEMBERSHIP)] - public class ProjectMembership : Identifiable, IEquatable, IXmlSerializable + public sealed class ProjectMembership : Identifiable { + #region Properties /// /// Gets or sets the project. /// /// The project. - [XmlElement(RedmineKeys.PROJECT)] - public IdentifiableName Project { get; set; } + public IdentifiableName Project { get; internal set; } /// /// Gets or sets the user. @@ -47,7 +49,6 @@ public class ProjectMembership : Identifiable, IEquatable /// The user. /// - [XmlElement(RedmineKeys.USER)] public IdentifiableName User { get; set; } /// @@ -56,45 +57,22 @@ public class ProjectMembership : Identifiable, IEquatable /// The group. /// - [XmlElement(RedmineKeys.GROUP)] - public IdentifiableName Group { get; set; } + public IdentifiableName Group { get; internal set; } /// /// Gets or sets the type. /// /// The type. - [XmlArray(RedmineKeys.ROLES)] - [XmlArrayItem(RedmineKeys.ROLE)] - public List Roles { get; set; } - - /// - /// - /// - /// - /// - public bool Equals(ProjectMembership other) - { - if (other == null) return false; - return (Id == other.Id - && Project.Equals(other.Project) - && Roles.Equals(other.Roles) - && (User != null ? User.Equals(other.User) : other.User == null) - && (Group != null ? Group.Equals(other.Group) : other.Group == null)); - } - - /// - /// - /// - /// - public XmlSchema GetSchema() { return null; } + public IList Roles { get; set; } + #endregion + #region Implementation of IXmlSerialization /// /// /// /// - public void ReadXml(XmlReader reader) + public override void ReadXml(XmlReader reader) { - if (reader == null) throw new ArgumentNullException(nameof(reader)); reader.Read(); while (!reader.EOF) { @@ -107,15 +85,10 @@ public void ReadXml(XmlReader reader) switch (reader.Name) { case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; - - case RedmineKeys.USER: User = new IdentifiableName(reader); break; - case RedmineKeys.GROUP: Group = new IdentifiableName(reader); break; - + case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; case RedmineKeys.ROLES: Roles = reader.ReadElementContentAsCollection(); break; - + case RedmineKeys.USER: User = new IdentifiableName(reader); break; default: reader.Read(); break; } } @@ -125,10 +98,29 @@ public void ReadXml(XmlReader reader) /// /// /// - public void WriteXml(XmlWriter writer) + public override void WriteXml(XmlWriter writer) + { + writer.WriteIdIfNotNull(RedmineKeys.USER_ID, User); + writer.WriteArray(RedmineKeys.ROLE_IDS, Roles, typeof(MembershipRole), RedmineKeys.ROLE_ID); + } + #endregion + + + + #region Implementation of IEquatable + /// + /// + /// + /// + /// + public override bool Equals(ProjectMembership other) { - writer.WriteIdIfNotNull(User, RedmineKeys.USER_ID); - writer.WriteArray(Roles, RedmineKeys.ROLE_IDS, typeof(MembershipRole), RedmineKeys.ROLE_ID); + if (other == null) return false; + return (Id == other.Id + && Project.Equals(other.Project) + && Roles.Equals(other.Roles) + && (User != null ? User.Equals(other.User) : other.User == null) + && (Group != null ? Group.Equals(other.Group) : other.Group == null)); } /// @@ -147,21 +139,13 @@ public override int GetHashCode() return hashCode; } } + #endregion /// /// /// /// - public override string ToString() - { - return - $"[ProjectMembership: {base.ToString()}, Project={Project}, User={User}, Group={Group}, Roles={Roles}]"; - } + private string DebuggerDisplay => $"[{nameof(ProjectMembership)}: {ToString()}, Project={Project}, User={User}, Group={Group}, Roles={Roles.Dump()}]"; - /// - public override bool Equals(object obj) - { - return Equals(obj as ProjectMembership); - } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs b/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs new file mode 100644 index 00000000..43251a43 --- /dev/null +++ b/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs @@ -0,0 +1,31 @@ +using System.Diagnostics; +using System.Xml.Serialization; + +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [XmlRoot(RedmineKeys.TIME_ENTRY_ACTIVITY)] + public sealed class ProjectTimeEntryActivity : IdentifiableName + { + /// + /// + /// + public ProjectTimeEntryActivity() { } + + internal ProjectTimeEntryActivity(int id, string name) + { + Id = id; + Name = name; + } + + /// + /// + /// + /// + private string DebuggerDisplay => $"[{nameof(ProjectTimeEntryActivity)}: {ToString()}]"; + + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/ProjectTracker.cs b/src/redmine-net-api/Types/ProjectTracker.cs old mode 100755 new mode 100644 index ccd5d6e5..95e60de2 --- a/src/redmine-net-api/Types/ProjectTracker.cs +++ b/src/redmine-net-api/Types/ProjectTracker.cs @@ -14,6 +14,7 @@ You may obtain a copy of the License at limitations under the License. */ +using System.Diagnostics; using System.Globalization; using System.Xml.Serialization; @@ -22,21 +23,49 @@ namespace Redmine.Net.Api.Types /// /// /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.TRACKER)] - public class ProjectTracker : IdentifiableName, IValue + public sealed class ProjectTracker : IdentifiableName, IValue { /// /// /// - public string Value{get{return Id.ToString (CultureInfo.InvariantCulture);}} + public ProjectTracker() { } + + /// + /// + /// + /// the tracker id: 1 for Bug, etc. + /// + public ProjectTracker(int trackerId, string name) + { + Id = trackerId; + Name = name; + } + + /// + /// + /// + /// + internal ProjectTracker(int trackerId) + { + Id = trackerId; + } + + #region Implementation of IValue + + /// + /// + /// + public string Value => Id.ToString(CultureInfo.InvariantCulture); + + #endregion /// /// /// /// - public override string ToString () - { - return $"[ProjectTracker: {base.ToString()}]"; - } + private string DebuggerDisplay => $"[{nameof(ProjectTracker)}: {ToString()}]"; + } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Query.cs b/src/redmine-net-api/Types/Query.cs old mode 100755 new mode 100644 index cfb93812..0c70a40a --- a/src/redmine-net-api/Types/Query.cs +++ b/src/redmine-net-api/Types/Query.cs @@ -15,6 +15,8 @@ limitations under the License. */ using System; +using System.Diagnostics; +using System.Globalization; using System.Xml; using System.Xml.Serialization; using Redmine.Net.Api.Extensions; @@ -25,23 +27,25 @@ namespace Redmine.Net.Api.Types /// /// Availability 1.3 /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.QUERY)] - public class Query : IdentifiableName, IEquatable + public sealed class Query : IdentifiableName, IEquatable { + #region Properties /// - /// Gets or sets a value indicating whether this instance is public. + /// Gets a value indicating whether this instance is public. /// /// true if this instance is public; otherwise, false. - [XmlElement(RedmineKeys.IS_PUBLIC)] - public bool IsPublic { get; set; } + public bool IsPublic { get; internal set; } /// - /// Gets or sets the project id. + /// Gets the project id. /// /// The project id. - [XmlElement(RedmineKeys.PROJECT_ID)] - public int? ProjectId { get; set; } + public int? ProjectId { get; internal set; } + #endregion + #region Implementation of IXmlSerialization /// /// /// @@ -60,24 +64,18 @@ public override void ReadXml(XmlReader reader) switch (reader.Name) { case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; - case RedmineKeys.IS_PUBLIC: IsPublic = reader.ReadElementContentAsBoolean(); break; - + case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; case RedmineKeys.PROJECT_ID: ProjectId = reader.ReadElementContentAsNullableInt(); break; - default: reader.Read(); break; } } } + #endregion - /// - /// - /// - /// - public override void WriteXml(XmlWriter writer) { } + + #region Implementation of IEquatable /// /// /// @@ -106,20 +104,13 @@ public override int GetHashCode() return hashCode; } } + #endregion /// /// /// /// - public override string ToString() - { - return $"[Query: {base.ToString()}, IsPublic={IsPublic}, ProjectId={ProjectId}]"; - } + private string DebuggerDisplay => $"[{nameof(Query)}: {ToString()}, IsPublic={IsPublic.ToString(CultureInfo.InvariantCulture)}, ProjectId={ProjectId?.ToString(CultureInfo.InvariantCulture)}]"; - /// - public override bool Equals(object obj) - { - return Equals(obj as Query); - } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Role.cs b/src/redmine-net-api/Types/Role.cs old mode 100755 new mode 100644 index 1030089e..777e9566 --- a/src/redmine-net-api/Types/Role.cs +++ b/src/redmine-net-api/Types/Role.cs @@ -16,6 +16,7 @@ limitations under the License. using System; using System.Collections.Generic; +using System.Diagnostics; using System.Xml; using System.Xml.Serialization; using Redmine.Net.Api.Extensions; @@ -26,26 +27,27 @@ namespace Redmine.Net.Api.Types /// /// Availability 1.4 /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.ROLE)] - public class Role : IdentifiableName, IEquatable + public sealed class Role : IdentifiableName, IEquatable { + #region Properties /// - /// Gets or sets the permissions. + /// Gets the permissions. /// /// /// The issue relations. /// - [XmlArray(RedmineKeys.PERMISSIONS)] - [XmlArrayItem(RedmineKeys.PERMISSION)] public IList Permissions { get; internal set; } + #endregion + #region Implementation of IXmlSerialization /// /// /// /// public override void ReadXml(XmlReader reader) { - if (reader == null) throw new ArgumentNullException(nameof(reader)); reader.Read(); while (!reader.EOF) { @@ -58,22 +60,17 @@ public override void ReadXml(XmlReader reader) switch (reader.Name) { case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; - case RedmineKeys.PERMISSIONS: Permissions = reader.ReadElementContentAsCollection(); break; - default: reader.Read(); break; } } } + #endregion - /// - /// - /// - /// - public override void WriteXml(XmlWriter writer) { } + + #region Implementation of IEquatable /// /// /// @@ -113,14 +110,13 @@ public override int GetHashCode() return hashCode; } } + #endregion /// /// /// /// - public override string ToString() - { - return $"[Role: Id={Id}, Name={Name}, Permissions={Permissions}]"; - } + private string DebuggerDisplay => $"[{nameof(Role)}: {ToString()}, Permissions={Permissions}]"; + } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/TimeEntry.cs b/src/redmine-net-api/Types/TimeEntry.cs index 59ce33b4..348642fd 100644 --- a/src/redmine-net-api/Types/TimeEntry.cs +++ b/src/redmine-net-api/Types/TimeEntry.cs @@ -16,8 +16,9 @@ limitations under the License. using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; using System.Xml; -using System.Xml.Schema; using System.Xml.Serialization; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; @@ -28,109 +29,86 @@ namespace Redmine.Net.Api.Types /// /// Availability 1.1 /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.TIME_ENTRY)] - public class TimeEntry : Identifiable, ICloneable, IEquatable, IXmlSerializable + public sealed class TimeEntry : Identifiable, ICloneable { + #region Properties private string comments; /// /// Gets or sets the issue id to log time on. /// /// The issue id. - [XmlAttribute(RedmineKeys.ISSUE)] public IdentifiableName Issue { get; set; } /// /// Gets or sets the project id to log time on. /// /// The project id. - [XmlAttribute(RedmineKeys.PROJECT)] public IdentifiableName Project { get; set; } /// /// Gets or sets the date the time was spent (default to the current date). /// /// The spent on. - [XmlAttribute(RedmineKeys.SPENT_ON)] public DateTime? SpentOn { get; set; } /// /// Gets or sets the number of spent hours. /// /// The hours. - [XmlAttribute(RedmineKeys.HOURS)] public decimal Hours { get; set; } /// - /// Gets or sets the activity id of the time activity. This parameter is required unless a default activity is defined in Redmine.. + /// Gets or sets the activity id of the time activity. This parameter is required unless a default activity is defined in Redmine. /// /// The activity id. - [XmlAttribute(RedmineKeys.ACTIVITY)] public IdentifiableName Activity { get; set; } /// - /// Gets or sets the user. + /// Gets the user. /// /// /// The user. /// - [XmlAttribute(RedmineKeys.USER)] - public IdentifiableName User { get; set; } + public IdentifiableName User { get; internal set; } /// /// Gets or sets the short description for the entry (255 characters max). /// /// The comments. - [XmlAttribute(RedmineKeys.COMMENTS)] public string Comments { - get { return comments; } - set { comments = value.Truncate(255); } + get => comments; + set => comments = value.Truncate(255); } /// - /// Gets or sets the created on. + /// Gets the created on. /// /// The created on. - [XmlElement(RedmineKeys.CREATED_ON)] - public DateTime? CreatedOn { get; set; } + public DateTime? CreatedOn { get; internal set; } /// - /// Gets or sets the updated on. + /// Gets the updated on. /// /// The updated on. - [XmlElement(RedmineKeys.UPDATED_ON)] - public DateTime? UpdatedOn { get; set; } + public DateTime? UpdatedOn { get; internal set; } /// /// Gets or sets the custom fields. /// /// The custom fields. - [XmlArray(RedmineKeys.CUSTOM_FIELDS)] - [XmlArrayItem(RedmineKeys.CUSTOM_FIELD)] - public IList CustomFields { get; internal set; } - - /// - /// - /// - /// - public object Clone() - { - var timeEntry = new TimeEntry { Activity = Activity, Comments = Comments, Hours = Hours, Issue = Issue, Project = Project, SpentOn = SpentOn, User = User, CustomFields = CustomFields }; - return timeEntry; - } - - /// - /// - /// - /// - public XmlSchema GetSchema() { return null; } + public IList CustomFields { get; set; } + #endregion + #region Implementation of IXmlSerialization /// /// /// /// - public void ReadXml(XmlReader reader) + public override void ReadXml(XmlReader reader) { reader.Read(); while (!reader.EOF) @@ -144,33 +122,19 @@ public void ReadXml(XmlReader reader) switch (reader.Name) { case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - + case RedmineKeys.ACTIVITY: Activity = new IdentifiableName(reader); break; + case RedmineKeys.ACTIVITY_ID: Activity = new IdentifiableName(reader); break; + case RedmineKeys.COMMENTS: Comments = reader.ReadElementContentAsString(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.HOURS: Hours = reader.ReadElementContentAsDecimal(); break; case RedmineKeys.ISSUE_ID: Issue = new IdentifiableName(reader); break; - case RedmineKeys.ISSUE: Issue = new IdentifiableName(reader); break; - - case RedmineKeys.PROJECT_ID: Project = new IdentifiableName(reader); break; - case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; - + case RedmineKeys.PROJECT_ID: Project = new IdentifiableName(reader); break; case RedmineKeys.SPENT_ON: SpentOn = reader.ReadElementContentAsNullableDateTime(); break; - - case RedmineKeys.USER: User = new IdentifiableName(reader); break; - - case RedmineKeys.HOURS: Hours = reader.ReadElementContentAsDecimal(); break; - - case RedmineKeys.ACTIVITY_ID: Activity = new IdentifiableName(reader); break; - - case RedmineKeys.ACTIVITY: Activity = new IdentifiableName(reader); break; - - case RedmineKeys.COMMENTS: Comments = reader.ReadElementContentAsString(); break; - - case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; - case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadElementContentAsNullableDateTime(); break; - - case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadElementContentAsCollection(); break; - + case RedmineKeys.USER: User = new IdentifiableName(reader); break; default: reader.Read(); break; } } @@ -180,28 +144,27 @@ public void ReadXml(XmlReader reader) /// /// /// - public void WriteXml(XmlWriter writer) + public override void WriteXml(XmlWriter writer) { - writer.WriteIdIfNotNull(Issue, RedmineKeys.ISSUE_ID); - writer.WriteIdIfNotNull(Project, RedmineKeys.PROJECT_ID); - - if (!SpentOn.HasValue) - SpentOn = DateTime.Now; - - writer.WriteDateOrEmpty(SpentOn, RedmineKeys.SPENT_ON); - writer.WriteValueOrEmpty(Hours, RedmineKeys.HOURS); - writer.WriteIdIfNotNull(Activity, RedmineKeys.ACTIVITY_ID); + writer.WriteIdIfNotNull(RedmineKeys.ISSUE_ID, Issue); + writer.WriteIdIfNotNull(RedmineKeys.PROJECT_ID, Project); + writer.WriteDateOrEmpty(RedmineKeys.SPENT_ON, SpentOn.GetValueOrDefault(DateTime.Now)); + writer.WriteValueOrEmpty(RedmineKeys.HOURS, Hours); + writer.WriteIdIfNotNull(RedmineKeys.ACTIVITY_ID, Activity); writer.WriteElementString(RedmineKeys.COMMENTS, Comments); - - writer.WriteArray(CustomFields, RedmineKeys.CUSTOM_FIELDS); + writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); } + #endregion + + + #region Implementation of IEquatable /// /// /// /// /// - public bool Equals(TimeEntry other) + public override bool Equals(TimeEntry other) { if (other == null) return false; return (Id == other.Id @@ -214,7 +177,7 @@ public bool Equals(TimeEntry other) && User == other.User && CreatedOn == other.CreatedOn && UpdatedOn == other.UpdatedOn - && (CustomFields?.Equals(other.CustomFields) ?? other.CustomFields == null)); + && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null)); } /// @@ -239,21 +202,47 @@ public override int GetHashCode() return hashCode; } } + #endregion + #region Implementation of ICloneable /// /// /// /// - public override string ToString() + public object Clone() { - return - $"[TimeEntry: {base.ToString()}, Issue={Issue}, Project={Project}, SpentOn={SpentOn}, Hours={Hours}, Activity={Activity}, User={User}, Comments={Comments}, CreatedOn={CreatedOn}, UpdatedOn={UpdatedOn}, CustomFields={CustomFields}]"; + var timeEntry = new TimeEntry + { + Activity = Activity + , + Comments = Comments + , + Hours = Hours + , + Issue = Issue, + Project = Project, + SpentOn = SpentOn, + User = User, + CustomFields = CustomFields + }; + return timeEntry; } + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => + $@"[{nameof(TimeEntry)}: {ToString()}, Issue={Issue}, Project={Project}, +SpentOn={SpentOn?.ToString("u", CultureInfo.InvariantCulture)}, +Hours={Hours.ToString("F", CultureInfo.InvariantCulture)}, +Activity={Activity}, +User={User}, +Comments={Comments}, +CreatedOn={CreatedOn?.ToString("u", CultureInfo.InvariantCulture)}, +UpdatedOn={UpdatedOn?.ToString("u", CultureInfo.InvariantCulture)}, +CustomFields={CustomFields.Dump()}]"; - /// - public override bool Equals(object obj) - { - return Equals(obj as TimeEntry); - } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/TimeEntryActivity.cs b/src/redmine-net-api/Types/TimeEntryActivity.cs old mode 100755 new mode 100644 index 2c6a9f12..8a00d9d7 --- a/src/redmine-net-api/Types/TimeEntryActivity.cs +++ b/src/redmine-net-api/Types/TimeEntryActivity.cs @@ -15,6 +15,8 @@ limitations under the License. */ using System; +using System.Diagnostics; +using System.Globalization; using System.Xml; using System.Xml.Serialization; using Redmine.Net.Api.Internals; @@ -24,21 +26,34 @@ namespace Redmine.Net.Api.Types /// /// Availability 2.2 /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.TIME_ENTRY_ACTIVITY)] - public class TimeEntryActivity : IdentifiableName, IEquatable + public sealed class TimeEntryActivity : IdentifiableName, IEquatable { + #region Properties /// /// /// - [XmlElement(RedmineKeys.IS_DEFAULT)] - public bool IsDefault { get; set; } + public TimeEntryActivity() { } + + internal TimeEntryActivity(int id, string name) + { + Id = id; + Name = name; + } + + /// + /// + /// + public bool IsDefault { get; internal set; } + #endregion #region Implementation of IXmlSerializable /// /// Generates an object from its XML representation. /// - /// The stream from which the object is deserialized. + /// The stream from which the object is deserialized. public override void ReadXml(XmlReader reader) { reader.Read(); @@ -53,11 +68,8 @@ public override void ReadXml(XmlReader reader) switch (reader.Name) { case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; - case RedmineKeys.IS_DEFAULT: IsDefault = reader.ReadElementContentAsBoolean(); break; - + case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; default: reader.Read(); break; } } @@ -71,6 +83,8 @@ public override void WriteXml(XmlWriter writer) { } #endregion + + #region Implementation of IEquatable /// @@ -120,9 +134,7 @@ public override int GetHashCode() /// /// /// - public override string ToString() - { - return $"[TimeEntryActivity: Id={Id}, Name={Name}, IsDefault={IsDefault}]"; - } + private string DebuggerDisplay => $"[{nameof(TimeEntryActivity)}:{ToString()}, IsDefault={IsDefault.ToString(CultureInfo.InvariantCulture)}]"; + } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Tracker.cs b/src/redmine-net-api/Types/Tracker.cs old mode 100755 new mode 100644 index 74d1c5c2..6d0dea10 --- a/src/redmine-net-api/Types/Tracker.cs +++ b/src/redmine-net-api/Types/Tracker.cs @@ -15,6 +15,7 @@ limitations under the License. */ using System; +using System.Diagnostics; using System.Xml; using System.Xml.Serialization; using Redmine.Net.Api.Internals; @@ -24,18 +25,15 @@ namespace Redmine.Net.Api.Types /// /// Availability 1.3 /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.TRACKER)] public class Tracker : IdentifiableName, IEquatable { - /// - /// - /// - public override void WriteXml(XmlWriter writer) { } - + #region Implementation of IXmlSerialization /// /// Generates an object from its XML representation. /// - /// The stream from which the object is deserialized. + /// The stream from which the object is deserialized. public override void ReadXml(XmlReader reader) { reader.Read(); @@ -50,14 +48,16 @@ public override void ReadXml(XmlReader reader) switch (reader.Name) { case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; - default: reader.Read(); break; } } } + #endregion + + + #region Implementation of IEquatable /// /// Indicates whether the current object is equal to another object of the same type. /// @@ -99,14 +99,13 @@ public override int GetHashCode() return hashCode; } } + #endregion /// /// /// /// - public override string ToString() - { - return $"[Tracker: Id={Id}, Name={Name}]"; - } + private string DebuggerDisplay => $"[{nameof(Tracker)}: {base.ToString()}]"; + } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/TrackerCustomField.cs b/src/redmine-net-api/Types/TrackerCustomField.cs old mode 100755 new mode 100644 index 2ce4ba12..c7370915 --- a/src/redmine-net-api/Types/TrackerCustomField.cs +++ b/src/redmine-net-api/Types/TrackerCustomField.cs @@ -14,6 +14,7 @@ You may obtain a copy of the License at limitations under the License. */ +using System.Diagnostics; using System.Xml; using System.Xml.Serialization; using Redmine.Net.Api.Extensions; @@ -23,9 +24,11 @@ namespace Redmine.Net.Api.Types /// /// /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.TRACKER)] - public class TrackerCustomField : Tracker + public sealed class TrackerCustomField : Tracker { + #region Implementation of IXmlSerialization /// /// /// @@ -36,14 +39,15 @@ public override void ReadXml(XmlReader reader) Name = reader.GetAttribute(RedmineKeys.NAME); reader.Read(); } + #endregion + + /// /// /// /// - public override string ToString () - { - return $"[TrackerCustomField: {base.ToString()}]"; - } + private string DebuggerDisplay => $"[{nameof(TrackerCustomField)}: {ToString()}]"; + } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Upload.cs b/src/redmine-net-api/Types/Upload.cs old mode 100755 new mode 100644 index 59540fb7..03c44f2c --- a/src/redmine-net-api/Types/Upload.cs +++ b/src/redmine-net-api/Types/Upload.cs @@ -15,6 +15,8 @@ limitations under the License. */ using System; +using System.Diagnostics; +using System.Xml; using System.Xml.Schema; using System.Xml.Serialization; using Redmine.Net.Api.Internals; @@ -24,14 +26,15 @@ namespace Redmine.Net.Api.Types /// /// Support for adding attachments through the REST API is added in Redmine 1.4.0. /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.UPLOAD)] - public class Upload : IEquatable + public sealed class Upload : IXmlSerializable, IEquatable { + #region Properties /// /// Gets or sets the uploaded token. /// /// The name of the file. - [XmlElement(RedmineKeys.TOKEN)] public string Token { get; set; } /// @@ -39,29 +42,70 @@ public class Upload : IEquatable /// Maximum allowed file size (1024000). /// /// The name of the file. - [XmlElement(RedmineKeys.FILENAME)] public string FileName { get; set; } /// /// Gets or sets the name of the file. /// /// The name of the file. - [XmlElement(RedmineKeys.CONTENT_TYPE)] public string ContentType { get; set; } /// /// Gets or sets the file description. (Undocumented feature) /// /// The file descroütopm. - [XmlElement(RedmineKeys.DESCRIPTION)] public string Description { get; set; } + #endregion + #region Implementation of IXmlSerialization /// /// /// /// - public static XmlSchema GetSchema() { return null; } + public XmlSchema GetSchema() { return null; } + /// + /// + /// + /// + public void ReadXml(XmlReader reader) + { + reader.Read(); + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.CONTENT_TYPE: ContentType = reader.ReadElementContentAsString(); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadElementContentAsString(); break; + case RedmineKeys.FILENAME: FileName = reader.ReadElementContentAsString(); break; + case RedmineKeys.TOKEN: Token = reader.ReadElementContentAsString(); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public void WriteXml(XmlWriter writer) + { + writer.WriteElementString(RedmineKeys.TOKEN, Token); + writer.WriteElementString(RedmineKeys.CONTENT_TYPE, ContentType); + writer.WriteElementString(RedmineKeys.FILENAME, FileName); + writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); + } + #endregion + + + + #region Implementation of IEquatable /// /// Indicates whether the current object is equal to another object of the same type. /// @@ -72,10 +116,10 @@ public class Upload : IEquatable public bool Equals(Upload other) { return other != null - && Token.Equals(other.Token, StringComparison.OrdinalIgnoreCase) - && FileName.Equals(other.FileName, StringComparison.OrdinalIgnoreCase) - && Description.Equals(other.Description, StringComparison.OrdinalIgnoreCase) - && ContentType.Equals(other.ContentType, StringComparison.OrdinalIgnoreCase); + && string.Equals(Token,other.Token, StringComparison.OrdinalIgnoreCase) + && string.Equals(FileName,other.FileName, StringComparison.OrdinalIgnoreCase) + && string.Equals(Description,other.Description, StringComparison.OrdinalIgnoreCase) + && string.Equals(ContentType,other.ContentType, StringComparison.OrdinalIgnoreCase); } /// @@ -107,15 +151,13 @@ public override int GetHashCode() return hashCode; } } + #endregion /// /// /// /// - public override string ToString() - { - return - $"[Upload: Token={Token}, FileName={FileName}, ContentType={ContentType}, Description={Description}]"; - } + private string DebuggerDisplay => $"[Upload: Token={Token}, FileName={FileName}, ContentType={ContentType}, Description={Description}]"; + } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/User.cs b/src/redmine-net-api/Types/User.cs index 5471206c..35ab0f25 100644 --- a/src/redmine-net-api/Types/User.cs +++ b/src/redmine-net-api/Types/User.cs @@ -16,9 +16,9 @@ limitations under the License. using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.Xml; -using System.Xml.Schema; using System.Xml.Serialization; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; @@ -28,42 +28,39 @@ namespace Redmine.Net.Api.Types /// /// Availability 1.1 /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.USER)] - public class User : Identifiable, IXmlSerializable, IEquatable + public sealed class User : Identifiable { + #region Properties /// /// Gets or sets the user login. /// /// The login. - [XmlElement(RedmineKeys.LOGIN)] - public string Login { get; set; } + public string Login { get; internal set; } /// /// Gets or sets the user password. /// /// The password. - [XmlElement(RedmineKeys.PASSWORD)] public string Password { get; set; } /// /// Gets or sets the first name. /// /// The first name. - [XmlElement(RedmineKeys.FIRSTNAME)] public string FirstName { get; set; } /// /// Gets or sets the last name. /// /// The last name. - [XmlElement(RedmineKeys.LASTNAME)] public string LastName { get; set; } /// /// Gets or sets the email. /// /// The email. - [XmlElement(RedmineKeys.MAIL)] public string Email { get; set; } /// @@ -72,47 +69,39 @@ public class User : Identifiable, IXmlSerializable, IEquatable /// /// The authentication mode id. /// - [XmlElement(RedmineKeys.AUTH_SOURCE_ID, IsNullable = true)] public int? AuthenticationModeId { get; set; } /// /// Gets or sets the created on. /// /// The created on. - [XmlElement(RedmineKeys.CREATED_ON, IsNullable = true)] - public DateTime? CreatedOn { get; set; } + public DateTime? CreatedOn { get; internal set; } /// - /// Gets or sets the last login on. + /// Gets the last login on. /// /// The last login on. - [XmlElement(RedmineKeys.LAST_LOGIN_ON, IsNullable = true)] - public DateTime? LastLoginOn { get; set; } + public DateTime? LastLoginOn { get; internal set; } /// /// Gets the API key of the user, visible for admins and for yourself (added in 2.3.0) /// - [XmlElement(RedmineKeys.API_KEY, IsNullable = true)] - public string ApiKey { get; set; } + public string ApiKey { get; internal set; } /// /// Gets the status of the user, visible for admins only (added in 2.4.0) /// - [XmlElement(RedmineKeys.STATUS, IsNullable = true)] public UserStatus Status { get; set; } /// /// /// - [XmlElement(RedmineKeys.MUST_CHANGE_PASSWD, IsNullable = true)] public bool MustChangePassword { get; set; } /// /// Gets or sets the custom fields. /// /// The custom fields. - [XmlArray(RedmineKeys.CUSTOM_FIELDS)] - [XmlArrayItem(RedmineKeys.CUSTOM_FIELD)] public List CustomFields { get; set; } /// @@ -121,8 +110,6 @@ public class User : Identifiable, IXmlSerializable, IEquatable /// /// The memberships. /// - [XmlArray(RedmineKeys.MEMBERSHIPS)] - [XmlArrayItem(RedmineKeys.MEMBERSHIP)] public List Memberships { get; internal set; } /// @@ -131,8 +118,6 @@ public class User : Identifiable, IXmlSerializable, IEquatable /// /// The groups. /// - [XmlArray(RedmineKeys.GROUPS)] - [XmlArrayItem(RedmineKeys.GROUP)] public List Groups { get; internal set; } /// @@ -141,23 +126,15 @@ public class User : Identifiable, IXmlSerializable, IEquatable /// /// only_my_events, only_assigned, [...] /// - [XmlElement(RedmineKeys.MAIL_NOTIFICATION)] public string MailNotification { get; set; } + #endregion - /// - /// - /// - /// - public XmlSchema GetSchema() - { - return null; - } - + #region Implementation of IXmlSerialization /// /// /// /// - public void ReadXml(XmlReader reader) + public override void ReadXml(XmlReader reader) { reader.Read(); while (!reader.EOF) @@ -171,35 +148,20 @@ public void ReadXml(XmlReader reader) switch (reader.Name) { case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.LOGIN: Login = reader.ReadElementContentAsString(); break; - + case RedmineKeys.API_KEY: ApiKey = reader.ReadElementContentAsString(); break; + case RedmineKeys.AUTH_SOURCE_ID: AuthenticationModeId = reader.ReadElementContentAsNullableInt(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadElementContentAsCollection(); break; case RedmineKeys.FIRSTNAME: FirstName = reader.ReadElementContentAsString(); break; - + case RedmineKeys.GROUPS: Groups = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.LAST_LOGIN_ON: LastLoginOn = reader.ReadElementContentAsNullableDateTime(); break; case RedmineKeys.LASTNAME: LastName = reader.ReadElementContentAsString(); break; - + case RedmineKeys.LOGIN: Login = reader.ReadElementContentAsString(); break; case RedmineKeys.MAIL: Email = reader.ReadElementContentAsString(); break; - case RedmineKeys.MAIL_NOTIFICATION: MailNotification = reader.ReadElementContentAsString(); break; - - case RedmineKeys.MUST_CHANGE_PASSWD: MustChangePassword = reader.ReadElementContentAsBoolean(); break; - - case RedmineKeys.AUTH_SOURCE_ID: AuthenticationModeId = reader.ReadElementContentAsNullableInt(); break; - - case RedmineKeys.LAST_LOGIN_ON: LastLoginOn = reader.ReadElementContentAsNullableDateTime(); break; - - case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; - - case RedmineKeys.API_KEY: ApiKey = reader.ReadElementContentAsString(); break; - - case RedmineKeys.STATUS: Status = (UserStatus)reader.ReadElementContentAsInt(); break; - - case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadElementContentAsCollection(); break; - case RedmineKeys.MEMBERSHIPS: Memberships = reader.ReadElementContentAsCollection(); break; - - case RedmineKeys.GROUPS: Groups = reader.ReadElementContentAsCollection(); break; - + case RedmineKeys.MUST_CHANGE_PASSWORD: MustChangePassword = reader.ReadElementContentAsBoolean(); break; + case RedmineKeys.STATUS: Status = (UserStatus)reader.ReadElementContentAsInt(); break; default: reader.Read(); break; } } @@ -209,60 +171,48 @@ public void ReadXml(XmlReader reader) /// /// /// - public void WriteXml(XmlWriter writer) + public override void WriteXml(XmlWriter writer) { writer.WriteElementString(RedmineKeys.LOGIN, Login); writer.WriteElementString(RedmineKeys.FIRSTNAME, FirstName); writer.WriteElementString(RedmineKeys.LASTNAME, LastName); writer.WriteElementString(RedmineKeys.MAIL, Email); - if(!string.IsNullOrEmpty(MailNotification)) - { - writer.WriteElementString(RedmineKeys.MAIL_NOTIFICATION, MailNotification); - } - - if (!string.IsNullOrEmpty(Password)) - { - writer.WriteElementString(RedmineKeys.PASSWORD, Password); - } - - if(AuthenticationModeId.HasValue) - { - writer.WriteValueOrEmpty(AuthenticationModeId, RedmineKeys.AUTH_SOURCE_ID); - } - - writer.WriteElementString(RedmineKeys.MUST_CHANGE_PASSWD, XmlConvert.ToString(MustChangePassword)); + writer.WriteElementString(RedmineKeys.MAIL_NOTIFICATION, MailNotification); + writer.WriteElementString(RedmineKeys.PASSWORD, Password); + writer.WriteValueOrEmpty(RedmineKeys.AUTH_SOURCE_ID, AuthenticationModeId); + writer.WriteElementString(RedmineKeys.MUST_CHANGE_PASSWORD, MustChangePassword.ToString().ToLowerInvariant()); writer.WriteElementString(RedmineKeys.STATUS, ((int)Status).ToString(CultureInfo.InvariantCulture)); - if(CustomFields != null) - { - writer.WriteArray(CustomFields, RedmineKeys.CUSTOM_FIELDS); - } + writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); } + #endregion + + + #region Implementation of IEquatable /// /// /// /// /// - public bool Equals(User other) + public override bool Equals(User other) { if (other == null) return false; return ( Id == other.Id - && Login.Equals(other.Login, StringComparison.OrdinalIgnoreCase) - //&& Password.Equals(other.Password) - && FirstName.Equals(other.FirstName, StringComparison.OrdinalIgnoreCase) - && LastName.Equals(other.LastName, StringComparison.OrdinalIgnoreCase) - && Email.Equals(other.Email, StringComparison.OrdinalIgnoreCase) - && MailNotification.Equals(other.MailNotification, StringComparison.OrdinalIgnoreCase) - && (ApiKey?.Equals(other.ApiKey, StringComparison.OrdinalIgnoreCase) ?? other.ApiKey == null) + && string.Equals(Login,other.Login, StringComparison.OrdinalIgnoreCase) + && string.Equals(FirstName,other.FirstName, StringComparison.OrdinalIgnoreCase) + && string.Equals(LastName,other.LastName, StringComparison.OrdinalIgnoreCase) + && string.Equals(Email,other.Email, StringComparison.OrdinalIgnoreCase) + && string.Equals(MailNotification,other.MailNotification, StringComparison.OrdinalIgnoreCase) + && (ApiKey != null ? string.Equals(ApiKey,other.ApiKey, StringComparison.OrdinalIgnoreCase) : other.ApiKey == null) && AuthenticationModeId == other.AuthenticationModeId && CreatedOn == other.CreatedOn && LastLoginOn == other.LastLoginOn && Status == other.Status && MustChangePassword == other.MustChangePassword - && (CustomFields?.Equals(other.CustomFields) ?? other.CustomFields == null) - && (Memberships?.Equals(other.Memberships) ?? other.Memberships == null) - && (Groups?.Equals(other.Groups) ?? other.Groups == null) + && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null) + && (Memberships != null ? Memberships.Equals(other.Memberships) : other.Memberships == null) + && (Groups != null ? Groups.Equals(other.Groups) : other.Groups == null) ); } @@ -293,21 +243,24 @@ public override int GetHashCode() return hashCode; } } + #endregion /// /// /// /// - public override string ToString() - { - return - $"[User: {Groups}, Login={Login}, Password={Password}, FirstName={FirstName}, LastName={LastName}, Email={Email}, EmailNotification={MailNotification}, AuthenticationModeId={AuthenticationModeId}, CreatedOn={CreatedOn}, LastLoginOn={LastLoginOn}, ApiKey={ApiKey}, Status={Status}, MustChangePassword={MustChangePassword}, CustomFields={CustomFields}, Memberships={Memberships}, Groups={Groups}]"; - } + private string DebuggerDisplay => + $@"[{nameof(User)}: {Groups}, Login={Login}, Password={Password}, FirstName={FirstName}, LastName={LastName}, Email={Email}, +EmailNotification={MailNotification}, +AuthenticationModeId={AuthenticationModeId?.ToString(CultureInfo.InvariantCulture)}, +CreatedOn={CreatedOn?.ToString("u", CultureInfo.InvariantCulture)}, +LastLoginOn={LastLoginOn?.ToString("u", CultureInfo.InvariantCulture)}, +ApiKey={ApiKey}, +Status={Status.ToString("G")}, +MustChangePassword={MustChangePassword.ToString(CultureInfo.InvariantCulture)}, +CustomFields={CustomFields.Dump()}, +Memberships={Memberships.Dump()}, +Groups={Groups.Dump()}]"; - /// - public override bool Equals(object obj) - { - return Equals(obj as User); - } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/UserGroup.cs b/src/redmine-net-api/Types/UserGroup.cs old mode 100755 new mode 100644 index f38211be..5b3538b1 --- a/src/redmine-net-api/Types/UserGroup.cs +++ b/src/redmine-net-api/Types/UserGroup.cs @@ -14,6 +14,7 @@ You may obtain a copy of the License at limitations under the License. */ +using System.Diagnostics; using System.Xml.Serialization; namespace Redmine.Net.Api.Types @@ -21,16 +22,15 @@ namespace Redmine.Net.Api.Types /// /// /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.GROUP)] - public class UserGroup : IdentifiableName + public sealed class UserGroup : IdentifiableName { /// /// /// /// - public override string ToString () - { - return $"[UserGroup: {base.ToString()}]"; - } + private string DebuggerDisplay => $"[{nameof(UserGroup)}: {ToString()}]"; + } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/UserStatus.cs b/src/redmine-net-api/Types/UserStatus.cs old mode 100755 new mode 100644 diff --git a/src/redmine-net-api/Types/Version.cs b/src/redmine-net-api/Types/Version.cs old mode 100755 new mode 100644 index 238a6214..dffbb4cb --- a/src/redmine-net-api/Types/Version.cs +++ b/src/redmine-net-api/Types/Version.cs @@ -16,6 +16,7 @@ limitations under the License. using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.Xml; using System.Xml.Serialization; @@ -27,66 +28,66 @@ namespace Redmine.Net.Api.Types /// /// Availability 1.3 /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.VERSION)] - public class Version : IdentifiableName, IEquatable + public sealed class Version : Identifiable { + #region Properties /// - /// Gets or sets the project. + /// Gets or sets the name. + /// + public string Name { get; set; } + + /// + /// Gets the project. /// /// The project. - [XmlElement(RedmineKeys.PROJECT)] - public IdentifiableName Project { get; set; } + public IdentifiableName Project { get; internal set; } /// /// Gets or sets the description. /// /// The description. - [XmlElement(RedmineKeys.DESCRIPTION)] public string Description { get; set; } /// /// Gets or sets the status. /// /// The status. - [XmlElement(RedmineKeys.STATUS)] public VersionStatus Status { get; set; } /// /// Gets or sets the due date. /// /// The due date. - [XmlElement(RedmineKeys.DUE_DATE, IsNullable = true)] public DateTime? DueDate { get; set; } /// /// Gets or sets the sharing. /// /// The sharing. - [XmlElement(RedmineKeys.SHARING)] public VersionSharing Sharing { get; set; } /// - /// Gets or sets the created on. + /// Gets the created on. /// /// The created on. - [XmlElement(RedmineKeys.CREATED_ON, IsNullable = true)] - public DateTime? CreatedOn { get; set; } + public DateTime? CreatedOn { get; internal set; } /// - /// Gets or sets the updated on. + /// Gets the updated on. /// /// The updated on. - [XmlElement(RedmineKeys.UPDATED_ON, IsNullable = true)] - public DateTime? UpdatedOn { get; set; } + public DateTime? UpdatedOn { get; internal set; } /// - /// Gets or sets the custom fields. + /// Gets the custom fields. /// /// The custom fields. - [XmlArray(RedmineKeys.CUSTOM_FIELDS)] - [XmlArrayItem(RedmineKeys.CUSTOM_FIELD)] public IList CustomFields { get; internal set; } + #endregion + #region Implementation of IXmlSerializable /// /// /// @@ -96,34 +97,18 @@ public override void ReadXml(XmlReader reader) reader.Read(); while (!reader.EOF) { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - switch (reader.Name) { case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; - - case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; - + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadElementContentAsCollection(); break; case RedmineKeys.DESCRIPTION: Description = reader.ReadElementContentAsString(); break; - - case RedmineKeys.STATUS: Status = (VersionStatus)Enum.Parse(typeof(VersionStatus), reader.ReadElementContentAsString(), true); break; - case RedmineKeys.DUE_DATE: DueDate = reader.ReadElementContentAsNullableDateTime(); break; - + case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; + case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; case RedmineKeys.SHARING: Sharing = (VersionSharing)Enum.Parse(typeof(VersionSharing), reader.ReadElementContentAsString(), true); break; - - case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; - + case RedmineKeys.STATUS: Status = (VersionStatus)Enum.Parse(typeof(VersionStatus), reader.ReadElementContentAsString(), true); break; case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadElementContentAsNullableDateTime(); break; - - case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadElementContentAsCollection(); break; - default: reader.Read(); break; } } @@ -136,19 +121,22 @@ public override void ReadXml(XmlReader reader) public override void WriteXml(XmlWriter writer) { writer.WriteElementString(RedmineKeys.NAME, Name); - writer.WriteElementString(RedmineKeys.STATUS, Status.ToString("G").ToLowerInv()); - writer.WriteElementString(RedmineKeys.SHARING, Sharing.ToString("G").ToLowerInv()); - - writer.WriteDateOrEmpty(DueDate, RedmineKeys.DUE_DATE); + writer.WriteElementString(RedmineKeys.STATUS, Status.ToString().ToLowerInvariant()); + writer.WriteElementString(RedmineKeys.SHARING, Sharing.ToString().ToLowerInvariant()); + writer.WriteDateOrEmpty(RedmineKeys.DUE_DATE, DueDate); writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); } + #endregion + + + #region Implementation of IEquatable /// /// /// /// /// - public bool Equals(Version other) + public override bool Equals(Version other) { if (other == null) return false; return (Id == other.Id && Name == other.Name @@ -159,9 +147,8 @@ public bool Equals(Version other) && Sharing == other.Sharing && CreatedOn == other.CreatedOn && UpdatedOn == other.UpdatedOn - && (CustomFields?.Equals(other.CustomFields) ?? other.CustomFields == null)); + && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null)); } - /// /// /// @@ -182,67 +169,19 @@ public override int GetHashCode() return hashCode; } } + #endregion /// /// /// /// - public override string ToString() - { - return - $"[Version: {base.ToString()}, Project={Project}, Description={Description}, Status={Status}, DueDate={DueDate}, Sharing={Sharing}, CreatedOn={CreatedOn}, UpdatedOn={UpdatedOn}, CustomFields={CustomFields}]"; - } + private string DebuggerDisplay => $@"[{nameof(Version)}: {ToString()}, Project={Project}, Description={Description}, +Status={Status:G}, + DueDate={DueDate?.ToString("u", CultureInfo.InvariantCulture)}, +Sharing={Sharing:G}, +CreatedOn={CreatedOn?.ToString("u", CultureInfo.InvariantCulture)}, +UpdatedOn={UpdatedOn?.ToString("u", CultureInfo.InvariantCulture)}, +CustomFields={CustomFields.Dump()}]"; - /// - public override bool Equals(object obj) - { - return Equals(obj as Version); - } - } - - /// - /// - /// - public enum VersionSharing - { - /// - /// - /// - none = 1, - /// - /// - /// - descendants, - /// - /// - /// - hierarchy, - /// - /// - /// - tree, - /// - /// - /// - system - } - - /// - /// - /// - public enum VersionStatus - { - /// - /// - /// - open = 1, - /// - /// - /// - locked, - /// - /// - /// - closed } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/VersionSharing.cs b/src/redmine-net-api/Types/VersionSharing.cs new file mode 100644 index 00000000..a4c0a155 --- /dev/null +++ b/src/redmine-net-api/Types/VersionSharing.cs @@ -0,0 +1,29 @@ +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + public enum VersionSharing + { + /// + /// + /// + None = 1, + /// + /// + /// + Descendants, + /// + /// + /// + Hierarchy, + /// + /// + /// + Tree, + /// + /// + /// + System + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/VersionStatus.cs b/src/redmine-net-api/Types/VersionStatus.cs new file mode 100644 index 00000000..ee86fedf --- /dev/null +++ b/src/redmine-net-api/Types/VersionStatus.cs @@ -0,0 +1,21 @@ +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + public enum VersionStatus + { + /// + /// + /// + Open = 1, + /// + /// + /// + Locked, + /// + /// + /// + Closed + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/Watcher.cs b/src/redmine-net-api/Types/Watcher.cs old mode 100755 new mode 100644 index adfcdd63..2fd8735c --- a/src/redmine-net-api/Types/Watcher.cs +++ b/src/redmine-net-api/Types/Watcher.cs @@ -15,6 +15,7 @@ limitations under the License. */ using System; +using System.Diagnostics; using System.Globalization; using System.Xml.Serialization; @@ -23,40 +24,35 @@ namespace Redmine.Net.Api.Types /// /// /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.USER)] - public class Watcher : IdentifiableName, IValue, ICloneable + public sealed class Watcher : IdentifiableName, IValue, ICloneable { - #region IValue implementation + #region Implementation of IValue /// /// /// - public string Value - { - get - { - return Id.ToString(CultureInfo.InvariantCulture); - } - } + public string Value => Id.ToString(CultureInfo.InvariantCulture); #endregion + #region Implementation of ICloneable /// /// /// /// - public override string ToString() + public object Clone() { - return $"[Watcher: {base.ToString()}]"; + var watcher = new Watcher { Id = Id, Name = Name }; + return watcher; } + #endregion /// /// /// /// - public object Clone() - { - var watcher = new Watcher { Id = Id, Name = Name }; - return watcher; - } + private string DebuggerDisplay => $"[{nameof(Watcher)}: {ToString()}]"; + } } \ No newline at end of file From d4793376242c27b6f4e8230672d88899ac94759f Mon Sep 17 00:00:00 2001 From: zapadi Date: Fri, 10 Jan 2020 21:18:31 +0200 Subject: [PATCH 092/549] Fix Invalid URI: The Uri scheme is too long --- src/redmine-net-api/Internals/XmlTextReaderBuilder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs b/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs index 404fc270..ec4d07d6 100644 --- a/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs +++ b/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs @@ -33,7 +33,7 @@ public static XmlReader Create(StringReader stringReader) /// public static XmlReader Create(string xml) { - using (var stringReader = new StringReader(xml)) + var stringReader = new StringReader(xml); { return XmlReader.Create(stringReader, new XmlReaderSettings() { @@ -68,7 +68,7 @@ public static XmlTextReader Create(StringReader stringReader) /// public static XmlTextReader Create(string xml) { - using (var stringReader = new StringReader(xml)) + var stringReader = new StringReader(xml); { return new XmlTextReader(stringReader) { From 7db9bda931f22f892956a44a066dbef049fadace Mon Sep 17 00:00:00 2001 From: zapadi Date: Fri, 10 Jan 2020 21:19:44 +0200 Subject: [PATCH 093/549] Add json serialization (NewtonJson) --- .../Extensions/JsonReaderExtensions.cs | 84 ++++++ .../Extensions/JsonWriterExtensions.cs | 260 ++++++++++++++++++ .../Serialization/IJsonSerializable.cs | 21 ++ .../Serialization/JsonObject.cs | 46 ++++ .../Serialization/JsonRedmineSerializer.cs | 159 +++++++++++ src/redmine-net-api/Types/Attachment.cs | 56 +++- src/redmine-net-api/Types/Attachments.cs | 29 +- src/redmine-net-api/Types/ChangeSet.cs | 45 ++- src/redmine-net-api/Types/CustomField.cs | 55 +++- .../Types/CustomFieldPossibleValue.cs | 47 +++- src/redmine-net-api/Types/CustomFieldValue.cs | 35 ++- src/redmine-net-api/Types/Detail.cs | 53 +++- src/redmine-net-api/Types/Error.cs | 29 +- src/redmine-net-api/Types/File.cs | 65 ++++- src/redmine-net-api/Types/Group.cs | 54 +++- src/redmine-net-api/Types/Identifiable.cs | 18 +- src/redmine-net-api/Types/IdentifiableName.cs | 59 +++- src/redmine-net-api/Types/Issue.cs | 172 +++++++++--- src/redmine-net-api/Types/IssueCategory.cs | 49 +++- src/redmine-net-api/Types/IssueChild.cs | 34 ++- src/redmine-net-api/Types/IssueCustomField.cs | 91 +++++- src/redmine-net-api/Types/IssuePriority.cs | 35 ++- src/redmine-net-api/Types/IssueRelation.cs | 54 +++- src/redmine-net-api/Types/IssueStatus.cs | 35 ++- src/redmine-net-api/Types/Journal.cs | 36 ++- src/redmine-net-api/Types/Membership.cs | 36 ++- src/redmine-net-api/Types/MembershipRole.cs | 41 ++- src/redmine-net-api/Types/News.cs | 45 ++- src/redmine-net-api/Types/Permission.cs | 38 ++- src/redmine-net-api/Types/Project.cs | 100 ++++++- .../Types/ProjectEnabledModule.cs | 2 +- .../Types/ProjectMembership.cs | 51 +++- src/redmine-net-api/Types/Query.cs | 44 ++- src/redmine-net-api/Types/Role.cs | 32 ++- src/redmine-net-api/Types/TimeEntry.cs | 76 ++++- .../Types/TimeEntryActivity.cs | 34 ++- src/redmine-net-api/Types/Tracker.cs | 34 ++- .../Types/TrackerCustomField.cs | 31 ++- src/redmine-net-api/Types/Upload.cs | 56 +++- src/redmine-net-api/Types/User.cs | 76 ++++- src/redmine-net-api/Types/Version.cs | 59 +++- src/redmine-net-api/Types/WikiPage.cs | 148 ++++++---- 42 files changed, 2309 insertions(+), 215 deletions(-) create mode 100644 src/redmine-net-api/Extensions/JsonReaderExtensions.cs create mode 100644 src/redmine-net-api/Extensions/JsonWriterExtensions.cs create mode 100644 src/redmine-net-api/Serialization/IJsonSerializable.cs create mode 100644 src/redmine-net-api/Serialization/JsonObject.cs create mode 100644 src/redmine-net-api/Serialization/JsonRedmineSerializer.cs mode change 100755 => 100644 src/redmine-net-api/Types/WikiPage.cs diff --git a/src/redmine-net-api/Extensions/JsonReaderExtensions.cs b/src/redmine-net-api/Extensions/JsonReaderExtensions.cs new file mode 100644 index 00000000..7cf3cfd2 --- /dev/null +++ b/src/redmine-net-api/Extensions/JsonReaderExtensions.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Serialization; + +namespace Redmine.Net.Api.Extensions +{ + /// + /// + /// + public static partial class JsonExtensions + { + /// + /// + /// + /// + /// + public static int ReadAsInt(this JsonReader reader) + { + return reader.ReadAsInt32().GetValueOrDefault(); + } + + /// + /// + /// + /// + /// + public static bool ReadAsBool(this JsonReader reader) + { + return reader.ReadAsBoolean().GetValueOrDefault(); + } + + /// + /// + /// + /// + /// + /// + /// + public static List ReadAsCollection(this JsonReader reader, bool readInnerArray = false) where T : class + { + var isJsonSerializable = typeof(IJsonSerializable).IsAssignableFrom(typeof(T)); + + if (!isJsonSerializable) + { + throw new RedmineException($"Entity of type '{typeof(T)}' should implement IJsonSerializable."); + } + + var col = new List(); + + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndArray) + { + break; + } + + if (readInnerArray) + { + if (reader.TokenType == JsonToken.PropertyName) + { + break; + } + } + + if (reader.TokenType == JsonToken.StartArray) + { + continue; + } + + var entity = Activator.CreateInstance(); + + ((IJsonSerializable)entity).ReadJson(reader); + + var des = entity; + + col.Add(des); + } + + return col; + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Extensions/JsonWriterExtensions.cs b/src/redmine-net-api/Extensions/JsonWriterExtensions.cs new file mode 100644 index 00000000..9b01c2d6 --- /dev/null +++ b/src/redmine-net-api/Extensions/JsonWriterExtensions.cs @@ -0,0 +1,260 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using Newtonsoft.Json; +using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.Extensions +{ + /// + /// + /// + public static partial class JsonExtensions + { + /// + /// + /// + /// + /// + /// + public static void WriteIdIfNotNull(this JsonWriter jsonWriter, string tag, IdentifiableName value) + { + if (value != null) + { + jsonWriter.WritePropertyName(tag); + jsonWriter.WriteValue(value.Id); + } + } + + /// + /// + /// + /// + /// + /// + /// + public static void WriteIfNotDefaultOrNull(this JsonWriter writer, string elementName, T value) + { + if (EqualityComparer.Default.Equals(value, default(T))) + { + return; + } + + if (value is bool) + { + writer.WriteProperty(elementName, value.ToString().ToLowerInvariant()); + return; + } + + writer.WriteProperty(elementName, string.Format(CultureInfo.InvariantCulture, "{0}", value.ToString())); + } + + /// + /// + /// + /// + /// + /// + /// + public static void WriteIdOrEmpty(this JsonWriter jsonWriter, string tag, IdentifiableName ident, string emptyValue = null) + { + if (ident != null) + { + jsonWriter.WriteProperty(tag, ident.Id); + } + else + { + jsonWriter.WriteProperty(tag, emptyValue); + } + } + + /// + /// + /// + /// + /// + /// + /// + public static void WriteDateOrEmpty(this JsonWriter jsonWriter, string tag, DateTime? val, string dateFormat = "yyyy-MM-dd") + { + if (!val.HasValue || val.Value.Equals(default(DateTime))) + { + jsonWriter.WriteProperty(tag, string.Empty); + } + else + { + jsonWriter.WriteProperty(tag, string.Format(NumberFormatInfo.InvariantInfo, "{0}", val.Value.ToString(dateFormat, CultureInfo.InvariantCulture))); + } + } + + /// + /// + /// + /// + /// + /// + /// + public static void WriteValueOrEmpty(this JsonWriter jsonWriter, string tag, T? val) where T : struct + { + if (!val.HasValue || EqualityComparer.Default.Equals(val.Value, default(T))) + { + jsonWriter.WriteProperty(tag, string.Empty); + } + else + { + jsonWriter.WriteProperty(tag, val.Value); + } + } + + /// + /// + /// + /// + /// + /// + /// + public static void WriteValueOrDefault(this JsonWriter jsonWriter, string tag, T? val) where T : struct + { + jsonWriter.WriteProperty(tag, val ?? default(T)); + } + + /// + /// + /// + /// + /// + /// + public static void WriteProperty(this JsonWriter jsonWriter, string tag, object value) + { + jsonWriter.WritePropertyName(tag); + jsonWriter.WriteValue(value); + } + + /// + /// + /// + /// + /// + /// + public static void WriteRepeatableElement(this JsonWriter jsonWriter, string tag, IEnumerable collection) + { + if (collection == null) + { + return; + } + + foreach (var value in collection) + { + jsonWriter.WritePropertyName(tag); + jsonWriter.WriteValue(value.Value); + } + } + + /// + /// + /// + /// + /// + /// + public static void WriteArrayIds(this JsonWriter jsonWriter, string tag, IEnumerable collection) + { + if (collection == null) + { + return; + } + + jsonWriter.WritePropertyName(tag); + jsonWriter.WriteStartArray(); + + StringBuilder sb = new StringBuilder(); + + foreach (var identifiableName in collection) + { + sb.Append(",").Append(identifiableName.Id.ToString(CultureInfo.InvariantCulture)); + } + + sb.Remove(0, 1); + jsonWriter.WriteValue(sb.ToString()); + sb= null; + + jsonWriter.WriteEndArray(); + } + + /// + /// + /// + /// + /// + /// + public static void WriteArrayNames(this JsonWriter jsonWriter, string tag, IEnumerable collection) + { + if (collection == null) + { + return; + } + + jsonWriter.WritePropertyName(tag); + jsonWriter.WriteStartArray(); + + StringBuilder sb = new StringBuilder(); + + foreach (var identifiableName in collection) + { + sb.Append(",").Append(identifiableName.Name); + } + + sb.Remove(0, 1); + jsonWriter.WriteValue(sb.ToString()); + sb = null; + + jsonWriter.WriteEndArray(); + } + + /// + /// + /// + /// + /// + /// + /// + public static void WriteArray(this JsonWriter jsonWriter, string tag, ICollection collection) where T : IJsonSerializable + { + if (collection == null) + { + return; + } + + jsonWriter.WritePropertyName(tag); + jsonWriter.WriteStartArray(); + + foreach (var item in collection) + { + item.WriteJson(jsonWriter); + } + + jsonWriter.WriteEndArray(); + } + + /// + /// + /// + /// + /// + /// + public static void WriteListAsProperty(this JsonWriter jsonWriter, string tag, ICollection collection) + { + if (collection == null) + { + return; + } + + foreach (var item in collection) + { + jsonWriter.WriteProperty(tag, item); + } + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Serialization/IJsonSerializable.cs b/src/redmine-net-api/Serialization/IJsonSerializable.cs new file mode 100644 index 00000000..efb4ec2f --- /dev/null +++ b/src/redmine-net-api/Serialization/IJsonSerializable.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json; + +namespace Redmine.Net.Api.Serialization +{ + /// + /// + /// + public interface IJsonSerializable + { + /// + /// + /// + /// + void WriteJson(JsonWriter writer); + /// + /// + /// + /// + void ReadJson(JsonReader reader); + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Serialization/JsonObject.cs b/src/redmine-net-api/Serialization/JsonObject.cs new file mode 100644 index 00000000..a42c4316 --- /dev/null +++ b/src/redmine-net-api/Serialization/JsonObject.cs @@ -0,0 +1,46 @@ +using System; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; + +namespace Redmine.Net.Api.Serialization +{ + /// + /// + /// + public sealed class JsonObject : IDisposable + { + private readonly bool hasRoot; + + /// + /// + /// + /// + /// + public JsonObject(JsonWriter writer, string root = null) + { + Writer = writer; + Writer.WriteStartObject(); + + if (!root.IsNullOrWhiteSpace()) + { + hasRoot = true; + Writer.WritePropertyName(root); + Writer.WriteStartObject(); + } + } + + private JsonWriter Writer { get; } + + /// + /// + /// + public void Dispose() + { + Writer.WriteEndObject(); + if (hasRoot) + { + Writer.WriteEndObject(); + } + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Serialization/JsonRedmineSerializer.cs b/src/redmine-net-api/Serialization/JsonRedmineSerializer.cs new file mode 100644 index 00000000..ba98e836 --- /dev/null +++ b/src/redmine-net-api/Serialization/JsonRedmineSerializer.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Newtonsoft.Json; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; + +namespace Redmine.Net.Api.Serialization +{ + internal sealed class JsonRedmineSerializer : IRedmineSerializer + { + public T Deserialize(string jsonResponse) where T : new() + { + if (jsonResponse.IsNullOrWhiteSpace()) + { + throw new ArgumentNullException(nameof(jsonResponse), $"Could not deserialize null or empty input for type '{typeof(T).Name}'."); + } + + var isJsonSerializable = typeof(IJsonSerializable).IsAssignableFrom(typeof(T)); + + if (!isJsonSerializable) + { + throw new RedmineException($"Entity of type '{typeof(T)}' should implement IJsonSerializable."); + } + + using (var stringReader = new StringReader(jsonResponse)) + { + using (JsonReader jsonReader = new JsonTextReader(stringReader)) + { + var obj = Activator.CreateInstance(); + + if (jsonReader.Read()) + { + if (jsonReader.Read()) + { + ((IJsonSerializable)obj).ReadJson(jsonReader); + } + } + + return obj; + } + } + } + + public PagedResults DeserializeToPagedResults(string jsonResponse) where T : class, new() + { + if (jsonResponse.IsNullOrWhiteSpace()) + { + throw new ArgumentNullException(nameof(jsonResponse), $"Could not deserialize null or empty input for type '{typeof(T).Name}'."); + } + + using (var sr = new StringReader(jsonResponse)) + { + using (JsonReader reader = new JsonTextReader(sr)) + { + var total = 0; + var offset = 0; + var limit = 0; + List list = null; + + while (reader.Read()) + { + if (reader.TokenType != JsonToken.PropertyName) continue; + + switch (reader.Value) + { + case RedmineKeys.TOTAL_COUNT: + total = reader.ReadAsInt32().GetValueOrDefault(); + break; + case RedmineKeys.OFFSET: + offset = reader.ReadAsInt32().GetValueOrDefault(); + break; + case RedmineKeys.LIMIT: + limit = reader.ReadAsInt32().GetValueOrDefault(); + break; + default: + list = reader.ReadAsCollection(); + break; + } + } + + return new PagedResults(list, total, offset, limit); + } + } + } + + public int Count(string jsonResponse) where T : class, new() + { + if (jsonResponse.IsNullOrWhiteSpace()) + { + throw new ArgumentNullException(nameof(jsonResponse), $"Could not deserialize null or empty input for type '{typeof(T).Name}'."); + } + + using (var sr = new StringReader(jsonResponse)) + { + using (JsonReader reader = new JsonTextReader(sr)) + { + var total = 0; + + while (reader.Read()) + { + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + if (reader.Value is RedmineKeys.TOTAL_COUNT) + { + total = reader.ReadAsInt32().GetValueOrDefault(); + return total; + } + } + + return total; + } + } + } + + public string Type { get; } = "json"; + + public string Serialize(T entity) where T : class + { + if (entity == default(T)) + { + throw new ArgumentNullException(nameof(entity), $"Could not serialize null of type {typeof(T).Name}"); + } + + var jsonSerializable = entity as IJsonSerializable; + + if (jsonSerializable == null) + { + throw new RedmineException($"Entity of type '{typeof(T)}' should implement IJsonSerializable."); + } + + var stringBuilder = new StringBuilder(); + + using (var sw = new StringWriter(stringBuilder)) + { + using (JsonWriter writer = new JsonTextWriter(sw)) + { + writer.Formatting = Newtonsoft.Json.Formatting.Indented; + writer.DateFormatHandling = DateFormatHandling.IsoDateFormat; + + jsonSerializable.WriteJson(writer); + + var json = stringBuilder.ToString(); + +#if NET20 + stringBuilder = null; +#else + stringBuilder.Clear(); +#endif + return json; + } + } + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/Attachment.cs b/src/redmine-net-api/Types/Attachment.cs index 66fa2c0b..dd9f053b 100644 --- a/src/redmine-net-api/Types/Attachment.cs +++ b/src/redmine-net-api/Types/Attachment.cs @@ -19,8 +19,10 @@ limitations under the License. using System.Globalization; using System.Xml; using System.Xml.Serialization; +using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api.Types { @@ -125,7 +127,55 @@ public override void WriteXml(XmlWriter writer) #endregion - + #region Implementation of IJsonSerializable + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.AUTHOR: Author = new IdentifiableName(reader); break; + case RedmineKeys.CONTENT_TYPE: ContentType = reader.ReadAsString(); break; + case RedmineKeys.CONTENT_URL: ContentUrl = reader.ReadAsString(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadAsString(); break; + case RedmineKeys.FILENAME: FileName = reader.ReadAsString(); break; + case RedmineKeys.FILE_SIZE: FileSize = reader.ReadAsInt(); break; + case RedmineKeys.THUMBNAIL_URL: ThumbnailUrl = reader.ReadAsString(); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + + public override void WriteJson(JsonWriter writer) + { + using (new JsonObject(writer, RedmineKeys.ATTACHMENT)) + { + writer.WriteProperty(RedmineKeys.FILENAME, FileName); + writer.WriteProperty(RedmineKeys.DESCRIPTION, Description); + } + } + #endregion #region Implementation of IEquatable /// @@ -136,7 +186,7 @@ public override void WriteXml(XmlWriter writer) public override bool Equals(Attachment other) { if (other == null) return false; - return (Id == other.Id + return Id == other.Id && FileName == other.FileName && FileSize == other.FileSize && ContentType == other.ContentType @@ -144,7 +194,7 @@ public override bool Equals(Attachment other) && ThumbnailUrl == other.ThumbnailUrl && CreatedOn == other.CreatedOn && Description == other.Description - && ContentUrl == other.ContentUrl); + && ContentUrl == other.ContentUrl; } /// diff --git a/src/redmine-net-api/Types/Attachments.cs b/src/redmine-net-api/Types/Attachments.cs index 174b74e7..6f4b0314 100644 --- a/src/redmine-net-api/Types/Attachments.cs +++ b/src/redmine-net-api/Types/Attachments.cs @@ -15,14 +15,39 @@ limitations under the License. */ using System.Collections.Generic; +using System.Globalization; +using Newtonsoft.Json; +using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api.Types { /// /// /// - internal class Attachments : Dictionary + internal class Attachments : Dictionary, IJsonSerializable { - + /// + /// + /// + /// + public void ReadJson(JsonReader reader) { } + + /// + /// + /// + /// + public void WriteJson(JsonWriter writer) + { + using (new JsonObject(writer, RedmineKeys.ATTACHMENTS)) + { + writer.WriteStartArray(); + foreach (var item in this) + { + writer.WritePropertyName(item.Key.ToString(CultureInfo.InvariantCulture)); + item.Value.WriteJson(writer); + } + writer.WriteEndArray(); + } + } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/ChangeSet.cs b/src/redmine-net-api/Types/ChangeSet.cs index 011c918c..c84c237d 100644 --- a/src/redmine-net-api/Types/ChangeSet.cs +++ b/src/redmine-net-api/Types/ChangeSet.cs @@ -20,8 +20,10 @@ limitations under the License. using System.Xml; using System.Xml.Schema; using System.Xml.Serialization; +using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api.Types { @@ -30,7 +32,7 @@ namespace Redmine.Net.Api.Types /// [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.CHANGE_SET)] - public sealed class ChangeSet : IXmlSerializable, IEquatable + public sealed class ChangeSet : IXmlSerializable, IJsonSerializable, IEquatable { #region Properties /// @@ -98,7 +100,46 @@ public void ReadXml(XmlReader reader) public void WriteXml(XmlWriter writer) { } #endregion - + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.COMMENTS: Comments = reader.ReadAsString(); break; + + case RedmineKeys.COMMITTED_ON: CommittedOn = reader.ReadAsDateTime(); break; + + case RedmineKeys.REVISION: Revision = reader.ReadAsInt(); break; + + case RedmineKeys.USER: User = new IdentifiableName(reader); break; + + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public void WriteJson(JsonWriter writer) { } + #endregion #region Implementation of IEquatable /// diff --git a/src/redmine-net-api/Types/CustomField.cs b/src/redmine-net-api/Types/CustomField.cs index 4fbb32b6..84a36208 100644 --- a/src/redmine-net-api/Types/CustomField.cs +++ b/src/redmine-net-api/Types/CustomField.cs @@ -20,6 +20,7 @@ limitations under the License. using System.Globalization; using System.Xml; using System.Xml.Serialization; +using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; @@ -145,7 +146,49 @@ public override void ReadXml(XmlReader reader) #endregion - + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.CUSTOMIZED_TYPE: CustomizedType = reader.ReadAsString(); break; + case RedmineKeys.DEFAULT_VALUE: DefaultValue = reader.ReadAsString(); break; + case RedmineKeys.FIELD_FORMAT: FieldFormat = reader.ReadAsString(); break; + case RedmineKeys.IS_FILTER: IsFilter = reader.ReadAsBool(); break; + case RedmineKeys.IS_REQUIRED: IsRequired = reader.ReadAsBool(); break; + case RedmineKeys.MAX_LENGTH: MaxLength = reader.ReadAsInt32(); break; + case RedmineKeys.MIN_LENGTH: MinLength = reader.ReadAsInt32(); break; + case RedmineKeys.MULTIPLE: Multiple = reader.ReadAsBool(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + case RedmineKeys.POSSIBLE_VALUES: PossibleValues = reader.ReadAsCollection(); break; + case RedmineKeys.REGEXP: Regexp = reader.ReadAsString(); break; + case RedmineKeys.ROLES: Roles = reader.ReadAsCollection(); break; + case RedmineKeys.SEARCHABLE: Searchable = reader.ReadAsBool(); break; + case RedmineKeys.TRACKERS: Trackers = reader.ReadAsCollection(); break; + case RedmineKeys.VISIBLE: Visible = reader.ReadAsBool(); break; + default: reader.Read(); break; + } + } + } + + #endregion #region Implementation of IEquatable /// @@ -163,13 +206,13 @@ public bool Equals(CustomField other) && Multiple == other.Multiple && Searchable == other.Searchable && Visible == other.Visible - && CustomizedType.Equals(other.CustomizedType) - && DefaultValue.Equals(other.DefaultValue) - && FieldFormat.Equals(other.FieldFormat) + && string.Equals(CustomizedType,other.CustomizedType, StringComparison.OrdinalIgnoreCase) + && string.Equals(DefaultValue,other.DefaultValue, StringComparison.OrdinalIgnoreCase) + && string.Equals(FieldFormat,other.FieldFormat, StringComparison.OrdinalIgnoreCase) && MaxLength == other.MaxLength && MinLength == other.MinLength - && Name.Equals(other.Name) - && Regexp.Equals(other.Regexp) + && string.Equals(Name,other.Name, StringComparison.OrdinalIgnoreCase) + && string.Equals(Regexp,other.Regexp, StringComparison.OrdinalIgnoreCase) && PossibleValues.Equals(other.PossibleValues) && Roles.Equals(other.Roles) && Trackers.Equals(other.Trackers); diff --git a/src/redmine-net-api/Types/CustomFieldPossibleValue.cs b/src/redmine-net-api/Types/CustomFieldPossibleValue.cs index 3078da08..4323c310 100644 --- a/src/redmine-net-api/Types/CustomFieldPossibleValue.cs +++ b/src/redmine-net-api/Types/CustomFieldPossibleValue.cs @@ -19,7 +19,9 @@ limitations under the License. using System.Xml; using System.Xml.Schema; using System.Xml.Serialization; +using Newtonsoft.Json; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api.Types { @@ -28,7 +30,7 @@ namespace Redmine.Net.Api.Types /// [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.POSSIBLE_VALUE)] - public sealed class CustomFieldPossibleValue : IXmlSerializable, IEquatable + public sealed class CustomFieldPossibleValue : IXmlSerializable, IJsonSerializable, IEquatable { #region Properties /// @@ -87,7 +89,46 @@ public void WriteXml(XmlWriter writer) { } #endregion - + #region Implementation of IJsonSerialization + + /// + /// + /// + /// + public void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.LABEL: + Label = reader.ReadAsString(); break; + + case RedmineKeys.VALUE: + + Value = reader.ReadAsString(); break; + default: + reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public void WriteJson(JsonWriter writer) { } + #endregion #region Implementation of IEquatable /// @@ -98,7 +139,7 @@ public void WriteXml(XmlWriter writer) { } public bool Equals(CustomFieldPossibleValue other) { if (other == null) return false; - return (Value == other.Value); + return Value == other.Value; } /// diff --git a/src/redmine-net-api/Types/CustomFieldValue.cs b/src/redmine-net-api/Types/CustomFieldValue.cs index 094b24da..d2b264b8 100644 --- a/src/redmine-net-api/Types/CustomFieldValue.cs +++ b/src/redmine-net-api/Types/CustomFieldValue.cs @@ -19,7 +19,9 @@ limitations under the License. using System.Xml; using System.Xml.Schema; using System.Xml.Serialization; +using Newtonsoft.Json; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api.Types { @@ -28,7 +30,7 @@ namespace Redmine.Net.Api.Types /// [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.VALUE)] - public class CustomFieldValue : IXmlSerializable, IEquatable, ICloneable + public class CustomFieldValue : IXmlSerializable, IJsonSerializable, IEquatable, ICloneable { /// /// @@ -103,7 +105,34 @@ public void WriteXml(XmlWriter writer) #endregion - + #region Implementation of IJsonSerialization + + /// + /// + /// + /// + public void ReadJson(JsonReader reader) + { + if (reader.TokenType == JsonToken.PropertyName) + { + return; + } + + if (reader.TokenType == JsonToken.String) + { + Info = reader.Value as string; + } + } + + /// + /// + /// + /// + public void WriteJson(JsonWriter writer) + { + } + + #endregion #region Implementation of IEquatable @@ -115,7 +144,7 @@ public void WriteXml(XmlWriter writer) public bool Equals(CustomFieldValue other) { if (other == null) return false; - return Info.Equals(other.Info); + return string.Equals(Info,other.Info,StringComparison.OrdinalIgnoreCase); } /// diff --git a/src/redmine-net-api/Types/Detail.cs b/src/redmine-net-api/Types/Detail.cs index 17219580..216ec805 100644 --- a/src/redmine-net-api/Types/Detail.cs +++ b/src/redmine-net-api/Types/Detail.cs @@ -19,7 +19,9 @@ limitations under the License. using System.Xml; using System.Xml.Schema; using System.Xml.Serialization; +using Newtonsoft.Json; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api.Types { @@ -28,7 +30,7 @@ namespace Redmine.Net.Api.Types /// [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.DETAIL)] - public sealed class Detail : IXmlSerializable, IEquatable + public sealed class Detail : IXmlSerializable, IJsonSerializable, IEquatable { /// /// @@ -121,7 +123,46 @@ public void ReadXml(XmlReader reader) public void WriteXml(XmlWriter writer) { } #endregion - + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public void WriteJson(JsonWriter writer) { } + + /// + /// + /// + /// + public void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + + case RedmineKeys.PROPERTY: Property = reader.ReadAsString(); break; + + case RedmineKeys.NEW_VALUE: NewValue = reader.ReadAsString(); break; + + case RedmineKeys.OLD_VALUE: OldValue = reader.ReadAsString(); break; + + default: reader.Read(); break; + } + } + } + #endregion #region Implementation of IEquatable /// @@ -132,10 +173,10 @@ public void WriteXml(XmlWriter writer) { } public bool Equals(Detail other) { if (other == null) return false; - return (Property != null ? string.Equals(Property,other.Property, StringComparison.InvariantCultureIgnoreCase) : other.Property == null) - && (Name != null ? string.Equals(Name,other.Name, StringComparison.InvariantCultureIgnoreCase) : other.Name == null) - && (OldValue != null ? string.Equals(OldValue,other.OldValue, StringComparison.InvariantCultureIgnoreCase) : other.OldValue == null) - && (NewValue != null ? string.Equals(NewValue,other.NewValue, StringComparison.InvariantCultureIgnoreCase) : other.NewValue == null); + return string.Equals(Property, other.Property, StringComparison.InvariantCultureIgnoreCase) + && string.Equals(Name, other.Name, StringComparison.InvariantCultureIgnoreCase) + && string.Equals(OldValue, other.OldValue, StringComparison.InvariantCultureIgnoreCase) + && string.Equals(NewValue, other.NewValue, StringComparison.InvariantCultureIgnoreCase); } /// diff --git a/src/redmine-net-api/Types/Error.cs b/src/redmine-net-api/Types/Error.cs index 70634087..c31be160 100644 --- a/src/redmine-net-api/Types/Error.cs +++ b/src/redmine-net-api/Types/Error.cs @@ -19,7 +19,9 @@ limitations under the License. using System.Xml; using System.Xml.Schema; using System.Xml.Serialization; +using Newtonsoft.Json; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api.Types { @@ -28,7 +30,7 @@ namespace Redmine.Net.Api.Types /// [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.ERROR)] - public sealed class Error : IXmlSerializable, IEquatable + public sealed class Error : IXmlSerializable, IJsonSerializable, IEquatable { /// /// @@ -81,7 +83,30 @@ public void ReadXml(XmlReader reader) public void WriteXml(XmlWriter writer) { } #endregion - + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public void ReadJson(JsonReader reader) + { + if (reader.TokenType == JsonToken.PropertyName) + { + reader.Read(); + } + + if (reader.TokenType == JsonToken.String) + { + Info = (string)reader.Value; + } + } + + /// + /// + /// + /// + public void WriteJson(JsonWriter writer) { } + #endregion #region Implementation of IEquatable diff --git a/src/redmine-net-api/Types/File.cs b/src/redmine-net-api/Types/File.cs index 294e9269..f8d017cf 100644 --- a/src/redmine-net-api/Types/File.cs +++ b/src/redmine-net-api/Types/File.cs @@ -22,6 +22,8 @@ limitations under the License. using System.Diagnostics; using System.Xml; using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api.Types { @@ -140,7 +142,63 @@ public override void WriteXml(XmlWriter writer) } #endregion - + #region Implementation of IJsonSerializable + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt32().GetValueOrDefault(); break; + case RedmineKeys.AUTHOR: Author = new IdentifiableName(reader); break; + case RedmineKeys.CONTENT_TYPE: ContentType = reader.ReadAsString(); break; + case RedmineKeys.CONTENT_URL: ContentUrl = reader.ReadAsString(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadAsString(); break; + case RedmineKeys.DIGEST: Digest = reader.ReadAsString(); break; + case RedmineKeys.DOWNLOADS: Downloads = reader.ReadAsInt32().GetValueOrDefault(); break; + case RedmineKeys.FILENAME: Filename = reader.ReadAsString(); break; + case RedmineKeys.FILE_SIZE: FileSize = reader.ReadAsInt32().GetValueOrDefault(); break; + case RedmineKeys.TOKEN: Token = reader.ReadAsString(); break; + case RedmineKeys.VERSION: Version = new IdentifiableName(reader); break; + case RedmineKeys.VERSION_ID: Version = new IdentifiableName() { Id = reader.ReadAsInt32().GetValueOrDefault() }; break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteJson(JsonWriter writer) + { + using (new JsonObject(writer, RedmineKeys.FILE)) + { + using (new JsonObject(writer)) + { + writer.WriteProperty(RedmineKeys.TOKEN, Token); + writer.WriteIdIfNotNull(RedmineKeys.VERSION_ID, Version); + writer.WriteProperty(RedmineKeys.FILENAME, Filename); + writer.WriteProperty(RedmineKeys.DESCRIPTION, Description); + } + } + } + #endregion #region Implementation of IEquatable /// @@ -151,7 +209,7 @@ public override void WriteXml(XmlWriter writer) public override bool Equals(File other) { if (other == null) return false; - return (Id == other.Id + return Id == other.Id && Filename == other.Filename && FileSize == other.FileSize && Description == other.Description @@ -162,8 +220,7 @@ public override bool Equals(File other) && Version == other.Version && Digest == other.Digest && Downloads == other.Downloads - && Token == other.Token - ); + && Token == other.Token; } /// diff --git a/src/redmine-net-api/Types/Group.cs b/src/redmine-net-api/Types/Group.cs index 7247aff6..cd523316 100644 --- a/src/redmine-net-api/Types/Group.cs +++ b/src/redmine-net-api/Types/Group.cs @@ -19,8 +19,10 @@ limitations under the License. using System.Diagnostics; using System.Xml; using System.Xml.Serialization; +using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api.Types { @@ -69,7 +71,7 @@ public Group(string name) /// /// Generates an object from its XML representation. /// - /// The stream from which the object is deserialized. + /// The stream from which the object is deserialized. public override void ReadXml(XmlReader reader) { reader.Read(); @@ -96,17 +98,59 @@ public override void ReadXml(XmlReader reader) /// /// Converts an object into its XML representation. /// - /// The stream to which the object is serialized. + /// The stream to which the object is serialized. public override void WriteXml(XmlWriter writer) { writer.WriteElementString(RedmineKeys.NAME, Name); - //TODO: change to repeatable elements writer.WriteArrayIds(RedmineKeys.USER_IDS, Users, typeof(int), GetGroupUserId); } #endregion - + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadAsCollection(); break; + case RedmineKeys.MEMBERSHIPS: Memberships = reader.ReadAsCollection(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + case RedmineKeys.USERS: Users = reader.ReadAsCollection(); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteJson(JsonWriter writer) + { + using (new JsonObject(writer, RedmineKeys.GROUP)) + { + writer.WriteProperty(RedmineKeys.NAME, Name); + writer.WriteRepeatableElement(RedmineKeys.USER_IDS, (IEnumerable)Users); + } + } + #endregion #region Implementation of IEquatable @@ -172,7 +216,7 @@ public override int GetHashCode() /// /// /// - public int GetGroupUserId(object gu) + public static int GetGroupUserId(object gu) { return ((GroupUser)gu).Id; } diff --git a/src/redmine-net-api/Types/Identifiable.cs b/src/redmine-net-api/Types/Identifiable.cs index 1f29a548..fc29ac9c 100644 --- a/src/redmine-net-api/Types/Identifiable.cs +++ b/src/redmine-net-api/Types/Identifiable.cs @@ -20,7 +20,9 @@ limitations under the License. using System.Xml; using System.Xml.Schema; using System.Xml.Serialization; +using Newtonsoft.Json; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api.Types { @@ -29,7 +31,7 @@ namespace Redmine.Net.Api.Types /// /// [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] - public abstract class Identifiable : IXmlSerializable, IEquatable, IEquatable> where T : Identifiable + public abstract class Identifiable : IXmlSerializable, IJsonSerializable, IEquatable, IEquatable> where T : Identifiable { #region Properties /// @@ -59,7 +61,19 @@ public virtual void ReadXml(XmlReader reader) { } public virtual void WriteXml(XmlWriter writer) { } #endregion - + #region Implementation of IJsonSerializable + /// + /// + /// + /// + public virtual void ReadJson(JsonReader reader) { } + + /// + /// + /// + /// + public virtual void WriteJson(JsonWriter writer) { } + #endregion #region Implementation of IEquatable> /// diff --git a/src/redmine-net-api/Types/IdentifiableName.cs b/src/redmine-net-api/Types/IdentifiableName.cs index b90d4fd1..a9c4da21 100644 --- a/src/redmine-net-api/Types/IdentifiableName.cs +++ b/src/redmine-net-api/Types/IdentifiableName.cs @@ -14,9 +14,11 @@ You may obtain a copy of the License at limitations under the License. */ +using System; using System.Diagnostics; using System.Globalization; using System.Xml; +using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; @@ -42,14 +44,24 @@ public IdentifiableName(XmlReader reader) Initialize(reader); } - + /// + /// + /// + /// + public IdentifiableName(JsonReader reader) + { + InitializeJsonReader(reader); + } private void Initialize(XmlReader reader) { ReadXml(reader); } - + private void InitializeJsonReader(JsonReader reader) + { + ReadJson(reader); + } #region Properties /// @@ -83,7 +95,45 @@ public override void WriteXml(XmlWriter writer) #endregion - + #region Implementation of IJsonSerializable + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType == JsonToken.PropertyName) + { + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + default: reader.Read(); break; + } + } + } + } + + /// + /// + /// + /// + public override void WriteJson(JsonWriter writer) + { + writer.WriteIdIfNotNull(RedmineKeys.ID, this); + if (!Name.IsNullOrWhiteSpace()) + { + writer.WriteProperty(RedmineKeys.NAME, Name); + } + } + #endregion #region Implementation of IEquatable /// @@ -94,7 +144,7 @@ public override void WriteXml(XmlWriter writer) public override bool Equals(IdentifiableName other) { if (other == null) return false; - return (Id == other.Id && Name == other.Name); + return Id == other.Id && string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase); } /// @@ -117,6 +167,5 @@ public override int GetHashCode() /// /// private string DebuggerDisplay => $"[{nameof(IdentifiableName)}: {base.ToString()}, Name={Name}]"; - } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Issue.cs b/src/redmine-net-api/Types/Issue.cs index dbf24337..7aeb8b14 100644 --- a/src/redmine-net-api/Types/Issue.cs +++ b/src/redmine-net-api/Types/Issue.cs @@ -20,8 +20,10 @@ limitations under the License. using System.Globalization; using System.Xml; using System.Xml.Serialization; +using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api.Types { @@ -320,11 +322,11 @@ public override void WriteXml(XmlWriter writer) if (Id != 0) { - writer.WriteElementString(RedmineKeys.PRIVATE_NOTES, PrivateNotes.ToString().ToLowerInvariant()); + writer.WriteElementString(RedmineKeys.PRIVATE_NOTES, PrivateNotes.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); } writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); - writer.WriteElementString(RedmineKeys.IS_PRIVATE, IsPrivate.ToString().ToLowerInvariant()); + writer.WriteElementString(RedmineKeys.IS_PRIVATE, IsPrivate.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); writer.WriteIdIfNotNull(RedmineKeys.PROJECT_ID, Project); writer.WriteIdIfNotNull(RedmineKeys.PRIORITY_ID, Priority); @@ -349,7 +351,113 @@ public override void WriteXml(XmlWriter writer) } #endregion - + #region Implementation of IJsonSerializable + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt32().GetValueOrDefault(); break; + case RedmineKeys.ASSIGNED_TO: AssignedTo = new IdentifiableName(reader); break; + case RedmineKeys.ATTACHMENTS: Attachments = reader.ReadAsCollection(); break; + case RedmineKeys.AUTHOR: Author = new IdentifiableName(reader); break; + case RedmineKeys.CATEGORY: Category = new IdentifiableName(reader); break; + case RedmineKeys.CHANGE_SETS: ChangeSets = reader.ReadAsCollection(); break; + case RedmineKeys.CHILDREN: Children = reader.ReadAsCollection(); break; + case RedmineKeys.CLOSED_ON: ClosedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadAsCollection(); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadAsString(); break; + case RedmineKeys.DONE_RATIO: DoneRatio = (float?)reader.ReadAsDouble(); break; + case RedmineKeys.DUE_DATE: DueDate = reader.ReadAsDateTime(); break; + case RedmineKeys.ESTIMATED_HOURS: EstimatedHours = (float?)reader.ReadAsDouble(); break; + case RedmineKeys.FIXED_VERSION: FixedVersion = new IdentifiableName(reader); break; + case RedmineKeys.IS_PRIVATE: IsPrivate = reader.ReadAsBoolean().GetValueOrDefault(); break; + case RedmineKeys.JOURNALS: Journals = reader.ReadAsCollection(); break; + case RedmineKeys.NOTES: Notes = reader.ReadAsString(); break; + case RedmineKeys.PARENT: ParentIssue = new IdentifiableName(reader); break; + case RedmineKeys.PRIORITY: Priority = new IdentifiableName(reader); break; + case RedmineKeys.PRIVATE_NOTES: PrivateNotes = reader.ReadAsBoolean().GetValueOrDefault(); break; + case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; + case RedmineKeys.RELATIONS: Relations = reader.ReadAsCollection(); break; + case RedmineKeys.SPENT_HOURS: SpentHours = (float?)reader.ReadAsDouble(); break; + case RedmineKeys.START_DATE: StartDate = reader.ReadAsDateTime(); break; + case RedmineKeys.STATUS: Status = new IdentifiableName(reader); break; + case RedmineKeys.SUBJECT: Subject = reader.ReadAsString(); break; + case RedmineKeys.TOTAL_ESTIMATED_HOURS: TotalEstimatedHours = (float?)reader.ReadAsDouble(); break; + case RedmineKeys.TOTAL_SPENT_HOURS: TotalSpentHours = (float?)reader.ReadAsDouble(); break; + case RedmineKeys.TRACKER: Tracker = new IdentifiableName(reader); break; + case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.WATCHERS: Watchers = reader.ReadAsCollection(); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteJson(JsonWriter writer) + { + using (new JsonObject(writer, RedmineKeys.ISSUE)) + { + writer.WriteProperty(RedmineKeys.SUBJECT, Subject); + writer.WriteProperty(RedmineKeys.DESCRIPTION, Description); + writer.WriteProperty(RedmineKeys.NOTES, Notes); + + if (Id != 0) + { + writer.WriteProperty(RedmineKeys.PRIVATE_NOTES, PrivateNotes.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + } + + writer.WriteProperty(RedmineKeys.IS_PRIVATE, IsPrivate.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + writer.WriteIdIfNotNull(RedmineKeys.PROJECT_ID, Project); + writer.WriteIdIfNotNull(RedmineKeys.PRIORITY_ID, Priority); + writer.WriteIdIfNotNull(RedmineKeys.STATUS_ID, Status); + writer.WriteIdIfNotNull(RedmineKeys.CATEGORY_ID, Category); + writer.WriteIdIfNotNull(RedmineKeys.TRACKER_ID, Tracker); + writer.WriteIdIfNotNull(RedmineKeys.ASSIGNED_TO_ID, AssignedTo); + writer.WriteIdIfNotNull(RedmineKeys.FIXED_VERSION_ID, FixedVersion); + writer.WriteValueOrEmpty(RedmineKeys.ESTIMATED_HOURS, EstimatedHours); + + writer.WriteIdOrEmpty(RedmineKeys.PARENT_ISSUE_ID, ParentIssue); + writer.WriteDateOrEmpty(RedmineKeys.START_DATE, StartDate); + writer.WriteDateOrEmpty(RedmineKeys.DUE_DATE, DueDate); + writer.WriteDateOrEmpty(RedmineKeys.UPDATED_ON, UpdatedOn); + + if (DoneRatio != null) + { + writer.WriteProperty(RedmineKeys.DONE_RATIO, DoneRatio.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (SpentHours != null) + { + writer.WriteProperty(RedmineKeys.SPENT_HOURS, SpentHours.Value.ToString(CultureInfo.InvariantCulture)); + } + + writer.WriteArray(RedmineKeys.UPLOADS, Uploads); + writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); + + writer.WriteRepeatableElement(RedmineKeys.WATCHER_USER_IDS, (IEnumerable)Watchers); + } + } + #endregion #region Implementation of IEquatable /// @@ -360,36 +468,34 @@ public override void WriteXml(XmlWriter writer) public override bool Equals(Issue other) { if (other == null) return false; - return ( - Id == other.Id - && Project == other.Project - && Tracker == other.Tracker - && Status == other.Status - && Priority == other.Priority - && Author == other.Author - && Category == other.Category - && Subject == other.Subject - && Description == other.Description - && StartDate == other.StartDate - && DueDate == other.DueDate - && DoneRatio == other.DoneRatio - && EstimatedHours == other.EstimatedHours - && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null) - && CreatedOn == other.CreatedOn - && UpdatedOn == other.UpdatedOn - && AssignedTo == other.AssignedTo - && FixedVersion == other.FixedVersion - && Notes == other.Notes - && (Watchers != null ? Watchers.Equals(other.Watchers) : other.Watchers == null) - && ClosedOn == other.ClosedOn - && SpentHours == other.SpentHours - && PrivateNotes == other.PrivateNotes - && (Attachments != null ? Attachments.Equals(other.Attachments) : other.Attachments == null) - && (ChangeSets != null ? ChangeSets.Equals(other.ChangeSets) : other.ChangeSets == null) - && (Children != null ? Children.Equals(other.Children) : other.Children == null) - && (Journals != null ? Journals.Equals(other.Journals) : other.Journals == null) - && (Relations != null ? Relations.Equals(other.Relations) : other.Relations == null) - ); + return Id == other.Id + && Project == other.Project + && Tracker == other.Tracker + && Status == other.Status + && Priority == other.Priority + && Author == other.Author + && Category == other.Category + && Subject == other.Subject + && Description == other.Description + && StartDate == other.StartDate + && DueDate == other.DueDate + && DoneRatio == other.DoneRatio + && EstimatedHours == other.EstimatedHours + && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null) + && CreatedOn == other.CreatedOn + && UpdatedOn == other.UpdatedOn + && AssignedTo == other.AssignedTo + && FixedVersion == other.FixedVersion + && Notes == other.Notes + && (Watchers != null ? Watchers.Equals(other.Watchers) : other.Watchers == null) + && ClosedOn == other.ClosedOn + && SpentHours == other.SpentHours + && PrivateNotes == other.PrivateNotes + && (Attachments != null ? Attachments.Equals(other.Attachments) : other.Attachments == null) + && (ChangeSets != null ? ChangeSets.Equals(other.ChangeSets) : other.ChangeSets == null) + && (Children != null ? Children.Equals(other.Children) : other.Children == null) + && (Journals != null ? Journals.Equals(other.Journals) : other.Journals == null) + && (Relations != null ? Relations.Equals(other.Relations) : other.Relations == null); } /// diff --git a/src/redmine-net-api/Types/IssueCategory.cs b/src/redmine-net-api/Types/IssueCategory.cs index 57c11d92..eb850ea6 100644 --- a/src/redmine-net-api/Types/IssueCategory.cs +++ b/src/redmine-net-api/Types/IssueCategory.cs @@ -17,8 +17,10 @@ limitations under the License. using System.Diagnostics; using System.Xml; using System.Xml.Serialization; +using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api.Types { @@ -94,7 +96,50 @@ public override void WriteXml(XmlWriter writer) } #endregion - + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.ASSIGNED_TO: AssignTo = new IdentifiableName(reader); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteJson(JsonWriter writer) + { + using (new JsonObject(writer, RedmineKeys.ISSUE_CATEGORY)) + { + writer.WriteIdIfNotNull(RedmineKeys.PROJECT_ID, Project); + writer.WriteProperty(RedmineKeys.NAME, Name); + writer.WriteIdIfNotNull(RedmineKeys.ASSIGNED_TO_ID, AssignTo); + } + } + #endregion #region Implementation of IEquatable /// @@ -105,7 +150,7 @@ public override void WriteXml(XmlWriter writer) public override bool Equals(IssueCategory other) { if (other == null) return false; - return (Id == other.Id && Project == other.Project && AssignTo == other.AssignTo && Name == other.Name); + return Id == other.Id && Project == other.Project && AssignTo == other.AssignTo && Name == other.Name; } /// diff --git a/src/redmine-net-api/Types/IssueChild.cs b/src/redmine-net-api/Types/IssueChild.cs index b498c3a4..5115ab33 100644 --- a/src/redmine-net-api/Types/IssueChild.cs +++ b/src/redmine-net-api/Types/IssueChild.cs @@ -19,6 +19,8 @@ limitations under the License. using System.Globalization; using System.Xml; using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; namespace Redmine.Net.Api.Types @@ -72,7 +74,35 @@ public override void ReadXml(XmlReader reader) } #endregion - + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.SUBJECT: Subject = reader.ReadAsString(); break; + case RedmineKeys.TRACKER: Tracker = new IdentifiableName(reader); break; + default: reader.Read(); break; + } + } + } + #endregion #region Implementation of IEquatable /// @@ -83,7 +113,7 @@ public override void ReadXml(XmlReader reader) public override bool Equals(IssueChild other) { if (other == null) return false; - return (Id == other.Id && Tracker == other.Tracker && Subject == other.Subject); + return Id == other.Id && Tracker == other.Tracker && Subject == other.Subject; } /// diff --git a/src/redmine-net-api/Types/IssueCustomField.cs b/src/redmine-net-api/Types/IssueCustomField.cs index 54ddec94..cf1c82ca 100644 --- a/src/redmine-net-api/Types/IssueCustomField.cs +++ b/src/redmine-net-api/Types/IssueCustomField.cs @@ -20,6 +20,7 @@ limitations under the License. using System.Globalization; using System.Xml; using System.Xml.Serialization; +using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; @@ -122,7 +123,80 @@ public override void WriteXml(XmlWriter writer) } #endregion - + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void WriteJson(JsonWriter writer) + { + if (Values == null) + { + return; + } + + var itemsCount = Values.Count; + + writer.WriteStartObject(); + writer.WriteProperty(RedmineKeys.ID, Id); + writer.WriteProperty(RedmineKeys.NAME, Name); + + if (itemsCount > 1) + { + writer.WritePropertyName(RedmineKeys.VALUE); + writer.WriteStartArray(); + foreach (var cfv in Values) + { + writer.WriteValue(cfv.Info); + } + writer.WriteEndArray(); + + writer.WriteProperty(RedmineKeys.MULTIPLE, Multiple.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + } + else + { + writer.WriteProperty(RedmineKeys.VALUE, itemsCount > 0 ? Values[0].Info : null); + } + + writer.WriteEndObject(); + } + + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.MULTIPLE: Multiple = reader.ReadAsBool(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + case RedmineKeys.VALUE: + reader.Read(); + switch (reader.TokenType) + { + case JsonToken.Null: break; + case JsonToken.StartArray: + Values = reader.ReadAsCollection(); + break; + default: + Values = new List { new CustomFieldValue { Info = reader.Value as string } }; + break; + } + break; + } + } + } + + #endregion #region Implementation of IEquatable /// @@ -133,10 +207,10 @@ public override void WriteXml(XmlWriter writer) public bool Equals(IssueCustomField other) { if (other == null) return false; - return (Id == other.Id + return Id == other.Id && Name == other.Name && Multiple == other.Multiple - && (Values != null ? Values.Equals(other.Values) : other.Values == null)); + && (Values != null ? Values.Equals(other.Values) : other.Values == null); } /// @@ -182,7 +256,7 @@ public object Clone() /// /// /// - public string GetValue(object item) + public static string GetValue(object item) { return ((CustomFieldValue)item).Info; } @@ -193,5 +267,14 @@ public string GetValue(object item) /// private string DebuggerDisplay => $"[{nameof(IssueCustomField)}: {ToString()} Values={Values.Dump()}, Multiple={Multiple.ToString(CultureInfo.InvariantCulture)}]"; + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + return Equals(obj as IssueCustomField); + } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/IssuePriority.cs b/src/redmine-net-api/Types/IssuePriority.cs index 30a5846d..8eabc866 100644 --- a/src/redmine-net-api/Types/IssuePriority.cs +++ b/src/redmine-net-api/Types/IssuePriority.cs @@ -19,6 +19,8 @@ limitations under the License. using System.Globalization; using System.Xml; using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; namespace Redmine.Net.Api.Types @@ -42,7 +44,7 @@ public sealed class IssuePriority : IdentifiableName, IEquatable /// /// Generates an object from its XML representation. /// - /// The stream from which the object is deserialized. + /// The stream from which the object is deserialized. public override void ReadXml(XmlReader reader) { reader.Read(); @@ -65,7 +67,36 @@ public override void ReadXml(XmlReader reader) } #endregion - + #region Implementation of IJsonSerialization + + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.IS_DEFAULT: IsDefault = reader.ReadAsBool(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + default: reader.Read(); break; + } + } + } + #endregion #region Implementation of IEquatable /// diff --git a/src/redmine-net-api/Types/IssueRelation.cs b/src/redmine-net-api/Types/IssueRelation.cs index 653d0e97..a6bae1f1 100644 --- a/src/redmine-net-api/Types/IssueRelation.cs +++ b/src/redmine-net-api/Types/IssueRelation.cs @@ -19,8 +19,10 @@ limitations under the License. using System.Globalization; using System.Xml; using System.Xml.Serialization; +using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api.Types { @@ -129,7 +131,53 @@ public override void WriteXml(XmlWriter writer) } #endregion - + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void WriteJson(JsonWriter writer) + { + using (new JsonObject(writer, RedmineKeys.RELATION)) + { + writer.WriteProperty(RedmineKeys.ISSUE_TO_ID, IssueToId); + writer.WriteProperty(RedmineKeys.RELATION_TYPE, Type); + if (Type == IssueRelationType.Precedes || Type == IssueRelationType.Follows) + { + writer.WriteValueOrEmpty(RedmineKeys.DELAY, Delay); + } + } + } + + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.DELAY: Delay = reader.ReadAsInt32(); break; + case RedmineKeys.ISSUE_ID: IssueId = reader.ReadAsInt(); break; + case RedmineKeys.ISSUE_TO_ID: IssueToId = reader.ReadAsInt(); break; + case RedmineKeys.RELATION_TYPE: Type = (IssueRelationType)reader.ReadAsInt(); break; + } + } + } + #endregion #region Implementation of IEquatable /// @@ -140,7 +188,7 @@ public override void WriteXml(XmlWriter writer) public override bool Equals(IssueRelation other) { if (other == null) return false; - return (Id == other.Id && IssueId == other.IssueId && IssueToId == other.IssueToId && Type == other.Type && Delay == other.Delay); + return Id == other.Id && IssueId == other.IssueId && IssueToId == other.IssueToId && Type == other.Type && Delay == other.Delay; } /// @@ -169,7 +217,7 @@ public override int GetHashCode() private string DebuggerDisplay => $@"[{nameof(IssueRelation)}: {ToString()}, IssueId={IssueId.ToString(CultureInfo.InvariantCulture)}, IssueToId={IssueToId.ToString(CultureInfo.InvariantCulture)}, -Type={Type.ToString("G")}, +Type={Type:G}, Delay={Delay?.ToString(CultureInfo.InvariantCulture)}]"; } diff --git a/src/redmine-net-api/Types/IssueStatus.cs b/src/redmine-net-api/Types/IssueStatus.cs index 1ae4f930..f6e28201 100644 --- a/src/redmine-net-api/Types/IssueStatus.cs +++ b/src/redmine-net-api/Types/IssueStatus.cs @@ -19,6 +19,8 @@ limitations under the License. using System.Globalization; using System.Xml; using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; namespace Redmine.Net.Api.Types @@ -74,7 +76,36 @@ public override void ReadXml(XmlReader reader) } #endregion - + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.IS_CLOSED: IsClosed = reader.ReadAsBool(); break; + case RedmineKeys.IS_DEFAULT: IsDefault = reader.ReadAsBool(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + default: reader.Read(); break; + } + } + } + #endregion #region Implementation of IEquatable /// @@ -85,7 +116,7 @@ public override void ReadXml(XmlReader reader) public bool Equals(IssueStatus other) { if (other == null) return false; - return (Id == other.Id && Name == other.Name && IsClosed == other.IsClosed && IsDefault == other.IsDefault); + return Id == other.Id && Name == other.Name && IsClosed == other.IsClosed && IsDefault == other.IsDefault; } /// diff --git a/src/redmine-net-api/Types/Journal.cs b/src/redmine-net-api/Types/Journal.cs index 047851c4..c7cd1950 100644 --- a/src/redmine-net-api/Types/Journal.cs +++ b/src/redmine-net-api/Types/Journal.cs @@ -20,6 +20,7 @@ limitations under the License. using System.Globalization; using System.Xml; using System.Xml.Serialization; +using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; @@ -102,7 +103,40 @@ public override void ReadXml(XmlReader reader) } #endregion - + #region Implementation of IJsonSerialization + + + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.DETAILS: Details = reader.ReadAsCollection(); break; + case RedmineKeys.NOTES: Notes = reader.ReadAsString(); break; + case RedmineKeys.PRIVATE_NOTES: PrivateNotes = reader.ReadAsBool(); break; + case RedmineKeys.USER: User = new IdentifiableName(reader); break; + default: reader.Read(); break; + } + } + } + #endregion #region Implementation of IEquatable /// diff --git a/src/redmine-net-api/Types/Membership.cs b/src/redmine-net-api/Types/Membership.cs index ea8551cd..956651a9 100644 --- a/src/redmine-net-api/Types/Membership.cs +++ b/src/redmine-net-api/Types/Membership.cs @@ -18,6 +18,7 @@ limitations under the License. using System.Diagnostics; using System.Xml; using System.Xml.Serialization; +using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; @@ -71,7 +72,35 @@ public override void ReadXml(XmlReader reader) } #endregion - + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; + case RedmineKeys.ROLES: Roles = reader.ReadAsCollection(); break; + default: reader.Read(); break; + } + } + } + #endregion #region Implementation of IEquatable /// @@ -82,10 +111,9 @@ public override void ReadXml(XmlReader reader) public override bool Equals(Membership other) { if (other == null) return false; - return ( - Id == other.Id && + return Id == other.Id && Project != null ? Project.Equals(other.Project) : other.Project == null && - Roles != null ? Roles.Equals(other.Roles) : other.Roles == null); + Roles != null ? Roles.Equals(other.Roles) : other.Roles == null; } /// diff --git a/src/redmine-net-api/Types/MembershipRole.cs b/src/redmine-net-api/Types/MembershipRole.cs index 65586fab..0b16f7d5 100644 --- a/src/redmine-net-api/Types/MembershipRole.cs +++ b/src/redmine-net-api/Types/MembershipRole.cs @@ -19,6 +19,7 @@ limitations under the License. using System.Globalization; using System.Xml; using System.Xml.Serialization; +using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; @@ -64,7 +65,45 @@ public override void WriteXml(XmlWriter writer) } #endregion - + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.INHERITED: Inherited = reader.ReadAsBool(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteJson(JsonWriter writer) + { + writer.WriteProperty(RedmineKeys.ID, Id.ToString(CultureInfo.InvariantCulture)); + } + + #endregion #region Implementation of IEquatable /// diff --git a/src/redmine-net-api/Types/News.cs b/src/redmine-net-api/Types/News.cs index abb4a7ec..3e738072 100644 --- a/src/redmine-net-api/Types/News.cs +++ b/src/redmine-net-api/Types/News.cs @@ -19,6 +19,7 @@ limitations under the License. using System.Globalization; using System.Xml; using System.Xml.Serialization; +using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; @@ -100,7 +101,39 @@ public override void ReadXml(XmlReader reader) } #endregion - + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.AUTHOR: Author = new IdentifiableName(reader); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadAsString(); break; + case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; + case RedmineKeys.SUMMARY: Summary = reader.ReadAsString(); break; + case RedmineKeys.TITLE: Title = reader.ReadAsString(); break; + default: reader.Read(); break; + } + } + } + #endregion #region Implementation of IEquatable /// @@ -111,13 +144,13 @@ public override void ReadXml(XmlReader reader) public override bool Equals(News other) { if (other == null) return false; - return (Id == other.Id + return Id == other.Id && Project == other.Project && Author == other.Author - && Title == other.Title - && Summary == other.Summary - && Description == other.Description - && CreatedOn == other.CreatedOn); + && string.Equals(Title,other.Title,StringComparison.OrdinalIgnoreCase) + && string.Equals(Summary, other.Summary, StringComparison.OrdinalIgnoreCase) + && string.Equals(Description, other.Description, StringComparison.OrdinalIgnoreCase) + && CreatedOn == other.CreatedOn; } /// diff --git a/src/redmine-net-api/Types/Permission.cs b/src/redmine-net-api/Types/Permission.cs index 656b6daf..185157c1 100644 --- a/src/redmine-net-api/Types/Permission.cs +++ b/src/redmine-net-api/Types/Permission.cs @@ -19,7 +19,9 @@ limitations under the License. using System.Xml; using System.Xml.Schema; using System.Xml.Serialization; +using Newtonsoft.Json; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api.Types { @@ -28,7 +30,7 @@ namespace Redmine.Net.Api.Types /// [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.PERMISSION)] - public sealed class Permission : IXmlSerializable, IEquatable + public sealed class Permission : IXmlSerializable, IJsonSerializable, IEquatable { #region Properties /// @@ -76,7 +78,39 @@ public void WriteXml(XmlWriter writer) { } #endregion - + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.PERMISSION: Info = reader.ReadAsString(); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public void WriteJson(JsonWriter writer) { } + #endregion #region Implementation of IEquatable /// diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs index 6bf01248..fd3dece0 100644 --- a/src/redmine-net-api/Types/Project.cs +++ b/src/redmine-net-api/Types/Project.cs @@ -20,8 +20,10 @@ limitations under the License. using System.Globalization; using System.Xml; using System.Xml.Serialization; +using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api.Types { @@ -141,7 +143,7 @@ public sealed class Project : IdentifiableName, IEquatable /// /// Generates an object from its XML representation. /// - /// The stream from which the object is deserialized. + /// The stream from which the object is deserialized. public override void ReadXml(XmlReader reader) { reader.Read(); @@ -204,7 +206,78 @@ public override void WriteXml(XmlWriter writer) } #endregion - + #region Implementation of IJsonSerialization + + + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadAsCollection(); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadAsString(); break; + case RedmineKeys.ENABLED_MODULES: EnabledModules = reader.ReadAsCollection(); break; + case RedmineKeys.HOMEPAGE: HomePage = reader.ReadAsString(); break; + case RedmineKeys.IDENTIFIER: Identifier = reader.ReadAsString(); break; + case RedmineKeys.INHERIT_MEMBERS: InheritMembers = reader.ReadAsBool(); break; + case RedmineKeys.IS_PUBLIC: IsPublic = reader.ReadAsBool(); break; + case RedmineKeys.ISSUE_CATEGORIES: IssueCategories = reader.ReadAsCollection(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + case RedmineKeys.PARENT: Parent = new IdentifiableName(reader); break; + case RedmineKeys.STATUS: Status = (ProjectStatus)reader.ReadAsInt(); break; + case RedmineKeys.TIME_ENTRY_ACTIVITIES: TimeEntryActivities = reader.ReadAsCollection(); break; + case RedmineKeys.TRACKERS: Trackers = reader.ReadAsCollection(); break; + case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadAsDateTime(); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteJson(JsonWriter writer) + { + using (new JsonObject(writer, RedmineKeys.PROJECT)) + { + writer.WriteProperty(RedmineKeys.NAME, Name); + writer.WriteProperty(RedmineKeys.IDENTIFIER, Identifier); + writer.WriteIfNotDefaultOrNull(RedmineKeys.DESCRIPTION, Description); + writer.WriteIfNotDefaultOrNull(RedmineKeys.HOMEPAGE, HomePage); + writer.WriteIfNotDefaultOrNull(RedmineKeys.INHERIT_MEMBERS, InheritMembers); + writer.WriteIfNotDefaultOrNull(RedmineKeys.IS_PUBLIC, IsPublic); + writer.WriteIdIfNotNull(RedmineKeys.PARENT_ID, Parent); + writer.WriteRepeatableElement(RedmineKeys.TRACKER_IDS, (IEnumerable)Trackers); + writer.WriteRepeatableElement(RedmineKeys.ENABLED_MODULE_NAMES, (IEnumerable)EnabledModules); + + if (Id == 0) + { + writer.WriteRepeatableElement(RedmineKeys.ISSUE_CUSTOM_FIELD_IDS, (IEnumerable)CustomFields); + return; + } + + writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); + } + } + #endregion #region Implementation of IEquatable /// @@ -219,12 +292,11 @@ public bool Equals(Project other) return false; } - return ( - Id == other.Id - && string.Equals(Identifier,other.Identifier, StringComparison.OrdinalIgnoreCase) - && string.Equals(Description,other.Description, StringComparison.OrdinalIgnoreCase) + return Id == other.Id + && string.Equals(Identifier, other.Identifier, StringComparison.OrdinalIgnoreCase) + && string.Equals(Description, other.Description, StringComparison.OrdinalIgnoreCase) && (Parent != null ? Parent.Equals(other.Parent) : other.Parent == null) - && (HomePage != null ? string.Equals(HomePage,other.HomePage, StringComparison.OrdinalIgnoreCase) : other.HomePage == null) + && string.Equals(HomePage, other.HomePage, StringComparison.OrdinalIgnoreCase) && CreatedOn == other.CreatedOn && UpdatedOn == other.UpdatedOn && Status == other.Status @@ -234,8 +306,7 @@ public bool Equals(Project other) && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null) && (IssueCategories != null ? IssueCategories.Equals(other.IssueCategories) : other.IssueCategories == null) && (EnabledModules != null ? EnabledModules.Equals(other.EnabledModules) : other.EnabledModules == null) - && (TimeEntryActivities != null ? TimeEntryActivities.Equals(other.TimeEntryActivities) : other.TimeEntryActivities == null) - ); + && (TimeEntryActivities != null ? TimeEntryActivities.Equals(other.TimeEntryActivities) : other.TimeEntryActivities == null); } /// @@ -275,7 +346,7 @@ public override int GetHashCode() $@"[Project: {ToString()}, Identifier={Identifier}, Description={Description}, Parent={Parent}, HomePage={HomePage}, CreatedOn={CreatedOn?.ToString("u", CultureInfo.InvariantCulture)}, UpdatedOn={UpdatedOn?.ToString("u", CultureInfo.InvariantCulture)}, -Status={Status.ToString("G")}, +Status={Status:G}, IsPublic={IsPublic.ToString(CultureInfo.InvariantCulture)}, InheritMembers={InheritMembers.ToString(CultureInfo.InvariantCulture)}, Trackers={Trackers.Dump()}, @@ -284,5 +355,14 @@ public override int GetHashCode() EnabledModules={EnabledModules.Dump()}, TimeEntryActivities = {TimeEntryActivities.Dump()}]"; + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + return Equals(obj as Project); + } } } diff --git a/src/redmine-net-api/Types/ProjectEnabledModule.cs b/src/redmine-net-api/Types/ProjectEnabledModule.cs index a39d8122..055703c8 100644 --- a/src/redmine-net-api/Types/ProjectEnabledModule.cs +++ b/src/redmine-net-api/Types/ProjectEnabledModule.cs @@ -42,7 +42,7 @@ public ProjectEnabledModule(string moduleName) { if (moduleName.IsNullOrWhiteSpace()) { - throw new ArgumentException(nameof(moduleName)); + throw new ArgumentException("The module name should be one of: boards, calendar, documents, files, gant, issue_tracking, news, repository, time_tracking, wiki.", nameof(moduleName)); } Name = moduleName; diff --git a/src/redmine-net-api/Types/ProjectMembership.cs b/src/redmine-net-api/Types/ProjectMembership.cs index 1cc92014..db571eda 100644 --- a/src/redmine-net-api/Types/ProjectMembership.cs +++ b/src/redmine-net-api/Types/ProjectMembership.cs @@ -18,8 +18,10 @@ limitations under the License. using System.Diagnostics; using System.Xml; using System.Xml.Serialization; +using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api.Types { @@ -105,7 +107,50 @@ public override void WriteXml(XmlWriter writer) } #endregion - + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.GROUP: Group = new IdentifiableName(reader); break; + case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; + case RedmineKeys.ROLES: Roles = reader.ReadAsCollection(); break; + case RedmineKeys.USER: User = new IdentifiableName(reader); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteJson(JsonWriter writer) + { + using (new JsonObject(writer, RedmineKeys.MEMBERSHIP)) + { + writer.WriteIdIfNotNull(RedmineKeys.USER_ID, User); + writer.WriteRepeatableElement(RedmineKeys.ROLE_IDS, (IEnumerable)Roles); + } + } + #endregion #region Implementation of IEquatable /// @@ -116,11 +161,11 @@ public override void WriteXml(XmlWriter writer) public override bool Equals(ProjectMembership other) { if (other == null) return false; - return (Id == other.Id + return Id == other.Id && Project.Equals(other.Project) && Roles.Equals(other.Roles) && (User != null ? User.Equals(other.User) : other.User == null) - && (Group != null ? Group.Equals(other.Group) : other.Group == null)); + && (Group != null ? Group.Equals(other.Group) : other.Group == null); } /// diff --git a/src/redmine-net-api/Types/Query.cs b/src/redmine-net-api/Types/Query.cs index 0c70a40a..53e3c8e4 100644 --- a/src/redmine-net-api/Types/Query.cs +++ b/src/redmine-net-api/Types/Query.cs @@ -19,6 +19,7 @@ limitations under the License. using System.Globalization; using System.Xml; using System.Xml.Serialization; +using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; @@ -73,7 +74,37 @@ public override void ReadXml(XmlReader reader) } #endregion - + #region Implementation of IJsonSerialization + + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.IS_PUBLIC: IsPublic = reader.ReadAsBool(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + case RedmineKeys.PROJECT_ID: ProjectId = reader.ReadAsInt32(); break; + default: reader.Read(); break; + } + } + } + #endregion #region Implementation of IEquatable /// @@ -85,7 +116,7 @@ public bool Equals(Query other) { if (other == null) return false; - return (other.Id == Id && other.Name == Name && other.IsPublic == IsPublic && other.ProjectId == ProjectId); + return other.Id == Id && other.Name == Name && other.IsPublic == IsPublic && other.ProjectId == ProjectId; } /// @@ -112,5 +143,14 @@ public override int GetHashCode() /// private string DebuggerDisplay => $"[{nameof(Query)}: {ToString()}, IsPublic={IsPublic.ToString(CultureInfo.InvariantCulture)}, ProjectId={ProjectId?.ToString(CultureInfo.InvariantCulture)}]"; + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + return Equals(obj as Query); + } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Role.cs b/src/redmine-net-api/Types/Role.cs index 777e9566..c0e544d5 100644 --- a/src/redmine-net-api/Types/Role.cs +++ b/src/redmine-net-api/Types/Role.cs @@ -19,6 +19,7 @@ limitations under the License. using System.Diagnostics; using System.Xml; using System.Xml.Serialization; +using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; @@ -68,7 +69,36 @@ public override void ReadXml(XmlReader reader) } #endregion - + #region Implementation of IJsonSerialization + + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + case RedmineKeys.PERMISSIONS: Permissions = reader.ReadAsCollection(); break; + default: reader.Read(); break; + } + } + } + #endregion #region Implementation of IEquatable /// diff --git a/src/redmine-net-api/Types/TimeEntry.cs b/src/redmine-net-api/Types/TimeEntry.cs index 348642fd..29770f01 100644 --- a/src/redmine-net-api/Types/TimeEntry.cs +++ b/src/redmine-net-api/Types/TimeEntry.cs @@ -20,8 +20,10 @@ limitations under the License. using System.Globalization; using System.Xml; using System.Xml.Serialization; +using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api.Types @@ -156,7 +158,64 @@ public override void WriteXml(XmlWriter writer) } #endregion - + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.ACTIVITY: Activity = new IdentifiableName(reader); break; + case RedmineKeys.ACTIVITY_ID: Activity = new IdentifiableName(reader); break; + case RedmineKeys.COMMENTS: Comments = reader.ReadAsString(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadAsCollection(); break; + case RedmineKeys.HOURS: Hours = reader.ReadAsDecimal().GetValueOrDefault(); break; + case RedmineKeys.ISSUE: Issue = new IdentifiableName(reader); break; + case RedmineKeys.ISSUE_ID: Issue = new IdentifiableName(reader); break; + case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; + case RedmineKeys.PROJECT_ID: Project = new IdentifiableName(reader); break; + case RedmineKeys.SPENT_ON: SpentOn = reader.ReadAsDateTime(); break; + case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.USER: User = new IdentifiableName(reader); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteJson(JsonWriter writer) + { + using (new JsonObject(writer, RedmineKeys.TIME_ENTRY)) + { + writer.WriteIdIfNotNull(RedmineKeys.ISSUE_ID, Issue); + writer.WriteIdIfNotNull(RedmineKeys.PROJECT_ID, Project); + writer.WriteIdIfNotNull(RedmineKeys.ACTIVITY_ID, Activity); + writer.WriteDateOrEmpty(RedmineKeys.SPENT_ON, SpentOn.GetValueOrDefault(DateTime.Now)); + writer.WriteProperty(RedmineKeys.HOURS, Hours); + writer.WriteProperty(RedmineKeys.COMMENTS, Comments); + writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); + } + } + #endregion #region Implementation of IEquatable /// @@ -167,7 +226,7 @@ public override void WriteXml(XmlWriter writer) public override bool Equals(TimeEntry other) { if (other == null) return false; - return (Id == other.Id + return Id == other.Id && Issue == other.Issue && Project == other.Project && SpentOn == other.SpentOn @@ -177,7 +236,7 @@ public override bool Equals(TimeEntry other) && User == other.User && CreatedOn == other.CreatedOn && UpdatedOn == other.UpdatedOn - && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null)); + && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null); } /// @@ -213,12 +272,9 @@ public object Clone() { var timeEntry = new TimeEntry { - Activity = Activity - , - Comments = Comments - , - Hours = Hours - , + Activity = Activity, + Comments = Comments, + Hours = Hours, Issue = Issue, Project = Project, SpentOn = SpentOn, @@ -234,7 +290,7 @@ public object Clone() /// /// private string DebuggerDisplay => - $@"[{nameof(TimeEntry)}: {ToString()}, Issue={Issue}, Project={Project}, + $@"[{nameof(TimeEntry)}: {ToString()}, Issue={Issue}, Project={Project}, SpentOn={SpentOn?.ToString("u", CultureInfo.InvariantCulture)}, Hours={Hours.ToString("F", CultureInfo.InvariantCulture)}, Activity={Activity}, diff --git a/src/redmine-net-api/Types/TimeEntryActivity.cs b/src/redmine-net-api/Types/TimeEntryActivity.cs index 8a00d9d7..ad0f9547 100644 --- a/src/redmine-net-api/Types/TimeEntryActivity.cs +++ b/src/redmine-net-api/Types/TimeEntryActivity.cs @@ -19,6 +19,8 @@ limitations under the License. using System.Globalization; using System.Xml; using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; namespace Redmine.Net.Api.Types @@ -53,7 +55,7 @@ internal TimeEntryActivity(int id, string name) /// /// Generates an object from its XML representation. /// - /// The stream from which the object is deserialized. + /// The stream from which the object is deserialized. public override void ReadXml(XmlReader reader) { reader.Read(); @@ -83,7 +85,35 @@ public override void WriteXml(XmlWriter writer) { } #endregion - + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.IS_DEFAULT: IsDefault = reader.ReadAsBool(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + default: reader.Read(); break; + } + } + } + #endregion #region Implementation of IEquatable diff --git a/src/redmine-net-api/Types/Tracker.cs b/src/redmine-net-api/Types/Tracker.cs index 6d0dea10..a63c7bc7 100644 --- a/src/redmine-net-api/Types/Tracker.cs +++ b/src/redmine-net-api/Types/Tracker.cs @@ -18,6 +18,8 @@ limitations under the License. using System.Diagnostics; using System.Xml; using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; namespace Redmine.Net.Api.Types @@ -33,7 +35,7 @@ public class Tracker : IdentifiableName, IEquatable /// /// Generates an object from its XML representation. /// - /// The stream from which the object is deserialized. + /// The stream from which the object is deserialized. public override void ReadXml(XmlReader reader) { reader.Read(); @@ -55,7 +57,35 @@ public override void ReadXml(XmlReader reader) } #endregion - + #region Implementation of IJsonSerialization + + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + default: reader.Read(); break; + } + } + } + #endregion #region Implementation of IEquatable /// diff --git a/src/redmine-net-api/Types/TrackerCustomField.cs b/src/redmine-net-api/Types/TrackerCustomField.cs index c7370915..d20ae818 100644 --- a/src/redmine-net-api/Types/TrackerCustomField.cs +++ b/src/redmine-net-api/Types/TrackerCustomField.cs @@ -17,6 +17,7 @@ limitations under the License. using System.Diagnostics; using System.Xml; using System.Xml.Serialization; +using Newtonsoft.Json; using Redmine.Net.Api.Extensions; namespace Redmine.Net.Api.Types @@ -41,7 +42,35 @@ public override void ReadXml(XmlReader reader) } #endregion - + #region Implementation of IJsonSerialization + + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + default: reader.Read(); break; + } + } + } + #endregion /// /// diff --git a/src/redmine-net-api/Types/Upload.cs b/src/redmine-net-api/Types/Upload.cs index 03c44f2c..9450442a 100644 --- a/src/redmine-net-api/Types/Upload.cs +++ b/src/redmine-net-api/Types/Upload.cs @@ -19,7 +19,10 @@ limitations under the License. using System.Xml; using System.Xml.Schema; using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api.Types { @@ -28,7 +31,7 @@ namespace Redmine.Net.Api.Types /// [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.UPLOAD)] - public sealed class Upload : IXmlSerializable, IEquatable + public sealed class Upload : IXmlSerializable, IJsonSerializable, IEquatable { #region Properties /// @@ -103,7 +106,48 @@ public void WriteXml(XmlWriter writer) } #endregion - + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.CONTENT_TYPE: ContentType = reader.ReadAsString(); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadAsString(); break; + case RedmineKeys.FILENAME: FileName = reader.ReadAsString(); break; + case RedmineKeys.TOKEN: Token = reader.ReadAsString(); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public void WriteJson(JsonWriter writer) + { + writer.WriteProperty(RedmineKeys.TOKEN, Token); + writer.WriteProperty(RedmineKeys.CONTENT_TYPE, ContentType); + writer.WriteProperty(RedmineKeys.FILENAME, FileName); + writer.WriteProperty(RedmineKeys.DESCRIPTION, Description); + } + #endregion #region Implementation of IEquatable /// @@ -116,10 +160,10 @@ public void WriteXml(XmlWriter writer) public bool Equals(Upload other) { return other != null - && string.Equals(Token,other.Token, StringComparison.OrdinalIgnoreCase) - && string.Equals(FileName,other.FileName, StringComparison.OrdinalIgnoreCase) - && string.Equals(Description,other.Description, StringComparison.OrdinalIgnoreCase) - && string.Equals(ContentType,other.ContentType, StringComparison.OrdinalIgnoreCase); + && string.Equals(Token, other.Token, StringComparison.OrdinalIgnoreCase) + && string.Equals(FileName, other.FileName, StringComparison.OrdinalIgnoreCase) + && string.Equals(Description, other.Description, StringComparison.OrdinalIgnoreCase) + && string.Equals(ContentType, other.ContentType, StringComparison.OrdinalIgnoreCase); } /// diff --git a/src/redmine-net-api/Types/User.cs b/src/redmine-net-api/Types/User.cs index 35ab0f25..90c63dc9 100644 --- a/src/redmine-net-api/Types/User.cs +++ b/src/redmine-net-api/Types/User.cs @@ -20,8 +20,10 @@ limitations under the License. using System.Globalization; using System.Xml; using System.Xml.Serialization; +using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api.Types { @@ -180,13 +182,73 @@ public override void WriteXml(XmlWriter writer) writer.WriteElementString(RedmineKeys.MAIL_NOTIFICATION, MailNotification); writer.WriteElementString(RedmineKeys.PASSWORD, Password); writer.WriteValueOrEmpty(RedmineKeys.AUTH_SOURCE_ID, AuthenticationModeId); - writer.WriteElementString(RedmineKeys.MUST_CHANGE_PASSWORD, MustChangePassword.ToString().ToLowerInvariant()); + writer.WriteElementString(RedmineKeys.MUST_CHANGE_PASSWORD, MustChangePassword.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); writer.WriteElementString(RedmineKeys.STATUS, ((int)Status).ToString(CultureInfo.InvariantCulture)); writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); } #endregion - + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.API_KEY: ApiKey = reader.ReadAsString(); break; + case RedmineKeys.AUTH_SOURCE_ID: AuthenticationModeId = reader.ReadAsInt32(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadAsCollection(); break; + case RedmineKeys.LAST_LOGIN_ON: LastLoginOn = reader.ReadAsDateTime(); break; + case RedmineKeys.LASTNAME: LastName = reader.ReadAsString(); break; + case RedmineKeys.LOGIN: Login = reader.ReadAsString(); break; + case RedmineKeys.FIRSTNAME: FirstName = reader.ReadAsString(); break; + case RedmineKeys.GROUPS: Groups = reader.ReadAsCollection(); break; + case RedmineKeys.MAIL: Email = reader.ReadAsString(); break; + case RedmineKeys.MAIL_NOTIFICATION: MailNotification = reader.ReadAsString(); break; + case RedmineKeys.MEMBERSHIPS: Memberships = reader.ReadAsCollection(); break; + case RedmineKeys.MUST_CHANGE_PASSWORD: MustChangePassword = reader.ReadAsBool(); break; + case RedmineKeys.STATUS: Status = (UserStatus)reader.ReadAsInt(); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteJson(JsonWriter writer) + { + using (new JsonObject(writer, RedmineKeys.USER)) + { + writer.WriteProperty(RedmineKeys.LOGIN, Login); + writer.WriteProperty(RedmineKeys.FIRSTNAME, FirstName); + writer.WriteProperty(RedmineKeys.LASTNAME, LastName); + writer.WriteProperty(RedmineKeys.MAIL, Email); + writer.WriteProperty(RedmineKeys.MAIL_NOTIFICATION, MailNotification); + writer.WriteProperty(RedmineKeys.PASSWORD, Password); + writer.WriteProperty(RedmineKeys.MUST_CHANGE_PASSWORD, MustChangePassword.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + writer.WriteValueOrEmpty(RedmineKeys.AUTH_SOURCE_ID, AuthenticationModeId); + writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); + } + } + #endregion #region Implementation of IEquatable /// @@ -197,14 +259,13 @@ public override void WriteXml(XmlWriter writer) public override bool Equals(User other) { if (other == null) return false; - return ( - Id == other.Id + return Id == other.Id && string.Equals(Login,other.Login, StringComparison.OrdinalIgnoreCase) && string.Equals(FirstName,other.FirstName, StringComparison.OrdinalIgnoreCase) && string.Equals(LastName,other.LastName, StringComparison.OrdinalIgnoreCase) && string.Equals(Email,other.Email, StringComparison.OrdinalIgnoreCase) && string.Equals(MailNotification,other.MailNotification, StringComparison.OrdinalIgnoreCase) - && (ApiKey != null ? string.Equals(ApiKey,other.ApiKey, StringComparison.OrdinalIgnoreCase) : other.ApiKey == null) + && string.Equals(ApiKey,other.ApiKey, StringComparison.OrdinalIgnoreCase) && AuthenticationModeId == other.AuthenticationModeId && CreatedOn == other.CreatedOn && LastLoginOn == other.LastLoginOn @@ -212,8 +273,7 @@ public override bool Equals(User other) && MustChangePassword == other.MustChangePassword && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null) && (Memberships != null ? Memberships.Equals(other.Memberships) : other.Memberships == null) - && (Groups != null ? Groups.Equals(other.Groups) : other.Groups == null) - ); + && (Groups != null ? Groups.Equals(other.Groups) : other.Groups == null); } /// @@ -256,7 +316,7 @@ public override int GetHashCode() CreatedOn={CreatedOn?.ToString("u", CultureInfo.InvariantCulture)}, LastLoginOn={LastLoginOn?.ToString("u", CultureInfo.InvariantCulture)}, ApiKey={ApiKey}, -Status={Status.ToString("G")}, +Status={Status:G}, MustChangePassword={MustChangePassword.ToString(CultureInfo.InvariantCulture)}, CustomFields={CustomFields.Dump()}, Memberships={Memberships.Dump()}, diff --git a/src/redmine-net-api/Types/Version.cs b/src/redmine-net-api/Types/Version.cs index dffbb4cb..960b7593 100644 --- a/src/redmine-net-api/Types/Version.cs +++ b/src/redmine-net-api/Types/Version.cs @@ -20,8 +20,10 @@ limitations under the License. using System.Globalization; using System.Xml; using System.Xml.Serialization; +using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api.Types { @@ -128,7 +130,58 @@ public override void WriteXml(XmlWriter writer) } #endregion - + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadAsCollection(); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadAsString(); break; + case RedmineKeys.DUE_DATE: DueDate = reader.ReadAsDateTime(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; + case RedmineKeys.SHARING: Sharing = (VersionSharing)Enum.Parse(typeof(VersionSharing), reader.ReadAsString(), true); break; + case RedmineKeys.STATUS: Status = (VersionStatus)Enum.Parse(typeof(VersionStatus), reader.ReadAsString(), true); break; + case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadAsDateTime(); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteJson(JsonWriter writer) + { + using (new JsonObject(writer, RedmineKeys.VERSION)) + { + writer.WriteProperty(RedmineKeys.NAME, Name); + writer.WriteProperty(RedmineKeys.STATUS, Status.ToString().ToLowerInvariant()); + writer.WriteProperty(RedmineKeys.SHARING, Sharing.ToString().ToLowerInvariant()); + writer.WriteProperty(RedmineKeys.DESCRIPTION, Description); + writer.WriteDateOrEmpty(RedmineKeys.DUE_DATE, DueDate); + } + } + #endregion #region Implementation of IEquatable /// @@ -139,7 +192,7 @@ public override void WriteXml(XmlWriter writer) public override bool Equals(Version other) { if (other == null) return false; - return (Id == other.Id && Name == other.Name + return Id == other.Id && Name == other.Name && Project == other.Project && Description == other.Description && Status == other.Status @@ -147,7 +200,7 @@ public override bool Equals(Version other) && Sharing == other.Sharing && CreatedOn == other.CreatedOn && UpdatedOn == other.UpdatedOn - && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null)); + && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null); } /// /// diff --git a/src/redmine-net-api/Types/WikiPage.cs b/src/redmine-net-api/Types/WikiPage.cs old mode 100755 new mode 100644 index 4a1124dd..9bc11a3b --- a/src/redmine-net-api/Types/WikiPage.cs +++ b/src/redmine-net-api/Types/WikiPage.cs @@ -16,73 +16,69 @@ limitations under the License. using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; using System.Xml; -using System.Xml.Schema; using System.Xml.Serialization; +using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api.Types { /// /// Availability 2.2 /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.WIKI_PAGE)] - public class WikiPage : Identifiable, IXmlSerializable, IEquatable + public sealed class WikiPage : Identifiable { + #region Properties /// - /// + /// Gets the title. /// - [XmlElement(RedmineKeys.TITLE)] - public string Title { get; set; } + public string Title { get; internal set; } /// - /// + /// Gets or sets the text. /// - [XmlElement(RedmineKeys.TEXT)] public string Text { get; set; } /// - /// + /// Gets or sets the comments /// - [XmlElement(RedmineKeys.COMMENTS)] public string Comments { get; set; } /// - /// + /// Gets or sets the version /// - [XmlElement(RedmineKeys.VERSION)] public int Version { get; set; } /// - /// + /// Gets the author. /// - [XmlElement(RedmineKeys.AUTHOR)] - public IdentifiableName Author { get; set; } + public IdentifiableName Author { get; internal set; } /// - /// Gets or sets the created on. + /// Gets the created on. /// /// The created on. - [XmlElement(RedmineKeys.CREATED_ON)] - public DateTime? CreatedOn { get; set; } + public DateTime? CreatedOn { get; internal set; } /// /// Gets or sets the updated on. /// /// The updated on. - [XmlElement(RedmineKeys.UPDATED_ON)] - public DateTime? UpdatedOn { get; set; } + public DateTime? UpdatedOn { get; internal set; } /// - /// Gets or sets the attachments. + /// Gets the attachments. /// /// /// The attachments. /// - [XmlArray(RedmineKeys.ATTACHMENTS)] - [XmlArrayItem(RedmineKeys.ATTACHMENT)] - public IList Attachments { get; internal set; } + public IList Attachments { get; set; } /// /// Sets the uploads. @@ -91,23 +87,16 @@ public class WikiPage : Identifiable, IXmlSerializable, IEquatable /// Availability starting with redmine version 3.3 - [XmlArray(RedmineKeys.UPLOADS)] - [XmlArrayItem(RedmineKeys.UPLOAD)] public IList Uploads { get; set; } + #endregion #region Implementation of IXmlSerializable - /// - /// - /// - /// - public XmlSchema GetSchema() { return null; } - /// /// /// /// - public void ReadXml(XmlReader reader) + public override void ReadXml(XmlReader reader) { reader.Read(); while (!reader.EOF) @@ -121,23 +110,63 @@ public void ReadXml(XmlReader reader) switch (reader.Name) { case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; - - case RedmineKeys.TITLE: Title = reader.ReadElementContentAsString(); break; - - case RedmineKeys.TEXT: Text = reader.ReadElementContentAsString(); break; - + case RedmineKeys.ATTACHMENTS: Attachments = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.AUTHOR: Author = new IdentifiableName(reader); break; case RedmineKeys.COMMENTS: Comments = reader.ReadElementContentAsString(); break; - + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.TEXT: Text = reader.ReadElementContentAsString(); break; + case RedmineKeys.TITLE: Title = reader.ReadElementContentAsString(); break; + case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadElementContentAsNullableDateTime(); break; case RedmineKeys.VERSION: Version = reader.ReadElementContentAsInt(); break; + default: reader.Read(); break; + } + } + } - case RedmineKeys.AUTHOR: Author = new IdentifiableName(reader); break; + /// + /// + /// + /// + public override void WriteXml(XmlWriter writer) + { + writer.WriteElementString(RedmineKeys.TEXT, Text); + writer.WriteElementString(RedmineKeys.COMMENTS, Comments); + writer.WriteValueOrEmpty(RedmineKeys.VERSION, Version); + writer.WriteArray(RedmineKeys.UPLOADS, Uploads); + } - case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; + #endregion - case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadElementContentAsNullableDateTime(); break; + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } - case RedmineKeys.ATTACHMENTS: Attachments = reader.ReadElementContentAsCollection(); break; + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.ATTACHMENTS: Attachments = reader.ReadAsCollection(); break; + case RedmineKeys.AUTHOR: Author = new IdentifiableName(reader); break; + case RedmineKeys.COMMENTS: Comments = reader.ReadAsString(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.TEXT: Text = reader.ReadAsString(); break; + case RedmineKeys.TITLE: Title = reader.ReadAsString(); break; + case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.VERSION: Version = reader.ReadAsInt(); break; default: reader.Read(); break; } } @@ -147,14 +176,16 @@ public void ReadXml(XmlReader reader) /// /// /// - public void WriteXml(XmlWriter writer) + public override void WriteJson(JsonWriter writer) { - writer.WriteElementString(RedmineKeys.TEXT, Text); - writer.WriteElementString(RedmineKeys.COMMENTS, Comments); - writer.WriteValueOrEmpty(Version, RedmineKeys.VERSION); - writer.WriteArray(Uploads, RedmineKeys.UPLOADS); + using (new JsonObject(writer, RedmineKeys.WIKI_PAGE)) + { + writer.WriteProperty(RedmineKeys.TEXT, Text); + writer.WriteProperty(RedmineKeys.COMMENTS, Comments); + writer.WriteValueOrEmpty(RedmineKeys.VERSION, Version); + writer.WriteArray(RedmineKeys.UPLOADS, Uploads); + } } - #endregion #region Implementation of IEquatable @@ -164,7 +195,7 @@ public void WriteXml(XmlWriter writer) /// /// /// - public bool Equals(WikiPage other) + public override bool Equals(WikiPage other) { if (other == null) return false; @@ -198,23 +229,18 @@ public override int GetHashCode() return hashCode; } } + #endregion /// /// /// /// - public override string ToString() - { - return - $"[WikiPage: {base.ToString()}, Title={Title}, Text={Text}, Comments={Comments}, Version={Version}, Author={Author}, CreatedOn={CreatedOn}, UpdatedOn={UpdatedOn}, Attachments={Attachments}]"; - } + private string DebuggerDisplay => $@"[{nameof(WikiPage)}: {ToString()}, Title={Title}, Text={Text}, Comments={Comments}, +Version={Version.ToString(CultureInfo.InvariantCulture)}, +Author={Author}, +CreatedOn={CreatedOn?.ToString("u", CultureInfo.InvariantCulture)}, +UpdatedOn={UpdatedOn?.ToString("u", CultureInfo.InvariantCulture)}, +Attachments={Attachments.Dump()}]"; - /// - public override bool Equals(object obj) - { - return Equals(obj as WikiPage); - } - - #endregion } } \ No newline at end of file From 12c09f09ebe0acb3695cbb2ad167abac639a1db0 Mon Sep 17 00:00:00 2001 From: zapadi Date: Fri, 10 Jan 2020 22:31:18 +0200 Subject: [PATCH 094/549] Serialization is dead! Long live serialization! Replaced the old serialisation access with the new one. --- .../Async/RedmineManagerAsync.cs | 3 +- .../Async/RedmineManagerAsync40.cs | 3 +- .../Async/RedmineManagerAsync45.cs | 36 +-- .../Extensions/WebExtensions.cs | 94 ++++--- src/redmine-net-api/IRedmineManager.cs | 3 +- .../Internals/RedmineSerializer.cs | 236 ------------------ src/redmine-net-api/Internals/UrlHelper.cs | 6 +- .../Internals/WebApiAsyncHelper.cs | 25 +- src/redmine-net-api/Internals/WebApiHelper.cs | 23 +- src/redmine-net-api/RedmineManager.cs | 59 +++-- .../Serialization/ISerialization.cs | 2 + src/redmine-net-api/Types/PaginatedObjects.cs | 40 --- 12 files changed, 150 insertions(+), 380 deletions(-) mode change 100755 => 100644 src/redmine-net-api/Extensions/WebExtensions.cs delete mode 100644 src/redmine-net-api/Internals/RedmineSerializer.cs mode change 100755 => 100644 src/redmine-net-api/Internals/WebApiHelper.cs delete mode 100755 src/redmine-net-api/Types/PaginatedObjects.cs diff --git a/src/redmine-net-api/Async/RedmineManagerAsync.cs b/src/redmine-net-api/Async/RedmineManagerAsync.cs index b7ee7ff7..dbea12b0 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync.cs @@ -2,6 +2,7 @@ #if NET20 using System.Collections.Generic; using System.Collections.Specialized; +using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Types; namespace Redmine.Net.Api.Async @@ -182,7 +183,7 @@ public static Task CreateObjectAsync(this RedmineManager redmineManager, T /// The redmine manager. /// The parameters. /// - public static Task> GetPaginatedObjectsAsync(this RedmineManager redmineManager, + public static Task> GetPaginatedObjectsAsync(this RedmineManager redmineManager, NameValueCollection parameters) where T : class, new() { return delegate { return redmineManager.GetPaginatedObjects(parameters); }; diff --git a/src/redmine-net-api/Async/RedmineManagerAsync40.cs b/src/redmine-net-api/Async/RedmineManagerAsync40.cs index 18506462..d658fcb0 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync40.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync40.cs @@ -21,6 +21,7 @@ limitations under the License. using System.Collections.Specialized; using System.Threading.Tasks; using Redmine.Net.Api.Types; +using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api.Async { @@ -208,7 +209,7 @@ public static Task RemoveWatcherFromIssueAsync(this RedmineManager redmineManage /// The redmine manager. /// The parameters. /// - public static Task> GetPaginatedObjectsAsync(this RedmineManager redmineManager, NameValueCollection parameters) where T : class, new() + public static Task> GetPaginatedObjectsAsync(this RedmineManager redmineManager, NameValueCollection parameters) where T : class, new() { return Task.Factory.StartNew(() => redmineManager.GetPaginatedObjects(parameters), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); } diff --git a/src/redmine-net-api/Async/RedmineManagerAsync45.cs b/src/redmine-net-api/Async/RedmineManagerAsync45.cs index be209248..635a14d7 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync45.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync45.cs @@ -16,6 +16,7 @@ limitations under the License. #if !(NET20 || NET40) +using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; @@ -24,6 +25,7 @@ limitations under the License. using System.Threading.Tasks; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Types; namespace Redmine.Net.Api.Async @@ -54,11 +56,15 @@ public static async Task GetCurrentUserAsync(this RedmineManager redmineMa /// public static async Task CreateOrUpdateWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage) { - var uri = UrlHelper.GetWikiCreateOrUpdaterUrl(redmineManager, projectId, pageName); - var data = RedmineSerializer.Serialize(wikiPage, redmineManager.MimeFormat); + var data = redmineManager.Serializer.Serialize(wikiPage); + if (string.IsNullOrEmpty(data)) return null; - var response = await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.PUT, data, "CreateOrUpdateWikiPageAsync").ConfigureAwait(false); - return RedmineSerializer.Deserialize(response, redmineManager.MimeFormat); + var url = UrlHelper.GetWikiCreateOrUpdaterUrl(redmineManager, projectId, pageName); + + url = Uri.EscapeUriString(url); + + var response = await WebApiAsyncHelper.ExecuteUpload(redmineManager, url, HttpVerbs.PUT, data, "CreateOrUpdateWikiPageAsync").ConfigureAwait(false); + return redmineManager.Serializer.Deserialize(response); } /// @@ -72,6 +78,7 @@ public static async Task DeleteWikiPageAsync(this RedmineManager redmineManager, string pageName) { var uri = UrlHelper.GetDeleteWikirUrl(redmineManager, projectId, pageName); + uri = Uri.EscapeUriString(uri); await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.DELETE, string.Empty, "DeleteWikiPageAsync").ConfigureAwait(false); } @@ -114,6 +121,7 @@ public static async Task GetWikiPageAsync(this RedmineManager redmineM NameValueCollection parameters, string pageName, uint version = 0) { var uri = UrlHelper.GetWikiPageUrl(redmineManager, projectId, pageName, version); + uri = Uri.EscapeUriString(uri); return await WebApiAsyncHelper.ExecuteDownload(redmineManager, uri, "GetWikiPageAsync", parameters).ConfigureAwait(false); } @@ -231,12 +239,12 @@ public static async Task RemoveWatcherFromIssueAsync(this RedmineManager redmine var tempResult = await GetPaginatedObjectsAsync(redmineManager,parameters).ConfigureAwait(false); if (tempResult != null) { - totalCount = tempResult.TotalCount; + totalCount = tempResult.TotalItems; } } catch (WebException wex) { - wex.HandleWebException("CountAsync", redmineManager.MimeFormat); + wex.HandleWebException(redmineManager.Serializer); } return totalCount; @@ -250,7 +258,7 @@ public static async Task RemoveWatcherFromIssueAsync(this RedmineManager redmine /// The redmine manager. /// The parameters. /// - public static async Task> GetPaginatedObjectsAsync(this RedmineManager redmineManager, + public static async Task> GetPaginatedObjectsAsync(this RedmineManager redmineManager, NameValueCollection parameters) where T : class, new() { @@ -292,12 +300,12 @@ public static async Task> GetObjectsAsync(this RedmineManager redmine { if (resultList == null) { - resultList = tempResult.Objects; - totalCount = tempResult.TotalCount; + resultList = new List(tempResult.Items); + totalCount = tempResult.TotalItems; } else { - resultList.AddRange(tempResult.Objects); + resultList.AddRange(tempResult.Items); } } offset += pageSize; @@ -305,7 +313,7 @@ public static async Task> GetObjectsAsync(this RedmineManager redmine } catch (WebException wex) { - wex.HandleWebException("GetObjectsAsync", redmineManager.MimeFormat); + wex.HandleWebException(redmineManager.Serializer); } return resultList; } @@ -350,10 +358,10 @@ public static async Task CreateObjectAsync(this RedmineManager redmineMana where T : class, new() { var uri = UrlHelper.GetCreateUrl(redmineManager, ownerId); - var data = RedmineSerializer.Serialize(entity, redmineManager.MimeFormat); + var data = redmineManager.Serializer.Serialize(entity); var response = await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.POST, data, "CreateObjectAsync").ConfigureAwait(false); - return RedmineSerializer.Deserialize(response, redmineManager.MimeFormat); + return redmineManager.Serializer.Deserialize(response); } /// @@ -368,7 +376,7 @@ public static async Task UpdateObjectAsync(this RedmineManager redmineManager where T : class, new() { var uri = UrlHelper.GetUploadUrl(redmineManager, id); - var data = RedmineSerializer.Serialize(entity, redmineManager.MimeFormat); + var data = redmineManager.Serializer.Serialize(entity); data = Regex.Replace(data, @"\r\n|\r|\n", "\r\n"); await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.PUT, data, "UpdateObjectAsync").ConfigureAwait(false); diff --git a/src/redmine-net-api/Extensions/WebExtensions.cs b/src/redmine-net-api/Extensions/WebExtensions.cs old mode 100755 new mode 100644 index 0a041489..ea219eb1 --- a/src/redmine-net-api/Extensions/WebExtensions.cs +++ b/src/redmine-net-api/Extensions/WebExtensions.cs @@ -14,27 +14,26 @@ You may obtain a copy of the License at limitations under the License. */ +using System; using System.Collections.Generic; using System.IO; - using System.Net; -using Redmine.Net.Api.Internals; using Redmine.Net.Api.Types; using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api.Extensions { /// /// /// - public static class WebExtensions + internal static class WebExceptionExtensions { /// /// Handles the web exception. /// /// The exception. - /// The method. - /// The MIME format. + /// /// Timeout! /// Bad domain name! /// @@ -45,56 +44,61 @@ public static class WebExtensions /// /// /// - public static void HandleWebException(this WebException exception, string method, MimeFormat mimeFormat) + public static void HandleWebException(this WebException exception, IRedmineSerializer serializer) { - if (exception == null) return; + if (exception == null) + { + return; + } + + var innerException = exception.InnerException ?? exception; switch (exception.Status) { - case WebExceptionStatus.Timeout: throw new RedmineTimeoutException("Timeout!", exception); - case WebExceptionStatus.NameResolutionFailure: throw new NameResolutionFailureException("Bad domain name!", exception); + case WebExceptionStatus.Timeout: + throw new RedmineTimeoutException(nameof(WebExceptionStatus.Timeout), innerException); + case WebExceptionStatus.NameResolutionFailure: + throw new NameResolutionFailureException("Bad domain name.", innerException); case WebExceptionStatus.ProtocolError: { var response = (HttpWebResponse)exception.Response; switch ((int)response.StatusCode) { + case (int)HttpStatusCode.NotFound: + throw new NotFoundException(response.StatusDescription, innerException); - case (int)HttpStatusCode.NotFound: - throw new NotFoundException (response.StatusDescription, exception); + case (int)HttpStatusCode.InternalServerError: + throw new InternalServerErrorException(response.StatusDescription, innerException); - case (int)HttpStatusCode.InternalServerError: - throw new InternalServerErrorException(response.StatusDescription, exception); + case (int)HttpStatusCode.Unauthorized: + throw new UnauthorizedException(response.StatusDescription, innerException); - case (int)HttpStatusCode.Unauthorized: - throw new UnauthorizedException(response.StatusDescription, exception); + case (int)HttpStatusCode.Forbidden: + throw new ForbiddenException(response.StatusDescription, innerException); - case (int)HttpStatusCode.Forbidden: - throw new ForbiddenException(response.StatusDescription, exception); - - case (int)HttpStatusCode.Conflict: - throw new ConflictException("The page that you are trying to update is staled!", exception); + case (int)HttpStatusCode.Conflict: + throw new ConflictException("The page that you are trying to update is staled!", innerException); case 422: - - var errors = GetRedmineExceptions(exception.Response, mimeFormat); + var errors = GetRedmineExceptions(exception.Response, serializer); var message = string.Empty; if (errors != null) { - for (var index = 0; index < errors.Count; index++) + foreach (var error in errors) { - var error = errors[index]; - message = message + (error.Info + "\n"); + message = message + error.Info + Environment.NewLine; } } - throw new RedmineException( - $"{method} has invalid or missing attribute parameters: {message}", exception); + throw new RedmineException("Invalid or missing attribute parameters: " + message, innerException); - case (int)HttpStatusCode.NotAcceptable: throw new NotAcceptableException(response.StatusDescription, exception); + case (int)HttpStatusCode.NotAcceptable: + throw new NotAcceptableException(response.StatusDescription, innerException); } } break; - default: throw new RedmineException(exception.Message, exception); + default: + throw new RedmineException(exception.Message, innerException); } } @@ -102,24 +106,36 @@ public static void HandleWebException(this WebException exception, string method /// Gets the redmine exceptions. /// /// The web response. - /// The MIME format. + /// /// - private static List GetRedmineExceptions(this WebResponse webResponse, MimeFormat mimeFormat) + private static IEnumerable GetRedmineExceptions(this WebResponse webResponse, IRedmineSerializer serializer) { - using (var dataStream = webResponse.GetResponseStream()) + using (var responseStream = webResponse.GetResponseStream()) { - if (dataStream == null) return null; - using (var reader = new StreamReader(dataStream)) + if (responseStream == null) + { + return null; + } + + using (var streamReader = new StreamReader(responseStream)) { - var responseFromServer = reader.ReadToEnd(); + var responseContent = streamReader.ReadToEnd(); - if (!responseFromServer.IsNullOrWhiteSpace()) + if (responseContent.IsNullOrWhiteSpace()) + { + return null; + } + + try + { + var result = serializer.DeserializeToPagedResults(responseContent); + return result.Items; + } + catch (Exception) { - var errors = RedmineSerializer.DeserializeList(responseFromServer, mimeFormat); - return errors.Objects; + throw; } } - return null; } } } diff --git a/src/redmine-net-api/IRedmineManager.cs b/src/redmine-net-api/IRedmineManager.cs index 71885597..e265eb02 100644 --- a/src/redmine-net-api/IRedmineManager.cs +++ b/src/redmine-net-api/IRedmineManager.cs @@ -19,6 +19,7 @@ limitations under the License. using System.Net; using System.Net.Security; using System.Security.Cryptography.X509Certificates; +using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api.Types { @@ -144,7 +145,7 @@ public interface IRedmineManager /// /// /// - PaginatedObjects GetPaginatedObjects(NameValueCollection parameters) where T : class, new(); + PagedResults GetPaginatedObjects(NameValueCollection parameters) where T : class, new(); /// /// diff --git a/src/redmine-net-api/Internals/RedmineSerializer.cs b/src/redmine-net-api/Internals/RedmineSerializer.cs deleted file mode 100644 index 188fe8ad..00000000 --- a/src/redmine-net-api/Internals/RedmineSerializer.cs +++ /dev/null @@ -1,236 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.IO; -#if !NET20 -using System.Linq; -#endif -using System.Xml; -using System.Xml.Serialization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Exceptions; - -namespace Redmine.Net.Api.Internals -{ - internal static partial class RedmineSerializer - { - private static readonly XmlWriterSettings xws = new XmlWriterSettings {OmitXmlDeclaration = true}; - /// - /// Serializes the specified System.Object and writes the XML document to a string. - /// - /// The type of objects to serialize. - /// The object to serialize. - /// - /// The System.String that contains the XML document. - /// - /// - // ReSharper disable once InconsistentNaming - private static string ToXML(T obj) where T : class - { - using (var stringWriter = new StringWriter()) - { - using (var xmlWriter = XmlWriter.Create(stringWriter, xws)) - { - var sr = new XmlSerializer(typeof (T)); - sr.Serialize(xmlWriter, obj); - return stringWriter.ToString(); - } - } - } - - /// - /// Deserializes the XML document contained by the specific System.String. - /// - /// The type of objects to deserialize. - /// The System.String that contains the XML document to deserialize. - /// - /// The T object being deserialized. - /// - /// An error occurred during deserialization. The original exception is available - /// using the System.Exception.InnerException property. - // ReSharper disable once InconsistentNaming - private static T FromXML(string xml) where T : class - { - if(xml.IsNullOrWhiteSpace()) throw new ArgumentNullException(nameof(xml)); - - using (var text = XmlTextReaderBuilder.Create(xml)) - { - var sr = new XmlSerializer(typeof (T)); - return sr.Deserialize(text) as T; - } - } - - /// - /// Serializes the specified type T and writes the XML document to a string. - /// - /// - /// The object. - /// The MIME format. - /// - /// Serialization error - public static string Serialize(T obj, MimeFormat mimeFormat) where T : class, new() - { - try - { - if (mimeFormat == MimeFormat.Json) - { -#if !NET20 - // return JsonSerializer(obj); -#endif - } - - return ToXML(obj); - } - catch (Exception ex) - { - throw new RedmineException("Serialization error", ex); - } - } - - /// - /// Deserializes the XML document contained by the specific System.String. - /// - /// - /// The response. - /// The MIME format. - /// - /// - /// Could not deserialize null! - /// or - /// Deserialization error - /// - /// - /// - /// - public static T Deserialize(string response, MimeFormat mimeFormat) where T : class, new() - { - if (string.IsNullOrEmpty(response)) throw new RedmineException("Could not deserialize null!"); - try - { - if (mimeFormat == MimeFormat.Json) - { -#if !NET20 - //var type = typeof (T); - //var jsonRoot = (string) null; - //if (type == typeof (IssueCategory)) jsonRoot = RedmineKeys.ISSUE_CATEGORY; - //if (type == typeof (IssueRelation)) jsonRoot = RedmineKeys.RELATION; - //if (type == typeof (TimeEntry)) jsonRoot = RedmineKeys.TIME_ENTRY; - //if (type == typeof (ProjectMembership)) jsonRoot = RedmineKeys.MEMBERSHIP; - //if (type == typeof (WikiPage)) jsonRoot = RedmineKeys.WIKI_PAGE; - //return JsonDeserialize(response, jsonRoot); -#endif - } - return FromXML(response); - } - catch (Exception ex) - { - throw new RedmineException("Deserialization error",ex); - } - } - - /// - /// Deserializes the list. - /// - /// - /// The response. - /// The MIME format. - /// - /// - /// Could not deserialize null! - /// or - /// Deserialization error - /// - public static PaginatedObjects DeserializeList(string response, MimeFormat mimeFormat) - where T : class, new() - { - try - { - if (response.IsNullOrWhiteSpace()) throw new RedmineException("Could not deserialize null!"); - if (mimeFormat == MimeFormat.Json) - { -#if !NET20 - // return JSonDeserializeList(response); -#endif - } - return XmlDeserializeList(response); - } - - catch (Exception ex) - { - throw new RedmineException("Deserialization error", ex); - } - } - -#if !NET20 - ///// - ///// js the son deserialize list. - ///// - ///// - ///// The response. - ///// - //private static PaginatedObjects JSonDeserializeList(string response) where T : class, new() - //{ - // var type = typeof(T); - // var jsonRoot = (string)null; - // if (type == typeof(Error)) jsonRoot = RedmineKeys.ERRORS; - // if (type == typeof(WikiPage)) jsonRoot = RedmineKeys.WIKI_PAGES; - // if (type == typeof(IssuePriority)) jsonRoot = RedmineKeys.ISSUE_PRIORITIES; - // if (type == typeof(TimeEntryActivity)) jsonRoot = RedmineKeys.TIME_ENTRY_ACTIVITIES; - - // if (string.IsNullOrEmpty(jsonRoot)) - // jsonRoot = RedmineManager.Sufixes[type]; - - // var result = JsonDeserializeToList(response, jsonRoot, out var totalItems, out var offset); - - // return new PaginatedObjects() - // { - // TotalCount = totalItems, - // Offset = offset, - // Objects = result.ToList() - // }; - //} -#endif - /// - /// XMLs the deserialize list. - /// - /// - /// The response. - /// - private static PaginatedObjects XmlDeserializeList(string response) where T : class, new() - { - using (var stringReader = new StringReader(response)) - { - using (var xmlReader = XmlTextReaderBuilder.Create(stringReader)) - { - xmlReader.Read(); - xmlReader.Read(); - - var totalItems = xmlReader.ReadAttributeAsInt(RedmineKeys.TOTAL_COUNT); - var offset = xmlReader.ReadAttributeAsInt(RedmineKeys.OFFSET); - var result = xmlReader.ReadElementContentAsCollection(); - return new PaginatedObjects() - { - TotalCount = totalItems, - Offset = offset, - Objects = result - }; - } - } - } - } -} \ No newline at end of file diff --git a/src/redmine-net-api/Internals/UrlHelper.cs b/src/redmine-net-api/Internals/UrlHelper.cs index 8bc02e5d..a188b495 100644 --- a/src/redmine-net-api/Internals/UrlHelper.cs +++ b/src/redmine-net-api/Internals/UrlHelper.cs @@ -18,6 +18,7 @@ limitations under the License. using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; +using System.Web; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Types; @@ -241,9 +242,10 @@ public static string GetWikisUrl(RedmineManager redmineManager, string projectId /// Name of the page. /// The version. /// - public static string GetWikiPageUrl(RedmineManager redmineManager, string projectId, - string pageName, uint version = 0) + public static string GetWikiPageUrl(RedmineManager redmineManager, string projectId, string pageName, uint version = 0) { + pageName = Uri.EscapeUriString(pageName); + var uri = version == 0 ? string.Format(CultureInfo.InvariantCulture,WIKI_PAGE_FORMAT, redmineManager.Host, projectId, pageName, redmineManager.Format) diff --git a/src/redmine-net-api/Internals/WebApiAsyncHelper.cs b/src/redmine-net-api/Internals/WebApiAsyncHelper.cs index 4912030a..d7e00a66 100644 --- a/src/redmine-net-api/Internals/WebApiAsyncHelper.cs +++ b/src/redmine-net-api/Internals/WebApiAsyncHelper.cs @@ -20,6 +20,7 @@ limitations under the License. using System.Text; using System.Threading.Tasks; using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Types; namespace Redmine.Net.Api.Internals @@ -53,7 +54,7 @@ public static async Task ExecuteUpload(RedmineManager redmineManager, st } catch (WebException webException) { - webException.HandleWebException(methodName, redmineManager.MimeFormat); + webException.HandleWebException(redmineManager.Serializer); } } @@ -80,11 +81,11 @@ public static async Task ExecuteDownload(RedmineManager redmineManager, st try { var response = await wc.DownloadStringTaskAsync(address).ConfigureAwait(false); - return RedmineSerializer.Deserialize(response, redmineManager.MimeFormat); + return redmineManager.Serializer.Deserialize(response); } catch (WebException webException) { - webException.HandleWebException(methodName, redmineManager.MimeFormat); + webException.HandleWebException(redmineManager.Serializer); } return default(T); } @@ -108,13 +109,13 @@ public static async Task> ExecuteDownloadList(RedmineManager redmineM try { var response = await wc.DownloadStringTaskAsync(address).ConfigureAwait(false); - var result = RedmineSerializer.DeserializeList(response, redmineManager.MimeFormat); + var result = redmineManager.Serializer.DeserializeToPagedResults(response); if (result != null) - return result.Objects; + return new List(result.Items); } catch (WebException webException) { - webException.HandleWebException(methodName, redmineManager.MimeFormat); + webException.HandleWebException(redmineManager.Serializer); } return null; } @@ -130,7 +131,7 @@ public static async Task> ExecuteDownloadList(RedmineManager redmineM /// Name of the method. /// The parameters. /// - public static async Task> ExecuteDownloadPaginatedList(RedmineManager redmineManager, string address, + public static async Task> ExecuteDownloadPaginatedList(RedmineManager redmineManager, string address, string methodName, NameValueCollection parameters = null) where T : class, new() { @@ -139,11 +140,11 @@ public static async Task> ExecuteDownloadPaginatedList(Re try { var response = await wc.DownloadStringTaskAsync(address).ConfigureAwait(false); - return RedmineSerializer.DeserializeList(response, redmineManager.MimeFormat); + return redmineManager.Serializer.DeserializeToPagedResults(response); } catch (WebException webException) { - webException.HandleWebException(methodName, redmineManager.MimeFormat); + webException.HandleWebException(redmineManager.Serializer); } return null; } @@ -166,7 +167,7 @@ public static async Task ExecuteDownloadFile(RedmineManager redmineManag } catch (WebException webException) { - webException.HandleWebException(methodName, redmineManager.MimeFormat); + webException.HandleWebException(redmineManager.Serializer); } return null; } @@ -188,11 +189,11 @@ public static async Task ExecuteUploadFile(RedmineManager redmineManager { var response = await wc.UploadDataTaskAsync(address, data).ConfigureAwait(false); var responseString = Encoding.ASCII.GetString(response); - return RedmineSerializer.Deserialize(responseString, redmineManager.MimeFormat); + return redmineManager.Serializer.Deserialize(responseString); } catch (WebException webException) { - webException.HandleWebException(methodName, redmineManager.MimeFormat); + webException.HandleWebException(redmineManager.Serializer); } return null; } diff --git a/src/redmine-net-api/Internals/WebApiHelper.cs b/src/redmine-net-api/Internals/WebApiHelper.cs old mode 100755 new mode 100644 index e5f22110..138e39a0 --- a/src/redmine-net-api/Internals/WebApiHelper.cs +++ b/src/redmine-net-api/Internals/WebApiHelper.cs @@ -18,6 +18,7 @@ limitations under the License. using System.Net; using System.Text; using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Types; namespace Redmine.Net.Api.Internals @@ -51,7 +52,7 @@ public static void ExecuteUpload(RedmineManager redmineManager, string address, } catch (WebException webException) { - webException.HandleWebException(methodName, redmineManager.MimeFormat); + webException.HandleWebException(redmineManager.Serializer); } } } @@ -78,12 +79,12 @@ public static T ExecuteUpload(RedmineManager redmineManager, string address, actionType == HttpVerbs.PATCH) { var response = wc.UploadString(address, actionType, data); - return RedmineSerializer.Deserialize(response, redmineManager.MimeFormat); + return redmineManager.Serializer.Deserialize(response); } } catch (WebException webException) { - webException.HandleWebException(methodName, redmineManager.MimeFormat); + webException.HandleWebException(redmineManager.Serializer); } return default(T); } @@ -108,11 +109,11 @@ public static T ExecuteDownload(RedmineManager redmineManager, string address { var response = wc.DownloadString(address); if (!string.IsNullOrEmpty(response)) - return RedmineSerializer.Deserialize(response, redmineManager.MimeFormat); + return redmineManager.Serializer.Deserialize(response); } catch (WebException webException) { - webException.HandleWebException(methodName, redmineManager.MimeFormat); + webException.HandleWebException(redmineManager.Serializer); } return default(T); } @@ -127,7 +128,7 @@ public static T ExecuteDownload(RedmineManager redmineManager, string address /// Name of the method. /// The parameters. /// - public static PaginatedObjects ExecuteDownloadList(RedmineManager redmineManager, string address, + public static PagedResults ExecuteDownloadList(RedmineManager redmineManager, string address, string methodName, NameValueCollection parameters = null) where T : class, new() { @@ -136,11 +137,11 @@ public static PaginatedObjects ExecuteDownloadList(RedmineManager redmineM try { var response = wc.DownloadString(address); - return RedmineSerializer.DeserializeList(response, redmineManager.MimeFormat); + return redmineManager.Serializer.DeserializeToPagedResults(response); } catch (WebException webException) { - webException.HandleWebException(methodName, redmineManager.MimeFormat); + webException.HandleWebException(redmineManager.Serializer); } return null; } @@ -163,7 +164,7 @@ public static byte[] ExecuteDownloadFile(RedmineManager redmineManager, string a } catch (WebException webException) { - webException.HandleWebException(methodName, redmineManager.MimeFormat); + webException.HandleWebException(redmineManager.Serializer); } return null; } @@ -185,11 +186,11 @@ public static Upload ExecuteUploadFile(RedmineManager redmineManager, string add { var response = wc.UploadData(address, data); var responseString = Encoding.ASCII.GetString(response); - return RedmineSerializer.Deserialize(responseString, redmineManager.MimeFormat); + return redmineManager.Serializer.Deserialize(responseString); } catch (WebException webException) { - webException.HandleWebException(methodName, redmineManager.MimeFormat); + webException.HandleWebException(redmineManager.Serializer); } return null; } diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index d0008040..cc83b910 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -23,10 +23,11 @@ limitations under the License. using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.RegularExpressions; +using System.Web; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; - +using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Types; using Group = Redmine.Net.Api.Types.Group; using Version = Redmine.Net.Api.Types.Version; @@ -64,6 +65,7 @@ public class RedmineManager : IRedmineManager {typeof(Watcher), "watchers"}, {typeof(IssueCustomField), "custom_fields"}, {typeof(CustomField), "custom_fields"} + // {typeof(WikiPage), ""} }; private static readonly Dictionary typesWithOffset = new Dictionary{ @@ -79,7 +81,7 @@ public class RedmineManager : IRedmineManager private readonly string basicAuthorization; private readonly CredentialCache cache; private string host; - + internal IRedmineSerializer Serializer { get; } /// /// Initializes a new instance of the class. /// @@ -106,7 +108,17 @@ public RedmineManager(string host, MimeFormat mimeFormat = MimeFormat.Xml, bool Host = host; MimeFormat = mimeFormat; - Format = mimeFormat == MimeFormat.Xml ? "xml" : "json"; + if (mimeFormat == MimeFormat.Xml) + { + Format = "xml"; + Serializer = new XmlRedmineSerializer(); + } + else + { + Format = "json"; + Serializer = new JsonRedmineSerializer(); + } + Proxy = proxy; SecurityProtocolType = securityProtocolType; @@ -185,10 +197,7 @@ public RedmineManager(string host, string login, string password, MimeFormat mim /// /// The sufixes. /// - public static Dictionary Sufixes - { - get { return routes; } - } + public static Dictionary Sufixes => routes; /// /// @@ -203,13 +212,12 @@ public static Dictionary Sufixes /// public string Host { - get { return host; } + get => host; private set { host = value; - Uri uriResult; - if (!Uri.TryCreate(host, UriKind.Absolute, out uriResult) || + if (!Uri.TryCreate(host, UriKind.Absolute, out Uri uriResult) || !(uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps)) { host = $"http://{host}/"; @@ -342,10 +350,13 @@ public void RemoveUserFromGroup(int groupId, int userId) /// public WikiPage CreateOrUpdateWikiPage(string projectId, string pageName, WikiPage wikiPage) { - var result = RedmineSerializer.Serialize(wikiPage, MimeFormat); + var result = Serializer.Serialize(wikiPage); if (string.IsNullOrEmpty(result)) return null; var url = UrlHelper.GetWikiCreateOrUpdaterUrl(this, projectId, pageName); + + url = Uri.EscapeUriString(url); + return WebApiHelper.ExecuteUpload(this, url, HttpVerbs.PUT, result, "CreateOrUpdateWikiPage"); } @@ -360,6 +371,7 @@ public WikiPage CreateOrUpdateWikiPage(string projectId, string pageName, WikiPa public WikiPage GetWikiPage(string projectId, NameValueCollection parameters, string pageName, uint version = 0) { var url = UrlHelper.GetWikiPageUrl(this, projectId, pageName, version); + url = Uri.EscapeUriString(url); return WebApiHelper.ExecuteDownload(this, url, "GetWikiPage", parameters); } @@ -372,7 +384,7 @@ public List GetAllWikiPages(string projectId) { var url = UrlHelper.GetWikisUrl(this, projectId); var result = WebApiHelper.ExecuteDownloadList(this, url, "GetAllWikiPages"); - return result?.Objects; + return result == null ? null :new List(result.Items); } /// @@ -384,6 +396,7 @@ public List GetAllWikiPages(string projectId) public void DeleteWikiPage(string projectId, string pageName) { var url = UrlHelper.GetDeleteWikirUrl(this, projectId, pageName); + url = Uri.EscapeUriString(url); WebApiHelper.ExecuteUpload(this, url, HttpVerbs.DELETE, string.Empty, "DeleteWikiPage"); } @@ -410,12 +423,12 @@ public void DeleteWikiPage(string projectId, string pageName) var tempResult = GetPaginatedObjects(parameters); if (tempResult != null) { - totalCount = tempResult.TotalCount; + totalCount = tempResult.TotalItems; } } catch (WebException wex) { - wex.HandleWebException("CountAsync", MimeFormat); + wex.HandleWebException(Serializer); } return totalCount; @@ -453,7 +466,7 @@ public void DeleteWikiPage(string projectId, string pageName) /// /// The parameters. /// - public PaginatedObjects GetPaginatedObjects(NameValueCollection parameters) where T : class, new() + public PagedResults GetPaginatedObjects(NameValueCollection parameters) where T : class, new() { var url = UrlHelper.GetListUrl(this, parameters); return WebApiHelper.ExecuteDownloadList(this, url, "GetObjectList", parameters); @@ -565,12 +578,12 @@ public void DeleteWikiPage(string projectId, string pageName) { if (resultList == null) { - resultList = tempResult.Objects; - totalCount = isLimitSet ? pageSize : tempResult.TotalCount; + resultList = new List(tempResult.Items); + totalCount = isLimitSet ? pageSize : tempResult.TotalItems; } else { - resultList.AddRange(tempResult.Objects); + resultList.AddRange(tempResult.Items); } } offset += pageSize; @@ -581,13 +594,13 @@ public void DeleteWikiPage(string projectId, string pageName) var result = GetPaginatedObjects(parameters); if (result != null) { - return result.Objects; + return new List(result.Items); } } } catch (WebException wex) { - wex.HandleWebException("GetObjectsAsync", MimeFormat); + wex.HandleWebException( Serializer); } return resultList; } @@ -638,7 +651,7 @@ public void DeleteWikiPage(string projectId, string pageName) public T CreateObject(T obj, string ownerId) where T : class, new() { var url = UrlHelper.GetCreateUrl(this, ownerId); - var data = RedmineSerializer.Serialize(obj, MimeFormat); + var data = Serializer.Serialize(obj); return WebApiHelper.ExecuteUpload(this, url, HttpVerbs.POST, data, "CreateObject"); } @@ -675,7 +688,7 @@ public void DeleteWikiPage(string projectId, string pageName) public void UpdateObject(string id, T obj, string projectId) where T : class, new() { var url = UrlHelper.GetUploadUrl(this, id); - var data = RedmineSerializer.Serialize(obj, MimeFormat); + var data = Serializer.Serialize(obj); data = Regex.Replace(data, @"\r\n|\r|\n", "\r\n"); WebApiHelper.ExecuteUpload(this, url, HttpVerbs.PUT, data, "UpdateObject"); } @@ -736,7 +749,7 @@ public void UpdateAttachment(int issueId, Attachment attachment) { var address = UrlHelper.GetAttachmentUpdateUrl(this, issueId); var attachments = new Attachments { { attachment.Id, attachment } }; - var data = RedmineSerializer.Serialize(attachments, MimeFormat); + var data = Serializer.Serialize(attachments); WebApiHelper.ExecuteUpload(this, address, HttpVerbs.PATCH, data, "UpdateAttachment"); } diff --git a/src/redmine-net-api/Serialization/ISerialization.cs b/src/redmine-net-api/Serialization/ISerialization.cs index 172fb95c..d571939d 100644 --- a/src/redmine-net-api/Serialization/ISerialization.cs +++ b/src/redmine-net-api/Serialization/ISerialization.cs @@ -6,6 +6,8 @@ internal interface IRedmineSerializer string Serialize(T obj) where T : class; + PagedResults DeserializeToPagedResults(string response) where T : class, new(); + T Deserialize(string response) where T : new(); } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/PaginatedObjects.cs b/src/redmine-net-api/Types/PaginatedObjects.cs deleted file mode 100755 index 2498e3a5..00000000 --- a/src/redmine-net-api/Types/PaginatedObjects.cs +++ /dev/null @@ -1,40 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System.Collections.Generic; - -namespace Redmine.Net.Api.Types -{ - /// - /// - /// - /// - public class PaginatedObjects - { - /// - /// - /// - public List Objects { get; internal set; } - /// - /// - /// - public int TotalCount { get; set; } - /// - /// - /// - public int Offset { get; set; } - } -} \ No newline at end of file From f11fcfaab226ad5a65298e37153bf57d04ac8be4 Mon Sep 17 00:00:00 2001 From: zapadi Date: Fri, 10 Jan 2020 22:39:50 +0200 Subject: [PATCH 095/549] Add xml serialization cache --- .../Serialization/CacheKeyFactory.cs | 72 ++++++++++ .../Serialization/IXmlSerializerCache.cs | 18 +++ .../Serialization/XmlSerializerCache.cs | 132 ++++++++++++++++++ 3 files changed, 222 insertions(+) create mode 100644 src/redmine-net-api/Serialization/CacheKeyFactory.cs create mode 100644 src/redmine-net-api/Serialization/IXmlSerializerCache.cs create mode 100644 src/redmine-net-api/Serialization/XmlSerializerCache.cs diff --git a/src/redmine-net-api/Serialization/CacheKeyFactory.cs b/src/redmine-net-api/Serialization/CacheKeyFactory.cs new file mode 100644 index 00000000..d6244b1b --- /dev/null +++ b/src/redmine-net-api/Serialization/CacheKeyFactory.cs @@ -0,0 +1,72 @@ +using System; +using System.Globalization; +using System.Text; +using System.Xml.Serialization; + +namespace Redmine.Net.Api.Serialization +{ + /// + /// The CacheKeyFactory extracts a unique signature + /// to identify each instance of an XmlSerializer + /// in the cache. + /// + internal static class CacheKeyFactory + { + + /// + /// Creates a unique signature for the passed + /// in parameter. MakeKey normalizes array content + /// and the content of the XmlAttributeOverrides before + /// creating the key. + /// + /// + /// + /// + /// + /// + /// + public static string Create(Type type, XmlAttributeOverrides overrides, Type[] types, XmlRootAttribute root, string defaultNamespace) + { + var keyBuilder = new StringBuilder(); + keyBuilder.Append(type.FullName); + keyBuilder.Append( "??" ); + keyBuilder.Append(overrides?.GetHashCode().ToString(CultureInfo.InvariantCulture)); + keyBuilder.Append( "??" ); + keyBuilder.Append(GetTypeArraySignature(types)); + keyBuilder.Append("??"); + keyBuilder.Append(root?.GetHashCode().ToString(CultureInfo.InvariantCulture)); + keyBuilder.Append("??"); + keyBuilder.Append(defaultNamespace); + + return keyBuilder.ToString(); + } + + /// + /// Creates a signature for the passed in Type array. The order + /// of the elements in the array does not influence the signature. + /// + /// + /// An instance independent signature of the Type array + public static string GetTypeArraySignature(Type[] types) + { + if (null == types || types.Length <= 0) + { + return null; + } + + // to make sure we don't account for the order + // of the types in the array, we create one SortedList + // with the type names, concatenate them and hash that. + var sorter = new string[types.Length]; + for (var index = 0; index < types.Length; index++) + { + Type t = types[index]; + sorter[index] = t.AssemblyQualifiedName; + } + + Array.Sort(sorter); + + return string.Join(":", sorter); + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Serialization/IXmlSerializerCache.cs b/src/redmine-net-api/Serialization/IXmlSerializerCache.cs new file mode 100644 index 00000000..72e458bf --- /dev/null +++ b/src/redmine-net-api/Serialization/IXmlSerializerCache.cs @@ -0,0 +1,18 @@ +using System; +using System.Xml.Serialization; + +namespace Redmine.Net.Api.Serialization +{ + internal interface IXmlSerializerCache + { + XmlSerializer GetSerializer(Type type, string defaultNamespace); + + XmlSerializer GetSerializer(Type type, XmlRootAttribute root); + + XmlSerializer GetSerializer(Type type, XmlAttributeOverrides overrides); + + XmlSerializer GetSerializer(Type type, Type[] types); + + XmlSerializer GetSerializer(Type type, XmlAttributeOverrides overrides, Type[] types, XmlRootAttribute root, string defaultNamespace); + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Serialization/XmlSerializerCache.cs b/src/redmine-net-api/Serialization/XmlSerializerCache.cs new file mode 100644 index 00000000..32935bd8 --- /dev/null +++ b/src/redmine-net-api/Serialization/XmlSerializerCache.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Xml.Serialization; + +namespace Redmine.Net.Api.Serialization +{ + /// + /// + /// + internal class XmlSerializerCache : IXmlSerializerCache + { + #if !(NET20 || NET40 || NET45 || NET451 || NET452) + private static readonly Type[] emptyTypes = Array.Empty(); + #else + private static readonly Type[] emptyTypes = new Type[0]; + #endif + + public static XmlSerializerCache Instance { get; } = new XmlSerializerCache(); + + private readonly Dictionary _serializers; + + private readonly object _syncRoot; + + private XmlSerializerCache() + { + _syncRoot = new object(); + _serializers = new Dictionary(); + } + + /// + /// Get an XmlSerializer instance for the + /// specified parameters. The method will check if + /// any any previously cached instances are compatible + /// with the parameters before constructing a new + /// XmlSerializer instance. + /// + /// + /// + /// + public XmlSerializer GetSerializer(Type type, string defaultNamespace) + { + return GetSerializer(type, null, emptyTypes, null, defaultNamespace); + } + + /// + /// Get an XmlSerializer instance for the + /// specified parameters. The method will check if + /// any any previously cached instances are compatible + /// with the parameters before constructing a new + /// XmlSerializer instance. + /// + /// + /// + /// + public XmlSerializer GetSerializer(Type type, XmlRootAttribute root) + { + return GetSerializer(type, null, emptyTypes, root, null); + } + + /// + /// Get an XmlSerializer instance for the + /// specified parameters. The method will check if + /// any any previously cached instances are compatible + /// with the parameters before constructing a new + /// XmlSerializer instance. + /// + /// + /// + /// + public XmlSerializer GetSerializer(Type type, XmlAttributeOverrides overrides) + { + return GetSerializer(type, overrides, emptyTypes, null, null); + } + + /// + /// Get an XmlSerializer instance for the + /// specified parameters. The method will check if + /// any any previously cached instances are compatible + /// with the parameters before constructing a new + /// XmlSerializer instance. + /// + /// + /// + /// + public XmlSerializer GetSerializer(Type type, Type[] types) + { + return GetSerializer(type, null, types, null, null); + } + + /// + /// Get an XmlSerializer instance for the + /// specified parameters. The method will check if + /// any any previously cached instances are compatible + /// with the parameters before constructing a new + /// XmlSerializer instance. + /// + /// + /// + /// + /// + /// + /// + public XmlSerializer GetSerializer(Type type, XmlAttributeOverrides overrides, Type[] types, XmlRootAttribute root, string defaultNamespace) + { + var key = CacheKeyFactory.Create(type, overrides, types, root, defaultNamespace); + + XmlSerializer serializer = null; + lock (_syncRoot) + { + if (_serializers.ContainsKey(key) == false) + { + lock (_syncRoot) + { + if (_serializers.ContainsKey(key) == false) + { + serializer = new XmlSerializer(type, overrides, types, root, defaultNamespace); + _serializers.Add(key, serializer); + } + } + } + else + { + serializer = _serializers[key]; + } + + Debug.Assert(serializer != null); + return serializer; + } + } + } +} \ No newline at end of file From cc18c3ff2c0ca9660d30369660a46da1af36fbc5 Mon Sep 17 00:00:00 2001 From: zapadi Date: Fri, 10 Jan 2020 22:40:48 +0200 Subject: [PATCH 096/549] Cleanup --- .../Extensions/NameValueCollectionExtensions.cs | 7 ++++++- src/redmine-net-api/Extensions/XmlReaderExtensions.cs | 1 - src/redmine-net-api/Extensions/XmlWriterExtensions.cs | 7 +++++++ 3 files changed, 13 insertions(+), 2 deletions(-) mode change 100755 => 100644 src/redmine-net-api/Extensions/NameValueCollectionExtensions.cs diff --git a/src/redmine-net-api/Extensions/NameValueCollectionExtensions.cs b/src/redmine-net-api/Extensions/NameValueCollectionExtensions.cs old mode 100755 new mode 100644 index 805f733a..1f56d451 --- a/src/redmine-net-api/Extensions/NameValueCollectionExtensions.cs +++ b/src/redmine-net-api/Extensions/NameValueCollectionExtensions.cs @@ -31,8 +31,13 @@ public static class NameValueCollectionExtensions /// public static string GetParameterValue(this NameValueCollection parameters, string parameterName) { - if (parameters == null) return null; + if (parameters == null) + { + return null; + } + var value = parameters.Get(parameterName); + return value.IsNullOrWhiteSpace() ? null : value; } } diff --git a/src/redmine-net-api/Extensions/XmlReaderExtensions.cs b/src/redmine-net-api/Extensions/XmlReaderExtensions.cs index 7f1b121b..48848043 100644 --- a/src/redmine-net-api/Extensions/XmlReaderExtensions.cs +++ b/src/redmine-net-api/Extensions/XmlReaderExtensions.cs @@ -15,7 +15,6 @@ limitations under the License. */ using System; -using System.Collections; using System.Collections.Generic; using System.Globalization; using System.IO; diff --git a/src/redmine-net-api/Extensions/XmlWriterExtensions.cs b/src/redmine-net-api/Extensions/XmlWriterExtensions.cs index 408af16f..8ce7c5b1 100644 --- a/src/redmine-net-api/Extensions/XmlWriterExtensions.cs +++ b/src/redmine-net-api/Extensions/XmlWriterExtensions.cs @@ -190,6 +190,13 @@ public static void WriteRepeatableElement(this XmlWriter xmlWriter, string eleme } } + /// + /// + /// + /// + /// + /// + /// public static void WriteArrayStringElement(this XmlWriter writer, string elementName, IEnumerable collection, Func f) { if (collection == null) From d1b86bd685c38e1552bcf6014f4cedb9c8e3a2f4 Mon Sep 17 00:00:00 2001 From: zapadi Date: Fri, 10 Jan 2020 22:42:38 +0200 Subject: [PATCH 097/549] Replace StringReader with XmlReader as parameter for XmlSerializer --- .../Serialization/XmlRedmineSerializer.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/redmine-net-api/Serialization/XmlRedmineSerializer.cs b/src/redmine-net-api/Serialization/XmlRedmineSerializer.cs index 74978872..41443d80 100644 --- a/src/redmine-net-api/Serialization/XmlRedmineSerializer.cs +++ b/src/redmine-net-api/Serialization/XmlRedmineSerializer.cs @@ -4,6 +4,7 @@ using System.Xml.Serialization; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; namespace Redmine.Net.Api.Serialization { @@ -163,16 +164,19 @@ private string ToXML(T entity) where T : class using (var textReader = new StringReader(xml)) { - var serializer = new XmlSerializer(typeof(TOut)); + using (var xmlReader = XmlTextReaderBuilder.Create(textReader)) + { + var serializer = new XmlSerializer(typeof(TOut)); - var entity = serializer.Deserialize(textReader); + var entity = serializer.Deserialize(xmlReader); - if (entity is TOut t) - { - return t; - } + if (entity is TOut t) + { + return t; + } - return default; + return default; + } } } } From ce586092e0990ba832a5f9be5200ea7b40b0591b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tobias=20G=C3=A4rtner?= Date: Thu, 23 Jan 2020 14:06:07 +0100 Subject: [PATCH 098/549] fix #248 --- .../Extensions/ExtensionAttribute.cs | 27 ------------------- 1 file changed, 27 deletions(-) delete mode 100755 src/redmine-net-api/Extensions/ExtensionAttribute.cs diff --git a/src/redmine-net-api/Extensions/ExtensionAttribute.cs b/src/redmine-net-api/Extensions/ExtensionAttribute.cs deleted file mode 100755 index c3787c71..00000000 --- a/src/redmine-net-api/Extensions/ExtensionAttribute.cs +++ /dev/null @@ -1,27 +0,0 @@ -/* - Copyright 2011 - 2016 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -namespace System.Runtime.CompilerServices -{ - /// - /// - /// - /// - [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method, AllowMultiple=false, Inherited=false)] - public class ExtensionAttribute: Attribute - { - } -} \ No newline at end of file From c5d88515dc0740e9235fed9b8e9557131a89988d Mon Sep 17 00:00:00 2001 From: Tobias Gaertner Date: Tue, 28 Jan 2020 19:42:43 +0100 Subject: [PATCH 099/549] conditionally compile ExtensionAttribute with NET20 --- .../Extensions/ExtensionAttribute.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100755 src/redmine-net-api/Extensions/ExtensionAttribute.cs diff --git a/src/redmine-net-api/Extensions/ExtensionAttribute.cs b/src/redmine-net-api/Extensions/ExtensionAttribute.cs new file mode 100755 index 00000000..e9542cc7 --- /dev/null +++ b/src/redmine-net-api/Extensions/ExtensionAttribute.cs @@ -0,0 +1,31 @@ +/* + Copyright 2011 - 2016 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#if NET20 + +namespace System.Runtime.CompilerServices +{ + /// + /// + /// + /// + [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method, AllowMultiple=false, Inherited=false)] + public class ExtensionAttribute: Attribute + { + } +} + +#endif \ No newline at end of file From 55c6cbf02f421fc8c8bc025fc6dab739c0fe553d Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 2 Feb 2020 10:16:59 +0200 Subject: [PATCH 100/549] Fix newton json ref --- src/redmine-net-api/redmine-net-api.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index c44497db..e9730b3e 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -113,6 +113,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -218,4 +219,4 @@ - \ No newline at end of file + From e77ed349021db18237b071d980cccb4e66930b87 Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 2 Feb 2020 10:24:49 +0200 Subject: [PATCH 101/549] Create dotnetcore.yml --- .github/workflows/dotnetcore.yml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/dotnetcore.yml diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml new file mode 100644 index 00000000..eb10033c --- /dev/null +++ b/.github/workflows/dotnetcore.yml @@ -0,0 +1,23 @@ +name: .NET Core + +on: [push] +env: + DOTNET_CLI_TELEMETRY_OPTOUT: 1 +jobs: + build: + + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macOS-latest] + dotnet: [ '3.1.100' ] + name: OS ${{ matrix.os }} - dotnet ${{ matrix.dotnet }} + + steps: + - uses: actions/checkout@v2 + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: ${{ matrix.dotnet }} + - name: Build with dotnet + run: dotnet build --configuration Release From 50f7908d86bcff177517e996fc1bf769723d8a85 Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 2 Feb 2020 11:19:42 +0200 Subject: [PATCH 102/549] Update dotnetcore.yml --- .github/workflows/dotnetcore.yml | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml index eb10033c..dccf09d0 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/dotnetcore.yml @@ -1,8 +1,20 @@ name: .NET Core -on: [push] +on: + push: + paths-ignore: + - '**/*.md' + - '**/*.gif' + - '**/*.png' + - LICENSE + - tests/* + tags: + - /v\d*\.\d*\.\d*/ + pull_request: + env: DOTNET_CLI_TELEMETRY_OPTOUT: 1 + jobs: build: @@ -19,5 +31,9 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: ${{ matrix.dotnet }} + - name: Get the version + id: get_version + run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} + # ${{ steps.get_version.outputs.VERSION }} - name: Build with dotnet - run: dotnet build --configuration Release + run: dotnet build redmine-net-api.sln --configuration Release From b4891f9bdc4da4df7828a4b442194409d0478484 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 2 Feb 2020 13:39:22 +0200 Subject: [PATCH 103/549] Remove unused methodName parameter --- .../Async/RedmineManagerAsync45.cs | 32 ++++++++--------- .../Internals/WebApiAsyncHelper.cs | 19 ++++------- src/redmine-net-api/Internals/WebApiHelper.cs | 19 ++++------- src/redmine-net-api/RedmineManager.cs | 34 +++++++++---------- 4 files changed, 45 insertions(+), 59 deletions(-) diff --git a/src/redmine-net-api/Async/RedmineManagerAsync45.cs b/src/redmine-net-api/Async/RedmineManagerAsync45.cs index 635a14d7..4ae7dc62 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync45.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync45.cs @@ -43,7 +43,7 @@ public static class RedmineManagerAsync public static async Task GetCurrentUserAsync(this RedmineManager redmineManager, NameValueCollection parameters = null) { var uri = UrlHelper.GetCurrentUserUrl(redmineManager); - return await WebApiAsyncHelper.ExecuteDownload(redmineManager, uri, "GetCurrentUserAsync", parameters).ConfigureAwait(false); + return await WebApiAsyncHelper.ExecuteDownload(redmineManager, uri, parameters).ConfigureAwait(false); } /// @@ -63,7 +63,7 @@ public static async Task CreateOrUpdateWikiPageAsync(this RedmineManag url = Uri.EscapeUriString(url); - var response = await WebApiAsyncHelper.ExecuteUpload(redmineManager, url, HttpVerbs.PUT, data, "CreateOrUpdateWikiPageAsync").ConfigureAwait(false); + var response = await WebApiAsyncHelper.ExecuteUpload(redmineManager, url, HttpVerbs.PUT, data).ConfigureAwait(false); return redmineManager.Serializer.Deserialize(response); } @@ -79,7 +79,7 @@ public static async Task DeleteWikiPageAsync(this RedmineManager redmineManager, { var uri = UrlHelper.GetDeleteWikirUrl(redmineManager, projectId, pageName); uri = Uri.EscapeUriString(uri); - await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.DELETE, string.Empty, "DeleteWikiPageAsync").ConfigureAwait(false); + await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.DELETE, string.Empty).ConfigureAwait(false); } /// @@ -94,7 +94,7 @@ public static async Task DeleteWikiPageAsync(this RedmineManager redmineManager, public static async Task UploadFileAsync(this RedmineManager redmineManager, byte[] data) { var uri = UrlHelper.GetUploadFileUrl(redmineManager); - return await WebApiAsyncHelper.ExecuteUploadFile(redmineManager, uri, data, "UploadFileAsync").ConfigureAwait(false); + return await WebApiAsyncHelper.ExecuteUploadFile(redmineManager, uri, data).ConfigureAwait(false); } /// @@ -105,7 +105,7 @@ public static async Task UploadFileAsync(this RedmineManager redmineMana /// public static async Task DownloadFileAsync(this RedmineManager redmineManager, string address) { - return await WebApiAsyncHelper.ExecuteDownloadFile(redmineManager, address, "DownloadFileAsync").ConfigureAwait(false); + return await WebApiAsyncHelper.ExecuteDownloadFile(redmineManager, address).ConfigureAwait(false); } /// @@ -122,7 +122,7 @@ public static async Task GetWikiPageAsync(this RedmineManager redmineM { var uri = UrlHelper.GetWikiPageUrl(redmineManager, projectId, pageName, version); uri = Uri.EscapeUriString(uri); - return await WebApiAsyncHelper.ExecuteDownload(redmineManager, uri, "GetWikiPageAsync", parameters).ConfigureAwait(false); + return await WebApiAsyncHelper.ExecuteDownload(redmineManager, uri, parameters).ConfigureAwait(false); } /// @@ -135,7 +135,7 @@ public static async Task GetWikiPageAsync(this RedmineManager redmineM public static async Task> GetAllWikiPagesAsync(this RedmineManager redmineManager, NameValueCollection parameters, string projectId) { var uri = UrlHelper.GetWikisUrl(redmineManager, projectId); - return await WebApiAsyncHelper.ExecuteDownloadList(redmineManager, uri, "GetAllWikiPagesAsync", parameters).ConfigureAwait(false); + return await WebApiAsyncHelper.ExecuteDownloadList(redmineManager, uri, parameters).ConfigureAwait(false); } /// @@ -152,7 +152,7 @@ public static async Task AddUserToGroupAsync(this RedmineManager redmineManager, var data = DataHelper.UserData(userId, redmineManager.MimeFormat); var uri = UrlHelper.GetAddUserToGroupUrl(redmineManager, groupId); - await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.POST, data, "AddUserToGroupAsync").ConfigureAwait(false); + await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.POST, data).ConfigureAwait(false); } /// @@ -165,7 +165,7 @@ public static async Task AddUserToGroupAsync(this RedmineManager redmineManager, public static async Task RemoveUserFromGroupAsync(this RedmineManager redmineManager, int groupId, int userId) { var uri = UrlHelper.GetRemoveUserFromGroupUrl(redmineManager, groupId, userId); - await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.DELETE, string.Empty, "DeleteUserFromGroupAsync").ConfigureAwait(false); + await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.DELETE, string.Empty).ConfigureAwait(false); } /// @@ -180,7 +180,7 @@ public static async Task AddWatcherToIssueAsync(this RedmineManager redmineManag var data = DataHelper.UserData(userId, redmineManager.MimeFormat); var uri = UrlHelper.GetAddWatcherUrl(redmineManager, issueId); - await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.POST, data, "AddWatcherAsync").ConfigureAwait(false); + await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.POST, data).ConfigureAwait(false); } /// @@ -193,7 +193,7 @@ public static async Task AddWatcherToIssueAsync(this RedmineManager redmineManag public static async Task RemoveWatcherFromIssueAsync(this RedmineManager redmineManager, int issueId, int userId) { var uri = UrlHelper.GetRemoveWatcherUrl(redmineManager, issueId, userId); - await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.DELETE, string.Empty, "RemoveWatcherAsync").ConfigureAwait(false); + await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.DELETE, string.Empty).ConfigureAwait(false); } /// @@ -263,7 +263,7 @@ public static async Task> GetPaginatedObjectsAsync(this Redmi where T : class, new() { var uri = UrlHelper.GetListUrl(redmineManager, parameters); - return await WebApiAsyncHelper.ExecuteDownloadPaginatedList(redmineManager, uri, "GetPaginatedObjectsAsync", parameters).ConfigureAwait(false); + return await WebApiAsyncHelper.ExecuteDownloadPaginatedList(redmineManager, uri, parameters).ConfigureAwait(false); } /// @@ -330,7 +330,7 @@ public static async Task GetObjectAsync(this RedmineManager redmineManager where T : class, new() { var uri = UrlHelper.GetGetUrl(redmineManager, id); - return await WebApiAsyncHelper.ExecuteDownload(redmineManager, uri, "GetobjectAsync", parameters).ConfigureAwait(false); + return await WebApiAsyncHelper.ExecuteDownload(redmineManager, uri, parameters).ConfigureAwait(false); } /// @@ -360,7 +360,7 @@ public static async Task CreateObjectAsync(this RedmineManager redmineMana var uri = UrlHelper.GetCreateUrl(redmineManager, ownerId); var data = redmineManager.Serializer.Serialize(entity); - var response = await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.POST, data, "CreateObjectAsync").ConfigureAwait(false); + var response = await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.POST, data).ConfigureAwait(false); return redmineManager.Serializer.Deserialize(response); } @@ -379,7 +379,7 @@ public static async Task UpdateObjectAsync(this RedmineManager redmineManager var data = redmineManager.Serializer.Serialize(entity); data = Regex.Replace(data, @"\r\n|\r|\n", "\r\n"); - await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.PUT, data, "UpdateObjectAsync").ConfigureAwait(false); + await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.PUT, data).ConfigureAwait(false); } /// @@ -393,7 +393,7 @@ public static async Task DeleteObjectAsync(this RedmineManager redmineManager where T : class, new() { var uri = UrlHelper.GetDeleteUrl(redmineManager, id); - await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.DELETE, string.Empty, "DeleteObjectAsync").ConfigureAwait(false); + await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.DELETE, string.Empty).ConfigureAwait(false); } } } diff --git a/src/redmine-net-api/Internals/WebApiAsyncHelper.cs b/src/redmine-net-api/Internals/WebApiAsyncHelper.cs index d7e00a66..9b463cd9 100644 --- a/src/redmine-net-api/Internals/WebApiAsyncHelper.cs +++ b/src/redmine-net-api/Internals/WebApiAsyncHelper.cs @@ -37,10 +37,8 @@ internal static class WebApiAsyncHelper /// The address. /// Type of the action. /// The data. - /// Name of the method. /// - public static async Task ExecuteUpload(RedmineManager redmineManager, string address, string actionType, string data, - string methodName) + public static async Task ExecuteUpload(RedmineManager redmineManager, string address, string actionType, string data) { using (var wc = redmineManager.CreateWebClient(null)) { @@ -69,10 +67,9 @@ public static async Task ExecuteUpload(RedmineManager redmineManager, st /// /// The redmine manager. /// The address. - /// Name of the method. /// The parameters. /// - public static async Task ExecuteDownload(RedmineManager redmineManager, string address, string methodName, + public static async Task ExecuteDownload(RedmineManager redmineManager, string address, NameValueCollection parameters = null) where T : class, new() { @@ -97,11 +94,10 @@ public static async Task ExecuteDownload(RedmineManager redmineManager, st /// /// The redmine manager. /// The address. - /// Name of the method. /// The parameters. /// public static async Task> ExecuteDownloadList(RedmineManager redmineManager, string address, - string methodName, + NameValueCollection parameters = null) where T : class, new() { using (var wc = redmineManager.CreateWebClient(parameters)) @@ -128,11 +124,10 @@ public static async Task> ExecuteDownloadList(RedmineManager redmineM /// /// The redmine manager. /// The address. - /// Name of the method. /// The parameters. /// public static async Task> ExecuteDownloadPaginatedList(RedmineManager redmineManager, string address, - string methodName, + NameValueCollection parameters = null) where T : class, new() { using (var wc = redmineManager.CreateWebClient(parameters)) @@ -155,9 +150,8 @@ public static async Task> ExecuteDownloadPaginatedList(Redmin /// /// The redmine manager. /// The address. - /// Name of the method. /// - public static async Task ExecuteDownloadFile(RedmineManager redmineManager, string address, string methodName) + public static async Task ExecuteDownloadFile(RedmineManager redmineManager, string address) { using (var wc = redmineManager.CreateWebClient(null, true)) { @@ -179,9 +173,8 @@ public static async Task ExecuteDownloadFile(RedmineManager redmineManag /// The redmine manager. /// The address. /// The data. - /// Name of the method. /// - public static async Task ExecuteUploadFile(RedmineManager redmineManager, string address, byte[] data, string methodName) + public static async Task ExecuteUploadFile(RedmineManager redmineManager, string address, byte[] data) { using (var wc = redmineManager.CreateWebClient(null, true)) { diff --git a/src/redmine-net-api/Internals/WebApiHelper.cs b/src/redmine-net-api/Internals/WebApiHelper.cs index 138e39a0..ed5e27c3 100644 --- a/src/redmine-net-api/Internals/WebApiHelper.cs +++ b/src/redmine-net-api/Internals/WebApiHelper.cs @@ -35,10 +35,9 @@ internal static class WebApiHelper /// The address. /// Type of the action. /// The data. - /// Name of the method. /// The parameters public static void ExecuteUpload(RedmineManager redmineManager, string address, string actionType, string data, - string methodName, NameValueCollection parameters = null) + NameValueCollection parameters = null) { using (var wc = redmineManager.CreateWebClient(parameters)) { @@ -65,10 +64,8 @@ public static void ExecuteUpload(RedmineManager redmineManager, string address, /// The address. /// Type of the action. /// The data. - /// Name of the method. /// - public static T ExecuteUpload(RedmineManager redmineManager, string address, string actionType, string data, - string methodName) + public static T ExecuteUpload(RedmineManager redmineManager, string address, string actionType, string data) where T : class, new() { using (var wc = redmineManager.CreateWebClient(null)) @@ -96,10 +93,9 @@ public static T ExecuteUpload(RedmineManager redmineManager, string address, /// /// The redmine manager. /// The address. - /// Name of the method. /// The parameters. /// - public static T ExecuteDownload(RedmineManager redmineManager, string address, string methodName, + public static T ExecuteDownload(RedmineManager redmineManager, string address, NameValueCollection parameters = null) where T : class, new() { @@ -125,11 +121,10 @@ public static T ExecuteDownload(RedmineManager redmineManager, string address /// /// The redmine manager. /// The address. - /// Name of the method. /// The parameters. /// public static PagedResults ExecuteDownloadList(RedmineManager redmineManager, string address, - string methodName, + NameValueCollection parameters = null) where T : class, new() { using (var wc = redmineManager.CreateWebClient(parameters)) @@ -152,9 +147,8 @@ public static PagedResults ExecuteDownloadList(RedmineManager redmineManag /// /// The redmine manager. /// The address. - /// Name of the method. /// - public static byte[] ExecuteDownloadFile(RedmineManager redmineManager, string address, string methodName) + public static byte[] ExecuteDownloadFile(RedmineManager redmineManager, string address) { using (var wc = redmineManager.CreateWebClient(null, true)) { @@ -176,9 +170,8 @@ public static byte[] ExecuteDownloadFile(RedmineManager redmineManager, string a /// The redmine manager. /// The address. /// The data. - /// Name of the method. /// - public static Upload ExecuteUploadFile(RedmineManager redmineManager, string address, byte[] data, string methodName) + public static Upload ExecuteUploadFile(RedmineManager redmineManager, string address, byte[] data) { using (var wc = redmineManager.CreateWebClient(null, true)) { diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index cc83b910..5b467239 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -294,7 +294,7 @@ private set public User GetCurrentUser(NameValueCollection parameters = null) { var url = UrlHelper.GetCurrentUserUrl(this); - return WebApiHelper.ExecuteDownload(this, url, "GetCurrentUser", parameters); + return WebApiHelper.ExecuteDownload(this, url, parameters); } /// @@ -305,7 +305,7 @@ public User GetCurrentUser(NameValueCollection parameters = null) public void AddWatcherToIssue(int issueId, int userId) { var url = UrlHelper.GetAddWatcherUrl(this, issueId); - WebApiHelper.ExecuteUpload(this, url, HttpVerbs.POST, DataHelper.UserData(userId, MimeFormat), "AddWatcher"); + WebApiHelper.ExecuteUpload(this, url, HttpVerbs.POST, DataHelper.UserData(userId, MimeFormat)); } /// @@ -316,7 +316,7 @@ public void AddWatcherToIssue(int issueId, int userId) public void RemoveWatcherFromIssue(int issueId, int userId) { var url = UrlHelper.GetRemoveWatcherUrl(this, issueId, userId); - WebApiHelper.ExecuteUpload(this, url, HttpVerbs.DELETE, string.Empty, "RemoveWatcher"); + WebApiHelper.ExecuteUpload(this, url, HttpVerbs.DELETE, string.Empty); } /// @@ -327,7 +327,7 @@ public void RemoveWatcherFromIssue(int issueId, int userId) public void AddUserToGroup(int groupId, int userId) { var url = UrlHelper.GetAddUserToGroupUrl(this, groupId); - WebApiHelper.ExecuteUpload(this, url, HttpVerbs.POST, DataHelper.UserData(userId, MimeFormat), "AddUser"); + WebApiHelper.ExecuteUpload(this, url, HttpVerbs.POST, DataHelper.UserData(userId, MimeFormat)); } /// @@ -338,7 +338,7 @@ public void AddUserToGroup(int groupId, int userId) public void RemoveUserFromGroup(int groupId, int userId) { var url = UrlHelper.GetRemoveUserFromGroupUrl(this, groupId, userId); - WebApiHelper.ExecuteUpload(this, url, HttpVerbs.DELETE, string.Empty, "DeleteUser"); + WebApiHelper.ExecuteUpload(this, url, HttpVerbs.DELETE, string.Empty); } /// @@ -357,7 +357,7 @@ public WikiPage CreateOrUpdateWikiPage(string projectId, string pageName, WikiPa url = Uri.EscapeUriString(url); - return WebApiHelper.ExecuteUpload(this, url, HttpVerbs.PUT, result, "CreateOrUpdateWikiPage"); + return WebApiHelper.ExecuteUpload(this, url, HttpVerbs.PUT, result); } /// @@ -372,7 +372,7 @@ public WikiPage GetWikiPage(string projectId, NameValueCollection parameters, st { var url = UrlHelper.GetWikiPageUrl(this, projectId, pageName, version); url = Uri.EscapeUriString(url); - return WebApiHelper.ExecuteDownload(this, url, "GetWikiPage", parameters); + return WebApiHelper.ExecuteDownload(this, url, parameters); } /// @@ -383,7 +383,7 @@ public WikiPage GetWikiPage(string projectId, NameValueCollection parameters, st public List GetAllWikiPages(string projectId) { var url = UrlHelper.GetWikisUrl(this, projectId); - var result = WebApiHelper.ExecuteDownloadList(this, url, "GetAllWikiPages"); + var result = WebApiHelper.ExecuteDownloadList(this, url); return result == null ? null :new List(result.Items); } @@ -397,7 +397,7 @@ public void DeleteWikiPage(string projectId, string pageName) { var url = UrlHelper.GetDeleteWikirUrl(this, projectId, pageName); url = Uri.EscapeUriString(url); - WebApiHelper.ExecuteUpload(this, url, HttpVerbs.DELETE, string.Empty, "DeleteWikiPage"); + WebApiHelper.ExecuteUpload(this, url, HttpVerbs.DELETE, string.Empty); } /// @@ -457,7 +457,7 @@ public void DeleteWikiPage(string projectId, string pageName) public T GetObject(string id, NameValueCollection parameters) where T : class, new() { var url = UrlHelper.GetGetUrl(this, id); - return WebApiHelper.ExecuteDownload(this, url, "GetObject", parameters); + return WebApiHelper.ExecuteDownload(this, url, parameters); } /// @@ -469,7 +469,7 @@ public void DeleteWikiPage(string projectId, string pageName) public PagedResults GetPaginatedObjects(NameValueCollection parameters) where T : class, new() { var url = UrlHelper.GetListUrl(this, parameters); - return WebApiHelper.ExecuteDownloadList(this, url, "GetObjectList", parameters); + return WebApiHelper.ExecuteDownloadList(this, url, parameters); } /// @@ -652,7 +652,7 @@ public void DeleteWikiPage(string projectId, string pageName) { var url = UrlHelper.GetCreateUrl(this, ownerId); var data = Serializer.Serialize(obj); - return WebApiHelper.ExecuteUpload(this, url, HttpVerbs.POST, data, "CreateObject"); + return WebApiHelper.ExecuteUpload(this, url, HttpVerbs.POST, data); } /// @@ -690,7 +690,7 @@ public void DeleteWikiPage(string projectId, string pageName) var url = UrlHelper.GetUploadUrl(this, id); var data = Serializer.Serialize(obj); data = Regex.Replace(data, @"\r\n|\r|\n", "\r\n"); - WebApiHelper.ExecuteUpload(this, url, HttpVerbs.PUT, data, "UpdateObject"); + WebApiHelper.ExecuteUpload(this, url, HttpVerbs.PUT, data); } /// @@ -716,7 +716,7 @@ public void DeleteWikiPage(string projectId, string pageName) public void DeleteObject(string id, NameValueCollection parameters = null) where T : class, new() { var url = UrlHelper.GetDeleteUrl(this, id); - WebApiHelper.ExecuteUpload(this, url, HttpVerbs.DELETE, string.Empty, "DeleteObject", parameters); + WebApiHelper.ExecuteUpload(this, url, HttpVerbs.DELETE, string.Empty, parameters); } /// @@ -737,7 +737,7 @@ public void DeleteWikiPage(string projectId, string pageName) public Upload UploadFile(byte[] data) { var url = UrlHelper.GetUploadFileUrl(this); - return WebApiHelper.ExecuteUploadFile(this, url, data, "UploadFile"); + return WebApiHelper.ExecuteUploadFile(this, url, data); } /// @@ -751,7 +751,7 @@ public void UpdateAttachment(int issueId, Attachment attachment) var attachments = new Attachments { { attachment.Id, attachment } }; var data = Serializer.Serialize(attachments); - WebApiHelper.ExecuteUpload(this, address, HttpVerbs.PATCH, data, "UpdateAttachment"); + WebApiHelper.ExecuteUpload(this, address, HttpVerbs.PATCH, data); } /// @@ -769,7 +769,7 @@ public void UpdateAttachment(int issueId, Attachment attachment) /// public byte[] DownloadFile(string address) { - return WebApiHelper.ExecuteDownloadFile(this, address, "DownloadFile"); + return WebApiHelper.ExecuteDownloadFile(this, address); } /// From 4face709838d342df165e5b7b1486b80166ecd77 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 2 Feb 2020 13:39:56 +0200 Subject: [PATCH 104/549] Add NoWarn CA2227 --- src/redmine-net-api/redmine-net-api.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index e9730b3e..1b87fd50 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -20,6 +20,7 @@ CA1716; CA1724; CA1806; + CA2227; From e94f38f41aab6dcf44dc3860d075673a938500a3 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 2 Feb 2020 13:46:29 +0200 Subject: [PATCH 105/549] SmallGet rid of CA1308 warnings --- .../Extensions/JsonWriterExtensions.cs | 16 ++++++++-------- .../Extensions/XmlWriterExtensions.cs | 6 +++--- src/redmine-net-api/Types/Issue.cs | 8 ++++---- src/redmine-net-api/Types/IssueCustomField.cs | 2 +- src/redmine-net-api/Types/User.cs | 4 ++-- src/redmine-net-api/Types/Version.cs | 8 ++++---- 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/redmine-net-api/Extensions/JsonWriterExtensions.cs b/src/redmine-net-api/Extensions/JsonWriterExtensions.cs index 9b01c2d6..d4032094 100644 --- a/src/redmine-net-api/Extensions/JsonWriterExtensions.cs +++ b/src/redmine-net-api/Extensions/JsonWriterExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Globalization; @@ -45,7 +45,7 @@ public static void WriteIfNotDefaultOrNull(this JsonWriter writer, string ele if (value is bool) { - writer.WriteProperty(elementName, value.ToString().ToLowerInvariant()); + writer.WriteProperty(elementName, value.ToString().ToLowerInv()); return; } @@ -63,7 +63,7 @@ public static void WriteIdOrEmpty(this JsonWriter jsonWriter, string tag, Identi { if (ident != null) { - jsonWriter.WriteProperty(tag, ident.Id); + jsonWriter.WriteProperty(tag, ident.Id.ToString(CultureInfo.InvariantCulture)); } else { @@ -86,7 +86,7 @@ public static void WriteDateOrEmpty(this JsonWriter jsonWriter, string tag, Date } else { - jsonWriter.WriteProperty(tag, string.Format(NumberFormatInfo.InvariantInfo, "{0}", val.Value.ToString(dateFormat, CultureInfo.InvariantCulture))); + jsonWriter.WriteProperty(tag, val.Value.ToString(dateFormat, CultureInfo.InvariantCulture)); } } @@ -173,10 +173,10 @@ public static void WriteArrayIds(this JsonWriter jsonWriter, string tag, IEnumer foreach (var identifiableName in collection) { - sb.Append(",").Append(identifiableName.Id.ToString(CultureInfo.InvariantCulture)); + sb.Append(identifiableName.Id.ToString(CultureInfo.InvariantCulture)).Append(","); } - sb.Remove(0, 1); + sb.Length -= 1; jsonWriter.WriteValue(sb.ToString()); sb= null; @@ -203,10 +203,10 @@ public static void WriteArrayNames(this JsonWriter jsonWriter, string tag, IEnum foreach (var identifiableName in collection) { - sb.Append(",").Append(identifiableName.Name); + sb.Append(identifiableName.Name).Append(","); } - sb.Remove(0, 1); + sb.Length -= 1; jsonWriter.WriteValue(sb.ToString()); sb = null; diff --git a/src/redmine-net-api/Extensions/XmlWriterExtensions.cs b/src/redmine-net-api/Extensions/XmlWriterExtensions.cs index 8ce7c5b1..9add243f 100644 --- a/src/redmine-net-api/Extensions/XmlWriterExtensions.cs +++ b/src/redmine-net-api/Extensions/XmlWriterExtensions.cs @@ -242,11 +242,11 @@ public static void WriteIfNotDefaultOrNull(this XmlWriter writer, string elem if (value is bool) { - writer.WriteElementString(elementName, value.ToString().ToLowerInvariant()); + writer.WriteElementString(elementName, value.ToString().ToLowerInv()); return; } - writer.WriteElementString(elementName, string.Format(CultureInfo.InvariantCulture, "{0}", value.ToString())); + writer.WriteElementString(elementName, value.ToString()); } /// @@ -264,7 +264,7 @@ public static void WriteValueOrEmpty(this XmlWriter writer, string elementNam } else { - writer.WriteElementString(elementName, string.Format(NumberFormatInfo.InvariantInfo, "{0}", val.Value)); + writer.WriteElementString(elementName, val.Value.ToString().ToLowerInv()); } } diff --git a/src/redmine-net-api/Types/Issue.cs b/src/redmine-net-api/Types/Issue.cs index 7aeb8b14..8273f57f 100644 --- a/src/redmine-net-api/Types/Issue.cs +++ b/src/redmine-net-api/Types/Issue.cs @@ -322,11 +322,11 @@ public override void WriteXml(XmlWriter writer) if (Id != 0) { - writer.WriteElementString(RedmineKeys.PRIVATE_NOTES, PrivateNotes.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + writer.WriteElementString(RedmineKeys.PRIVATE_NOTES, PrivateNotes.ToString(CultureInfo.InvariantCulture).ToLowerInv()); } writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); - writer.WriteElementString(RedmineKeys.IS_PRIVATE, IsPrivate.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + writer.WriteElementString(RedmineKeys.IS_PRIVATE, IsPrivate.ToString(CultureInfo.InvariantCulture).ToLowerInv()); writer.WriteIdIfNotNull(RedmineKeys.PROJECT_ID, Project); writer.WriteIdIfNotNull(RedmineKeys.PRIORITY_ID, Priority); @@ -423,10 +423,10 @@ public override void WriteJson(JsonWriter writer) if (Id != 0) { - writer.WriteProperty(RedmineKeys.PRIVATE_NOTES, PrivateNotes.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + writer.WriteProperty(RedmineKeys.PRIVATE_NOTES, PrivateNotes.ToString(CultureInfo.InvariantCulture).ToLowerInv()); } - writer.WriteProperty(RedmineKeys.IS_PRIVATE, IsPrivate.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + writer.WriteProperty(RedmineKeys.IS_PRIVATE, IsPrivate.ToString(CultureInfo.InvariantCulture).ToLowerInv()); writer.WriteIdIfNotNull(RedmineKeys.PROJECT_ID, Project); writer.WriteIdIfNotNull(RedmineKeys.PRIORITY_ID, Priority); writer.WriteIdIfNotNull(RedmineKeys.STATUS_ID, Status); diff --git a/src/redmine-net-api/Types/IssueCustomField.cs b/src/redmine-net-api/Types/IssueCustomField.cs index cf1c82ca..1a03aa27 100644 --- a/src/redmine-net-api/Types/IssueCustomField.cs +++ b/src/redmine-net-api/Types/IssueCustomField.cs @@ -151,7 +151,7 @@ public override void WriteJson(JsonWriter writer) } writer.WriteEndArray(); - writer.WriteProperty(RedmineKeys.MULTIPLE, Multiple.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + writer.WriteProperty(RedmineKeys.MULTIPLE, Multiple.ToString(CultureInfo.InvariantCulture).ToLowerInv()); } else { diff --git a/src/redmine-net-api/Types/User.cs b/src/redmine-net-api/Types/User.cs index 90c63dc9..d4c9fa59 100644 --- a/src/redmine-net-api/Types/User.cs +++ b/src/redmine-net-api/Types/User.cs @@ -182,7 +182,7 @@ public override void WriteXml(XmlWriter writer) writer.WriteElementString(RedmineKeys.MAIL_NOTIFICATION, MailNotification); writer.WriteElementString(RedmineKeys.PASSWORD, Password); writer.WriteValueOrEmpty(RedmineKeys.AUTH_SOURCE_ID, AuthenticationModeId); - writer.WriteElementString(RedmineKeys.MUST_CHANGE_PASSWORD, MustChangePassword.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + writer.WriteElementString(RedmineKeys.MUST_CHANGE_PASSWORD, MustChangePassword.ToString(CultureInfo.InvariantCulture).ToLowerInv()); writer.WriteElementString(RedmineKeys.STATUS, ((int)Status).ToString(CultureInfo.InvariantCulture)); writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); } @@ -243,7 +243,7 @@ public override void WriteJson(JsonWriter writer) writer.WriteProperty(RedmineKeys.MAIL, Email); writer.WriteProperty(RedmineKeys.MAIL_NOTIFICATION, MailNotification); writer.WriteProperty(RedmineKeys.PASSWORD, Password); - writer.WriteProperty(RedmineKeys.MUST_CHANGE_PASSWORD, MustChangePassword.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + writer.WriteProperty(RedmineKeys.MUST_CHANGE_PASSWORD, MustChangePassword.ToString(CultureInfo.InvariantCulture).ToLowerInv()); writer.WriteValueOrEmpty(RedmineKeys.AUTH_SOURCE_ID, AuthenticationModeId); writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); } diff --git a/src/redmine-net-api/Types/Version.cs b/src/redmine-net-api/Types/Version.cs index 960b7593..a5353243 100644 --- a/src/redmine-net-api/Types/Version.cs +++ b/src/redmine-net-api/Types/Version.cs @@ -123,8 +123,8 @@ public override void ReadXml(XmlReader reader) public override void WriteXml(XmlWriter writer) { writer.WriteElementString(RedmineKeys.NAME, Name); - writer.WriteElementString(RedmineKeys.STATUS, Status.ToString().ToLowerInvariant()); - writer.WriteElementString(RedmineKeys.SHARING, Sharing.ToString().ToLowerInvariant()); + writer.WriteElementString(RedmineKeys.STATUS, Status.ToString().ToLowerInv()); + writer.WriteElementString(RedmineKeys.SHARING, Sharing.ToString().ToLowerInv()); writer.WriteDateOrEmpty(RedmineKeys.DUE_DATE, DueDate); writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); } @@ -175,8 +175,8 @@ public override void WriteJson(JsonWriter writer) using (new JsonObject(writer, RedmineKeys.VERSION)) { writer.WriteProperty(RedmineKeys.NAME, Name); - writer.WriteProperty(RedmineKeys.STATUS, Status.ToString().ToLowerInvariant()); - writer.WriteProperty(RedmineKeys.SHARING, Sharing.ToString().ToLowerInvariant()); + writer.WriteProperty(RedmineKeys.STATUS, Status.ToString().ToLowerInv()); + writer.WriteProperty(RedmineKeys.SHARING, Sharing.ToString().ToLowerInv()); writer.WriteProperty(RedmineKeys.DESCRIPTION, Description); writer.WriteDateOrEmpty(RedmineKeys.DUE_DATE, DueDate); } From c6c16bbadb21f147a2b591da87594ac64ca17a0e Mon Sep 17 00:00:00 2001 From: zapadi Date: Fri, 17 Jan 2020 09:03:47 +0200 Subject: [PATCH 106/549] Fix tests compile error --- .../Tests/Async/UserAsyncTests.cs | 2 +- .../Tests/Sync/GroupTests.cs | 4 ++-- .../Tests/Sync/IssueCategoryTests.cs | 12 +++++----- .../Tests/Sync/IssueRelationTests.cs | 4 ++-- .../Tests/Sync/IssueTests.cs | 24 +++++++++++++++---- .../Tests/Sync/ProjectMembershipTests.cs | 8 +++---- .../Tests/Sync/VersionTests.cs | 8 +++---- .../Tests/Sync/WikiPageTests.cs | 18 ++++++++++++++ 8 files changed, 56 insertions(+), 24 deletions(-) diff --git a/tests/redmine-net-api.Tests/Tests/Async/UserAsyncTests.cs b/tests/redmine-net-api.Tests/Tests/Async/UserAsyncTests.cs index 30075e8e..9f72feab 100644 --- a/tests/redmine-net-api.Tests/Tests/Async/UserAsyncTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Async/UserAsyncTests.cs @@ -65,7 +65,7 @@ public async Task Should_Get_X_Users_From_Offset_Y() }); Assert.NotNull(result); - Assert.All (result.Objects, u => Assert.IsType (u)); + Assert.All (result.Items, u => Assert.IsType (u)); } [Fact] diff --git a/tests/redmine-net-api.Tests/Tests/Sync/GroupTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/GroupTests.cs index d0f27027..1dc79a32 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/GroupTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/GroupTests.cs @@ -64,8 +64,8 @@ public void Should_Update_Group() Assert.NotNull(updatedGroup); Assert.True(updatedGroup.Name.Equals(UPDATED_GROUP_NAME), "Group name was not updated."); Assert.NotNull(updatedGroup.Users); - Assert.True(updatedGroup.Users.Find(u => u.Id == UPDATED_GROUP_USER_ID) != null, - "User was not added to group."); + // Assert.True(updatedGroup.Users.Find(u => u.Id == UPDATED_GROUP_USER_ID) != null, + //"User was not added to group."); } [Fact, Order(3)] diff --git a/tests/redmine-net-api.Tests/Tests/Sync/IssueCategoryTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/IssueCategoryTests.cs index 3c4bd254..41f5e33f 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/IssueCategoryTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/IssueCategoryTests.cs @@ -32,7 +32,7 @@ public void Should_Create_IssueCategory() var issueCategory = new IssueCategory { Name = NEW_ISSUE_CATEGORY_NAME, - AsignTo = new IdentifiableName {Id = NEW_ISSUE_CATEGORY_ASIGNEE_ID} + AssignTo = new IdentifiableName {Id = NEW_ISSUE_CATEGORY_ASIGNEE_ID} }; var savedIssueCategory = fixture.RedmineManager.CreateObject(issueCategory, PROJECT_ID); @@ -80,8 +80,8 @@ public void Should_Get_IssueCategory_By_Id() Assert.NotNull(issueCategory); Assert.True(issueCategory.Name.Equals(NEW_ISSUE_CATEGORY_NAME), "Issue category name is invalid."); - Assert.NotNull(issueCategory.AsignTo); - Assert.True(issueCategory.AsignTo.Name.Contains(ISSUE_CATEGORY_ASIGNEE_NAME_TO_GET), + Assert.NotNull(issueCategory.AssignTo); + Assert.True(issueCategory.AssignTo.Name.Contains(ISSUE_CATEGORY_ASIGNEE_NAME_TO_GET), "Asignee name is invalid."); Assert.NotNull(issueCategory.Project); Assert.True(issueCategory.Project.Name.Equals(ISSUE_CATEGORY_PROJECT_NAME_TO_GET), @@ -96,7 +96,7 @@ public void Should_Update_IssueCategory() var issueCategory = fixture.RedmineManager.GetObject(CREATED_ISSUE_CATEGORY_ID, null); issueCategory.Name = ISSUE_CATEGORY_NAME_TO_UPDATE; - issueCategory.AsignTo = new IdentifiableName {Id = ISSUE_CATEGORY_ASIGNEE_ID_TO_UPDATE}; + issueCategory.AssignTo = new IdentifiableName {Id = ISSUE_CATEGORY_ASIGNEE_ID_TO_UPDATE}; fixture.RedmineManager.UpdateObject(CREATED_ISSUE_CATEGORY_ID, issueCategory); @@ -105,8 +105,8 @@ public void Should_Update_IssueCategory() Assert.NotNull(updatedIssueCategory); Assert.True(updatedIssueCategory.Name.Equals(ISSUE_CATEGORY_NAME_TO_UPDATE), "Issue category name was not updated."); - Assert.NotNull(updatedIssueCategory.AsignTo); - Assert.True(updatedIssueCategory.AsignTo.Id == ISSUE_CATEGORY_ASIGNEE_ID_TO_UPDATE, + Assert.NotNull(updatedIssueCategory.AssignTo); + Assert.True(updatedIssueCategory.AssignTo.Id == ISSUE_CATEGORY_ASIGNEE_ID_TO_UPDATE, "Issue category asignee was not updated."); } diff --git a/tests/redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs index 3508d9fd..a06d9084 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs @@ -40,12 +40,12 @@ public IssueRelationTests(RedmineFixture fixture) private const int RELATED_ISSUE_ID = 94; private const int RELATION_DELAY = 2; - private const IssueRelationType OPPOSED_RELATION_TYPE = IssueRelationType.precedes; + private const IssueRelationType OPPOSED_RELATION_TYPE = IssueRelationType.Precedes; [Fact, Order(1)] public void Should_Add_Issue_Relation() { - const IssueRelationType RELATION_TYPE = IssueRelationType.follows; + const IssueRelationType RELATION_TYPE = IssueRelationType.Follows; var relation = new IssueRelation { IssueToId = RELATED_ISSUE_ID, diff --git a/tests/redmine-net-api.Tests/Tests/Sync/IssueTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/IssueTests.cs index dc6e4c3f..c39c3e50 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/IssueTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/IssueTests.cs @@ -45,8 +45,8 @@ public void Should_Get_Paginated_Issues() var issues = fixture.RedmineManager.GetPaginatedObjects(new NameValueCollection { { RedmineKeys.OFFSET, OFFSET.ToString() }, { RedmineKeys.LIMIT, NUMBER_OF_PAGINATED_ISSUES.ToString() }, { "sort", "id:desc" } }); - Assert.NotNull(issues.Objects); - Assert.True(issues.Objects.Count <= NUMBER_OF_PAGINATED_ISSUES, "number of issues ( "+ issues.Objects.Count +" ) != " + NUMBER_OF_PAGINATED_ISSUES.ToString()); + Assert.NotNull(issues.Items); + //Assert.True(issues.Items.Count <= NUMBER_OF_PAGINATED_ISSUES, "number of issues ( "+ issues.Items.Count +" ) != " + NUMBER_OF_PAGINATED_ISSUES.ToString()); } [Fact, Order(3)] @@ -62,7 +62,7 @@ public void Should_Get_Issues_By_subproject_Id() { const string SUBPROJECT_ID = "redmine-net-testr"; - var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { RedmineKeys.SUBPROJECT_ID, SUBPROJECT_ID } }); + var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { RedmineKeys.SUB_PROJECT_ID, SUBPROJECT_ID } }); Assert.NotNull(issues); } @@ -72,7 +72,7 @@ public void Should_Get_Issues_By_Project_Without_Subproject() { const string ALL_SUBPROJECTS = "!*"; - var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { RedmineKeys.PROJECT_ID, PROJECT_ID }, { RedmineKeys.SUBPROJECT_ID, ALL_SUBPROJECTS } }); + var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { RedmineKeys.PROJECT_ID, PROJECT_ID }, { RedmineKeys.SUB_PROJECT_ID, ALL_SUBPROJECTS } }); Assert.NotNull(issues); } @@ -119,7 +119,7 @@ public void Should_Get_Issue_By_Id() { const string ISSUE_ID = "96"; - var issue = fixture.RedmineManager.GetObject(ISSUE_ID, new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.CHILDREN + "," + RedmineKeys.ATTACHMENTS + "," + RedmineKeys.RELATIONS + "," + RedmineKeys.CHANGESETS + "," + RedmineKeys.JOURNALS + "," + RedmineKeys.WATCHERS } }); + var issue = fixture.RedmineManager.GetObject(ISSUE_ID, new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.CHILDREN + "," + RedmineKeys.ATTACHMENTS + "," + RedmineKeys.RELATIONS + "," + RedmineKeys.CHANGE_SETS + "," + RedmineKeys.JOURNALS + "," + RedmineKeys.WATCHERS } }); Assert.NotNull(issue); //TODO: add conditions for all associated data if nedeed @@ -298,5 +298,19 @@ public void Should_Clone_Issue() Assert.True(issueToClone.CustomFields.Count != clonedIssue.CustomFields.Count); } + + + [Fact] + public void Should_Get_Issue_With_Hours() + { + const string ISSUE_ID = "1"; + + var issue = fixture.RedmineManager.GetObject(ISSUE_ID, null); + + Assert.Equal(8.0f,issue.EstimatedHours); + Assert.Equal(8.0f,issue.TotalEstimatedHours); + Assert.Equal(5.0f,issue.TotalSpentHours); + Assert.Equal(5.0f,issue.SpentHours); + } } } \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs index 6a6489e0..898df782 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs @@ -57,8 +57,8 @@ public void Should_Add_Project_Membership() Assert.NotNull(createdPm); Assert.True(createdPm.User.Id == NEW_PROJECT_MEMBERSHIP_USER_ID, "User is invalid."); Assert.NotNull(createdPm.Roles); - Assert.True(createdPm.Roles.Exists(r => r.Id == NEW_PROJECT_MEMBERSHIP_ROLE_ID), - string.Format("Role id {0} does not exist.", NEW_PROJECT_MEMBERSHIP_ROLE_ID)); + //Assert.True(createdPm.Roles.Exists(r => r.Id == NEW_PROJECT_MEMBERSHIP_ROLE_ID), + // string.Format("Role id {0} does not exist.", NEW_PROJECT_MEMBERSHIP_ROLE_ID)); } [Fact,Order(99)] @@ -118,8 +118,8 @@ public void Should_Update_Project_Membership() Assert.NotNull(updatedPm); Assert.NotNull(updatedPm.Roles); - Assert.True(updatedPm.Roles.Find(r => r.Id == UPDATED_PROJECT_MEMBERSHIP_ROLE_ID) != null, - string.Format("Role with id {0} was not found in roles list.", UPDATED_PROJECT_MEMBERSHIP_ROLE_ID)); + //Assert.True(updatedPm.Roles.Find(r => r.Id == UPDATED_PROJECT_MEMBERSHIP_ROLE_ID) != null, + // string.Format("Role with id {0} was not found in roles list.", UPDATED_PROJECT_MEMBERSHIP_ROLE_ID)); } } } \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Tests/Sync/VersionTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/VersionTests.cs index cc9abde9..5f9660e6 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/VersionTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/VersionTests.cs @@ -45,8 +45,8 @@ public VersionTests(RedmineFixture fixture) public void Should_Create_Version() { const string NEW_VERSION_NAME = "VersionTesting"; - const VersionStatus NEW_VERSION_STATUS = VersionStatus.locked; - const VersionSharing NEW_VERSION_SHARING = VersionSharing.hierarchy; + const VersionStatus NEW_VERSION_STATUS = VersionStatus.Locked; + const VersionSharing NEW_VERSION_SHARING = VersionSharing.Hierarchy; DateTime NEW_VERSION_DUE_DATE = DateTime.Now.AddDays(7); const string NEW_VERSION_DESCRIPTION = "Version description"; @@ -115,8 +115,8 @@ public void Should_Update_Version() { const string UPDATED_VERSION_ID = "15"; const string UPDATED_VERSION_NAME = "Updated version"; - const VersionStatus UPDATED_VERSION_STATUS = VersionStatus.closed; - const VersionSharing UPDATED_VERSION_SHARING = VersionSharing.system; + const VersionStatus UPDATED_VERSION_STATUS = VersionStatus.Closed; + const VersionSharing UPDATED_VERSION_SHARING = VersionSharing.System; const string UPDATED_VERSION_DESCRIPTION = "Updated description"; DateTime UPDATED_VERSION_DUE_DATE = DateTime.Now.AddMonths(1); diff --git a/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs index 5674056e..809c4e03 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs @@ -108,5 +108,23 @@ public void Should_Get_Wiki_Page_By_Version() Assert.Equal(oldPage.Title, WIKI_PAGE_NAME); Assert.True(oldPage.Version == WIKI_PAGE_VERSION, "Wiki page version is invalid."); } + + [Fact] + public void Should_Create_Wiki() + { + var author = new IdentifiableName(); + author.Id = 1; + + var result = fixture.RedmineManager.CreateOrUpdateWikiPage("1","pagina2",new WikiPage + { + Text = "ana are mere multe si rosii!", + Comments = "asa", + Version = 1 + }); + + Assert.NotNull(result); + + } + } } \ No newline at end of file From c313bf112f5d3ce5fbb48b5274bb80d3fb7e809f Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 2 Feb 2020 13:54:13 +0200 Subject: [PATCH 107/549] Update dotnetcore.yml Remove tags regex --- .github/workflows/dotnetcore.yml | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml index dccf09d0..1dcfefd5 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/dotnetcore.yml @@ -1,15 +1,13 @@ -name: .NET Core +name: Redmine .NET Api on: push: paths-ignore: - - '**/*.md' - - '**/*.gif' - - '**/*.png' - - LICENSE - - tests/* - tags: - - /v\d*\.\d*\.\d*/ + - '**/*.md' + - '**/*.gif' + - '**/*.png' + - LICENSE + - tests/* pull_request: env: @@ -31,9 +29,9 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: ${{ matrix.dotnet }} - - name: Get the version - id: get_version - run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} - # ${{ steps.get_version.outputs.VERSION }} + #- name: Get the version + # id: get_version + # run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} + # ${{ steps.get_version.outputs.VERSION }} - name: Build with dotnet run: dotnet build redmine-net-api.sln --configuration Release From 118d4761f9da9bc6f25fd4a8a99535d81564722b Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 2 Feb 2020 14:03:34 +0200 Subject: [PATCH 108/549] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 419816ca..154284f9 100755 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ ![Nuget](https://img.shields.io/nuget/dt/redmine-net-api) +![Redmine .NET Api](https://github.com/zapadi/redmine-net-api/workflows/Redmine%20.NET%20Api/badge.svg?branch=master) ![Appveyor last build status](https://ci.appveyor.com/api/projects/status/github/zapadi/redmine-net-api?branch=master&svg=true&passingText=master%20-%20OK&failingText=ups...) [![NuGet package](https://img.shields.io/nuget/v/redmine-api.svg)](https://www.nuget.org/packages/redmine-api) Buy Me A Coffee From fc4374eb11281e6afff9a8dfdabad82e987a396c Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 2 Feb 2020 14:10:58 +0200 Subject: [PATCH 109/549] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 154284f9..c3c8f8b3 100755 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ redmine-net-api is a library for communicating with a Redmine project management application. * Uses [Redmine's REST API.](http://www.redmine.org/projects/redmine/wiki/Rest_api/) -* Supports both XML and **JSON(requires .NET Framework 3.5 or higher)** formats. +* Supports both XML and **JSON** formats. * Supports GZipped responses from servers. * This API provides access and basic CRUD operations (create, read, update, delete) for the resources described below: From ccf2ce111edd5430fd40aa9bcefb0eaedd0da787 Mon Sep 17 00:00:00 2001 From: zapadi Date: Fri, 17 Jan 2020 09:01:07 +0200 Subject: [PATCH 110/549] Fix redmine keys --- src/redmine-net-api/RedmineKeys.cs | 29 ++++++++++++++++++++++--- src/redmine-net-api/Types/Attachment.cs | 8 +++---- src/redmine-net-api/Types/File.cs | 8 +++---- src/redmine-net-api/Types/Upload.cs | 8 +++---- src/redmine-net-api/Types/User.cs | 16 +++++++------- 5 files changed, 46 insertions(+), 23 deletions(-) diff --git a/src/redmine-net-api/RedmineKeys.cs b/src/redmine-net-api/RedmineKeys.cs index 69c18200..deb03f4d 100644 --- a/src/redmine-net-api/RedmineKeys.cs +++ b/src/redmine-net-api/RedmineKeys.cs @@ -31,6 +31,10 @@ public static class RedmineKeys /// /// /// + public const string ADMIN = "admin"; + /// + /// + /// public const string ALL = "*"; /// /// @@ -105,6 +109,11 @@ public static class RedmineKeys /// public const string CREATED_ON = "created_on"; + /// + /// + /// + public const string CURRENT = "current"; + /// /// /// @@ -193,15 +202,20 @@ public static class RedmineKeys /// /// /// - public const string FILENAME = "filename"; + public const string FILE_NAME = "filename"; /// /// /// public const string FILE_SIZE = "filesize"; + + /// + /// + /// + public const string FILES = "files"; /// /// /// - public const string FIRSTNAME = "firstname"; + public const string FIRST_NAME = "firstname"; /// /// /// @@ -334,7 +348,7 @@ public static class RedmineKeys /// /// /// - public const string LASTNAME = "lastname"; + public const string LAST_NAME = "lastname"; /// /// /// @@ -650,6 +664,10 @@ public static class RedmineKeys /// /// /// + public const string VERSIONS = "versions"; + /// + /// + /// public const string VISIBLE = "visible"; /// /// @@ -666,10 +684,15 @@ public static class RedmineKeys /// /// /// + public const string WIKI = "wiki"; + /// + /// + /// public const string WIKI_PAGE = "wiki_page"; /// /// /// public const string WIKI_PAGES = "wiki_pages"; + } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Attachment.cs b/src/redmine-net-api/Types/Attachment.cs index dd9f053b..9d8fb591 100644 --- a/src/redmine-net-api/Types/Attachment.cs +++ b/src/redmine-net-api/Types/Attachment.cs @@ -107,7 +107,7 @@ public override void ReadXml(XmlReader reader) case RedmineKeys.CONTENT_URL: ContentUrl = reader.ReadElementContentAsString(); break; case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; case RedmineKeys.DESCRIPTION: Description = reader.ReadElementContentAsString(); break; - case RedmineKeys.FILENAME: FileName = reader.ReadElementContentAsString(); break; + case RedmineKeys.FILE_NAME: FileName = reader.ReadElementContentAsString(); break; case RedmineKeys.FILE_SIZE: FileSize = reader.ReadElementContentAsInt(); break; case RedmineKeys.THUMBNAIL_URL: ThumbnailUrl = reader.ReadElementContentAsString(); break; default: reader.Read(); break; @@ -122,7 +122,7 @@ public override void ReadXml(XmlReader reader) public override void WriteXml(XmlWriter writer) { writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); - writer.WriteElementString(RedmineKeys.FILENAME, FileName); + writer.WriteElementString(RedmineKeys.FILE_NAME, FileName); } #endregion @@ -154,7 +154,7 @@ public override void ReadJson(JsonReader reader) case RedmineKeys.CONTENT_URL: ContentUrl = reader.ReadAsString(); break; case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadAsDateTime(); break; case RedmineKeys.DESCRIPTION: Description = reader.ReadAsString(); break; - case RedmineKeys.FILENAME: FileName = reader.ReadAsString(); break; + case RedmineKeys.FILE_NAME: FileName = reader.ReadAsString(); break; case RedmineKeys.FILE_SIZE: FileSize = reader.ReadAsInt(); break; case RedmineKeys.THUMBNAIL_URL: ThumbnailUrl = reader.ReadAsString(); break; default: reader.Read(); break; @@ -171,7 +171,7 @@ public override void WriteJson(JsonWriter writer) { using (new JsonObject(writer, RedmineKeys.ATTACHMENT)) { - writer.WriteProperty(RedmineKeys.FILENAME, FileName); + writer.WriteProperty(RedmineKeys.FILE_NAME, FileName); writer.WriteProperty(RedmineKeys.DESCRIPTION, Description); } } diff --git a/src/redmine-net-api/Types/File.cs b/src/redmine-net-api/Types/File.cs index f8d017cf..cc837c80 100644 --- a/src/redmine-net-api/Types/File.cs +++ b/src/redmine-net-api/Types/File.cs @@ -119,7 +119,7 @@ public override void ReadXml(XmlReader reader) case RedmineKeys.DESCRIPTION: Description = reader.ReadElementContentAsString(); break; case RedmineKeys.DIGEST: Digest = reader.ReadElementContentAsString(); break; case RedmineKeys.DOWNLOADS: Downloads = reader.ReadElementContentAsInt(); break; - case RedmineKeys.FILENAME: Filename = reader.ReadElementContentAsString(); break; + case RedmineKeys.FILE_NAME: Filename = reader.ReadElementContentAsString(); break; case RedmineKeys.FILE_SIZE: FileSize = reader.ReadElementContentAsInt(); break; case RedmineKeys.TOKEN: Token = reader.ReadElementContentAsString(); break; case RedmineKeys.VERSION: Version = new IdentifiableName(reader); break; @@ -137,7 +137,7 @@ public override void WriteXml(XmlWriter writer) { writer.WriteElementString(RedmineKeys.TOKEN, Token); writer.WriteIdIfNotNull(RedmineKeys.VERSION_ID, Version); - writer.WriteElementString(RedmineKeys.FILENAME, Filename); + writer.WriteElementString(RedmineKeys.FILE_NAME, Filename); writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); } #endregion @@ -171,7 +171,7 @@ public override void ReadJson(JsonReader reader) case RedmineKeys.DESCRIPTION: Description = reader.ReadAsString(); break; case RedmineKeys.DIGEST: Digest = reader.ReadAsString(); break; case RedmineKeys.DOWNLOADS: Downloads = reader.ReadAsInt32().GetValueOrDefault(); break; - case RedmineKeys.FILENAME: Filename = reader.ReadAsString(); break; + case RedmineKeys.FILE_NAME: Filename = reader.ReadAsString(); break; case RedmineKeys.FILE_SIZE: FileSize = reader.ReadAsInt32().GetValueOrDefault(); break; case RedmineKeys.TOKEN: Token = reader.ReadAsString(); break; case RedmineKeys.VERSION: Version = new IdentifiableName(reader); break; @@ -193,7 +193,7 @@ public override void WriteJson(JsonWriter writer) { writer.WriteProperty(RedmineKeys.TOKEN, Token); writer.WriteIdIfNotNull(RedmineKeys.VERSION_ID, Version); - writer.WriteProperty(RedmineKeys.FILENAME, Filename); + writer.WriteProperty(RedmineKeys.FILE_NAME, Filename); writer.WriteProperty(RedmineKeys.DESCRIPTION, Description); } } diff --git a/src/redmine-net-api/Types/Upload.cs b/src/redmine-net-api/Types/Upload.cs index 9450442a..6074fcf1 100644 --- a/src/redmine-net-api/Types/Upload.cs +++ b/src/redmine-net-api/Types/Upload.cs @@ -86,7 +86,7 @@ public void ReadXml(XmlReader reader) { case RedmineKeys.CONTENT_TYPE: ContentType = reader.ReadElementContentAsString(); break; case RedmineKeys.DESCRIPTION: Description = reader.ReadElementContentAsString(); break; - case RedmineKeys.FILENAME: FileName = reader.ReadElementContentAsString(); break; + case RedmineKeys.FILE_NAME: FileName = reader.ReadElementContentAsString(); break; case RedmineKeys.TOKEN: Token = reader.ReadElementContentAsString(); break; default: reader.Read(); break; } @@ -101,7 +101,7 @@ public void WriteXml(XmlWriter writer) { writer.WriteElementString(RedmineKeys.TOKEN, Token); writer.WriteElementString(RedmineKeys.CONTENT_TYPE, ContentType); - writer.WriteElementString(RedmineKeys.FILENAME, FileName); + writer.WriteElementString(RedmineKeys.FILE_NAME, FileName); writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); } #endregion @@ -129,7 +129,7 @@ public void ReadJson(JsonReader reader) { case RedmineKeys.CONTENT_TYPE: ContentType = reader.ReadAsString(); break; case RedmineKeys.DESCRIPTION: Description = reader.ReadAsString(); break; - case RedmineKeys.FILENAME: FileName = reader.ReadAsString(); break; + case RedmineKeys.FILE_NAME: FileName = reader.ReadAsString(); break; case RedmineKeys.TOKEN: Token = reader.ReadAsString(); break; default: reader.Read(); break; } @@ -144,7 +144,7 @@ public void WriteJson(JsonWriter writer) { writer.WriteProperty(RedmineKeys.TOKEN, Token); writer.WriteProperty(RedmineKeys.CONTENT_TYPE, ContentType); - writer.WriteProperty(RedmineKeys.FILENAME, FileName); + writer.WriteProperty(RedmineKeys.FILE_NAME, FileName); writer.WriteProperty(RedmineKeys.DESCRIPTION, Description); } #endregion diff --git a/src/redmine-net-api/Types/User.cs b/src/redmine-net-api/Types/User.cs index d4c9fa59..d815ced8 100644 --- a/src/redmine-net-api/Types/User.cs +++ b/src/redmine-net-api/Types/User.cs @@ -154,10 +154,10 @@ public override void ReadXml(XmlReader reader) case RedmineKeys.AUTH_SOURCE_ID: AuthenticationModeId = reader.ReadElementContentAsNullableInt(); break; case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadElementContentAsCollection(); break; - case RedmineKeys.FIRSTNAME: FirstName = reader.ReadElementContentAsString(); break; + case RedmineKeys.FIRST_NAME: FirstName = reader.ReadElementContentAsString(); break; case RedmineKeys.GROUPS: Groups = reader.ReadElementContentAsCollection(); break; case RedmineKeys.LAST_LOGIN_ON: LastLoginOn = reader.ReadElementContentAsNullableDateTime(); break; - case RedmineKeys.LASTNAME: LastName = reader.ReadElementContentAsString(); break; + case RedmineKeys.LAST_NAME: LastName = reader.ReadElementContentAsString(); break; case RedmineKeys.LOGIN: Login = reader.ReadElementContentAsString(); break; case RedmineKeys.MAIL: Email = reader.ReadElementContentAsString(); break; case RedmineKeys.MAIL_NOTIFICATION: MailNotification = reader.ReadElementContentAsString(); break; @@ -176,8 +176,8 @@ public override void ReadXml(XmlReader reader) public override void WriteXml(XmlWriter writer) { writer.WriteElementString(RedmineKeys.LOGIN, Login); - writer.WriteElementString(RedmineKeys.FIRSTNAME, FirstName); - writer.WriteElementString(RedmineKeys.LASTNAME, LastName); + writer.WriteElementString(RedmineKeys.FIRST_NAME, FirstName); + writer.WriteElementString(RedmineKeys.LAST_NAME, LastName); writer.WriteElementString(RedmineKeys.MAIL, Email); writer.WriteElementString(RedmineKeys.MAIL_NOTIFICATION, MailNotification); writer.WriteElementString(RedmineKeys.PASSWORD, Password); @@ -215,9 +215,9 @@ public override void ReadJson(JsonReader reader) case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadAsDateTime(); break; case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadAsCollection(); break; case RedmineKeys.LAST_LOGIN_ON: LastLoginOn = reader.ReadAsDateTime(); break; - case RedmineKeys.LASTNAME: LastName = reader.ReadAsString(); break; + case RedmineKeys.LAST_NAME: LastName = reader.ReadAsString(); break; case RedmineKeys.LOGIN: Login = reader.ReadAsString(); break; - case RedmineKeys.FIRSTNAME: FirstName = reader.ReadAsString(); break; + case RedmineKeys.FIRST_NAME: FirstName = reader.ReadAsString(); break; case RedmineKeys.GROUPS: Groups = reader.ReadAsCollection(); break; case RedmineKeys.MAIL: Email = reader.ReadAsString(); break; case RedmineKeys.MAIL_NOTIFICATION: MailNotification = reader.ReadAsString(); break; @@ -238,8 +238,8 @@ public override void WriteJson(JsonWriter writer) using (new JsonObject(writer, RedmineKeys.USER)) { writer.WriteProperty(RedmineKeys.LOGIN, Login); - writer.WriteProperty(RedmineKeys.FIRSTNAME, FirstName); - writer.WriteProperty(RedmineKeys.LASTNAME, LastName); + writer.WriteProperty(RedmineKeys.FIRST_NAME, FirstName); + writer.WriteProperty(RedmineKeys.LAST_NAME, LastName); writer.WriteProperty(RedmineKeys.MAIL, Email); writer.WriteProperty(RedmineKeys.MAIL_NOTIFICATION, MailNotification); writer.WriteProperty(RedmineKeys.PASSWORD, Password); From 12c196720a60fa09637abfc0a331b0b347120d26 Mon Sep 17 00:00:00 2001 From: zapadi Date: Fri, 17 Jan 2020 08:59:25 +0200 Subject: [PATCH 111/549] Add GET verb. --- src/redmine-net-api/HttpVerbs.cs | 4 ++++ 1 file changed, 4 insertions(+) mode change 100755 => 100644 src/redmine-net-api/HttpVerbs.cs diff --git a/src/redmine-net-api/HttpVerbs.cs b/src/redmine-net-api/HttpVerbs.cs old mode 100755 new mode 100644 index 30a95d28..b66ef937 --- a/src/redmine-net-api/HttpVerbs.cs +++ b/src/redmine-net-api/HttpVerbs.cs @@ -22,6 +22,10 @@ namespace Redmine.Net.Api /// public static class HttpVerbs { + /// + /// Represents an HTTP GET protocol method that is used to get an entity identified by a URI. + /// + public const string GET = "GET"; /// /// Represents an HTTP PUT protocol method that is used to replace an entity identified by a URI. /// From 02d8620c6392709725bba7c0991792f837dd6424 Mon Sep 17 00:00:00 2001 From: zapadi Date: Fri, 17 Jan 2020 09:05:15 +0200 Subject: [PATCH 112/549] Add usersecrets to test project --- tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj index a11a5962..e809acc2 100644 --- a/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj +++ b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj @@ -1,4 +1,4 @@ - + @@ -9,6 +9,7 @@ false redmine.net.api.Tests redmine-net-api.Tests + f8b9e946-b547-42f1-861c-f719dca00a84 From 76ac3c9c1a9adb3be976eb874bbcc60b1cacc168 Mon Sep 17 00:00:00 2001 From: zapadi Date: Fri, 17 Jan 2020 09:04:37 +0200 Subject: [PATCH 113/549] Add ToQueryString extension --- .../NameValueCollectionExtensions.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/redmine-net-api/Extensions/NameValueCollectionExtensions.cs b/src/redmine-net-api/Extensions/NameValueCollectionExtensions.cs index 1f56d451..1843ebe4 100644 --- a/src/redmine-net-api/Extensions/NameValueCollectionExtensions.cs +++ b/src/redmine-net-api/Extensions/NameValueCollectionExtensions.cs @@ -15,6 +15,7 @@ limitations under the License. */ using System.Collections.Specialized; +using System.Text; namespace Redmine.Net.Api.Extensions { @@ -40,5 +41,52 @@ public static string GetParameterValue(this NameValueCollection parameters, stri return value.IsNullOrWhiteSpace() ? null : value; } + + /// + /// Gets the parameter value. + /// + /// The parameters. + /// Name of the parameter. + /// + public static string GetValue(this NameValueCollection parameters, string parameterName) + { + if (parameters == null) + { + return null; + } + + var value = parameters.Get(parameterName); + + return value.IsNullOrWhiteSpace() ? null : value; + } + + /// + /// + /// + /// + /// + public static string ToQueryString(this NameValueCollection requestParameters) + { + if (requestParameters == null || requestParameters.Count == 0) + { + return null; + } + + var stringBuilder = new StringBuilder(); + + for (var index = 0; index < requestParameters.Count; ++index) + { + stringBuilder.AppendFormat("{0}={1}&",requestParameters.AllKeys[index],requestParameters[index]); + + } + + stringBuilder.Length -= 1; + + stringBuilder.Insert(0, "?"); + + return stringBuilder.ToString(); + + } + } } \ No newline at end of file From fb505cf9c0aac4123f9581d23be2bf7ddaeb0cf4 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 2 Feb 2020 14:50:47 +0200 Subject: [PATCH 114/549] Add ToSecureString extension --- .../Extensions/StringExtensions.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/redmine-net-api/Extensions/StringExtensions.cs b/src/redmine-net-api/Extensions/StringExtensions.cs index ea2d011e..64b4fe14 100644 --- a/src/redmine-net-api/Extensions/StringExtensions.cs +++ b/src/redmine-net-api/Extensions/StringExtensions.cs @@ -1,4 +1,6 @@ +using System; using System.Diagnostics.CodeAnalysis; +using System.Security; namespace Redmine.Net.Api.Extensions { @@ -62,5 +64,32 @@ public static string ToLowerInv(this string text) { return text.IsNullOrWhiteSpace() ? text : text.ToLowerInvariant(); } + + /// + /// Transforms a string into a SecureString. + /// + /// + /// The string to transform. + /// + /// + /// A secure string representing the contents of the original string. + /// + internal static SecureString ToSecureString(this string value) + { + if (value.IsNullOrWhiteSpace()) + { + return null; + } + + using (var rv = new SecureString()) + { + foreach (var c in value) + { + rv.AppendChar(c); + } + + return rv; + } + } } } \ No newline at end of file From 56f0b7aade25df709d3b10ad987934f8ebd39307 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 2 Feb 2020 14:52:31 +0200 Subject: [PATCH 115/549] Replace AppendFormat with Append (ToQueryString extension) --- .../NameValueCollectionExtensions.cs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/redmine-net-api/Extensions/NameValueCollectionExtensions.cs b/src/redmine-net-api/Extensions/NameValueCollectionExtensions.cs index 1843ebe4..3157ae68 100644 --- a/src/redmine-net-api/Extensions/NameValueCollectionExtensions.cs +++ b/src/redmine-net-api/Extensions/NameValueCollectionExtensions.cs @@ -15,6 +15,7 @@ limitations under the License. */ using System.Collections.Specialized; +using System.Globalization; using System.Text; namespace Redmine.Net.Api.Extensions @@ -76,16 +77,23 @@ public static string ToQueryString(this NameValueCollection requestParameters) for (var index = 0; index < requestParameters.Count; ++index) { - stringBuilder.AppendFormat("{0}={1}&",requestParameters.AllKeys[index],requestParameters[index]); - + stringBuilder + .Append(requestParameters.AllKeys[index].ToString(CultureInfo.InvariantCulture)) + .Append("=") + .Append(requestParameters[index].ToString(CultureInfo.InvariantCulture)) + .Append("&"); } stringBuilder.Length -= 1; - stringBuilder.Insert(0, "?"); - - return stringBuilder.ToString(); + var queryString = stringBuilder.ToString(); + #if !(NET20) + stringBuilder.Clear(); + #endif + stringBuilder = null; + + return queryString; } } From 044b510a0eb7302937db0ac5156cd0685e2b39d9 Mon Sep 17 00:00:00 2001 From: Tobias Gaertner Date: Tue, 18 Feb 2020 10:24:32 +0100 Subject: [PATCH 116/549] fix serialization of CustomFieldRole --- src/redmine-net-api/Types/CustomFieldRole.cs | 6 ++++++ src/redmine-net-api/redmine-net-api.csproj | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/redmine-net-api/Types/CustomFieldRole.cs b/src/redmine-net-api/Types/CustomFieldRole.cs index 9aa7020f..62e6d568 100644 --- a/src/redmine-net-api/Types/CustomFieldRole.cs +++ b/src/redmine-net-api/Types/CustomFieldRole.cs @@ -26,6 +26,12 @@ namespace Redmine.Net.Api.Types [XmlRoot(RedmineKeys.ROLE)] public sealed class CustomFieldRole : IdentifiableName { + /// + /// Initializes a new instance of the class. + /// + /// Serialization + public CustomFieldRole() { } + internal CustomFieldRole(int id, string name) { Id = id; diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 1b87fd50..5ebff589 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -49,7 +49,8 @@ https://github.com/zapadi/redmine-net-api ... Redmine .NET API Client - 3.0.6 + 3.0.6 + 3.0.6.1 From 13f8deb4b309717be537918f875b91a42e570739 Mon Sep 17 00:00:00 2001 From: zapadi Date: Fri, 21 Feb 2020 09:35:56 +0200 Subject: [PATCH 117/549] Update nuget key --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index a8e24db6..541dc970 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -82,7 +82,7 @@ for: - provider: NuGet name: production api_key: - secure: fo+5VNPIRQ98jFPBZSd4SsOVyXEsTxnQ52VWTrg3sgH1GwGXhi70Q561eimlmRhy + secure: iQKBODPsLcVrf7JQV5IR1jDHq01NiqEDmgj8N0Ahktuu76dKCs827tLggGMO9Mkd skip_symbols: true on: branch: master From f9906e46e00bce2aaeeeb27faa8ad3f22918f837 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 5 Apr 2020 18:30:41 +0300 Subject: [PATCH 118/549] Fix #257 --- src/redmine-net-api/Types/CustomField.cs | 4 ++++ src/redmine-net-api/Types/CustomFieldRole.cs | 5 +++++ src/redmine-net-api/Types/CustomFieldValue.cs | 4 ++-- src/redmine-net-api/Types/Group.cs | 4 ++++ src/redmine-net-api/Types/GroupUser.cs | 5 +++++ src/redmine-net-api/Types/IssueCustomField.cs | 4 ++++ src/redmine-net-api/Types/Project.cs | 6 ++++++ src/redmine-net-api/Types/ProjectEnabledModule.cs | 5 +++++ src/redmine-net-api/Types/ProjectIssueCategory.cs | 5 +++++ src/redmine-net-api/Types/ProjectTimeEntryActivity.cs | 5 +++++ src/redmine-net-api/Types/ProjectTracker.cs | 5 +++++ src/redmine-net-api/Types/Role.cs | 4 ++++ src/redmine-net-api/Types/TimeEntryActivity.cs | 8 +++++++- src/redmine-net-api/Types/UserGroup.cs | 5 +++++ src/redmine-net-api/Types/Watcher.cs | 6 ++++++ 15 files changed, 72 insertions(+), 3 deletions(-) diff --git a/src/redmine-net-api/Types/CustomField.cs b/src/redmine-net-api/Types/CustomField.cs index 84a36208..48705cb9 100644 --- a/src/redmine-net-api/Types/CustomField.cs +++ b/src/redmine-net-api/Types/CustomField.cs @@ -37,6 +37,10 @@ public sealed class CustomField : IdentifiableName, IEquatable /// /// /// + new public string Name { get; set; } + /// + /// + /// public string CustomizedType { get; internal set; } /// diff --git a/src/redmine-net-api/Types/CustomFieldRole.cs b/src/redmine-net-api/Types/CustomFieldRole.cs index 62e6d568..73a4bce8 100644 --- a/src/redmine-net-api/Types/CustomFieldRole.cs +++ b/src/redmine-net-api/Types/CustomFieldRole.cs @@ -38,6 +38,11 @@ internal CustomFieldRole(int id, string name) Name = name; } + /// + /// + /// + new public string Name { get; set; } + /// /// /// diff --git a/src/redmine-net-api/Types/CustomFieldValue.cs b/src/redmine-net-api/Types/CustomFieldValue.cs index d2b264b8..3d902482 100644 --- a/src/redmine-net-api/Types/CustomFieldValue.cs +++ b/src/redmine-net-api/Types/CustomFieldValue.cs @@ -47,13 +47,13 @@ public CustomFieldValue(string value) { Info = value; } - + #region Properties /// /// /// - public string Info { get; internal set; } + public string Info { get; set; } #endregion diff --git a/src/redmine-net-api/Types/Group.cs b/src/redmine-net-api/Types/Group.cs index cd523316..18926c40 100644 --- a/src/redmine-net-api/Types/Group.cs +++ b/src/redmine-net-api/Types/Group.cs @@ -49,6 +49,10 @@ public Group(string name) #region Properties /// + /// + /// + new public string Name { get; set; } + /// /// Represents the group's users. /// public IList Users { get; internal set; } diff --git a/src/redmine-net-api/Types/GroupUser.cs b/src/redmine-net-api/Types/GroupUser.cs index cb2da9d9..341122cf 100644 --- a/src/redmine-net-api/Types/GroupUser.cs +++ b/src/redmine-net-api/Types/GroupUser.cs @@ -27,6 +27,11 @@ namespace Redmine.Net.Api.Types [XmlRoot(RedmineKeys.USER)] public sealed class GroupUser : IdentifiableName, IValue { + /// + /// + /// + new public string Name { get; set; } + #region Implementation of IValue /// /// diff --git a/src/redmine-net-api/Types/IssueCustomField.cs b/src/redmine-net-api/Types/IssueCustomField.cs index 1a03aa27..71f66478 100644 --- a/src/redmine-net-api/Types/IssueCustomField.cs +++ b/src/redmine-net-api/Types/IssueCustomField.cs @@ -35,6 +35,10 @@ public sealed class IssueCustomField : IdentifiableName, IEquatable + /// + /// + new public string Name { get; set; } + /// /// Gets or sets the value. /// /// The value. diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs index fd3dece0..b0bcf23f 100644 --- a/src/redmine-net-api/Types/Project.cs +++ b/src/redmine-net-api/Types/Project.cs @@ -35,6 +35,12 @@ namespace Redmine.Net.Api.Types public sealed class Project : IdentifiableName, IEquatable { #region Properties + + /// + /// + /// + new public string Name { get; set; } + /// /// Gets or sets the identifier. /// diff --git a/src/redmine-net-api/Types/ProjectEnabledModule.cs b/src/redmine-net-api/Types/ProjectEnabledModule.cs index 055703c8..ce9b6ff0 100644 --- a/src/redmine-net-api/Types/ProjectEnabledModule.cs +++ b/src/redmine-net-api/Types/ProjectEnabledModule.cs @@ -50,6 +50,11 @@ public ProjectEnabledModule(string moduleName) #endregion + /// + /// + /// + new public string Name { get; set; } + #region Implementation of IValue /// /// diff --git a/src/redmine-net-api/Types/ProjectIssueCategory.cs b/src/redmine-net-api/Types/ProjectIssueCategory.cs index b4c9aa0d..30455354 100644 --- a/src/redmine-net-api/Types/ProjectIssueCategory.cs +++ b/src/redmine-net-api/Types/ProjectIssueCategory.cs @@ -37,6 +37,11 @@ internal ProjectIssueCategory(int id, string name) Name = name; } + /// + /// + /// + new public string Name { get; set; } + /// /// /// diff --git a/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs b/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs index 43251a43..fbdcac35 100644 --- a/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs +++ b/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs @@ -21,6 +21,11 @@ internal ProjectTimeEntryActivity(int id, string name) Name = name; } + /// + /// + /// + new public string Name { get; set; } + /// /// /// diff --git a/src/redmine-net-api/Types/ProjectTracker.cs b/src/redmine-net-api/Types/ProjectTracker.cs index 95e60de2..5c06cab9 100644 --- a/src/redmine-net-api/Types/ProjectTracker.cs +++ b/src/redmine-net-api/Types/ProjectTracker.cs @@ -52,6 +52,11 @@ internal ProjectTracker(int trackerId) Id = trackerId; } + /// + /// + /// + new public string Name { get; set; } + #region Implementation of IValue /// diff --git a/src/redmine-net-api/Types/Role.cs b/src/redmine-net-api/Types/Role.cs index c0e544d5..bb07bc55 100644 --- a/src/redmine-net-api/Types/Role.cs +++ b/src/redmine-net-api/Types/Role.cs @@ -34,6 +34,10 @@ public sealed class Role : IdentifiableName, IEquatable { #region Properties /// + /// + /// + new public string Name { get; set; } + /// /// Gets the permissions. /// /// diff --git a/src/redmine-net-api/Types/TimeEntryActivity.cs b/src/redmine-net-api/Types/TimeEntryActivity.cs index ad0f9547..ab000304 100644 --- a/src/redmine-net-api/Types/TimeEntryActivity.cs +++ b/src/redmine-net-api/Types/TimeEntryActivity.cs @@ -32,7 +32,6 @@ namespace Redmine.Net.Api.Types [XmlRoot(RedmineKeys.TIME_ENTRY_ACTIVITY)] public sealed class TimeEntryActivity : IdentifiableName, IEquatable { - #region Properties /// /// /// @@ -44,6 +43,13 @@ internal TimeEntryActivity(int id, string name) Name = name; } + #region Properties + + /// + /// + /// + new public string Name { get; set; } + /// /// /// diff --git a/src/redmine-net-api/Types/UserGroup.cs b/src/redmine-net-api/Types/UserGroup.cs index 5b3538b1..72942128 100644 --- a/src/redmine-net-api/Types/UserGroup.cs +++ b/src/redmine-net-api/Types/UserGroup.cs @@ -26,6 +26,11 @@ namespace Redmine.Net.Api.Types [XmlRoot(RedmineKeys.GROUP)] public sealed class UserGroup : IdentifiableName { + /// + /// + /// + new public string Name { get; set; } + /// /// /// diff --git a/src/redmine-net-api/Types/Watcher.cs b/src/redmine-net-api/Types/Watcher.cs index 2fd8735c..4f64a590 100644 --- a/src/redmine-net-api/Types/Watcher.cs +++ b/src/redmine-net-api/Types/Watcher.cs @@ -28,6 +28,12 @@ namespace Redmine.Net.Api.Types [XmlRoot(RedmineKeys.USER)] public sealed class Watcher : IdentifiableName, IValue, ICloneable { + /// + /// + /// + new public string Name { get; set; } + + #region Implementation of IValue /// /// From c30f4f012fb81ef76108a5d35d6c4ac6d02858fe Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 5 Apr 2020 18:31:02 +0300 Subject: [PATCH 119/549] Small refactoring --- .../Internals/XmlTextReaderBuilder.cs | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs b/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs index ec4d07d6..8a3a63d7 100644 --- a/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs +++ b/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs @@ -9,6 +9,14 @@ namespace Redmine.Net.Api.Internals public static class XmlTextReaderBuilder { #if NET20 + private static readonly XmlReaderSettings xmlReaderSettings = new XmlReaderSettings() + { + ProhibitDtd = true, + XmlResolver = null, + IgnoreComments = true, + IgnoreWhitespace = true, + }; + /// /// /// @@ -16,13 +24,7 @@ public static class XmlTextReaderBuilder /// public static XmlReader Create(StringReader stringReader) { - return XmlReader.Create(stringReader, new XmlReaderSettings() - { - ProhibitDtd = true, - XmlResolver = null, - IgnoreComments = true, - IgnoreWhitespace = true, - }); + return XmlReader.Create(stringReader, xmlReaderSettings); } @@ -35,13 +37,7 @@ public static XmlReader Create(string xml) { var stringReader = new StringReader(xml); { - return XmlReader.Create(stringReader, new XmlReaderSettings() - { - ProhibitDtd = true, - XmlResolver = null, - IgnoreComments = true, - IgnoreWhitespace = true, - }); + return XmlReader.Create(stringReader, xmlReaderSettings); } } #else From 30cba497c415b257e7672c20796a81dbeea34235 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 5 Apr 2020 18:30:41 +0300 Subject: [PATCH 120/549] Fix #257,#253 --- src/redmine-net-api/Types/CustomField.cs | 4 ++++ src/redmine-net-api/Types/CustomFieldRole.cs | 5 +++++ src/redmine-net-api/Types/CustomFieldValue.cs | 4 ++-- src/redmine-net-api/Types/Group.cs | 4 ++++ src/redmine-net-api/Types/GroupUser.cs | 5 +++++ src/redmine-net-api/Types/IssueCustomField.cs | 4 ++++ src/redmine-net-api/Types/Project.cs | 6 ++++++ src/redmine-net-api/Types/ProjectEnabledModule.cs | 5 +++++ src/redmine-net-api/Types/ProjectIssueCategory.cs | 5 +++++ src/redmine-net-api/Types/ProjectTimeEntryActivity.cs | 5 +++++ src/redmine-net-api/Types/ProjectTracker.cs | 5 +++++ src/redmine-net-api/Types/Role.cs | 4 ++++ src/redmine-net-api/Types/TimeEntryActivity.cs | 8 +++++++- src/redmine-net-api/Types/UserGroup.cs | 5 +++++ src/redmine-net-api/Types/Watcher.cs | 6 ++++++ 15 files changed, 72 insertions(+), 3 deletions(-) diff --git a/src/redmine-net-api/Types/CustomField.cs b/src/redmine-net-api/Types/CustomField.cs index 84a36208..48705cb9 100644 --- a/src/redmine-net-api/Types/CustomField.cs +++ b/src/redmine-net-api/Types/CustomField.cs @@ -37,6 +37,10 @@ public sealed class CustomField : IdentifiableName, IEquatable /// /// /// + new public string Name { get; set; } + /// + /// + /// public string CustomizedType { get; internal set; } /// diff --git a/src/redmine-net-api/Types/CustomFieldRole.cs b/src/redmine-net-api/Types/CustomFieldRole.cs index 62e6d568..73a4bce8 100644 --- a/src/redmine-net-api/Types/CustomFieldRole.cs +++ b/src/redmine-net-api/Types/CustomFieldRole.cs @@ -38,6 +38,11 @@ internal CustomFieldRole(int id, string name) Name = name; } + /// + /// + /// + new public string Name { get; set; } + /// /// /// diff --git a/src/redmine-net-api/Types/CustomFieldValue.cs b/src/redmine-net-api/Types/CustomFieldValue.cs index d2b264b8..3d902482 100644 --- a/src/redmine-net-api/Types/CustomFieldValue.cs +++ b/src/redmine-net-api/Types/CustomFieldValue.cs @@ -47,13 +47,13 @@ public CustomFieldValue(string value) { Info = value; } - + #region Properties /// /// /// - public string Info { get; internal set; } + public string Info { get; set; } #endregion diff --git a/src/redmine-net-api/Types/Group.cs b/src/redmine-net-api/Types/Group.cs index cd523316..18926c40 100644 --- a/src/redmine-net-api/Types/Group.cs +++ b/src/redmine-net-api/Types/Group.cs @@ -49,6 +49,10 @@ public Group(string name) #region Properties /// + /// + /// + new public string Name { get; set; } + /// /// Represents the group's users. /// public IList Users { get; internal set; } diff --git a/src/redmine-net-api/Types/GroupUser.cs b/src/redmine-net-api/Types/GroupUser.cs index cb2da9d9..341122cf 100644 --- a/src/redmine-net-api/Types/GroupUser.cs +++ b/src/redmine-net-api/Types/GroupUser.cs @@ -27,6 +27,11 @@ namespace Redmine.Net.Api.Types [XmlRoot(RedmineKeys.USER)] public sealed class GroupUser : IdentifiableName, IValue { + /// + /// + /// + new public string Name { get; set; } + #region Implementation of IValue /// /// diff --git a/src/redmine-net-api/Types/IssueCustomField.cs b/src/redmine-net-api/Types/IssueCustomField.cs index 1a03aa27..71f66478 100644 --- a/src/redmine-net-api/Types/IssueCustomField.cs +++ b/src/redmine-net-api/Types/IssueCustomField.cs @@ -35,6 +35,10 @@ public sealed class IssueCustomField : IdentifiableName, IEquatable + /// + /// + new public string Name { get; set; } + /// /// Gets or sets the value. /// /// The value. diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs index fd3dece0..b0bcf23f 100644 --- a/src/redmine-net-api/Types/Project.cs +++ b/src/redmine-net-api/Types/Project.cs @@ -35,6 +35,12 @@ namespace Redmine.Net.Api.Types public sealed class Project : IdentifiableName, IEquatable { #region Properties + + /// + /// + /// + new public string Name { get; set; } + /// /// Gets or sets the identifier. /// diff --git a/src/redmine-net-api/Types/ProjectEnabledModule.cs b/src/redmine-net-api/Types/ProjectEnabledModule.cs index 055703c8..ce9b6ff0 100644 --- a/src/redmine-net-api/Types/ProjectEnabledModule.cs +++ b/src/redmine-net-api/Types/ProjectEnabledModule.cs @@ -50,6 +50,11 @@ public ProjectEnabledModule(string moduleName) #endregion + /// + /// + /// + new public string Name { get; set; } + #region Implementation of IValue /// /// diff --git a/src/redmine-net-api/Types/ProjectIssueCategory.cs b/src/redmine-net-api/Types/ProjectIssueCategory.cs index b4c9aa0d..30455354 100644 --- a/src/redmine-net-api/Types/ProjectIssueCategory.cs +++ b/src/redmine-net-api/Types/ProjectIssueCategory.cs @@ -37,6 +37,11 @@ internal ProjectIssueCategory(int id, string name) Name = name; } + /// + /// + /// + new public string Name { get; set; } + /// /// /// diff --git a/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs b/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs index 43251a43..fbdcac35 100644 --- a/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs +++ b/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs @@ -21,6 +21,11 @@ internal ProjectTimeEntryActivity(int id, string name) Name = name; } + /// + /// + /// + new public string Name { get; set; } + /// /// /// diff --git a/src/redmine-net-api/Types/ProjectTracker.cs b/src/redmine-net-api/Types/ProjectTracker.cs index 95e60de2..5c06cab9 100644 --- a/src/redmine-net-api/Types/ProjectTracker.cs +++ b/src/redmine-net-api/Types/ProjectTracker.cs @@ -52,6 +52,11 @@ internal ProjectTracker(int trackerId) Id = trackerId; } + /// + /// + /// + new public string Name { get; set; } + #region Implementation of IValue /// diff --git a/src/redmine-net-api/Types/Role.cs b/src/redmine-net-api/Types/Role.cs index c0e544d5..bb07bc55 100644 --- a/src/redmine-net-api/Types/Role.cs +++ b/src/redmine-net-api/Types/Role.cs @@ -34,6 +34,10 @@ public sealed class Role : IdentifiableName, IEquatable { #region Properties /// + /// + /// + new public string Name { get; set; } + /// /// Gets the permissions. /// /// diff --git a/src/redmine-net-api/Types/TimeEntryActivity.cs b/src/redmine-net-api/Types/TimeEntryActivity.cs index ad0f9547..ab000304 100644 --- a/src/redmine-net-api/Types/TimeEntryActivity.cs +++ b/src/redmine-net-api/Types/TimeEntryActivity.cs @@ -32,7 +32,6 @@ namespace Redmine.Net.Api.Types [XmlRoot(RedmineKeys.TIME_ENTRY_ACTIVITY)] public sealed class TimeEntryActivity : IdentifiableName, IEquatable { - #region Properties /// /// /// @@ -44,6 +43,13 @@ internal TimeEntryActivity(int id, string name) Name = name; } + #region Properties + + /// + /// + /// + new public string Name { get; set; } + /// /// /// diff --git a/src/redmine-net-api/Types/UserGroup.cs b/src/redmine-net-api/Types/UserGroup.cs index 5b3538b1..72942128 100644 --- a/src/redmine-net-api/Types/UserGroup.cs +++ b/src/redmine-net-api/Types/UserGroup.cs @@ -26,6 +26,11 @@ namespace Redmine.Net.Api.Types [XmlRoot(RedmineKeys.GROUP)] public sealed class UserGroup : IdentifiableName { + /// + /// + /// + new public string Name { get; set; } + /// /// /// diff --git a/src/redmine-net-api/Types/Watcher.cs b/src/redmine-net-api/Types/Watcher.cs index 2fd8735c..4f64a590 100644 --- a/src/redmine-net-api/Types/Watcher.cs +++ b/src/redmine-net-api/Types/Watcher.cs @@ -28,6 +28,12 @@ namespace Redmine.Net.Api.Types [XmlRoot(RedmineKeys.USER)] public sealed class Watcher : IdentifiableName, IValue, ICloneable { + /// + /// + /// + new public string Name { get; set; } + + #region Implementation of IValue /// /// From 1133f70569933bee615d20c55ece4e3b0e72c1df Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 5 Apr 2020 18:31:02 +0300 Subject: [PATCH 121/549] Small refactoring --- .../Internals/XmlTextReaderBuilder.cs | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs b/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs index ec4d07d6..8a3a63d7 100644 --- a/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs +++ b/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs @@ -9,6 +9,14 @@ namespace Redmine.Net.Api.Internals public static class XmlTextReaderBuilder { #if NET20 + private static readonly XmlReaderSettings xmlReaderSettings = new XmlReaderSettings() + { + ProhibitDtd = true, + XmlResolver = null, + IgnoreComments = true, + IgnoreWhitespace = true, + }; + /// /// /// @@ -16,13 +24,7 @@ public static class XmlTextReaderBuilder /// public static XmlReader Create(StringReader stringReader) { - return XmlReader.Create(stringReader, new XmlReaderSettings() - { - ProhibitDtd = true, - XmlResolver = null, - IgnoreComments = true, - IgnoreWhitespace = true, - }); + return XmlReader.Create(stringReader, xmlReaderSettings); } @@ -35,13 +37,7 @@ public static XmlReader Create(string xml) { var stringReader = new StringReader(xml); { - return XmlReader.Create(stringReader, new XmlReaderSettings() - { - ProhibitDtd = true, - XmlResolver = null, - IgnoreComments = true, - IgnoreWhitespace = true, - }); + return XmlReader.Create(stringReader, xmlReaderSettings); } } #else From 489396abe3d16ef20a9aa7e499d1168788c6c106 Mon Sep 17 00:00:00 2001 From: Zapadi Date: Fri, 10 Apr 2020 09:48:13 +0300 Subject: [PATCH 122/549] Fix #259 --- src/redmine-net-api/RedmineManager.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index 5b467239..5c4ffbe8 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -573,8 +573,10 @@ public void DeleteWikiPage(string projectId, string pageName) do { parameters.Set(RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)); + var tempResult = GetPaginatedObjects(parameters); - if (tempResult != null) + + if (tempResult?.Items != null) { if (resultList == null) { @@ -592,7 +594,7 @@ public void DeleteWikiPage(string projectId, string pageName) else { var result = GetPaginatedObjects(parameters); - if (result != null) + if (result?.Items != null) { return new List(result.Items); } From ed6e1eb3e97eeeb15c9108cbfd1199b9a4cc401f Mon Sep 17 00:00:00 2001 From: Zapadi Date: Fri, 17 Apr 2020 20:40:20 +0300 Subject: [PATCH 123/549] Consistency --- .../Async/RedmineManagerAsync40.cs | 2 +- .../Extensions/JsonWriterExtensions.cs | 29 ++++----- .../Extensions/WebExtensions.cs | 25 ++++---- .../Extensions/XmlReaderExtensions.cs | 27 ++++++--- .../Extensions/XmlWriterExtensions.cs | 31 ++++++---- src/redmine-net-api/Internals/UrlHelper.cs | 59 +++++++++---------- src/redmine-net-api/Internals/WebApiHelper.cs | 21 ++++--- .../Internals/XmlTextReaderBuilder.cs | 6 +- .../Serialization/PagedResults.cs | 10 ++-- .../Serialization/XmlRedmineSerializer.cs | 12 ++-- .../Serialization/XmlSerializerCache.cs | 30 +++++----- src/redmine-net-api/Types/CustomField.cs | 2 +- src/redmine-net-api/Types/CustomFieldRole.cs | 2 +- src/redmine-net-api/Types/Group.cs | 4 +- src/redmine-net-api/Types/GroupUser.cs | 2 +- src/redmine-net-api/Types/IssueCustomField.cs | 2 +- .../Types/IssueRelationType.cs | 4 +- src/redmine-net-api/Types/Project.cs | 2 +- .../Types/ProjectEnabledModule.cs | 2 +- .../Types/ProjectIssueCategory.cs | 2 +- .../Types/ProjectTimeEntryActivity.cs | 2 +- src/redmine-net-api/Types/ProjectTracker.cs | 2 +- src/redmine-net-api/Types/Role.cs | 2 +- .../Types/TimeEntryActivity.cs | 2 +- src/redmine-net-api/Types/UserGroup.cs | 2 +- src/redmine-net-api/Types/UserStatus.cs | 8 +-- src/redmine-net-api/Types/Watcher.cs | 2 +- 27 files changed, 154 insertions(+), 140 deletions(-) diff --git a/src/redmine-net-api/Async/RedmineManagerAsync40.cs b/src/redmine-net-api/Async/RedmineManagerAsync40.cs index d658fcb0..303b2136 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync40.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync40.cs @@ -15,10 +15,10 @@ limitations under the License. */ -using System.Threading; #if NET40 using System.Collections.Generic; using System.Collections.Specialized; +using System.Threading; using System.Threading.Tasks; using Redmine.Net.Api.Types; using Redmine.Net.Api.Serialization; diff --git a/src/redmine-net-api/Extensions/JsonWriterExtensions.cs b/src/redmine-net-api/Extensions/JsonWriterExtensions.cs index d4032094..3d00f176 100644 --- a/src/redmine-net-api/Extensions/JsonWriterExtensions.cs +++ b/src/redmine-net-api/Extensions/JsonWriterExtensions.cs @@ -22,11 +22,13 @@ public static partial class JsonExtensions /// public static void WriteIdIfNotNull(this JsonWriter jsonWriter, string tag, IdentifiableName value) { - if (value != null) + if (value == null) { - jsonWriter.WritePropertyName(tag); - jsonWriter.WriteValue(value.Id); + return; } + + jsonWriter.WritePropertyName(tag); + jsonWriter.WriteValue(value.Id); } /// @@ -38,7 +40,7 @@ public static void WriteIdIfNotNull(this JsonWriter jsonWriter, string tag, Iden /// public static void WriteIfNotDefaultOrNull(this JsonWriter writer, string elementName, T value) { - if (EqualityComparer.Default.Equals(value, default(T))) + if (EqualityComparer.Default.Equals(value, default)) { return; } @@ -61,14 +63,7 @@ public static void WriteIfNotDefaultOrNull(this JsonWriter writer, string ele /// public static void WriteIdOrEmpty(this JsonWriter jsonWriter, string tag, IdentifiableName ident, string emptyValue = null) { - if (ident != null) - { - jsonWriter.WriteProperty(tag, ident.Id.ToString(CultureInfo.InvariantCulture)); - } - else - { - jsonWriter.WriteProperty(tag, emptyValue); - } + jsonWriter.WriteProperty(tag, ident != null ? ident.Id.ToString(CultureInfo.InvariantCulture) : emptyValue); } /// @@ -80,7 +75,7 @@ public static void WriteIdOrEmpty(this JsonWriter jsonWriter, string tag, Identi /// public static void WriteDateOrEmpty(this JsonWriter jsonWriter, string tag, DateTime? val, string dateFormat = "yyyy-MM-dd") { - if (!val.HasValue || val.Value.Equals(default(DateTime))) + if (!val.HasValue || val.Value.Equals(default)) { jsonWriter.WriteProperty(tag, string.Empty); } @@ -99,7 +94,7 @@ public static void WriteDateOrEmpty(this JsonWriter jsonWriter, string tag, Date /// public static void WriteValueOrEmpty(this JsonWriter jsonWriter, string tag, T? val) where T : struct { - if (!val.HasValue || EqualityComparer.Default.Equals(val.Value, default(T))) + if (!val.HasValue || EqualityComparer.Default.Equals(val.Value, default)) { jsonWriter.WriteProperty(tag, string.Empty); } @@ -118,7 +113,7 @@ public static void WriteValueOrEmpty(this JsonWriter jsonWriter, string tag, /// public static void WriteValueOrDefault(this JsonWriter jsonWriter, string tag, T? val) where T : struct { - jsonWriter.WriteProperty(tag, val ?? default(T)); + jsonWriter.WriteProperty(tag, val ?? default); } /// @@ -169,7 +164,7 @@ public static void WriteArrayIds(this JsonWriter jsonWriter, string tag, IEnumer jsonWriter.WritePropertyName(tag); jsonWriter.WriteStartArray(); - StringBuilder sb = new StringBuilder(); + var sb = new StringBuilder(); foreach (var identifiableName in collection) { @@ -199,7 +194,7 @@ public static void WriteArrayNames(this JsonWriter jsonWriter, string tag, IEnum jsonWriter.WritePropertyName(tag); jsonWriter.WriteStartArray(); - StringBuilder sb = new StringBuilder(); + var sb = new StringBuilder(); foreach (var identifiableName in collection) { diff --git a/src/redmine-net-api/Extensions/WebExtensions.cs b/src/redmine-net-api/Extensions/WebExtensions.cs index ea219eb1..048f20d7 100644 --- a/src/redmine-net-api/Extensions/WebExtensions.cs +++ b/src/redmine-net-api/Extensions/WebExtensions.cs @@ -18,9 +18,9 @@ limitations under the License. using System.Collections.Generic; using System.IO; using System.Net; -using Redmine.Net.Api.Types; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Types; namespace Redmine.Net.Api.Extensions { @@ -82,13 +82,15 @@ public static void HandleWebException(this WebException exception, IRedmineSeria case 422: var errors = GetRedmineExceptions(exception.Response, serializer); var message = string.Empty; - if (errors != null) + + if (errors == null) + throw new RedmineException($"Invalid or missing attribute parameters: {message}", innerException); + + foreach (var error in errors) { - foreach (var error in errors) - { - message = message + error.Info + Environment.NewLine; - } + message = message + error.Info + Environment.NewLine; } + throw new RedmineException("Invalid or missing attribute parameters: " + message, innerException); case (int)HttpStatusCode.NotAcceptable: @@ -126,15 +128,8 @@ private static IEnumerable GetRedmineExceptions(this WebResponse webRespo return null; } - try - { - var result = serializer.DeserializeToPagedResults(responseContent); - return result.Items; - } - catch (Exception) - { - throw; - } + var result = serializer.DeserializeToPagedResults(responseContent); + return result.Items; } } } diff --git a/src/redmine-net-api/Extensions/XmlReaderExtensions.cs b/src/redmine-net-api/Extensions/XmlReaderExtensions.cs index 48848043..6002ca98 100644 --- a/src/redmine-net-api/Extensions/XmlReaderExtensions.cs +++ b/src/redmine-net-api/Extensions/XmlReaderExtensions.cs @@ -45,7 +45,7 @@ public static int ReadAttributeAsInt(this XmlReader reader, string attributeName if (attribute.IsNullOrWhiteSpace() || !int.TryParse(attribute, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out var result)) { - return default(int); + return default; } return result; @@ -63,7 +63,7 @@ public static int ReadAttributeAsInt(this XmlReader reader, string attributeName if (attribute.IsNullOrWhiteSpace() || !int.TryParse(attribute, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out var result)) { - return default(int?); + return default; } return result; @@ -96,12 +96,14 @@ public static bool ReadAttributeAsBoolean(this XmlReader reader, string attribut { var content = reader.ReadElementContentAsString(); - if (content.IsNullOrWhiteSpace() || !DateTime.TryParse(content, out var result)) + if (!content.IsNullOrWhiteSpace() && DateTime.TryParse(content, out var result)) { - if (!DateTime.TryParseExact(content, INCLUDE_DATE_TIME_FORMAT, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) - { - return null; - } + return result; + } + + if (!DateTime.TryParseExact(content, INCLUDE_DATE_TIME_FORMAT, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)) + { + return null; } return result; @@ -170,6 +172,11 @@ public static List ReadElementContentAsCollection(this XmlReader reader) w var serializer = new XmlSerializer(typeof(T)); var outerXml = reader.ReadOuterXml(); + if (string.IsNullOrEmpty(outerXml)) + { + return null; + } + using (var stringReader = new StringReader(outerXml)) { using (var xmlTextReader = XmlTextReaderBuilder.Create(stringReader)) @@ -231,6 +238,12 @@ public static IEnumerable ReadElementContentAsEnumerable(this XmlReader re { var serializer = new XmlSerializer(typeof(T)); var outerXml = reader.ReadOuterXml(); + + if (string.IsNullOrEmpty(outerXml)) + { + yield return null; + } + using (var stringReader = new StringReader(outerXml)) { using (var xmlTextReader = XmlTextReaderBuilder.Create(stringReader)) diff --git a/src/redmine-net-api/Extensions/XmlWriterExtensions.cs b/src/redmine-net-api/Extensions/XmlWriterExtensions.cs index 9add243f..9e24e63f 100644 --- a/src/redmine-net-api/Extensions/XmlWriterExtensions.cs +++ b/src/redmine-net-api/Extensions/XmlWriterExtensions.cs @@ -20,7 +20,6 @@ limitations under the License. using System.Globalization; using System.Xml; using System.Xml.Serialization; -using Redmine.Net.Api.Internals; using Redmine.Net.Api.Types; namespace Redmine.Net.Api.Extensions @@ -32,11 +31,11 @@ public static partial class XmlExtensions { #if !(NET20 || NET40 || NET45 || NET451 || NET452) - private static readonly Type[] emptyTypeArray = Array.Empty(); + private static readonly Type[] EmptyTypeArray = Array.Empty(); #else - private static readonly Type[] emptyTypeArray = new Type[0]; + private static readonly Type[] EmptyTypeArray = new Type[0]; #endif - private static readonly XmlAttributeOverrides xmlAttributeOverrides = new XmlAttributeOverrides(); + private static readonly XmlAttributeOverrides XmlAttributeOverrides = new XmlAttributeOverrides(); /// /// Writes the id if not null. @@ -60,7 +59,11 @@ public static void WriteIdIfNotNull(this XmlWriter writer, string elementName, I /// Name of the element. public static void WriteArray(this XmlWriter writer, string elementName, IEnumerable collection) { - if (collection == null) return; + if (collection == null) + { + return; + } + writer.WriteStartElement(elementName); writer.WriteAttributeString("type", "array"); @@ -108,7 +111,10 @@ public static void WriteArray(this XmlWriter writer, string elementName, IEnu /// The f. public static void WriteArrayIds(this XmlWriter writer, string elementName, IEnumerable collection, Type type, Func f) { - if (collection == null || f == null) return; + if (collection == null || f == null) + { + return; + } writer.WriteStartElement(elementName); writer.WriteAttributeString("type", "array"); @@ -134,14 +140,17 @@ public static void WriteArrayIds(this XmlWriter writer, string elementName, IEnu /// The default namespace. public static void WriteArray(this XmlWriter writer, string elementName, IEnumerable collection, Type type, string root, string defaultNamespace = null) { - if (collection == null) return; + if (collection == null) + { + return; + } writer.WriteStartElement(elementName); writer.WriteAttributeString("type", "array"); var rootAttribute = new XmlRootAttribute(root); - var serializer = new XmlSerializer(type, xmlAttributeOverrides, emptyTypeArray, rootAttribute, + var serializer = new XmlSerializer(type, XmlAttributeOverrides, EmptyTypeArray, rootAttribute, defaultNamespace); foreach (var item in collection) @@ -235,7 +244,7 @@ public static void WriteIdOrEmpty(this XmlWriter writer, string elementName, Ide /// The tag. public static void WriteIfNotDefaultOrNull(this XmlWriter writer, string elementName, T value) { - if (EqualityComparer.Default.Equals(value, default(T))) + if (EqualityComparer.Default.Equals(value, default)) { return; } @@ -258,7 +267,7 @@ public static void WriteIfNotDefaultOrNull(this XmlWriter writer, string elem /// The tag. public static void WriteValueOrEmpty(this XmlWriter writer, string elementName, T? val) where T : struct { - if (!val.HasValue || EqualityComparer.Default.Equals(val.Value, default(T))) + if (!val.HasValue || EqualityComparer.Default.Equals(val.Value, default)) { writer.WriteElementString(elementName, string.Empty); } @@ -277,7 +286,7 @@ public static void WriteValueOrEmpty(this XmlWriter writer, string elementNam /// public static void WriteDateOrEmpty(this XmlWriter writer, string elementName, DateTime? val, string dateTimeFormat = "yyyy-MM-dd") { - if (!val.HasValue || val.Value.Equals(default(DateTime))) + if (!val.HasValue || val.Value.Equals(default)) { writer.WriteElementString(elementName, string.Empty); } diff --git a/src/redmine-net-api/Internals/UrlHelper.cs b/src/redmine-net-api/Internals/UrlHelper.cs index a188b495..7f3bc7cd 100644 --- a/src/redmine-net-api/Internals/UrlHelper.cs +++ b/src/redmine-net-api/Internals/UrlHelper.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +18,6 @@ limitations under the License. using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; -using System.Web; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Types; @@ -33,41 +32,41 @@ internal static class UrlHelper { /// /// - const string REQUEST_FORMAT = "{0}/{1}/{2}.{3}"; + private const string REQUEST_FORMAT = "{0}/{1}/{2}.{3}"; /// /// - const string FORMAT = "{0}/{1}.{2}"; + private const string FORMAT = "{0}/{1}.{2}"; /// /// - const string WIKI_INDEX_FORMAT = "{0}/projects/{1}/wiki/index.{2}"; + private const string WIKI_INDEX_FORMAT = "{0}/projects/{1}/wiki/index.{2}"; /// /// - const string WIKI_PAGE_FORMAT = "{0}/projects/{1}/wiki/{2}.{3}"; + private const string WIKI_PAGE_FORMAT = "{0}/projects/{1}/wiki/{2}.{3}"; /// /// - const string WIKI_VERSION_FORMAT = "{0}/projects/{1}/wiki/{2}/{3}.{4}"; + private const string WIKI_VERSION_FORMAT = "{0}/projects/{1}/wiki/{2}/{3}.{4}"; /// /// - const string ENTITY_WITH_PARENT_FORMAT = "{0}/{1}/{2}/{3}.{4}"; + private const string ENTITY_WITH_PARENT_FORMAT = "{0}/{1}/{2}/{3}.{4}"; /// /// - const string ATTACHMENT_UPDATE_FORMAT = "{0}/attachments/issues/{1}.{2}"; + private const string ATTACHMENT_UPDATE_FORMAT = "{0}/attachments/issues/{1}.{2}"; /// /// /// - const string FILE_URL_FORMAT = "{0}/projects/{1}/files.{2}"; + private const string FILE_URL_FORMAT = "{0}/projects/{1}/files.{2}"; /// /// - const string CURRENT_USER_URI = "current"; + private const string CURRENT_USER_URI = "current"; /// /// Gets the upload URL. /// @@ -81,9 +80,9 @@ public static string GetUploadUrl(RedmineManager redmineManager, string id) { var type = typeof(T); - if (!RedmineManager.Sufixes.ContainsKey(type)) throw new KeyNotFoundException(type.Name); + if (!RedmineManager.Suffixes.ContainsKey(type)) throw new KeyNotFoundException(type.Name); - return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, RedmineManager.Sufixes[type], id, + return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, RedmineManager.Suffixes[type], id, redmineManager.Format); } @@ -104,19 +103,19 @@ public static string GetUploadUrl(RedmineManager redmineManager, string id) { var type = typeof(T); - if (!RedmineManager.Sufixes.ContainsKey(type)) throw new KeyNotFoundException(type.Name); + if (!RedmineManager.Suffixes.ContainsKey(type)) throw new KeyNotFoundException(type.Name); if (type == typeof(Version) || type == typeof(IssueCategory) || type == typeof(ProjectMembership)) { if (string.IsNullOrEmpty(ownerId)) throw new RedmineException("The owner id(project id) is mandatory!"); return string.Format(CultureInfo.InvariantCulture,ENTITY_WITH_PARENT_FORMAT, redmineManager.Host, RedmineKeys.PROJECTS, - ownerId, RedmineManager.Sufixes[type], redmineManager.Format); + ownerId, RedmineManager.Suffixes[type], redmineManager.Format); } if (type == typeof(IssueRelation)) { if (string.IsNullOrEmpty(ownerId)) throw new RedmineException("The owner id(issue id) is mandatory!"); return string.Format(CultureInfo.InvariantCulture,ENTITY_WITH_PARENT_FORMAT, redmineManager.Host, RedmineKeys.ISSUES, - ownerId, RedmineManager.Sufixes[type], redmineManager.Format); + ownerId, RedmineManager.Suffixes[type], redmineManager.Format); } if (type == typeof(File)) @@ -128,7 +127,7 @@ public static string GetUploadUrl(RedmineManager redmineManager, string id) return string.Format(CultureInfo.InvariantCulture,FILE_URL_FORMAT, redmineManager.Host, ownerId, redmineManager.Format); } - return string.Format(CultureInfo.InvariantCulture,FORMAT, redmineManager.Host, RedmineManager.Sufixes[type], + return string.Format(CultureInfo.InvariantCulture,FORMAT, redmineManager.Host, RedmineManager.Suffixes[type], redmineManager.Format); } @@ -145,9 +144,9 @@ public static string GetUploadUrl(RedmineManager redmineManager, string id) { var type = typeof(T); - if (!RedmineManager.Sufixes.ContainsKey(type)) throw new KeyNotFoundException(type.Name); + if (!RedmineManager.Suffixes.ContainsKey(type)) throw new KeyNotFoundException(type.Name); - return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, RedmineManager.Sufixes[type], id, + return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, RedmineManager.Suffixes[type], id, redmineManager.Format); } @@ -163,9 +162,9 @@ public static string GetUploadUrl(RedmineManager redmineManager, string id) { var type = typeof(T); - if (!RedmineManager.Sufixes.ContainsKey(type)) throw new KeyNotFoundException(type.Name); + if (!RedmineManager.Suffixes.ContainsKey(type)) throw new KeyNotFoundException(type.Name); - return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, RedmineManager.Sufixes[type], id, + return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, RedmineManager.Suffixes[type], id, redmineManager.Format); } @@ -187,7 +186,7 @@ public static string GetListUrl(RedmineManager redmineManager, NameValueColle { var type = typeof(T); - if (!RedmineManager.Sufixes.ContainsKey(type)) throw new KeyNotFoundException(type.Name); + if (!RedmineManager.Suffixes.ContainsKey(type)) throw new KeyNotFoundException(type.Name); if (type == typeof(Version) || type == typeof(IssueCategory) || type == typeof(ProjectMembership)) { @@ -196,7 +195,7 @@ public static string GetListUrl(RedmineManager redmineManager, NameValueColle throw new RedmineException("The project id is mandatory! \nCheck if you have included the parameter project_id to parameters."); return string.Format(CultureInfo.InvariantCulture,ENTITY_WITH_PARENT_FORMAT, redmineManager.Host, RedmineKeys.PROJECTS, - projectId, RedmineManager.Sufixes[type], redmineManager.Format); + projectId, RedmineManager.Suffixes[type], redmineManager.Format); } if (type == typeof(IssueRelation)) { @@ -205,7 +204,7 @@ public static string GetListUrl(RedmineManager redmineManager, NameValueColle throw new RedmineException("The issue id is mandatory! \nCheck if you have included the parameter issue_id to parameters"); return string.Format(CultureInfo.InvariantCulture,ENTITY_WITH_PARENT_FORMAT, redmineManager.Host, RedmineKeys.ISSUES, - issueId, RedmineManager.Sufixes[type], redmineManager.Format); + issueId, RedmineManager.Suffixes[type], redmineManager.Format); } if (type == typeof(File)) @@ -218,7 +217,7 @@ public static string GetListUrl(RedmineManager redmineManager, NameValueColle return string.Format(CultureInfo.InvariantCulture,FILE_URL_FORMAT, redmineManager.Host, projectId, redmineManager.Format); } - return string.Format(CultureInfo.InvariantCulture,FORMAT, redmineManager.Host, RedmineManager.Sufixes[type], + return string.Format(CultureInfo.InvariantCulture,FORMAT, redmineManager.Host, RedmineManager.Suffixes[type], redmineManager.Format); } @@ -263,7 +262,7 @@ public static string GetWikiPageUrl(RedmineManager redmineManager, string projec public static string GetAddUserToGroupUrl(RedmineManager redmineManager, int groupId) { return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, - RedmineManager.Sufixes[typeof(Group)], + RedmineManager.Suffixes[typeof(Group)], $"{groupId.ToString(CultureInfo.InvariantCulture)}/users", redmineManager.Format); } @@ -277,7 +276,7 @@ public static string GetAddUserToGroupUrl(RedmineManager redmineManager, int gro public static string GetRemoveUserFromGroupUrl(RedmineManager redmineManager, int groupId, int userId) { return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, - RedmineManager.Sufixes[typeof(Group)], + RedmineManager.Suffixes[typeof(Group)], $"{groupId.ToString(CultureInfo.InvariantCulture)}/users/{userId.ToString(CultureInfo.InvariantCulture)}", redmineManager.Format); } @@ -300,7 +299,7 @@ public static string GetUploadFileUrl(RedmineManager redmineManager) public static string GetCurrentUserUrl(RedmineManager redmineManager) { return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, - RedmineManager.Sufixes[typeof(User)], CURRENT_USER_URI, + RedmineManager.Suffixes[typeof(User)], CURRENT_USER_URI, redmineManager.Format); } @@ -339,7 +338,7 @@ public static string GetDeleteWikirUrl(RedmineManager redmineManager, string pro public static string GetAddWatcherUrl(RedmineManager redmineManager, int issueId) { return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, - RedmineManager.Sufixes[typeof(Issue)], $"{issueId.ToString(CultureInfo.InvariantCulture)}/watchers", + RedmineManager.Suffixes[typeof(Issue)], $"{issueId.ToString(CultureInfo.InvariantCulture)}/watchers", redmineManager.Format); } @@ -353,7 +352,7 @@ public static string GetAddWatcherUrl(RedmineManager redmineManager, int issueId public static string GetRemoveWatcherUrl(RedmineManager redmineManager, int issueId, int userId) { return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, - RedmineManager.Sufixes[typeof(Issue)], $"{issueId.ToString(CultureInfo.InvariantCulture)}/watchers/{userId.ToString(CultureInfo.InvariantCulture)}", + RedmineManager.Suffixes[typeof(Issue)], $"{issueId.ToString(CultureInfo.InvariantCulture)}/watchers/{userId.ToString(CultureInfo.InvariantCulture)}", redmineManager.Format); } diff --git a/src/redmine-net-api/Internals/WebApiHelper.cs b/src/redmine-net-api/Internals/WebApiHelper.cs index ed5e27c3..fcb0284c 100644 --- a/src/redmine-net-api/Internals/WebApiHelper.cs +++ b/src/redmine-net-api/Internals/WebApiHelper.cs @@ -28,15 +28,15 @@ namespace Redmine.Net.Api.Internals /// internal static class WebApiHelper { - /// - /// Executes the upload. - /// - /// The redmine manager. - /// The address. - /// Type of the action. - /// The data. - /// The parameters - public static void ExecuteUpload(RedmineManager redmineManager, string address, string actionType, string data, + /// + /// Executes the upload. + /// + /// The redmine manager. + /// The address. + /// Type of the action. + /// The data. + /// The parameters + public static void ExecuteUpload(RedmineManager redmineManager, string address, string actionType, string data, NameValueCollection parameters = null) { using (var wc = redmineManager.CreateWebClient(parameters)) @@ -95,7 +95,7 @@ public static T ExecuteUpload(RedmineManager redmineManager, string address, /// The address. /// The parameters. /// - public static T ExecuteDownload(RedmineManager redmineManager, string address, + public static T ExecuteDownload(RedmineManager redmineManager, string address, NameValueCollection parameters = null) where T : class, new() { @@ -124,7 +124,6 @@ public static T ExecuteDownload(RedmineManager redmineManager, string address /// The parameters. /// public static PagedResults ExecuteDownloadList(RedmineManager redmineManager, string address, - NameValueCollection parameters = null) where T : class, new() { using (var wc = redmineManager.CreateWebClient(parameters)) diff --git a/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs b/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs index 8a3a63d7..ec7d29ec 100644 --- a/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs +++ b/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs @@ -9,7 +9,7 @@ namespace Redmine.Net.Api.Internals public static class XmlTextReaderBuilder { #if NET20 - private static readonly XmlReaderSettings xmlReaderSettings = new XmlReaderSettings() + private static readonly XmlReaderSettings XmlReaderSettings = new XmlReaderSettings() { ProhibitDtd = true, XmlResolver = null, @@ -24,7 +24,7 @@ public static class XmlTextReaderBuilder /// public static XmlReader Create(StringReader stringReader) { - return XmlReader.Create(stringReader, xmlReaderSettings); + return XmlReader.Create(stringReader, XmlReaderSettings); } @@ -37,7 +37,7 @@ public static XmlReader Create(string xml) { var stringReader = new StringReader(xml); { - return XmlReader.Create(stringReader, xmlReaderSettings); + return XmlReader.Create(stringReader, XmlReaderSettings); } } #else diff --git a/src/redmine-net-api/Serialization/PagedResults.cs b/src/redmine-net-api/Serialization/PagedResults.cs index 46031d83..a6900f7c 100644 --- a/src/redmine-net-api/Serialization/PagedResults.cs +++ b/src/redmine-net-api/Serialization/PagedResults.cs @@ -21,12 +21,14 @@ public PagedResults(IEnumerable items, int total, int offset, int pageSize Offset = offset; PageSize = pageSize; - if (pageSize > 0) + if (pageSize <= 0) { - CurrentPage = offset / pageSize + 1; - - TotalPages = total / pageSize + 1; + return; } + + CurrentPage = offset / pageSize + 1; + + TotalPages = total / pageSize + 1; } /// diff --git a/src/redmine-net-api/Serialization/XmlRedmineSerializer.cs b/src/redmine-net-api/Serialization/XmlRedmineSerializer.cs index 41443d80..0bed6bef 100644 --- a/src/redmine-net-api/Serialization/XmlRedmineSerializer.cs +++ b/src/redmine-net-api/Serialization/XmlRedmineSerializer.cs @@ -13,7 +13,7 @@ internal sealed class XmlRedmineSerializer : IRedmineSerializer public XmlRedmineSerializer() { - XMLWriterSettings = new XmlWriterSettings + xmlWriterSettings = new XmlWriterSettings { OmitXmlDeclaration = true }; @@ -21,10 +21,10 @@ public XmlRedmineSerializer() public XmlRedmineSerializer(XmlWriterSettings xmlWriterSettings) { - XMLWriterSettings = xmlWriterSettings; + this.xmlWriterSettings = xmlWriterSettings; } - private readonly XmlWriterSettings XMLWriterSettings; + private readonly XmlWriterSettings xmlWriterSettings; public T Deserialize(string response) where T : new() { @@ -99,10 +99,12 @@ public string Serialize(T entity) where T : class xmlReader.Read(); var totalItems = xmlReader.ReadAttributeAsInt(RedmineKeys.TOTAL_COUNT); + if (onlyCount) { return new PagedResults(null, totalItems, 0, 0); } + var offset = xmlReader.ReadAttributeAsInt(RedmineKeys.OFFSET); var limit = xmlReader.ReadAttributeAsInt(RedmineKeys.LIMIT); var result = xmlReader.ReadElementContentAsCollection(); @@ -131,7 +133,7 @@ private string ToXML(T entity) where T : class using (var stringWriter = new StringWriter()) { - using (var xmlWriter = XmlWriter.Create(stringWriter, XMLWriterSettings)) + using (var xmlWriter = XmlWriter.Create(stringWriter, xmlWriterSettings)) { var serializer = new XmlSerializer(typeof(T)); @@ -155,7 +157,7 @@ private string ToXML(T entity) where T : class /// using the System.Exception.InnerException property. /// // ReSharper disable once InconsistentNaming - private TOut XmlDeserializeEntity(string xml) where TOut : new() + private static TOut XmlDeserializeEntity(string xml) where TOut : new() { if (xml.IsNullOrWhiteSpace()) { diff --git a/src/redmine-net-api/Serialization/XmlSerializerCache.cs b/src/redmine-net-api/Serialization/XmlSerializerCache.cs index 32935bd8..cadbd6a9 100644 --- a/src/redmine-net-api/Serialization/XmlSerializerCache.cs +++ b/src/redmine-net-api/Serialization/XmlSerializerCache.cs @@ -11,21 +11,21 @@ namespace Redmine.Net.Api.Serialization internal class XmlSerializerCache : IXmlSerializerCache { #if !(NET20 || NET40 || NET45 || NET451 || NET452) - private static readonly Type[] emptyTypes = Array.Empty(); + private static readonly Type[] EmptyTypes = Array.Empty(); #else - private static readonly Type[] emptyTypes = new Type[0]; + private static readonly Type[] EmptyTypes = new Type[0]; #endif public static XmlSerializerCache Instance { get; } = new XmlSerializerCache(); - private readonly Dictionary _serializers; + private readonly Dictionary serializers; - private readonly object _syncRoot; + private readonly object syncRoot; private XmlSerializerCache() { - _syncRoot = new object(); - _serializers = new Dictionary(); + syncRoot = new object(); + serializers = new Dictionary(); } /// @@ -40,7 +40,7 @@ private XmlSerializerCache() /// public XmlSerializer GetSerializer(Type type, string defaultNamespace) { - return GetSerializer(type, null, emptyTypes, null, defaultNamespace); + return GetSerializer(type, null, EmptyTypes, null, defaultNamespace); } /// @@ -55,7 +55,7 @@ public XmlSerializer GetSerializer(Type type, string defaultNamespace) /// public XmlSerializer GetSerializer(Type type, XmlRootAttribute root) { - return GetSerializer(type, null, emptyTypes, root, null); + return GetSerializer(type, null, EmptyTypes, root, null); } /// @@ -70,7 +70,7 @@ public XmlSerializer GetSerializer(Type type, XmlRootAttribute root) /// public XmlSerializer GetSerializer(Type type, XmlAttributeOverrides overrides) { - return GetSerializer(type, overrides, emptyTypes, null, null); + return GetSerializer(type, overrides, EmptyTypes, null, null); } /// @@ -106,22 +106,22 @@ public XmlSerializer GetSerializer(Type type, XmlAttributeOverrides overrides, T var key = CacheKeyFactory.Create(type, overrides, types, root, defaultNamespace); XmlSerializer serializer = null; - lock (_syncRoot) + lock (syncRoot) { - if (_serializers.ContainsKey(key) == false) + if (serializers.ContainsKey(key) == false) { - lock (_syncRoot) + lock (syncRoot) { - if (_serializers.ContainsKey(key) == false) + if (serializers.ContainsKey(key) == false) { serializer = new XmlSerializer(type, overrides, types, root, defaultNamespace); - _serializers.Add(key, serializer); + serializers.Add(key, serializer); } } } else { - serializer = _serializers[key]; + serializer = serializers[key]; } Debug.Assert(serializer != null); diff --git a/src/redmine-net-api/Types/CustomField.cs b/src/redmine-net-api/Types/CustomField.cs index 48705cb9..0c7b0afd 100644 --- a/src/redmine-net-api/Types/CustomField.cs +++ b/src/redmine-net-api/Types/CustomField.cs @@ -37,7 +37,7 @@ public sealed class CustomField : IdentifiableName, IEquatable /// /// /// - new public string Name { get; set; } + public new string Name { get; set; } /// /// /// diff --git a/src/redmine-net-api/Types/CustomFieldRole.cs b/src/redmine-net-api/Types/CustomFieldRole.cs index 73a4bce8..261eba5a 100644 --- a/src/redmine-net-api/Types/CustomFieldRole.cs +++ b/src/redmine-net-api/Types/CustomFieldRole.cs @@ -41,7 +41,7 @@ internal CustomFieldRole(int id, string name) /// /// /// - new public string Name { get; set; } + public new string Name { get; set; } /// /// diff --git a/src/redmine-net-api/Types/Group.cs b/src/redmine-net-api/Types/Group.cs index 18926c40..37cb6655 100644 --- a/src/redmine-net-api/Types/Group.cs +++ b/src/redmine-net-api/Types/Group.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -51,7 +51,7 @@ public Group(string name) /// /// /// - new public string Name { get; set; } + public new string Name { get; set; } /// /// Represents the group's users. /// diff --git a/src/redmine-net-api/Types/GroupUser.cs b/src/redmine-net-api/Types/GroupUser.cs index 341122cf..4d2dcffe 100644 --- a/src/redmine-net-api/Types/GroupUser.cs +++ b/src/redmine-net-api/Types/GroupUser.cs @@ -30,7 +30,7 @@ public sealed class GroupUser : IdentifiableName, IValue /// /// /// - new public string Name { get; set; } + public new string Name { get; set; } #region Implementation of IValue /// diff --git a/src/redmine-net-api/Types/IssueCustomField.cs b/src/redmine-net-api/Types/IssueCustomField.cs index 71f66478..f30f8713 100644 --- a/src/redmine-net-api/Types/IssueCustomField.cs +++ b/src/redmine-net-api/Types/IssueCustomField.cs @@ -37,7 +37,7 @@ public sealed class IssueCustomField : IdentifiableName, IEquatable /// /// - new public string Name { get; set; } + public new string Name { get; set; } /// /// Gets or sets the value. /// diff --git a/src/redmine-net-api/Types/IssueRelationType.cs b/src/redmine-net-api/Types/IssueRelationType.cs index 8355bcee..1d80d177 100644 --- a/src/redmine-net-api/Types/IssueRelationType.cs +++ b/src/redmine-net-api/Types/IssueRelationType.cs @@ -52,10 +52,10 @@ public enum IssueRelationType /// /// /// - copied_to, + CopiedTo, /// /// /// - copied_from + CopiedFrom } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs index b0bcf23f..c6ade8e9 100644 --- a/src/redmine-net-api/Types/Project.cs +++ b/src/redmine-net-api/Types/Project.cs @@ -39,7 +39,7 @@ public sealed class Project : IdentifiableName, IEquatable /// /// /// - new public string Name { get; set; } + public new string Name { get; set; } /// /// Gets or sets the identifier. diff --git a/src/redmine-net-api/Types/ProjectEnabledModule.cs b/src/redmine-net-api/Types/ProjectEnabledModule.cs index ce9b6ff0..d2a57d89 100644 --- a/src/redmine-net-api/Types/ProjectEnabledModule.cs +++ b/src/redmine-net-api/Types/ProjectEnabledModule.cs @@ -53,7 +53,7 @@ public ProjectEnabledModule(string moduleName) /// /// /// - new public string Name { get; set; } + public new string Name { get; set; } #region Implementation of IValue /// diff --git a/src/redmine-net-api/Types/ProjectIssueCategory.cs b/src/redmine-net-api/Types/ProjectIssueCategory.cs index 30455354..5af0986d 100644 --- a/src/redmine-net-api/Types/ProjectIssueCategory.cs +++ b/src/redmine-net-api/Types/ProjectIssueCategory.cs @@ -40,7 +40,7 @@ internal ProjectIssueCategory(int id, string name) /// /// /// - new public string Name { get; set; } + public new string Name { get; set; } /// /// diff --git a/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs b/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs index fbdcac35..9633f284 100644 --- a/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs +++ b/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs @@ -24,7 +24,7 @@ internal ProjectTimeEntryActivity(int id, string name) /// /// /// - new public string Name { get; set; } + public new string Name { get; set; } /// /// diff --git a/src/redmine-net-api/Types/ProjectTracker.cs b/src/redmine-net-api/Types/ProjectTracker.cs index 5c06cab9..b42c5468 100644 --- a/src/redmine-net-api/Types/ProjectTracker.cs +++ b/src/redmine-net-api/Types/ProjectTracker.cs @@ -55,7 +55,7 @@ internal ProjectTracker(int trackerId) /// /// /// - new public string Name { get; set; } + public new string Name { get; set; } #region Implementation of IValue diff --git a/src/redmine-net-api/Types/Role.cs b/src/redmine-net-api/Types/Role.cs index bb07bc55..3e6b905c 100644 --- a/src/redmine-net-api/Types/Role.cs +++ b/src/redmine-net-api/Types/Role.cs @@ -36,7 +36,7 @@ public sealed class Role : IdentifiableName, IEquatable /// /// /// - new public string Name { get; set; } + public new string Name { get; set; } /// /// Gets the permissions. /// diff --git a/src/redmine-net-api/Types/TimeEntryActivity.cs b/src/redmine-net-api/Types/TimeEntryActivity.cs index ab000304..9fade0c3 100644 --- a/src/redmine-net-api/Types/TimeEntryActivity.cs +++ b/src/redmine-net-api/Types/TimeEntryActivity.cs @@ -48,7 +48,7 @@ internal TimeEntryActivity(int id, string name) /// /// /// - new public string Name { get; set; } + public new string Name { get; set; } /// /// diff --git a/src/redmine-net-api/Types/UserGroup.cs b/src/redmine-net-api/Types/UserGroup.cs index 72942128..1de94606 100644 --- a/src/redmine-net-api/Types/UserGroup.cs +++ b/src/redmine-net-api/Types/UserGroup.cs @@ -29,7 +29,7 @@ public sealed class UserGroup : IdentifiableName /// /// /// - new public string Name { get; set; } + public new string Name { get; set; } /// /// diff --git a/src/redmine-net-api/Types/UserStatus.cs b/src/redmine-net-api/Types/UserStatus.cs index 0581e354..992d13b2 100644 --- a/src/redmine-net-api/Types/UserStatus.cs +++ b/src/redmine-net-api/Types/UserStatus.cs @@ -24,18 +24,18 @@ public enum UserStatus /// /// /// - STATUS_ANONYMOUS = 0, + StatusAnonymous = 0, /// /// /// - STATUS_ACTIVE = 1, + StatusActive = 1, /// /// /// - STATUS_REGISTERED = 2, + StatusRegistered = 2, /// /// /// - STATUS_LOCKED = 3 + StatusLocked = 3 } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Watcher.cs b/src/redmine-net-api/Types/Watcher.cs index 4f64a590..0980205c 100644 --- a/src/redmine-net-api/Types/Watcher.cs +++ b/src/redmine-net-api/Types/Watcher.cs @@ -31,7 +31,7 @@ public sealed class Watcher : IdentifiableName, IValue, ICloneable /// /// /// - new public string Name { get; set; } + public new string Name { get; set; } #region Implementation of IValue From 1610a4bada78970549281096dc8cad0860fd092e Mon Sep 17 00:00:00 2001 From: Zapadi Date: Fri, 17 Apr 2020 20:42:01 +0300 Subject: [PATCH 124/549] Fix typo --- src/redmine-net-api/Async/RedmineManagerAsync45.cs | 2 +- src/redmine-net-api/Internals/UrlHelper.cs | 6 +++--- src/redmine-net-api/RedmineManager.cs | 6 ++++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/redmine-net-api/Async/RedmineManagerAsync45.cs b/src/redmine-net-api/Async/RedmineManagerAsync45.cs index 4ae7dc62..aab48dfb 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync45.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync45.cs @@ -77,7 +77,7 @@ public static async Task CreateOrUpdateWikiPageAsync(this RedmineManag public static async Task DeleteWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName) { - var uri = UrlHelper.GetDeleteWikirUrl(redmineManager, projectId, pageName); + var uri = UrlHelper.GetDeleteWikiUrl(redmineManager, projectId, pageName); uri = Uri.EscapeUriString(uri); await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.DELETE, string.Empty).ConfigureAwait(false); } diff --git a/src/redmine-net-api/Internals/UrlHelper.cs b/src/redmine-net-api/Internals/UrlHelper.cs index 7f3bc7cd..7a33d45e 100644 --- a/src/redmine-net-api/Internals/UrlHelper.cs +++ b/src/redmine-net-api/Internals/UrlHelper.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -317,13 +317,13 @@ public static string GetWikiCreateOrUpdaterUrl(RedmineManager redmineManager, st } /// - /// Gets the delete wikir URL. + /// Gets the delete wiki URL. /// /// The redmine manager. /// The project identifier. /// Name of the page. /// - public static string GetDeleteWikirUrl(RedmineManager redmineManager, string projectId, string pageName) + public static string GetDeleteWikiUrl(RedmineManager redmineManager, string projectId, string pageName) { return string.Format(CultureInfo.InvariantCulture,WIKI_PAGE_FORMAT, redmineManager.Host, projectId, pageName, redmineManager.Format); diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index 5c4ffbe8..341470a1 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -395,8 +395,10 @@ public List GetAllWikiPages(string projectId) /// The wiki page name. public void DeleteWikiPage(string projectId, string pageName) { - var url = UrlHelper.GetDeleteWikirUrl(this, projectId, pageName); + var url = UrlHelper.GetDeleteWikiUrl(this, projectId, pageName); + url = Uri.EscapeUriString(url); + WebApiHelper.ExecuteUpload(this, url, HttpVerbs.DELETE, string.Empty); } From 25df4295a0591805bc4abe7d9ceac7ca8ba9eb43 Mon Sep 17 00:00:00 2001 From: Zapadi Date: Fri, 17 Apr 2020 20:42:24 +0300 Subject: [PATCH 125/549] Add RemoveTrailingSlash extension --- src/redmine-net-api/Extensions/StringExtensions.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/redmine-net-api/Extensions/StringExtensions.cs b/src/redmine-net-api/Extensions/StringExtensions.cs index 64b4fe14..2ba360ed 100644 --- a/src/redmine-net-api/Extensions/StringExtensions.cs +++ b/src/redmine-net-api/Extensions/StringExtensions.cs @@ -91,5 +91,18 @@ internal static SecureString ToSecureString(this string value) return rv; } } + + internal static string RemoveTrailingSlash(this string s) + { + if (string.IsNullOrEmpty(s)) + return s; + + if (s.EndsWith("/", StringComparison.OrdinalIgnoreCase) || s.EndsWith("\"", StringComparison.OrdinalIgnoreCase)) + { + return s.Substring(0, s.Length - 1); + } + + return s; + } } } \ No newline at end of file From b99221b22b0336c7cc42bcc26e33aadd8e004c7d Mon Sep 17 00:00:00 2001 From: Zapadi Date: Fri, 17 Apr 2020 20:45:16 +0300 Subject: [PATCH 126/549] Remove DeleteObject(id) --- src/redmine-net-api/IRedmineManager.cs | 6 ------ src/redmine-net-api/RedmineManager.cs | 12 ------------ 2 files changed, 18 deletions(-) diff --git a/src/redmine-net-api/IRedmineManager.cs b/src/redmine-net-api/IRedmineManager.cs index e265eb02..5dcc2d04 100644 --- a/src/redmine-net-api/IRedmineManager.cs +++ b/src/redmine-net-api/IRedmineManager.cs @@ -228,12 +228,6 @@ public interface IRedmineManager /// void UpdateObject(string id, T entity, string projectId) where T : class, new(); - /// - /// - /// - /// - /// - void DeleteObject(string id) where T : class, new(); /// /// /// diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index 341470a1..d09380b9 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -697,18 +697,6 @@ public void DeleteWikiPage(string projectId, string pageName) WebApiHelper.ExecuteUpload(this, url, HttpVerbs.PUT, data); } - /// - /// Deletes the Redmine object. - /// - /// The type of objects to delete. - /// The id of the object to delete - /// - /// - public void DeleteObject(string id) where T : class, new() - { - DeleteObject(id, null); - } - /// /// Deletes the Redmine object. /// From 3ab768dcdd25dfddcb6e3942537f62b406e54f11 Mon Sep 17 00:00:00 2001 From: Zapadi Date: Fri, 17 Apr 2020 20:46:45 +0300 Subject: [PATCH 127/549] Remove configuration properties (Debug, Release) --- src/redmine-net-api/redmine-net-api.csproj | 24 +--------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 5ebff589..78f8e176 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -1,4 +1,4 @@ - + @@ -190,28 +190,6 @@ - - - full - true - - - - full - true - - - - ..\bin\Release\ - pdbonly - true - - - - ..\bin\Release\ - pdbonly - true - From 38614e1be99796395d7bdd0fed6f90d92ef6d14e Mon Sep 17 00:00:00 2001 From: Zapadi Date: Fri, 17 Apr 2020 20:48:49 +0300 Subject: [PATCH 128/549] Add static method to create IdentifiableName with id --- src/redmine-net-api/Types/File.cs | 4 ++-- src/redmine-net-api/Types/IdentifiableName.cs | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/redmine-net-api/Types/File.cs b/src/redmine-net-api/Types/File.cs index cc837c80..5391565b 100644 --- a/src/redmine-net-api/Types/File.cs +++ b/src/redmine-net-api/Types/File.cs @@ -123,7 +123,7 @@ public override void ReadXml(XmlReader reader) case RedmineKeys.FILE_SIZE: FileSize = reader.ReadElementContentAsInt(); break; case RedmineKeys.TOKEN: Token = reader.ReadElementContentAsString(); break; case RedmineKeys.VERSION: Version = new IdentifiableName(reader); break; - case RedmineKeys.VERSION_ID: Version = new IdentifiableName() { Id = reader.ReadElementContentAsInt() }; break; + case RedmineKeys.VERSION_ID: Version = IdentifiableName.Create(reader.ReadElementContentAsInt()); break; default: reader.Read(); break; } } @@ -175,7 +175,7 @@ public override void ReadJson(JsonReader reader) case RedmineKeys.FILE_SIZE: FileSize = reader.ReadAsInt32().GetValueOrDefault(); break; case RedmineKeys.TOKEN: Token = reader.ReadAsString(); break; case RedmineKeys.VERSION: Version = new IdentifiableName(reader); break; - case RedmineKeys.VERSION_ID: Version = new IdentifiableName() { Id = reader.ReadAsInt32().GetValueOrDefault() }; break; + case RedmineKeys.VERSION_ID: Version = IdentifiableName.Create(reader.ReadAsInt32().GetValueOrDefault()); break; default: reader.Read(); break; } } diff --git a/src/redmine-net-api/Types/IdentifiableName.cs b/src/redmine-net-api/Types/IdentifiableName.cs index a9c4da21..271cefef 100644 --- a/src/redmine-net-api/Types/IdentifiableName.cs +++ b/src/redmine-net-api/Types/IdentifiableName.cs @@ -30,6 +30,16 @@ namespace Redmine.Net.Api.Types [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] public class IdentifiableName : Identifiable { + /// + /// + /// + /// + /// + public static IdentifiableName Create(int id) + { + return new IdentifiableName {Id = id}; + } + /// /// Initializes a new instance of the class. /// From 95ccb5596c0a3b20bb93ef5204ad2e01133ebd0c Mon Sep 17 00:00:00 2001 From: Zapadi Date: Fri, 17 Apr 2020 20:49:53 +0300 Subject: [PATCH 129/549] Tmp fix #258 --- src/redmine-net-api/Types/Issue.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/redmine-net-api/Types/Issue.cs b/src/redmine-net-api/Types/Issue.cs index 8273f57f..858eece9 100644 --- a/src/redmine-net-api/Types/Issue.cs +++ b/src/redmine-net-api/Types/Issue.cs @@ -577,6 +577,16 @@ public object Clone() } #endregion + /// + /// + /// + /// + public IdentifiableName AsParent() + { + return IdentifiableName.Create(Id); + } + + /// /// /// From 1fab9b9810e8a70299d5f19124ecbc796c2ba85c Mon Sep 17 00:00:00 2001 From: Zapadi Date: Fri, 17 Apr 2020 20:50:44 +0300 Subject: [PATCH 130/549] Remove internal --- src/redmine-net-api/Types/Group.cs | 4 ++-- src/redmine-net-api/Types/Identifiable.cs | 2 +- src/redmine-net-api/Types/User.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/redmine-net-api/Types/Group.cs b/src/redmine-net-api/Types/Group.cs index 37cb6655..82d2b366 100644 --- a/src/redmine-net-api/Types/Group.cs +++ b/src/redmine-net-api/Types/Group.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -55,7 +55,7 @@ public Group(string name) /// /// Represents the group's users. /// - public IList Users { get; internal set; } + public IList Users { get; set; } /// /// Gets or sets the custom fields. diff --git a/src/redmine-net-api/Types/Identifiable.cs b/src/redmine-net-api/Types/Identifiable.cs index fc29ac9c..5fc2ec10 100644 --- a/src/redmine-net-api/Types/Identifiable.cs +++ b/src/redmine-net-api/Types/Identifiable.cs @@ -38,7 +38,7 @@ public abstract class Identifiable : IXmlSerializable, IJsonSerializable, IEq /// Gets the id. /// /// The id. - public int Id { get; protected internal set; } + public int Id { get; protected set; } #endregion #region Implementation of IXmlSerialization diff --git a/src/redmine-net-api/Types/User.cs b/src/redmine-net-api/Types/User.cs index d815ced8..eeec1cec 100644 --- a/src/redmine-net-api/Types/User.cs +++ b/src/redmine-net-api/Types/User.cs @@ -39,7 +39,7 @@ public sealed class User : Identifiable /// Gets or sets the user login. /// /// The login. - public string Login { get; internal set; } + public string Login { get; set; } /// /// Gets or sets the user password. From bc1b2733204ffb166c736a9d0d13b54a5d482824 Mon Sep 17 00:00:00 2001 From: Zapadi Date: Fri, 17 Apr 2020 20:52:23 +0300 Subject: [PATCH 131/549] Override GetWebResponse. Add support for redirect & cookies --- src/redmine-net-api/RedmineWebClient.cs | 203 +++++++++++++++++++++--- 1 file changed, 177 insertions(+), 26 deletions(-) diff --git a/src/redmine-net-api/RedmineWebClient.cs b/src/redmine-net-api/RedmineWebClient.cs index c3e7b7f3..391cf841 100644 --- a/src/redmine-net-api/RedmineWebClient.cs +++ b/src/redmine-net-api/RedmineWebClient.cs @@ -16,7 +16,8 @@ limitations under the License. using System; using System.Net; -using Redmine.Net.Api.Types; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api { @@ -25,8 +26,8 @@ namespace Redmine.Net.Api /// public class RedmineWebClient : WebClient { - private const string UA = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:8.0) Gecko/20100101 Firefox/8.0"; - + private const string UA = "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.163 Safari/535.1"; + private string redirectUrl = string.Empty; /// /// /// @@ -57,7 +58,7 @@ public RedmineWebClient() public bool UseCookies { get; set; } /// - /// in miliseconds + /// in milliseconds /// /// /// The timeout. @@ -88,6 +89,21 @@ public RedmineWebClient() /// public bool KeepAlive { get; set; } + /// + /// + /// + public string Scheme { get; set; } = "https"; + + /// + /// + /// + public RedirectType Redirect { get; set; } + + /// + /// + /// + internal IRedmineSerializer RedmineSerializer { get; set; } + /// /// Returns a object for the specified resource. /// @@ -98,40 +114,175 @@ public RedmineWebClient() protected override WebRequest GetWebRequest(Uri address) { var wr = base.GetWebRequest(address); - var httpWebRequest = wr as HttpWebRequest; - if (httpWebRequest != null) + if (!(wr is HttpWebRequest httpWebRequest)) + { + return base.GetWebRequest(address); + } + + if (UseCookies) + { + httpWebRequest.Headers.Add(HttpRequestHeader.Cookie, "redmineCookie"); + httpWebRequest.CookieContainer = CookieContainer; + } + + httpWebRequest.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | + DecompressionMethods.None; + httpWebRequest.PreAuthenticate = PreAuthenticate; + httpWebRequest.KeepAlive = KeepAlive; + httpWebRequest.UseDefaultCredentials = UseDefaultCredentials; + httpWebRequest.Credentials = Credentials; + httpWebRequest.UserAgent = UA; + httpWebRequest.CachePolicy = CachePolicy; + + if (UseProxy) { - if (UseCookies) + if (Proxy != null) { - httpWebRequest.Headers.Add(HttpRequestHeader.Cookie, "redmineCookie"); - httpWebRequest.CookieContainer = CookieContainer; + Proxy.Credentials = Credentials; + httpWebRequest.Proxy = Proxy; } - httpWebRequest.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | - DecompressionMethods.None; - httpWebRequest.PreAuthenticate = PreAuthenticate; - httpWebRequest.KeepAlive = KeepAlive; - httpWebRequest.UseDefaultCredentials = UseDefaultCredentials; - httpWebRequest.Credentials = Credentials; - httpWebRequest.UserAgent = UA; - httpWebRequest.CachePolicy = CachePolicy; - - if (UseProxy) + } + + if (Timeout != null) + { + httpWebRequest.Timeout = Timeout.Value.Milliseconds; + } + + return httpWebRequest; + + } + + /// + /// + /// + /// + /// + protected override WebResponse GetWebResponse(WebRequest request) + { + WebResponse response = null; + + try + { + response = base.GetWebResponse(request); + } + catch (WebException webException) + { + webException.HandleWebException(RedmineSerializer); + } + + if (response == null) + { + return null; + } + + if (response is HttpWebResponse) + { + HandleRedirect(request, response); + HandleCookies(request, response); + } + + return response; + } + + /// + /// Handles redirect response if needed + /// + /// Request + /// Response + protected void HandleRedirect(WebRequest request, WebResponse response) + { + var webResponse = response as HttpWebResponse; + + if (Redirect == RedirectType.None) + { + return; + } + + if (webResponse == null) + { + return; + } + + var code = webResponse.StatusCode; + + if (code == HttpStatusCode.Found || code == HttpStatusCode.SeeOther || code == HttpStatusCode.MovedPermanently || code == HttpStatusCode.Moved) + { + redirectUrl = webResponse.Headers["Location"]; + + var isAbsoluteUri = new Uri(redirectUrl).IsAbsoluteUri; + + if (!isAbsoluteUri) { - if (Proxy != null) + var webRequest = request as HttpWebRequest; + var host = webRequest?.Headers["Host"] ?? string.Empty; + + if (Redirect == RedirectType.All) { - Proxy.Credentials = Credentials; + host = $"{host}{webRequest?.RequestUri.AbsolutePath}"; + + host = host.Substring(0, host.LastIndexOf('/')); } - httpWebRequest.Proxy = Proxy; + + // Have to make sure that the "/" symbol is between the "host" and "redirect" strings + if (!redirectUrl.StartsWith("/", StringComparison.OrdinalIgnoreCase) && !host.EndsWith("/", StringComparison.OrdinalIgnoreCase)) + { + redirectUrl = $"/{redirectUrl}"; + } + + redirectUrl = $"{host}{redirectUrl}"; } - if (Timeout != null) - httpWebRequest.Timeout = Timeout.Value.Milliseconds; + if (!redirectUrl.StartsWith(Scheme, StringComparison.OrdinalIgnoreCase)) + { + redirectUrl = $"{Scheme}://{redirectUrl}"; + } + } + else + { + redirectUrl = string.Empty; + } + } + + + + /// + /// Handles additional cookies + /// + /// Request + /// Response + protected void HandleCookies(WebRequest request, WebResponse response) + { + if (!(response is HttpWebResponse webResponse)) return; + + var webRequest = request as HttpWebRequest; + var col = new CookieCollection(); - return httpWebRequest; + foreach (Cookie c in webResponse.Cookies) + { + col.Add(new Cookie(c.Name, c.Value, c.Path, webRequest?.Headers["Host"])); } - return base.GetWebRequest(address); + CookieContainer.Add(col); } } + + /// + /// + /// + public enum RedirectType + { + /// + /// + /// + None, + /// + /// + /// + OnlyHost, + /// + /// + /// + All + }; } \ No newline at end of file From 8f000f855f13dca394acd7de075bda4e9ebd6de5 Mon Sep 17 00:00:00 2001 From: Zapadi Date: Fri, 17 Apr 2020 20:52:42 +0300 Subject: [PATCH 132/549] Cleanup, consistency & fixes --- src/redmine-net-api/RedmineManager.cs | 180 +++++++++++++++----------- 1 file changed, 103 insertions(+), 77 deletions(-) diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index d09380b9..34ea7984 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,7 +23,6 @@ limitations under the License. using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.RegularExpressions; -using System.Web; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; @@ -43,7 +42,7 @@ public class RedmineManager : IRedmineManager /// public const int DEFAULT_PAGE_SIZE_VALUE = 25; - private static readonly Dictionary routes = new Dictionary + private static readonly Dictionary Routes = new Dictionary { {typeof(Issue), "issues"}, {typeof(Project), "projects"}, @@ -65,10 +64,9 @@ public class RedmineManager : IRedmineManager {typeof(Watcher), "watchers"}, {typeof(IssueCustomField), "custom_fields"}, {typeof(CustomField), "custom_fields"} - // {typeof(WikiPage), ""} }; - - private static readonly Dictionary typesWithOffset = new Dictionary{ + + private static readonly Dictionary TypesWithOffset = new Dictionary{ {typeof(Issue), true}, {typeof(Project), true}, {typeof(User), true}, @@ -81,7 +79,9 @@ public class RedmineManager : IRedmineManager private readonly string basicAuthorization; private readonly CredentialCache cache; private string host; + internal IRedmineSerializer Serializer { get; } + /// /// Initializes a new instance of the class. /// @@ -90,24 +90,23 @@ public class RedmineManager : IRedmineManager /// if set to true [verify server cert]. /// The proxy. /// Use this parameter to specify a SecurityProtcolType. Note: it is recommended to leave this parameter at its default value as this setting also affects the calling application process. + /// http or https. Default is https /// /// Host is not defined! /// or /// The host is not valid! /// public RedmineManager(string host, MimeFormat mimeFormat = MimeFormat.Xml, bool verifyServerCert = true, - IWebProxy proxy = null, SecurityProtocolType securityProtocolType = default(SecurityProtocolType)) + IWebProxy proxy = null, SecurityProtocolType securityProtocolType = default, string scheme = "https") { if (string.IsNullOrEmpty(host)) throw new RedmineException("Host is not defined!"); - PageSize = 25; - - if (default(SecurityProtocolType) == securityProtocolType) - { - securityProtocolType = ServicePointManager.SecurityProtocol; - } + PageSize = 25; + Scheme = scheme; Host = host; MimeFormat = mimeFormat; + Proxy = proxy; + if (mimeFormat == MimeFormat.Xml) { Format = "xml"; @@ -119,10 +118,15 @@ public RedmineManager(string host, MimeFormat mimeFormat = MimeFormat.Xml, bool Serializer = new JsonRedmineSerializer(); } - Proxy = proxy; + if (default == securityProtocolType) + { + securityProtocolType = ServicePointManager.SecurityProtocol; + } + SecurityProtocolType = securityProtocolType; ServicePointManager.SecurityProtocol = securityProtocolType; + if (!verifyServerCert) { ServicePointManager.ServerCertificateValidationCallback += RemoteCertValidate; @@ -151,7 +155,7 @@ public RedmineManager(string host, MimeFormat mimeFormat = MimeFormat.Xml, bool /// Use this parameter to specify a SecurityProtcolType. Note: it is recommended to leave this parameter at its default value as this setting also affects the calling application process. public RedmineManager(string host, string apiKey, MimeFormat mimeFormat = MimeFormat.Xml, bool verifyServerCert = true, IWebProxy proxy = null, - SecurityProtocolType securityProtocolType = default(SecurityProtocolType)) + SecurityProtocolType securityProtocolType = default) : this(host, mimeFormat, verifyServerCert, proxy, securityProtocolType) { ApiKey = apiKey; @@ -180,30 +184,33 @@ public RedmineManager(string host, string apiKey, MimeFormat mimeFormat = MimeFo /// Use this parameter to specify a SecurityProtcolType. Note: it is recommended to leave this parameter at its default value as this setting also affects the calling application process. public RedmineManager(string host, string login, string password, MimeFormat mimeFormat = MimeFormat.Xml, bool verifyServerCert = true, IWebProxy proxy = null, - SecurityProtocolType securityProtocolType = default(SecurityProtocolType)) + SecurityProtocolType securityProtocolType = default) : this(host, mimeFormat, verifyServerCert, proxy, securityProtocolType) { cache = new CredentialCache { { new Uri(host), "Basic", new NetworkCredential(login, password) } }; - var token = Convert.ToBase64String(Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture,"{0}:{1}", login, password))); - basicAuthorization = string.Format(CultureInfo.InvariantCulture,"Basic {0}", token); - - + var token = Convert.ToBase64String(Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "{0}:{1}", login, password))); + basicAuthorization = string.Format(CultureInfo.InvariantCulture, "Basic {0}", token); } /// - /// Gets the sufixes. + /// Gets the suffixes. /// /// - /// The sufixes. + /// The suffixes. /// - public static Dictionary Sufixes => routes; + public static Dictionary Suffixes => Routes; /// /// /// public string Format { get; } - + + /// + /// + /// + public string Scheme { get; private set; } + /// /// Gets the host. /// @@ -217,14 +224,17 @@ private set { host = value; - if (!Uri.TryCreate(host, UriKind.Absolute, out Uri uriResult) || - !(uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps)) + if (Uri.TryCreate(host, UriKind.Absolute, out Uri uriResult) && + (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps)) { - host = $"http://{host}/"; + return; } - if (!Uri.TryCreate(host, UriKind.Absolute, out uriResult)) - throw new RedmineException("The host is not valid!"); + host = $"{Scheme ?? "https"}://{host}"; + + if (!Uri.TryCreate(host, UriKind.Absolute, out uriResult)) throw new RedmineException("The host is not valid!"); + + Scheme = uriResult.Scheme; } } @@ -234,7 +244,7 @@ private set /// /// The API key. /// - public string ApiKey { get; private set; } + public string ApiKey { get; } /// /// Maximum page-size when retrieving complete object lists @@ -264,7 +274,7 @@ private set /// /// The MIME format. /// - public MimeFormat MimeFormat { get; private set; } + public MimeFormat MimeFormat { get; } /// /// Gets the proxy. @@ -272,7 +282,7 @@ private set /// /// The proxy. /// - public IWebProxy Proxy { get; private set; } + public IWebProxy Proxy { get; } /// /// Gets the type of the security protocol. @@ -280,7 +290,7 @@ private set /// /// The type of the security protocol. /// - public SecurityProtocolType SecurityProtocolType { get; private set; } + public SecurityProtocolType SecurityProtocolType { get; } /// /// Returns the user whose credentials are used to access the API. @@ -294,7 +304,7 @@ private set public User GetCurrentUser(NameValueCollection parameters = null) { var url = UrlHelper.GetCurrentUserUrl(this); - return WebApiHelper.ExecuteDownload(this, url, parameters); + return WebApiHelper.ExecuteDownload(this, url, parameters); } /// @@ -351,12 +361,16 @@ public void RemoveUserFromGroup(int groupId, int userId) public WikiPage CreateOrUpdateWikiPage(string projectId, string pageName, WikiPage wikiPage) { var result = Serializer.Serialize(wikiPage); - if (string.IsNullOrEmpty(result)) return null; + + if (string.IsNullOrEmpty(result)) + { + return null; + } var url = UrlHelper.GetWikiCreateOrUpdaterUrl(this, projectId, pageName); - + url = Uri.EscapeUriString(url); - + return WebApiHelper.ExecuteUpload(this, url, HttpVerbs.PUT, result); } @@ -370,9 +384,11 @@ public WikiPage CreateOrUpdateWikiPage(string projectId, string pageName, WikiPa /// public WikiPage GetWikiPage(string projectId, NameValueCollection parameters, string pageName, uint version = 0) { - var url = UrlHelper.GetWikiPageUrl(this, projectId, pageName, version); + var url = UrlHelper.GetWikiPageUrl(this, projectId, pageName, version); + url = Uri.EscapeUriString(url); - return WebApiHelper.ExecuteDownload(this, url, parameters); + + return WebApiHelper.ExecuteDownload(this, url, parameters); } /// @@ -383,8 +399,10 @@ public WikiPage GetWikiPage(string projectId, NameValueCollection parameters, st public List GetAllWikiPages(string projectId) { var url = UrlHelper.GetWikisUrl(this, projectId); + var result = WebApiHelper.ExecuteDownloadList(this, url); - return result == null ? null :new List(result.Items); + + return result == null ? null : new List(result.Items); } /// @@ -423,6 +441,7 @@ public void DeleteWikiPage(string projectId, string pageName) try { var tempResult = GetPaginatedObjects(parameters); + if (tempResult != null) { totalCount = tempResult.TotalItems; @@ -484,7 +503,7 @@ public void DeleteWikiPage(string projectId, string pageName) { var parameters = new NameValueCollection(); - if (include != null) + if (include != null && include.Length > 0) { parameters.Add(RedmineKeys.INCLUDE, string.Join(",", include)); } @@ -510,9 +529,11 @@ public void DeleteWikiPage(string projectId, string pageName) public List GetObjects(int limit, int offset, params string[] include) where T : class, new() { var parameters = new NameValueCollection(); + parameters.Add(RedmineKeys.LIMIT, limit.ToString(CultureInfo.InvariantCulture)); parameters.Add(RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)); - if (include != null) + + if (include != null && include.Length > 0) { parameters.Add(RedmineKeys.INCLUDE, string.Join(",", include)); } @@ -561,7 +582,7 @@ public void DeleteWikiPage(string projectId, string pageName) isLimitSet = int.TryParse(parameters[RedmineKeys.LIMIT], out pageSize); int.TryParse(parameters[RedmineKeys.OFFSET], out offset); } - if (pageSize == default(int)) + if (pageSize == default) { pageSize = PageSize > 0 ? PageSize : DEFAULT_PAGE_SIZE_VALUE; parameters.Set(RedmineKeys.LIMIT, pageSize.ToString(CultureInfo.InvariantCulture)); @@ -569,29 +590,33 @@ public void DeleteWikiPage(string projectId, string pageName) try { - var hasOffset = typesWithOffset.ContainsKey(typeof(T)); - if(hasOffset) + var hasOffset = TypesWithOffset.ContainsKey(typeof(T)); + if (hasOffset) { - do - { - parameters.Set(RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)); - - var tempResult = GetPaginatedObjects(parameters); - - if (tempResult?.Items != null) - { - if (resultList == null) - { - resultList = new List(tempResult.Items); - totalCount = isLimitSet ? pageSize : tempResult.TotalItems; - } - else - { - resultList.AddRange(tempResult.Items); - } - } - offset += pageSize; - } while (offset < totalCount); + do + { + parameters.Set(RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)); + + var tempResult = GetPaginatedObjects(parameters); + + totalCount = isLimitSet ? pageSize : tempResult.TotalItems; + + if (tempResult?.Items != null) + { + if (resultList == null) + { + resultList = new List(tempResult.Items); + + } + else + { + resultList.AddRange(tempResult.Items); + } + } + + offset += pageSize; + + } while (offset < totalCount); } else { @@ -599,12 +624,12 @@ public void DeleteWikiPage(string projectId, string pageName) if (result?.Items != null) { return new List(result.Items); - } + } } } catch (WebException wex) { - wex.HandleWebException( Serializer); + wex.HandleWebException(Serializer); } return resultList; } @@ -655,7 +680,9 @@ public void DeleteWikiPage(string projectId, string pageName) public T CreateObject(T obj, string ownerId) where T : class, new() { var url = UrlHelper.GetCreateUrl(this, ownerId); + var data = Serializer.Serialize(obj); + return WebApiHelper.ExecuteUpload(this, url, HttpVerbs.POST, data); } @@ -692,8 +719,11 @@ public void DeleteWikiPage(string projectId, string pageName) public void UpdateObject(string id, T obj, string projectId) where T : class, new() { var url = UrlHelper.GetUploadUrl(this, id); + var data = Serializer.Serialize(obj); + data = Regex.Replace(data, @"\r\n|\r|\n", "\r\n"); + WebApiHelper.ExecuteUpload(this, url, HttpVerbs.PUT, data); } @@ -740,7 +770,9 @@ public Upload UploadFile(byte[] data) public void UpdateAttachment(int issueId, Attachment attachment) { var address = UrlHelper.GetAttachmentUpdateUrl(this, issueId); + var attachments = new Attachments { { attachment.Id, attachment } }; + var data = Serializer.Serialize(attachments); WebApiHelper.ExecuteUpload(this, address, HttpVerbs.PATCH, data); @@ -773,7 +805,8 @@ public byte[] DownloadFile(string address) /// public virtual RedmineWebClient CreateWebClient(NameValueCollection parameters, bool uploadFile = false) { - var webClient = new RedmineWebClient { Proxy = Proxy }; + var webClient = new RedmineWebClient { Proxy = Proxy, Scheme = Scheme, RedmineSerializer = Serializer}; + if (!uploadFile) { webClient.Headers.Add(HttpRequestHeader.ContentType, MimeFormat == MimeFormat.Xml @@ -829,14 +862,7 @@ public virtual RedmineWebClient CreateWebClient(NameValueCollection parameters, /// public virtual bool RemoteCertValidate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors sslPolicyErrors) { - if (sslPolicyErrors == SslPolicyErrors.None) - { - return true; - } - - // Logger.Current.Error("X509Certificate [{0}] Policy Error: '{1}'", cert.Subject, error); - - return false; + return sslPolicyErrors == SslPolicyErrors.None; } } -} +} \ No newline at end of file From cb9b781b1eb62ab6ed5c94284164cf2102aa15ea Mon Sep 17 00:00:00 2001 From: Zapadi Date: Fri, 17 Apr 2020 20:53:47 +0300 Subject: [PATCH 133/549] ... --- redmine-net-api.sln | 41 ++++++++----------- redmine-net-api.sln.DotSettings | 4 ++ src/redmine-net-api/redmine-net-api.csproj | 5 ++- .../redmine-net-api.csproj.DotSettings | 2 + 4 files changed, 27 insertions(+), 25 deletions(-) create mode 100644 redmine-net-api.sln.DotSettings create mode 100644 src/redmine-net-api/redmine-net-api.csproj.DotSettings diff --git a/redmine-net-api.sln b/redmine-net-api.sln index e06a01cf..07ba8903 100644 --- a/redmine-net-api.sln +++ b/redmine-net-api.sln @@ -12,40 +12,35 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "redmine-net-api.Tests", "tests\redmine-net-api.Tests\redmine-net-api.Tests.csproj", "{900EF0B3-0233-45DA-811F-4C59483E8452}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionFolder", "SolutionFolder", "{E8C35EC2-DD90-46E8-9B63-84EFD5F2FDE3}" -ProjectSection(SolutionItems) = preProject - appveyor.yml = appveyor.yml - docker-compose.yml = docker-compose.yml - CONTRIBUTING.md = CONTRIBUTING.md - LICENSE = LICENSE - logo.png = logo.png - README.md = README.md - redmine-net-api.snk = redmine-net-api.snk -EndProjectSection + ProjectSection(SolutionItems) = preProject + appveyor.yml = appveyor.yml + CONTRIBUTING.md = CONTRIBUTING.md + docker-compose.yml = docker-compose.yml + LICENSE = LICENSE + logo.png = logo.png + README.md = README.md + redmine-net-api.snk = redmine-net-api.snk + EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU - DebugJSON|Any CPU = DebugJSON|Any CPU - DebugXML|Any CPU = DebugXML|Any CPU + DebugJson|Any CPU = DebugJson|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {0E6B9B72-445D-4E71-8D29-48C4A009AB03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0E6B9B72-445D-4E71-8D29-48C4A009AB03}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0E6B9B72-445D-4E71-8D29-48C4A009AB03}.DebugJSON|Any CPU.ActiveCfg = Debug|Any CPU - {0E6B9B72-445D-4E71-8D29-48C4A009AB03}.DebugJSON|Any CPU.Build.0 = Debug|Any CPU - {0E6B9B72-445D-4E71-8D29-48C4A009AB03}.DebugXML|Any CPU.ActiveCfg = Debug|Any CPU - {0E6B9B72-445D-4E71-8D29-48C4A009AB03}.DebugXML|Any CPU.Build.0 = Debug|Any CPU - {0E6B9B72-445D-4E71-8D29-48C4A009AB03}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0E6B9B72-445D-4E71-8D29-48C4A009AB03}.Release|Any CPU.Build.0 = Release|Any CPU + {0E6B9B72-445D-4E71-8D29-48C4A009AB03}.DebugJson|Any CPU.ActiveCfg = DebugJson|Any CPU + {0E6B9B72-445D-4E71-8D29-48C4A009AB03}.DebugJson|Any CPU.Build.0 = DebugJson|Any CPU + {0E6B9B72-445D-4E71-8D29-48C4A009AB03}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {0E6B9B72-445D-4E71-8D29-48C4A009AB03}.Release|Any CPU.Build.0 = Debug|Any CPU {900EF0B3-0233-45DA-811F-4C59483E8452}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {900EF0B3-0233-45DA-811F-4C59483E8452}.Debug|Any CPU.Build.0 = Debug|Any CPU - {900EF0B3-0233-45DA-811F-4C59483E8452}.DebugJSON|Any CPU.ActiveCfg = Debug|Any CPU - {900EF0B3-0233-45DA-811F-4C59483E8452}.DebugJSON|Any CPU.Build.0 = Debug|Any CPU - {900EF0B3-0233-45DA-811F-4C59483E8452}.DebugXML|Any CPU.ActiveCfg = Debug|Any CPU - {900EF0B3-0233-45DA-811F-4C59483E8452}.DebugXML|Any CPU.Build.0 = Debug|Any CPU - {900EF0B3-0233-45DA-811F-4C59483E8452}.Release|Any CPU.ActiveCfg = Release|Any CPU - {900EF0B3-0233-45DA-811F-4C59483E8452}.Release|Any CPU.Build.0 = Release|Any CPU + {900EF0B3-0233-45DA-811F-4C59483E8452}.DebugJson|Any CPU.ActiveCfg = DebugJson|Any CPU + {900EF0B3-0233-45DA-811F-4C59483E8452}.DebugJson|Any CPU.Build.0 = DebugJson|Any CPU + {900EF0B3-0233-45DA-811F-4C59483E8452}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {900EF0B3-0233-45DA-811F-4C59483E8452}.Release|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/redmine-net-api.sln.DotSettings b/redmine-net-api.sln.DotSettings new file mode 100644 index 00000000..9134cb35 --- /dev/null +++ b/redmine-net-api.sln.DotSettings @@ -0,0 +1,4 @@ + + True + True + True \ No newline at end of file diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 78f8e176..76c99e9c 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -1,7 +1,8 @@ - + + net48 net20;net40;net45;net451;net452;net46;net461;net462;net47;net471;net472;net48; false Redmine.Net.Api @@ -9,7 +10,7 @@ False true TRACE - Debug;Release + Debug;Release;DebugJson PackageReference NU5105; diff --git a/src/redmine-net-api/redmine-net-api.csproj.DotSettings b/src/redmine-net-api/redmine-net-api.csproj.DotSettings new file mode 100644 index 00000000..b9fd6ee4 --- /dev/null +++ b/src/redmine-net-api/redmine-net-api.csproj.DotSettings @@ -0,0 +1,2 @@ + + CSharp80 \ No newline at end of file From 9ee8e0200ccd061cc500242c79ce2a726432f602 Mon Sep 17 00:00:00 2001 From: Zapadi Date: Fri, 17 Apr 2020 20:55:46 +0300 Subject: [PATCH 134/549] Fix tests --- .../Infrastructure/CaseOrder.cs | 2 +- .../Infrastructure/CollectionOrderer.cs | 2 +- .../Infrastructure/OrderAttribute.cs | 2 +- .../Infrastructure/RedmineCollection.cs | 2 +- .../Tests/Async/AttachmentAsyncTests.cs | 184 +++--- .../Tests/Async/IssueAsyncTests.cs | 6 +- .../Tests/Async/UserAsyncTests.cs | 393 +++++------ .../Tests/Async/WikiPageAsyncTests.cs | 11 +- .../Tests/Sync/AttachmentTests.cs | 8 +- .../Tests/Sync/CustomFieldTests.cs | 4 +- .../Tests/Sync/GroupTests.cs | 98 +-- .../Tests/Sync/IssueCategoryTests.cs | 37 +- .../Tests/Sync/IssuePriorityTests.cs | 2 +- .../Tests/Sync/IssueRelationTests.cs | 4 +- .../Tests/Sync/IssueStatusTests.cs | 2 +- .../Tests/Sync/IssueTests.cs | 613 +++++++++--------- .../Tests/Sync/NewsTests.cs | 4 +- .../Tests/Sync/ProjectMembershipTests.cs | 10 +- .../Tests/Sync/ProjectTests.cs | 201 +++--- .../Tests/Sync/QueryTests.cs | 4 +- .../Tests/Sync/RoleTests.cs | 4 +- .../Tests/Sync/TimeEntryActivtiyTests.cs | 4 +- .../Tests/Sync/TimeEntryTests.cs | 27 +- .../Tests/Sync/TrackerTests.cs | 2 +- .../Tests/Sync/UserTests.cs | 32 +- .../Tests/Sync/VersionTests.cs | 14 +- .../Tests/Sync/WikiPageTests.cs | 57 +- 27 files changed, 874 insertions(+), 855 deletions(-) diff --git a/tests/redmine-net-api.Tests/Infrastructure/CaseOrder.cs b/tests/redmine-net-api.Tests/Infrastructure/CaseOrder.cs index 1f36a704..22096ad4 100644 --- a/tests/redmine-net-api.Tests/Infrastructure/CaseOrder.cs +++ b/tests/redmine-net-api.Tests/Infrastructure/CaseOrder.cs @@ -6,7 +6,7 @@ using Xunit.Abstractions; using Xunit.Sdk; -namespace redmine.net.api.Tests.Infrastructure +namespace Padi.RedmineApi.Tests.Infrastructure { /// /// Custom xUnit test case orderer that uses the OrderAttribute diff --git a/tests/redmine-net-api.Tests/Infrastructure/CollectionOrderer.cs b/tests/redmine-net-api.Tests/Infrastructure/CollectionOrderer.cs index ca8575de..fdbe5d03 100644 --- a/tests/redmine-net-api.Tests/Infrastructure/CollectionOrderer.cs +++ b/tests/redmine-net-api.Tests/Infrastructure/CollectionOrderer.cs @@ -7,7 +7,7 @@ using Xunit; using Xunit.Abstractions; -namespace redmine.net.api.Tests.Infrastructure +namespace Padi.RedmineApi.Tests.Infrastructure { /// /// Custom xUnit test collection orderer that uses the OrderAttribute diff --git a/tests/redmine-net-api.Tests/Infrastructure/OrderAttribute.cs b/tests/redmine-net-api.Tests/Infrastructure/OrderAttribute.cs index 2c3cce8e..b13b5af8 100644 --- a/tests/redmine-net-api.Tests/Infrastructure/OrderAttribute.cs +++ b/tests/redmine-net-api.Tests/Infrastructure/OrderAttribute.cs @@ -1,6 +1,6 @@ using System; -namespace redmine.net.api.Tests.Infrastructure +namespace Padi.RedmineApi.Tests.Infrastructure { public class OrderAttribute : Attribute { diff --git a/tests/redmine-net-api.Tests/Infrastructure/RedmineCollection.cs b/tests/redmine-net-api.Tests/Infrastructure/RedmineCollection.cs index f40dc201..831b2245 100644 --- a/tests/redmine-net-api.Tests/Infrastructure/RedmineCollection.cs +++ b/tests/redmine-net-api.Tests/Infrastructure/RedmineCollection.cs @@ -1,7 +1,7 @@ #if !(NET20 || NET40) using Xunit; -namespace redmine.net.api.Tests.Infrastructure +namespace Padi.RedmineApi.Tests.Infrastructure { [CollectionDefinition("RedmineCollection")] public class RedmineCollection : ICollectionFixture diff --git a/tests/redmine-net-api.Tests/Tests/Async/AttachmentAsyncTests.cs b/tests/redmine-net-api.Tests/Tests/Async/AttachmentAsyncTests.cs index ff57814c..b7f91e12 100644 --- a/tests/redmine-net-api.Tests/Tests/Async/AttachmentAsyncTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Async/AttachmentAsyncTests.cs @@ -1,97 +1,105 @@ #if !(NET20 || NET40) -using Redmine.Net.Api.Async; -using Redmine.Net.Api.Types; + using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Threading.Tasks; - +using Redmine.Net.Api.Async; +using Redmine.Net.Api.Types; using Xunit; -namespace redmine.net.api.Tests.Tests.Async +namespace Padi.RedmineApi.Tests.Tests.Async { - [Collection("RedmineCollection")] - public class AttachmentAsyncTests - { - private const string ATTACHMENT_ID = "10"; - - private readonly RedmineFixture fixture; - public AttachmentAsyncTests (RedmineFixture fixture) - { - this.fixture = fixture; - } - - [Fact] - public async Task Should_Get_Attachment_By_Id() - { - var attachment = await fixture.RedmineManager.GetObjectAsync(ATTACHMENT_ID, null); - - Assert.NotNull(attachment); - Assert.IsType(attachment); - } - - [Fact] - public async Task Should_Upload_Attachment() - { - //read document from specified path - string documentPath = AppDomain.CurrentDomain.BaseDirectory+ "/uploadAttachment.pages"; - byte[] documentData = System.IO.File.ReadAllBytes(documentPath); - - //upload attachment to redmine - Upload attachment = await fixture.RedmineManager.UploadFileAsync(documentData); - - //set attachment properties - attachment.FileName = "uploadAttachment.pages"; - attachment.Description = "File uploaded using REST API"; - attachment.ContentType = "text/plain"; - - //create list of attachments to be added to issue - IList attachments = new List(); - attachments.Add(attachment); - - Issue issue = new Issue(); - issue.Project = new Project { Id = 9 }; - issue.Tracker = new IdentifiableName { Id = 3 }; - issue.Status = new IdentifiableName { Id = 6 }; - issue.Priority = new IdentifiableName { Id = 9 }; - issue.Subject = "Issue with attachments"; - issue.Description = "Issue description..."; - issue.Category = new IdentifiableName { Id = 18 }; - issue.FixedVersion = new IdentifiableName { Id = 9 }; - issue.AssignedTo = new IdentifiableName { Id = 8 }; - issue.ParentIssue = new IdentifiableName { Id = 96 }; - issue.CustomFields = new List(); - issue.CustomFields.Add(new IssueCustomField { Id = 13, Values = new List { new CustomFieldValue { Info = "Issue custom field completed" } } }); - issue.IsPrivate = true; - issue.EstimatedHours = 12; - issue.StartDate = DateTime.Now; - issue.DueDate = DateTime.Now.AddMonths(1); - issue.Uploads = attachments; - issue.Watchers = new List(); - issue.Watchers.Add(new Watcher { Id = 8 }); - issue.Watchers.Add(new Watcher { Id = 2 }); - - //create issue and attach document - Issue issueWithAttachment = await fixture.RedmineManager.CreateObjectAsync(issue); - - issue = await fixture.RedmineManager.GetObjectAsync(issueWithAttachment.Id.ToString(), new NameValueCollection { { "include", "attachments" } }); - - Assert.NotNull(issue); - Assert.IsType(issue); - - Assert.True(issue.Attachments.Count == 1, "Attachments count != 1"); - Assert.True(issue.Attachments[0].FileName == attachment.FileName); - } - - [Fact] - public async Task Sould_Download_Attachment() - { - var attachment = await fixture.RedmineManager.GetObjectAsync(ATTACHMENT_ID, null); - - var document = await fixture.RedmineManager.DownloadFileAsync(attachment.ContentUrl); - - Assert.NotNull(document); - } - } + [Collection("RedmineCollection")] + public class AttachmentAsyncTests + { + private const string ATTACHMENT_ID = "10"; + + private readonly RedmineFixture fixture; + public AttachmentAsyncTests(RedmineFixture fixture) + { + this.fixture = fixture; + } + + [Fact] + public async Task Should_Get_Attachment_By_Id() + { + var attachment = await fixture.RedmineManager.GetObjectAsync(ATTACHMENT_ID, null); + + Assert.NotNull(attachment); + Assert.IsType(attachment); + } + + [Fact] + public async Task Should_Upload_Attachment() + { + //read document from specified path + var documentPath = AppDomain.CurrentDomain.BaseDirectory + "/uploadAttachment.pages"; + var documentData = System.IO.File.ReadAllBytes(documentPath); + + //upload attachment to redmine + var attachment = await fixture.RedmineManager.UploadFileAsync(documentData); + + //set attachment properties + attachment.FileName = "uploadAttachment.pages"; + attachment.Description = "File uploaded using REST API"; + attachment.ContentType = "text/plain"; + + //create list of attachments to be added to issue + IList attachments = new List(); + attachments.Add(attachment); + + + var icf = (IssueCustomField)IdentifiableName.Create(13); + icf.Values = new List { new CustomFieldValue { Info = "Issue custom field completed" } }; + + var issue = new Issue + { + Project = IdentifiableName.Create(9), + Tracker = IdentifiableName.Create(3), + Status = IdentifiableName.Create(6), + Priority = IdentifiableName.Create(9), + Subject = "Issue with attachments", + Description = "Issue description...", + Category = IdentifiableName.Create(18), + FixedVersion = IdentifiableName.Create(9), + AssignedTo = IdentifiableName.Create(8), + ParentIssue = IdentifiableName.Create(96), + CustomFields = new List {icf}, + IsPrivate = true, + EstimatedHours = 12, + StartDate = DateTime.Now, + DueDate = DateTime.Now.AddMonths(1), + Uploads = attachments, + Watchers = new List + { + (Watcher) IdentifiableName.Create(8), + (Watcher) IdentifiableName.Create(2) + } + }; + + //create issue and attach document + var issueWithAttachment = await fixture.RedmineManager.CreateObjectAsync(issue); + + issue = await fixture.RedmineManager.GetObjectAsync(issueWithAttachment.Id.ToString(), + new NameValueCollection { { "include", "attachments" } }); + + Assert.NotNull(issue); + Assert.IsType(issue); + + Assert.True(issue.Attachments.Count == 1, "Attachments count != 1"); + Assert.True(issue.Attachments[0].FileName == attachment.FileName); + } + + [Fact] + public async Task Sould_Download_Attachment() + { + var attachment = await fixture.RedmineManager.GetObjectAsync(ATTACHMENT_ID, null); + + var document = await fixture.RedmineManager.DownloadFileAsync(attachment.ContentUrl); + + Assert.NotNull(document); + } + } } -#endif +#endif \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Tests/Async/IssueAsyncTests.cs b/tests/redmine-net-api.Tests/Tests/Async/IssueAsyncTests.cs index 2d7052ab..b800e3c4 100644 --- a/tests/redmine-net-api.Tests/Tests/Async/IssueAsyncTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Async/IssueAsyncTests.cs @@ -6,7 +6,7 @@ using Redmine.Net.Api.Types; using Xunit; -namespace redmine.net.api.Tests.Tests.Async +namespace Padi.RedmineApi.Tests.Tests.Async { [Collection("RedmineCollection")] public class IssueAsyncTests @@ -25,7 +25,7 @@ public async Task Should_Add_Watcher_To_Issue() { await fixture.RedmineManager.AddWatcherToIssueAsync(WATCHER_ISSUE_ID, WATCHER_USER_ID); - Issue issue = await fixture.RedmineManager.GetObjectAsync(WATCHER_ISSUE_ID.ToString(), new NameValueCollection { { "include", "watchers" } }); + var issue = await fixture.RedmineManager.GetObjectAsync(WATCHER_ISSUE_ID.ToString(), new NameValueCollection { { "include", "watchers" } }); Assert.NotNull(issue); Assert.True(issue.Watchers.Count == 1, "Number of watchers != 1"); @@ -37,7 +37,7 @@ public async Task Should_Remove_Watcher_From_Issue() { await fixture.RedmineManager.RemoveWatcherFromIssueAsync(WATCHER_ISSUE_ID, WATCHER_USER_ID); - Issue issue = await fixture.RedmineManager.GetObjectAsync(WATCHER_ISSUE_ID.ToString(), new NameValueCollection { { "include", "watchers" } }); + var issue = await fixture.RedmineManager.GetObjectAsync(WATCHER_ISSUE_ID.ToString(), new NameValueCollection { { "include", "watchers" } }); Assert.True(issue.Watchers == null || ((List)issue.Watchers).Find(w => w.Id == WATCHER_USER_ID) == null); } diff --git a/tests/redmine-net-api.Tests/Tests/Async/UserAsyncTests.cs b/tests/redmine-net-api.Tests/Tests/Async/UserAsyncTests.cs index 9f72feab..3570e5ae 100644 --- a/tests/redmine-net-api.Tests/Tests/Async/UserAsyncTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Async/UserAsyncTests.cs @@ -11,199 +11,206 @@ using Redmine.Net.Api.Types; using Xunit; -namespace redmine.net.api.Tests.Tests.Async +namespace Padi.RedmineApi.Tests.Tests.Async { - [Collection("RedmineCollection")] - public class UserAsyncTests - { - private const string USER_ID = "8"; - private const string LIMIT = "2"; - private const string OFFSET = "1"; - private const int GROUP_ID = 9; - - private readonly RedmineFixture fixture; - public UserAsyncTests (RedmineFixture fixture) - { - this.fixture = fixture; - } - - [Fact] - public async Task Should_Get_CurrentUser() - { - var currentUser = await fixture.RedmineManager.GetCurrentUserAsync(); - Assert.NotNull(currentUser); - } - - [Fact] - public async Task Should_Get_User_By_Id() - { - var user = await fixture.RedmineManager.GetObjectAsync(USER_ID, null); - Assert.NotNull(user); - } - - [Fact] - public async Task Should_Get_User_By_Id_Including_Groups_And_Memberships() - { - var user = await fixture.RedmineManager.GetObjectAsync(USER_ID, new NameValueCollection() { { RedmineKeys.INCLUDE, "groups,memberships" } }); - - Assert.NotNull(user); - - Assert.NotNull (user.Groups); - Assert.True(user.Groups.Count == 1, "Group count != 1"); - - Assert.NotNull (user.Memberships); - Assert.True(user.Memberships.Count == 3, "Membership count != 3"); - } - - [Fact] - public async Task Should_Get_X_Users_From_Offset_Y() - { - var result = await fixture.RedmineManager.GetPaginatedObjectsAsync(new NameValueCollection() { - { RedmineKeys.INCLUDE, "groups, memberships" }, - {RedmineKeys.LIMIT,LIMIT }, - {RedmineKeys.OFFSET,OFFSET } - }); - - Assert.NotNull(result); - Assert.All (result.Items, u => Assert.IsType (u)); - } - - [Fact] - public async Task Should_Get_All_Users_With_Groups_And_Memberships() - { - List users = await fixture.RedmineManager.GetObjectsAsync(new NameValueCollection { { RedmineKeys.INCLUDE, "groups, memberships" } }); - - Assert.NotNull(users); - Assert.All (users, u => Assert.IsType (u)); - } - - [Fact] - public async Task Should_Get_Active_Users() - { - var users = await fixture.RedmineManager.GetObjectsAsync(new NameValueCollection() - { - { RedmineKeys.STATUS, ((int)UserStatus.STATUS_ACTIVE).ToString(CultureInfo.InvariantCulture) } - }); - - Assert.NotNull(users); - Assert.True(users.Count == 6); - Assert.All (users, u => Assert.IsType (u)); - } - - [Fact] - public async Task Should_Get_Anonymous_Users() - { - var users = await fixture.RedmineManager.GetObjectsAsync(new NameValueCollection() - { - { RedmineKeys.STATUS, ((int)UserStatus.STATUS_ANONYMOUS).ToString(CultureInfo.InvariantCulture) } - }); - - Assert.NotNull(users); - Assert.True(users.Count == 0); - Assert.All (users, u => Assert.IsType (u)); - } - - [Fact] - public async Task Should_Get_Locked_Users() - { - var users = await fixture.RedmineManager.GetObjectsAsync(new NameValueCollection() - { - { RedmineKeys.STATUS, ((int)UserStatus.STATUS_LOCKED).ToString(CultureInfo.InvariantCulture) } - }); - - Assert.NotNull(users); - Assert.True(users.Count == 1); - Assert.All (users, u => Assert.IsType (u)); - } - - [Fact] - public async Task Should_Get_Registered_Users() - { - var users = await fixture.RedmineManager.GetObjectsAsync(new NameValueCollection() - { - { RedmineKeys.STATUS, ((int)UserStatus.STATUS_REGISTERED).ToString(CultureInfo.InvariantCulture) } - }); - - Assert.NotNull(users); - Assert.True(users.Count == 1); - Assert.All (users, u => Assert.IsType (u)); - } - - [Fact] - public async Task Should_Get_Users_By_Group() - { - var users = await fixture.RedmineManager.GetObjectsAsync(new NameValueCollection() - { - {RedmineKeys.GROUP_ID, GROUP_ID.ToString(CultureInfo.InvariantCulture)} - }); - - Assert.NotNull(users); - Assert.True(users.Count == 3); - Assert.All (users, u => Assert.IsType (u)); - } - - [Fact] - public async Task Should_Add_User_To_Group() - { - await fixture.RedmineManager.AddUserToGroupAsync(GROUP_ID, int.Parse(USER_ID)); - - User user = fixture.RedmineManager.GetObject(USER_ID.ToString(CultureInfo.InvariantCulture), new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.GROUPS } }); - - Assert.NotNull (user.Groups); - Assert.True(user.Groups.FirstOrDefault(g => g.Id == GROUP_ID) != null); - } - - [Fact] - public async Task Should_Remove_User_From_Group() - { - await fixture.RedmineManager.RemoveUserFromGroupAsync(GROUP_ID, int.Parse(USER_ID)); - - User user = await fixture.RedmineManager.GetObjectAsync(USER_ID.ToString(CultureInfo.InvariantCulture), new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.GROUPS } }); - - Assert.True(user.Groups == null || user.Groups.FirstOrDefault(g => g.Id == GROUP_ID) == null); - } - - [Fact] - public async Task Should_Create_User() - { - User user = new User(); - user.Login = "userTestLogin4"; - user.FirstName = "userTestFirstName"; - user.LastName = "userTestLastName"; - user.Email = "testTest4@redmineapi.com"; - user.Password = "123456"; - user.AuthenticationModeId = 1; - user.MustChangePassword = false; - user.CustomFields = new List(); - user.CustomFields.Add(new IssueCustomField { Id = 4, Values = new List { new CustomFieldValue { Info = "userTestCustomField:" + DateTime.UtcNow } } }); - - var createdUser = await fixture.RedmineManager.CreateObjectAsync(user); - - Assert.Equal(user.Login, createdUser.Login); - Assert.Equal(user.Email, createdUser.Email); - } - - [Fact] - public async Task Should_Update_User() - { - var userId = 59.ToString(); - User user = fixture.RedmineManager.GetObject(userId, null); - user.FirstName = "modified first name"; - await fixture.RedmineManager.UpdateObjectAsync(userId, user); - - User updatedUser = await fixture.RedmineManager.GetObjectAsync(userId, null); - - Assert.Equal(user.FirstName, updatedUser.FirstName); - } - - [Fact] - public async Task Should_Delete_User() - { - var userId = 62.ToString(); - await fixture.RedmineManager.DeleteObjectAsync(userId); - await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetObjectAsync(userId, null)); - - } - } + [Collection("RedmineCollection")] + public class UserAsyncTests + { + private const string USER_ID = "8"; + private const string LIMIT = "2"; + private const string OFFSET = "1"; + private const int GROUP_ID = 9; + + private readonly RedmineFixture fixture; + public UserAsyncTests(RedmineFixture fixture) + { + this.fixture = fixture; + } + + [Fact] + public async Task Should_Get_CurrentUser() + { + var currentUser = await fixture.RedmineManager.GetCurrentUserAsync(); + Assert.NotNull(currentUser); + } + + [Fact] + public async Task Should_Get_User_By_Id() + { + var user = await fixture.RedmineManager.GetObjectAsync(USER_ID, null); + Assert.NotNull(user); + } + + [Fact] + public async Task Should_Get_User_By_Id_Including_Groups_And_Memberships() + { + var user = await fixture.RedmineManager.GetObjectAsync(USER_ID, new NameValueCollection() { { RedmineKeys.INCLUDE, "groups,memberships" } }); + + Assert.NotNull(user); + + Assert.NotNull(user.Groups); + Assert.True(user.Groups.Count == 1, "Group count != 1"); + + Assert.NotNull(user.Memberships); + Assert.True(user.Memberships.Count == 3, "Membership count != 3"); + } + + [Fact] + public async Task Should_Get_X_Users_From_Offset_Y() + { + var result = await fixture.RedmineManager.GetPaginatedObjectsAsync(new NameValueCollection() { + { RedmineKeys.INCLUDE, "groups, memberships" }, + {RedmineKeys.LIMIT,LIMIT }, + {RedmineKeys.OFFSET,OFFSET } + }); + + Assert.NotNull(result); + Assert.All(result.Items, u => Assert.IsType(u)); + } + + [Fact] + public async Task Should_Get_All_Users_With_Groups_And_Memberships() + { + var users = await fixture.RedmineManager.GetObjectsAsync(new NameValueCollection { { RedmineKeys.INCLUDE, "groups, memberships" } }); + + Assert.NotNull(users); + Assert.All(users, u => Assert.IsType(u)); + } + + [Fact] + public async Task Should_Get_Active_Users() + { + var users = await fixture.RedmineManager.GetObjectsAsync(new NameValueCollection() + { + { RedmineKeys.STATUS, ((int)UserStatus.StatusActive).ToString(CultureInfo.InvariantCulture) } + }); + + Assert.NotNull(users); + Assert.True(users.Count == 6); + Assert.All(users, u => Assert.IsType(u)); + } + + [Fact] + public async Task Should_Get_Anonymous_Users() + { + var users = await fixture.RedmineManager.GetObjectsAsync(new NameValueCollection() + { + { RedmineKeys.STATUS, ((int)UserStatus.StatusAnonymous).ToString(CultureInfo.InvariantCulture) } + }); + + Assert.NotNull(users); + Assert.True(users.Count == 0); + Assert.All(users, u => Assert.IsType(u)); + } + + [Fact] + public async Task Should_Get_Locked_Users() + { + var users = await fixture.RedmineManager.GetObjectsAsync(new NameValueCollection() + { + { RedmineKeys.STATUS, ((int)UserStatus.StatusLocked).ToString(CultureInfo.InvariantCulture) } + }); + + Assert.NotNull(users); + Assert.True(users.Count == 1); + Assert.All(users, u => Assert.IsType(u)); + } + + [Fact] + public async Task Should_Get_Registered_Users() + { + var users = await fixture.RedmineManager.GetObjectsAsync(new NameValueCollection() + { + { RedmineKeys.STATUS, ((int)UserStatus.StatusRegistered).ToString(CultureInfo.InvariantCulture) } + }); + + Assert.NotNull(users); + Assert.True(users.Count == 1); + Assert.All(users, u => Assert.IsType(u)); + } + + [Fact] + public async Task Should_Get_Users_By_Group() + { + var users = await fixture.RedmineManager.GetObjectsAsync(new NameValueCollection() + { + {RedmineKeys.GROUP_ID, GROUP_ID.ToString(CultureInfo.InvariantCulture)} + }); + + Assert.NotNull(users); + Assert.True(users.Count == 3); + Assert.All(users, u => Assert.IsType(u)); + } + + [Fact] + public async Task Should_Add_User_To_Group() + { + await fixture.RedmineManager.AddUserToGroupAsync(GROUP_ID, int.Parse(USER_ID)); + + var user = fixture.RedmineManager.GetObject(USER_ID.ToString(CultureInfo.InvariantCulture), new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.GROUPS } }); + + Assert.NotNull(user.Groups); + Assert.True(user.Groups.FirstOrDefault(g => g.Id == GROUP_ID) != null); + } + + [Fact] + public async Task Should_Remove_User_From_Group() + { + await fixture.RedmineManager.RemoveUserFromGroupAsync(GROUP_ID, int.Parse(USER_ID)); + + var user = await fixture.RedmineManager.GetObjectAsync(USER_ID.ToString(CultureInfo.InvariantCulture), new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.GROUPS } }); + + Assert.True(user.Groups == null || user.Groups.FirstOrDefault(g => g.Id == GROUP_ID) == null); + } + + [Fact] + public async Task Should_Create_User() + { + var user = new User + { + Login = "userTestLogin4", + FirstName = "userTestFirstName", + LastName = "userTestLastName", + Email = "testTest4@redmineapi.com", + Password = "123456", + AuthenticationModeId = 1, + MustChangePassword = false + }; + + + var icf = (IssueCustomField)IdentifiableName.Create(4); + icf.Values = new List { new CustomFieldValue { Info = "userTestCustomField:" + DateTime.UtcNow } }; + + user.CustomFields = new List(); + user.CustomFields.Add(icf); + + var createdUser = await fixture.RedmineManager.CreateObjectAsync(user); + + Assert.Equal(user.Login, createdUser.Login); + Assert.Equal(user.Email, createdUser.Email); + } + + [Fact] + public async Task Should_Update_User() + { + var userId = 59.ToString(); + var user = fixture.RedmineManager.GetObject(userId, null); + user.FirstName = "modified first name"; + await fixture.RedmineManager.UpdateObjectAsync(userId, user); + + var updatedUser = await fixture.RedmineManager.GetObjectAsync(userId, null); + + Assert.Equal(user.FirstName, updatedUser.FirstName); + } + + [Fact] + public async Task Should_Delete_User() + { + var userId = 62.ToString(); + await fixture.RedmineManager.DeleteObjectAsync(userId); + await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetObjectAsync(userId, null)); + + } + } } #endif \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Tests/Async/WikiPageAsyncTests.cs b/tests/redmine-net-api.Tests/Tests/Async/WikiPageAsyncTests.cs index 9336513d..08c85e30 100644 --- a/tests/redmine-net-api.Tests/Tests/Async/WikiPageAsyncTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Async/WikiPageAsyncTests.cs @@ -1,5 +1,4 @@ #if !(NET20 || NET40) -using System.Collections.Generic; using System.Collections.Specialized; using System.Threading.Tasks; using Redmine.Net.Api.Async; @@ -7,7 +6,7 @@ using Redmine.Net.Api.Types; using Xunit; -namespace redmine.net.api.Tests.Tests.Async +namespace Padi.RedmineApi.Tests.Tests.Async { [Collection("RedmineCollection")] public class WikiPageAsyncTests @@ -29,7 +28,7 @@ public WikiPageAsyncTests(RedmineFixture fixture) [Fact] public async Task Should_Add_Or_Update_Page() { - WikiPage page = await fixture.RedmineManager.CreateOrUpdateWikiPageAsync(PROJECT_ID, WIKI_PAGE_NAME, new WikiPage { Text = WIKI_PAGE_UPDATED_TEXT, Comments = WIKI_PAGE_COMMENT }); + var page = await fixture.RedmineManager.CreateOrUpdateWikiPageAsync(PROJECT_ID, WIKI_PAGE_NAME, new WikiPage { Text = WIKI_PAGE_UPDATED_TEXT, Comments = WIKI_PAGE_COMMENT }); Assert.NotNull(page); Assert.True(page.Title == WIKI_PAGE_NAME, "Wiki page " + WIKI_PAGE_NAME + " does not exist."); @@ -38,7 +37,7 @@ public async Task Should_Add_Or_Update_Page() [Fact] public async Task Should_Get_All_Pages() { - List pages = await fixture.RedmineManager.GetAllWikiPagesAsync(null, PROJECT_ID); + var pages = await fixture.RedmineManager.GetAllWikiPagesAsync(null, PROJECT_ID); Assert.NotNull(pages); @@ -49,7 +48,7 @@ public async Task Should_Get_All_Pages() [Fact] public async Task Should_Get_Page_By_Name() { - WikiPage page = await fixture.RedmineManager.GetWikiPageAsync(PROJECT_ID, new NameValueCollection { { "include", "attachments" } }, WIKI_PAGE_NAME); + var page = await fixture.RedmineManager.GetWikiPageAsync(PROJECT_ID, new NameValueCollection { { "include", "attachments" } }, WIKI_PAGE_NAME); Assert.NotNull(page); Assert.True(page.Title == WIKI_PAGE_NAME, "Wiki page " + WIKI_PAGE_NAME + " does not exist."); @@ -58,7 +57,7 @@ public async Task Should_Get_Page_By_Name() [Fact] public async Task Should_Get_Wiki_Page_Old_Version() { - WikiPage oldPage = await fixture.RedmineManager.GetWikiPageAsync(PROJECT_ID, new NameValueCollection { { "include", "attachments" } }, WIKI_PAGE_NAME, WIKI_PAGE_VERSION); + var oldPage = await fixture.RedmineManager.GetWikiPageAsync(PROJECT_ID, new NameValueCollection { { "include", "attachments" } }, WIKI_PAGE_NAME, WIKI_PAGE_VERSION); Assert.True(oldPage.Title == WIKI_PAGE_NAME, "Wiki page " + WIKI_PAGE_NAME + " does not exist."); Assert.True(oldPage.Version == WIKI_PAGE_VERSION, "Wiki page version " + WIKI_PAGE_VERSION + " does not exist."); diff --git a/tests/redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs index edec151e..b5debca6 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs @@ -17,13 +17,13 @@ limitations under the License. using System; using System.Collections.Generic; using System.Collections.Specialized; -using redmine.net.api.Tests.Infrastructure; +using Padi.RedmineApi.Tests.Infrastructure; using Redmine.Net.Api; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Types; using Xunit; -namespace redmine.net.api.Tests.Tests.Sync +namespace Padi.RedmineApi.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "Attachments")] #if !(NET20 || NET40) @@ -44,7 +44,7 @@ public AttachmentTests(RedmineFixture fixture) [Fact, Order(1)] public void Should_Download_Attachment() { - var url = Helper.Uri + "/attachments/download/" + ATTACHMENT_ID + "/" + ATTACHMENT_FILE_NAME; + var url = fixture.Credentials.Uri + "/attachments/download/" + ATTACHMENT_ID + "/" + ATTACHMENT_FILE_NAME; var document = fixture.RedmineManager.DownloadFile(url); @@ -89,7 +89,7 @@ public void Should_Upload_Attachment() var issue = new Issue { - Project = new Project { Id = PROJECT_ID }, + Project = IdentifiableName.Create(PROJECT_ID ), Subject = ISSUE_SUBJECT, Uploads = attachments }; diff --git a/tests/redmine-net-api.Tests/Tests/Sync/CustomFieldTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/CustomFieldTests.cs index 267efdbd..100968a2 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/CustomFieldTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/CustomFieldTests.cs @@ -14,11 +14,11 @@ You may obtain a copy of the License at limitations under the License. */ -using redmine.net.api.Tests.Infrastructure; +using Padi.RedmineApi.Tests.Infrastructure; using Redmine.Net.Api.Types; using Xunit; -namespace redmine.net.api.Tests.Tests.Sync +namespace Padi.RedmineApi.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "CustomFields")] #if !(NET20 || NET40) diff --git a/tests/redmine-net-api.Tests/Tests/Sync/GroupTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/GroupTests.cs index 1dc79a32..2129a0bf 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/GroupTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/GroupTests.cs @@ -1,14 +1,14 @@ using System.Collections.Generic; using System.Collections.Specialized; -using redmine.net.api.Tests.Infrastructure; +using Padi.RedmineApi.Tests.Infrastructure; using Redmine.Net.Api; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Types; using Xunit; -namespace redmine.net.api.Tests.Tests.Sync +namespace Padi.RedmineApi.Tests.Tests.Sync { - [Trait("Redmine-Net-Api", "Groups")] + [Trait("Redmine-Net-Api", "Groups")] #if !(NET20 || NET40) [Collection("RedmineCollection")] #endif @@ -19,7 +19,7 @@ public GroupTests(RedmineFixture fixture) this.fixture = fixture; } - private readonly RedmineFixture fixture; + private readonly RedmineFixture fixture; private const string GROUP_ID = "57"; private const int NUMBER_OF_MEMBERSHIPS = 1; @@ -28,103 +28,103 @@ public GroupTests(RedmineFixture fixture) [Fact, Order(1)] public void Should_Add_Group() { - const string NEW_GROUP_NAME = "Developers1"; - const int NEW_GROUP_USER_ID = 8; + const string NEW_GROUP_NAME = "Developers1"; + const int NEW_GROUP_USER_ID = 8; - var group = new Group(); + var group = new Group(); group.Name = NEW_GROUP_NAME; - group.Users = new List {new GroupUser {Id = NEW_GROUP_USER_ID}}; + group.Users = new List { (GroupUser)IdentifiableName.Create(NEW_GROUP_USER_ID )}; Group savedGroup = null; var exception = - (RedmineException) Record.Exception(() => savedGroup = fixture.RedmineManager.CreateObject(group)); + (RedmineException)Record.Exception(() => savedGroup = fixture.RedmineManager.CreateObject(group)); Assert.Null(exception); Assert.NotNull(savedGroup); Assert.True(group.Name.Equals(savedGroup.Name), "Group name is not valid."); } - [Fact, Order(2)] - public void Should_Update_Group() - { - const string UPDATED_GROUP_ID = "58"; - const string UPDATED_GROUP_NAME = "Best Developers"; - const int UPDATED_GROUP_USER_ID = 2; + [Fact, Order(2)] + public void Should_Update_Group() + { + const string UPDATED_GROUP_ID = "58"; + const string UPDATED_GROUP_NAME = "Best Developers"; + const int UPDATED_GROUP_USER_ID = 2; - var group = fixture.RedmineManager.GetObject(UPDATED_GROUP_ID, - new NameValueCollection {{RedmineKeys.INCLUDE, RedmineKeys.USERS}}); - group.Name = UPDATED_GROUP_NAME; - group.Users.Add(new GroupUser {Id = UPDATED_GROUP_USER_ID}); + var group = fixture.RedmineManager.GetObject(UPDATED_GROUP_ID, + new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.USERS } }); + group.Name = UPDATED_GROUP_NAME; + group.Users.Add((GroupUser)IdentifiableName.Create(UPDATED_GROUP_USER_ID)); - fixture.RedmineManager.UpdateObject(UPDATED_GROUP_ID, group); + fixture.RedmineManager.UpdateObject(UPDATED_GROUP_ID, group); - var updatedGroup = fixture.RedmineManager.GetObject(UPDATED_GROUP_ID, - new NameValueCollection {{RedmineKeys.INCLUDE, RedmineKeys.USERS}}); + var updatedGroup = fixture.RedmineManager.GetObject(UPDATED_GROUP_ID, + new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.USERS } }); - Assert.NotNull(updatedGroup); - Assert.True(updatedGroup.Name.Equals(UPDATED_GROUP_NAME), "Group name was not updated."); - Assert.NotNull(updatedGroup.Users); - // Assert.True(updatedGroup.Users.Find(u => u.Id == UPDATED_GROUP_USER_ID) != null, - //"User was not added to group."); - } + Assert.NotNull(updatedGroup); + Assert.True(updatedGroup.Name.Equals(UPDATED_GROUP_NAME), "Group name was not updated."); + Assert.NotNull(updatedGroup.Users); + // Assert.True(updatedGroup.Users.Find(u => u.Id == UPDATED_GROUP_USER_ID) != null, + //"User was not added to group."); + } [Fact, Order(3)] public void Should_Get_All_Groups() { - const int NUMBER_OF_GROUPS = 3; + const int NUMBER_OF_GROUPS = 3; - var groups = fixture.RedmineManager.GetObjects(); + var groups = fixture.RedmineManager.GetObjects(); Assert.NotNull(groups); - Assert.True(groups.Count == NUMBER_OF_GROUPS, "Number of groups ( "+groups.Count+" ) != " + NUMBER_OF_GROUPS); + Assert.True(groups.Count == NUMBER_OF_GROUPS, "Number of groups ( " + groups.Count + " ) != " + NUMBER_OF_GROUPS); } [Fact, Order(4)] public void Should_Get_Group_With_All_Associated_Data() { var group = fixture.RedmineManager.GetObject(GROUP_ID, - new NameValueCollection {{RedmineKeys.INCLUDE, RedmineKeys.MEMBERSHIPS + "," + RedmineKeys.USERS}}); + new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.MEMBERSHIPS + "," + RedmineKeys.USERS } }); Assert.NotNull(group); Assert.True(group.Memberships.Count == NUMBER_OF_MEMBERSHIPS, "Number of memberships != " + NUMBER_OF_MEMBERSHIPS); - Assert.True(group.Users.Count == NUMBER_OF_USERS, "Number of users ( "+ group.Users.Count +" ) != " + NUMBER_OF_USERS); - Assert.True(group.Name.Equals("Test"), "Group name is not valid."); + Assert.True(group.Users.Count == NUMBER_OF_USERS, "Number of users ( " + group.Users.Count + " ) != " + NUMBER_OF_USERS); + Assert.True(group.Name.Equals("Test"), "Group name is not valid."); } [Fact, Order(5)] public void Should_Get_Group_With_Memberships() { var group = fixture.RedmineManager.GetObject(GROUP_ID, - new NameValueCollection {{RedmineKeys.INCLUDE, RedmineKeys.MEMBERSHIPS}}); + new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.MEMBERSHIPS } }); Assert.NotNull(group); Assert.True(group.Memberships.Count == NUMBER_OF_MEMBERSHIPS, - "Number of memberships ( "+ group.Memberships.Count +" ) != " + NUMBER_OF_MEMBERSHIPS); + "Number of memberships ( " + group.Memberships.Count + " ) != " + NUMBER_OF_MEMBERSHIPS); } [Fact, Order(6)] public void Should_Get_Group_With_Users() { var group = fixture.RedmineManager.GetObject(GROUP_ID, - new NameValueCollection {{RedmineKeys.INCLUDE, RedmineKeys.USERS}}); + new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.USERS } }); Assert.NotNull(group); - Assert.True(group.Users.Count == NUMBER_OF_USERS, "Number of users ( "+ group.Users.Count +" ) != " + NUMBER_OF_USERS); + Assert.True(group.Users.Count == NUMBER_OF_USERS, "Number of users ( " + group.Users.Count + " ) != " + NUMBER_OF_USERS); } - [Fact, Order(99)] - public void Should_Delete_Group() - { - const string DELETED_GROUP_ID = "63"; - - var exception = - (RedmineException) - Record.Exception(() => fixture.RedmineManager.DeleteObject(DELETED_GROUP_ID)); - Assert.Null(exception); - Assert.Throws(() => fixture.RedmineManager.GetObject(DELETED_GROUP_ID, null)); - } + [Fact, Order(99)] + public void Should_Delete_Group() + { + const string DELETED_GROUP_ID = "63"; + + var exception = + (RedmineException) + Record.Exception(() => fixture.RedmineManager.DeleteObject(DELETED_GROUP_ID)); + Assert.Null(exception); + Assert.Throws(() => fixture.RedmineManager.GetObject(DELETED_GROUP_ID, null)); + } } } \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Tests/Sync/IssueCategoryTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/IssueCategoryTests.cs index 41f5e33f..f0346abf 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/IssueCategoryTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/IssueCategoryTests.cs @@ -1,11 +1,11 @@ using System.Collections.Specialized; -using redmine.net.api.Tests.Infrastructure; +using Padi.RedmineApi.Tests.Infrastructure; using Redmine.Net.Api; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Types; using Xunit; -namespace redmine.net.api.Tests.Tests.Sync +namespace Padi.RedmineApi.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "IssueCategories")] #if !(NET20 || NET40) @@ -13,31 +13,30 @@ namespace redmine.net.api.Tests.Tests.Sync #endif public class IssueCategoryTests { + private readonly RedmineFixture fixture; + + private const string PROJECT_ID = "redmine-net-testq"; + private const string NEW_ISSUE_CATEGORY_NAME = "Test category"; + private const int NEW_ISSUE_CATEGORY_ASIGNEE_ID = 1; + private static string createdIssueCategoryId; + public IssueCategoryTests(RedmineFixture fixture) { this.fixture = fixture; } - private readonly RedmineFixture fixture; - - const string PROJECT_ID = "redmine-net-testq"; - const string NEW_ISSUE_CATEGORY_NAME = "Test category"; - const int NEW_ISSUE_CATEGORY_ASIGNEE_ID = 1; - - private static string CREATED_ISSUE_CATEGORY_ID; - [Fact, Order(1)] public void Should_Create_IssueCategory() { var issueCategory = new IssueCategory { Name = NEW_ISSUE_CATEGORY_NAME, - AssignTo = new IdentifiableName {Id = NEW_ISSUE_CATEGORY_ASIGNEE_ID} + AssignTo = IdentifiableName.Create(NEW_ISSUE_CATEGORY_ASIGNEE_ID) }; var savedIssueCategory = fixture.RedmineManager.CreateObject(issueCategory, PROJECT_ID); - CREATED_ISSUE_CATEGORY_ID = savedIssueCategory.Id.ToString(); + createdIssueCategoryId = savedIssueCategory.Id.ToString(); Assert.NotNull(savedIssueCategory); Assert.True(savedIssueCategory.Name.Equals(NEW_ISSUE_CATEGORY_NAME), "Saved issue category name is invalid."); @@ -49,10 +48,10 @@ public void Should_Delete_IssueCategory() var exception = (RedmineException) Record.Exception( - () => fixture.RedmineManager.DeleteObject(CREATED_ISSUE_CATEGORY_ID)); + () => fixture.RedmineManager.DeleteObject(createdIssueCategoryId)); Assert.Null(exception); Assert.Throws( - () => fixture.RedmineManager.GetObject(CREATED_ISSUE_CATEGORY_ID, null)); + () => fixture.RedmineManager.GetObject(createdIssueCategoryId, null)); } [Fact, Order(2)] @@ -76,7 +75,7 @@ public void Should_Get_IssueCategory_By_Id() const string ISSUE_CATEGORY_PROJECT_NAME_TO_GET = "Redmine tests"; const string ISSUE_CATEGORY_ASIGNEE_NAME_TO_GET = "Redmine"; - var issueCategory = fixture.RedmineManager.GetObject(CREATED_ISSUE_CATEGORY_ID, null); + var issueCategory = fixture.RedmineManager.GetObject(createdIssueCategoryId, null); Assert.NotNull(issueCategory); Assert.True(issueCategory.Name.Equals(NEW_ISSUE_CATEGORY_NAME), "Issue category name is invalid."); @@ -94,13 +93,13 @@ public void Should_Update_IssueCategory() const string ISSUE_CATEGORY_NAME_TO_UPDATE = "Category updated"; const int ISSUE_CATEGORY_ASIGNEE_ID_TO_UPDATE = 2; - var issueCategory = fixture.RedmineManager.GetObject(CREATED_ISSUE_CATEGORY_ID, null); + var issueCategory = fixture.RedmineManager.GetObject(createdIssueCategoryId, null); issueCategory.Name = ISSUE_CATEGORY_NAME_TO_UPDATE; - issueCategory.AssignTo = new IdentifiableName {Id = ISSUE_CATEGORY_ASIGNEE_ID_TO_UPDATE}; + issueCategory.AssignTo = IdentifiableName.Create(ISSUE_CATEGORY_ASIGNEE_ID_TO_UPDATE); - fixture.RedmineManager.UpdateObject(CREATED_ISSUE_CATEGORY_ID, issueCategory); + fixture.RedmineManager.UpdateObject(createdIssueCategoryId, issueCategory); - var updatedIssueCategory = fixture.RedmineManager.GetObject(CREATED_ISSUE_CATEGORY_ID, null); + var updatedIssueCategory = fixture.RedmineManager.GetObject(createdIssueCategoryId, null); Assert.NotNull(updatedIssueCategory); Assert.True(updatedIssueCategory.Name.Equals(ISSUE_CATEGORY_NAME_TO_UPDATE), diff --git a/tests/redmine-net-api.Tests/Tests/Sync/IssuePriorityTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/IssuePriorityTests.cs index cefbabe4..0e0a6fe5 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/IssuePriorityTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/IssuePriorityTests.cs @@ -17,7 +17,7 @@ limitations under the License. using Redmine.Net.Api.Types; using Xunit; -namespace redmine.net.api.Tests.Tests.Sync +namespace Padi.RedmineApi.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "IssuePriorities")] #if !(NET20 || NET40) diff --git a/tests/redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs index a06d9084..c9de2a44 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs @@ -15,13 +15,13 @@ limitations under the License. */ using System.Collections.Specialized; -using redmine.net.api.Tests.Infrastructure; +using Padi.RedmineApi.Tests.Infrastructure; using Redmine.Net.Api; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Types; using Xunit; -namespace redmine.net.api.Tests.Tests.Sync +namespace Padi.RedmineApi.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "IssueRelations")] #if !(NET20 || NET40) diff --git a/tests/redmine-net-api.Tests/Tests/Sync/IssueStatusTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/IssueStatusTests.cs index 815423e1..59d32eaa 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/IssueStatusTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/IssueStatusTests.cs @@ -17,7 +17,7 @@ limitations under the License. using Redmine.Net.Api.Types; using Xunit; -namespace redmine.net.api.Tests.Tests.Sync +namespace Padi.RedmineApi.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "IssueStatuses")] #if !(NET20 || NET40) diff --git a/tests/redmine-net-api.Tests/Tests/Sync/IssueTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/IssueTests.cs index c39c3e50..14601c11 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/IssueTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/IssueTests.cs @@ -1,316 +1,329 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; -using redmine.net.api.Tests.Infrastructure; +using Padi.RedmineApi.Tests.Infrastructure; using Redmine.Net.Api; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Types; using Xunit; -namespace redmine.net.api.Tests.Tests.Sync +namespace Padi.RedmineApi.Tests.Tests.Sync { - [Trait("Redmine-Net-Api", "Issues")] + [Trait("Redmine-Net-Api", "Issues")] #if !(NET20 || NET40) [Collection("RedmineCollection")] #endif - public class IssueTests - { - public IssueTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - private readonly RedmineFixture fixture; + public class IssueTests + { + public IssueTests(RedmineFixture fixture) + { + this.fixture = fixture; + } + private readonly RedmineFixture fixture; + + //filters + private const string PROJECT_ID = "redmine-net-testq"; + + //watcher + private const int WATCHER_ISSUE_ID = 96; + private const int WATCHER_USER_ID = 8; + + + [Fact, Order(1)] + public void Should_Get_All_Issues() + { + var issues = fixture.RedmineManager.GetObjects(); + + Assert.NotNull(issues); + } + + [Fact, Order(2)] + public void Should_Get_Paginated_Issues() + { + const int NUMBER_OF_PAGINATED_ISSUES = 3; + const int OFFSET = 1; + + var issues = fixture.RedmineManager.GetPaginatedObjects(new NameValueCollection + { + { RedmineKeys.OFFSET, OFFSET.ToString() }, { RedmineKeys.LIMIT, NUMBER_OF_PAGINATED_ISSUES.ToString() }, { "sort", "id:desc" } + }); + + Assert.NotNull(issues.Items); + //Assert.True(issues.Items.Count <= NUMBER_OF_PAGINATED_ISSUES, "number of issues ( "+ issues.Items.Count +" ) != " + NUMBER_OF_PAGINATED_ISSUES.ToString()); + } + + [Fact, Order(3)] + public void Should_Get_Issues_By_Project_Id() + { + var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { RedmineKeys.PROJECT_ID, PROJECT_ID } }); + + Assert.NotNull(issues); + } + + [Fact, Order(4)] + public void Should_Get_Issues_By_SubProject_Id() + { + const string SUB_PROJECT_ID_VALUE = "redmine-net-testr"; + + var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { RedmineKeys.SUB_PROJECT_ID, SUB_PROJECT_ID_VALUE } }); + + Assert.NotNull(issues); + } + + [Fact, Order(5)] + public void Should_Get_Issues_By_Project_Without_SubProject() + { + const string ALL_SUB_PROJECTS = "!*"; + + var issues = fixture.RedmineManager.GetObjects(new NameValueCollection + { + { RedmineKeys.PROJECT_ID, PROJECT_ID }, { RedmineKeys.SUB_PROJECT_ID, ALL_SUB_PROJECTS } + }); + + Assert.NotNull(issues); + } + + [Fact, Order(6)] + public void Should_Get_Issues_By_Tracker() + { + const string TRACKER_ID = "3"; + var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { RedmineKeys.TRACKER_ID, TRACKER_ID } }); + + Assert.NotNull(issues); + } + + [Fact, Order(7)] + public void Should_Get_Issues_By_Status() + { + const string STATUS_ID = "*"; + var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { RedmineKeys.STATUS_ID, STATUS_ID } }); + Assert.NotNull(issues); + } + + [Fact, Order(8)] + public void Should_Get_Issues_By_Assignee() + { + const string ASSIGNED_TO_ID = "me"; + var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { RedmineKeys.ASSIGNED_TO_ID, ASSIGNED_TO_ID } }); + + Assert.NotNull(issues); + } + + [Fact, Order(9)] + public void Should_Get_Issues_By_Custom_Field() + { + const string CUSTOM_FIELD_NAME = "cf_13"; + const string CUSTOM_FIELD_VALUE = "Testx"; + + var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { CUSTOM_FIELD_NAME, CUSTOM_FIELD_VALUE } }); + + Assert.NotNull(issues); + } + + [Fact, Order(10)] + public void Should_Get_Issue_By_Id() + { + const string ISSUE_ID = "96"; + + var issue = fixture.RedmineManager.GetObject(ISSUE_ID, new NameValueCollection + { + { RedmineKeys.INCLUDE, $"{RedmineKeys.CHILDREN},{RedmineKeys.ATTACHMENTS},{RedmineKeys.RELATIONS},{RedmineKeys.CHANGE_SETS},{RedmineKeys.JOURNALS},{RedmineKeys.WATCHERS}" } + }); + + Assert.NotNull(issue); + } + + [Fact, Order(11)] + public void Should_Add_Issue() + { + const bool NEW_ISSUE_IS_PRIVATE = true; + + const int NEW_ISSUE_PROJECT_ID = 9; + const int NEW_ISSUE_TRACKER_ID = 3; + const int NEW_ISSUE_STATUS_ID = 6; + const int NEW_ISSUE_PRIORITY_ID = 9; + const int NEW_ISSUE_CATEGORY_ID = 18; + const int NEW_ISSUE_FIXED_VERSION_ID = 9; + const int NEW_ISSUE_ASSIGNED_TO_ID = 8; + const int NEW_ISSUE_PARENT_ISSUE_ID = 96; + const int NEW_ISSUE_CUSTOM_FIELD_ID = 13; + const int NEW_ISSUE_ESTIMATED_HOURS = 12; + const int NEW_ISSUE_FIRST_WATCHER_ID = 2; + const int NEW_ISSUE_SECOND_WATCHER_ID = 8; + + const string NEW_ISSUE_CUSTOM_FIELD_VALUE = "Issue custom field completed"; + const string NEW_ISSUE_SUBJECT = "Issue created using Rest API"; + const string NEW_ISSUE_DESCRIPTION = "Issue description..."; + + var newIssueStartDate = DateTime.Now; + var newIssueDueDate = DateTime.Now.AddDays(10); + + var icf = (IssueCustomField)IdentifiableName.Create(NEW_ISSUE_CUSTOM_FIELD_ID); + icf.Values = new List { new CustomFieldValue { Info = NEW_ISSUE_CUSTOM_FIELD_VALUE } }; + + var issue = new Issue + { + Project = IdentifiableName.Create(NEW_ISSUE_PROJECT_ID), + Tracker = IdentifiableName.Create(NEW_ISSUE_TRACKER_ID), + Status = IdentifiableName.Create(NEW_ISSUE_STATUS_ID), + Priority = IdentifiableName.Create(NEW_ISSUE_PRIORITY_ID), + Subject = NEW_ISSUE_SUBJECT, + Description = NEW_ISSUE_DESCRIPTION, + Category = IdentifiableName.Create(NEW_ISSUE_CATEGORY_ID), + FixedVersion = IdentifiableName.Create(NEW_ISSUE_FIXED_VERSION_ID), + AssignedTo = IdentifiableName.Create(NEW_ISSUE_ASSIGNED_TO_ID), + ParentIssue = IdentifiableName.Create(NEW_ISSUE_PARENT_ISSUE_ID), + + CustomFields = new List { icf }, + IsPrivate = NEW_ISSUE_IS_PRIVATE, + EstimatedHours = NEW_ISSUE_ESTIMATED_HOURS, + StartDate = newIssueStartDate, + DueDate = newIssueDueDate, + Watchers = new List + { + (Watcher) IdentifiableName.Create(NEW_ISSUE_FIRST_WATCHER_ID), + (Watcher) IdentifiableName.Create(NEW_ISSUE_SECOND_WATCHER_ID) + } + }; + + var savedIssue = fixture.RedmineManager.CreateObject(issue); + + Assert.NotNull(savedIssue); + Assert.True(issue.Subject.Equals(savedIssue.Subject), "Issue subject is invalid."); + Assert.NotEqual(issue, savedIssue); + } + + [Fact, Order(12)] + public void Should_Update_Issue() + { + const string UPDATED_ISSUE_ID = "98"; + const string UPDATED_ISSUE_SUBJECT = "Issue updated subject"; + const string UPDATED_ISSUE_DESCRIPTION = null; + const int UPDATED_ISSUE_PROJECT_ID = 9; + const int UPDATED_ISSUE_TRACKER_ID = 3; + const int UPDATED_ISSUE_PRIORITY_ID = 8; + const int UPDATED_ISSUE_CATEGORY_ID = 18; + const int UPDATED_ISSUE_ASSIGNED_TO_ID = 2; + const int UPDATED_ISSUE_PARENT_ISSUE_ID = 91; + const int UPDATED_ISSUE_CUSTOM_FIELD_ID = 13; + const string UPDATED_ISSUE_CUSTOM_FIELD_VALUE = "Another custom field completed"; + const int UPDATED_ISSUE_ESTIMATED_HOURS = 23; + const string UPDATED_ISSUE_NOTES = "A lot is changed"; + const bool UPDATED_ISSUE_PRIVATE_NOTES = true; + + DateTime? updatedIssueStartDate = default(DateTime?); + + var updatedIssueDueDate = DateTime.Now.AddMonths(1); + + var issue = fixture.RedmineManager.GetObject(UPDATED_ISSUE_ID, new NameValueCollection + { + { RedmineKeys.INCLUDE, $"{RedmineKeys.CHILDREN},{RedmineKeys.ATTACHMENTS},{RedmineKeys.RELATIONS},{RedmineKeys.CHANGE_SETS},{RedmineKeys.JOURNALS},{RedmineKeys.WATCHERS}" } + }); + + issue.Subject = UPDATED_ISSUE_SUBJECT; + issue.Description = UPDATED_ISSUE_DESCRIPTION; + issue.StartDate = updatedIssueStartDate; + issue.DueDate = updatedIssueDueDate; + issue.Project = IdentifiableName.Create(UPDATED_ISSUE_PROJECT_ID); + issue.Tracker = IdentifiableName.Create(UPDATED_ISSUE_TRACKER_ID); + issue.Priority = IdentifiableName.Create(UPDATED_ISSUE_PRIORITY_ID); + issue.Category = IdentifiableName.Create(UPDATED_ISSUE_CATEGORY_ID); + issue.AssignedTo = IdentifiableName.Create(UPDATED_ISSUE_ASSIGNED_TO_ID); + issue.ParentIssue = IdentifiableName.Create(UPDATED_ISSUE_PARENT_ISSUE_ID); + + var icf = (IssueCustomField)IdentifiableName.Create(UPDATED_ISSUE_CUSTOM_FIELD_ID); + icf.Values = new List { new CustomFieldValue { Info = UPDATED_ISSUE_CUSTOM_FIELD_VALUE } }; + + issue.CustomFields?.Add(icf); + issue.EstimatedHours = UPDATED_ISSUE_ESTIMATED_HOURS; + issue.Notes = UPDATED_ISSUE_NOTES; + issue.PrivateNotes = UPDATED_ISSUE_PRIVATE_NOTES; + + fixture.RedmineManager.UpdateObject(UPDATED_ISSUE_ID, issue); + + var updatedIssue = fixture.RedmineManager.GetObject(UPDATED_ISSUE_ID, new NameValueCollection + { + { RedmineKeys.INCLUDE, $"{RedmineKeys.CHILDREN},{RedmineKeys.ATTACHMENTS},{RedmineKeys.RELATIONS},{RedmineKeys.CHANGE_SETS},{RedmineKeys.JOURNALS},{RedmineKeys.WATCHERS}" } + }); + + Assert.NotNull(updatedIssue); + Assert.True(issue.Subject.Equals(updatedIssue.Subject), "Issue subject is invalid."); + + } + + [Fact, Order(99)] + public void Should_Delete_Issue() + { + const string DELETED_ISSUE_ID = "90"; + + var exception = (RedmineException)Record.Exception(() => fixture.RedmineManager.DeleteObject(DELETED_ISSUE_ID)); + Assert.Null(exception); + Assert.Throws(() => fixture.RedmineManager.GetObject(DELETED_ISSUE_ID, null)); + } + + [Fact, Order(13)] + public void Should_Add_Watcher_To_Issue() + { + fixture.RedmineManager.AddWatcherToIssue(WATCHER_ISSUE_ID, WATCHER_USER_ID); + + var issue = fixture.RedmineManager.GetObject(WATCHER_ISSUE_ID.ToString(), new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.WATCHERS } }); + + Assert.NotNull(issue); + Assert.NotNull(issue.Watchers); + Assert.True(((List)issue.Watchers).Find(w => w.Id == WATCHER_USER_ID) != null, "Watcher was not added."); + } + + [Fact, Order(14)] + public void Should_Remove_Watcher_From_Issue() + { + fixture.RedmineManager.RemoveWatcherFromIssue(WATCHER_ISSUE_ID, WATCHER_USER_ID); + + var issue = fixture.RedmineManager.GetObject(WATCHER_ISSUE_ID.ToString(), new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.WATCHERS } }); + + Assert.NotNull(issue); + Assert.True(issue.Watchers == null || ((List)issue.Watchers).Find(w => w.Id == WATCHER_USER_ID) == null, "Watcher was not removed."); + } + + [Fact, Order(15)] + public void Should_Clone_Issue() + { + const string ISSUE_TO_CLONE_SUBJECT = "Issue to clone"; + const int ISSUE_TO_CLONE_CUSTOM_FIELD_ID = 13; + const string ISSUE_TO_CLONE_CUSTOM_FIELD_VALUE = "Issue to clone custom field value"; + const int CLONED_ISSUE_CUSTOM_FIELD_ID = 13; + const string CLONED_ISSUE_CUSTOM_FIELD_VALUE = "Cloned issue custom field value"; + + var icfc = (IssueCustomField)IdentifiableName.Create(ISSUE_TO_CLONE_CUSTOM_FIELD_ID); + icfc.Values = new List { new CustomFieldValue { Info = ISSUE_TO_CLONE_CUSTOM_FIELD_VALUE } }; + + var issueToClone = new Issue + { + Subject = ISSUE_TO_CLONE_SUBJECT, + CustomFields = new List() { icfc } + }; + + var clonedIssue = (Issue)issueToClone.Clone(); + + var icf = (IssueCustomField)IdentifiableName.Create(CLONED_ISSUE_CUSTOM_FIELD_ID); + icf.Values = new List { new CustomFieldValue { Info = CLONED_ISSUE_CUSTOM_FIELD_VALUE } }; + + clonedIssue.CustomFields.Add(icf); - //filters - private const string PROJECT_ID = "redmine-net-testq"; + Assert.True(issueToClone.CustomFields.Count != clonedIssue.CustomFields.Count); + } - //watcher - private const int WATCHER_ISSUE_ID = 96; - private const int WATCHER_USER_ID = 8; - - [Fact, Order(1)] - public void Should_Get_All_Issues() - { - var issues = fixture.RedmineManager.GetObjects(); - - Assert.NotNull(issues); - } - - [Fact, Order(2)] - public void Should_Get_Paginated_Issues() - { - const int NUMBER_OF_PAGINATED_ISSUES = 3; - const int OFFSET = 1; - - var issues = fixture.RedmineManager.GetPaginatedObjects(new NameValueCollection { { RedmineKeys.OFFSET, OFFSET.ToString() }, { RedmineKeys.LIMIT, NUMBER_OF_PAGINATED_ISSUES.ToString() }, { "sort", "id:desc" } }); - - Assert.NotNull(issues.Items); - //Assert.True(issues.Items.Count <= NUMBER_OF_PAGINATED_ISSUES, "number of issues ( "+ issues.Items.Count +" ) != " + NUMBER_OF_PAGINATED_ISSUES.ToString()); - } - - [Fact, Order(3)] - public void Should_Get_Issues_By_Project_Id() - { - var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { RedmineKeys.PROJECT_ID, PROJECT_ID } }); - - Assert.NotNull(issues); - } - - [Fact, Order(4)] - public void Should_Get_Issues_By_subproject_Id() - { - const string SUBPROJECT_ID = "redmine-net-testr"; - - var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { RedmineKeys.SUB_PROJECT_ID, SUBPROJECT_ID } }); - - Assert.NotNull(issues); - } - - [Fact, Order(5)] - public void Should_Get_Issues_By_Project_Without_Subproject() - { - const string ALL_SUBPROJECTS = "!*"; - - var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { RedmineKeys.PROJECT_ID, PROJECT_ID }, { RedmineKeys.SUB_PROJECT_ID, ALL_SUBPROJECTS } }); - - Assert.NotNull(issues); - } - - [Fact, Order(6)] - public void Should_Get_Issues_By_Tracker() - { - const string TRACKER_ID = "3"; - var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { RedmineKeys.TRACKER_ID, TRACKER_ID } }); - - Assert.NotNull(issues); - } - - [Fact, Order(7)] - public void Should_Get_Issues_By_Status() - { - const string STATUS_ID = "*"; - var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { RedmineKeys.STATUS_ID, STATUS_ID } }); - Assert.NotNull(issues); - } - - [Fact, Order(8)] - public void Should_Get_Issues_By_Asignee() - { - const string ASSIGNED_TO_ID = "me"; - var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { RedmineKeys.ASSIGNED_TO_ID, ASSIGNED_TO_ID } }); - - Assert.NotNull(issues); - } - - [Fact, Order(9)] - public void Should_Get_Issues_By_Custom_Field() - { - const string CUSTOM_FIELD_NAME = "cf_13"; - const string CUSTOM_FIELD_VALUE = "Testx"; - - var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { CUSTOM_FIELD_NAME, CUSTOM_FIELD_VALUE } }); - - Assert.NotNull(issues); - } - - [Fact, Order(10)] - public void Should_Get_Issue_By_Id() - { - const string ISSUE_ID = "96"; - - var issue = fixture.RedmineManager.GetObject(ISSUE_ID, new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.CHILDREN + "," + RedmineKeys.ATTACHMENTS + "," + RedmineKeys.RELATIONS + "," + RedmineKeys.CHANGE_SETS + "," + RedmineKeys.JOURNALS + "," + RedmineKeys.WATCHERS } }); - - Assert.NotNull(issue); - //TODO: add conditions for all associated data if nedeed - } - - [Fact, Order(11)] - public void Should_Add_Issue() - { - const int NEW_ISSUE_PROJECT_ID = 9; - const int NEW_ISSUE_TRACKER_ID = 3; - const int NEW_ISSUE_STATUS_ID = 6; - const int NEW_ISSUE_PRIORITY_ID = 9; - const string NEW_ISSUE_SUBJECT = "Issue created using Rest API"; - const string NEW_ISSUE_DESCRIPTION = "Issue description..."; - const int NEW_ISSUE_CATEGORY_ID = 18; - const int NEW_ISSUE_FIXED_VERSION_ID = 9; - const int NEW_ISSUE_ASSIGNED_TO_ID = 8; - const int NEW_ISSUE_PARENT_ISSUE_ID = 96; - const int NEW_ISSUE_CUSTOM_FIELD_ID = 13; - const string NEW_ISSUE_CUSTOM_FIELD_VALUE = "Issue custom field completed"; - const bool NEW_ISSUE_IS_PRIVATE = true; - const int NEW_ISSUE_ESTIMATED_HOURS = 12; - DateTime NEW_ISSUE_START_DATE = DateTime.Now; - DateTime NEW_ISSUE_DUE_DATE = DateTime.Now.AddDays(10); - const int NEW_ISSUE_FIRST_WATCHER_ID = 2; - const int NEW_ISSUE_SECOND_WATCHER_ID = 8; - - Issue issue = new Issue - { - Project = new Project {Id = NEW_ISSUE_PROJECT_ID}, - Tracker = new IdentifiableName {Id = NEW_ISSUE_TRACKER_ID}, - Status = new IdentifiableName {Id = NEW_ISSUE_STATUS_ID}, - Priority = new IdentifiableName {Id = NEW_ISSUE_PRIORITY_ID}, - Subject = NEW_ISSUE_SUBJECT, - Description = NEW_ISSUE_DESCRIPTION, - Category = new IdentifiableName {Id = NEW_ISSUE_CATEGORY_ID}, - FixedVersion = new IdentifiableName {Id = NEW_ISSUE_FIXED_VERSION_ID}, - AssignedTo = new IdentifiableName {Id = NEW_ISSUE_ASSIGNED_TO_ID}, - ParentIssue = new IdentifiableName {Id = NEW_ISSUE_PARENT_ISSUE_ID}, - CustomFields = new List - { - new IssueCustomField - { - Id = NEW_ISSUE_CUSTOM_FIELD_ID, - Values = new List {new CustomFieldValue {Info = NEW_ISSUE_CUSTOM_FIELD_VALUE}} - } - }, - IsPrivate = NEW_ISSUE_IS_PRIVATE, - EstimatedHours = NEW_ISSUE_ESTIMATED_HOURS, - StartDate = NEW_ISSUE_START_DATE, - DueDate = NEW_ISSUE_DUE_DATE, - Watchers = new List - { - new Watcher {Id = NEW_ISSUE_FIRST_WATCHER_ID}, - new Watcher {Id = NEW_ISSUE_SECOND_WATCHER_ID} - } - }; - - Issue savedIssue = fixture.RedmineManager.CreateObject(issue); - - Assert.NotNull(savedIssue); - Assert.True(issue.Subject.Equals(savedIssue.Subject), "Issue subject is invalid."); - Assert.NotEqual(issue, savedIssue); - } - - [Fact, Order(12)] - public void Should_Update_Issue() - { - const string UPDATED_ISSUE_ID = "98"; - const string UPDATED_ISSUE_SUBJECT = "Issue updated subject"; - const string UPDATED_ISSUE_DESCRIPTION = null; - DateTime? UPDATED_ISSUE_START_DATE = null; - DateTime UPDATED_ISSUE_DUE_DATE = DateTime.Now.AddMonths(1); - const int UPDATED_ISSUE_PROJECT_ID = 9; - const int UPDATED_ISSUE_TRACKER_ID = 3; - const int UPDATED_ISSUE_PRIORITY_ID = 8; - const int UPDATED_ISSUE_CATEGORY_ID = 18; - const int UPDATED_ISSUE_ASSIGNED_TO_ID = 2; - const int UPDATED_ISSUE_PARENT_ISSUE_ID = 91; - const int UPDATED_ISSUE_CUSTOM_FIELD_ID = 13; - const string UPDATED_ISSUE_CUSTOM_FIELD_VALUE = "Another custom field completed"; - const int UPDATED_ISSUE_ESTIMATED_HOURS = 23; - const string UPDATED_ISSUE_NOTES = "A lot is changed"; - const bool UPDATED_ISSUE_PRIVATE_NOTES = true; - - var issue = fixture.RedmineManager.GetObject(UPDATED_ISSUE_ID, new NameValueCollection { { "include", "children,attachments,relations,changesets,journals,watchers" } }); - issue.Subject = UPDATED_ISSUE_SUBJECT; - issue.Description = UPDATED_ISSUE_DESCRIPTION; - issue.StartDate = UPDATED_ISSUE_START_DATE; - issue.DueDate = UPDATED_ISSUE_DUE_DATE; - issue.Project.Id = UPDATED_ISSUE_PROJECT_ID; - issue.Tracker.Id = UPDATED_ISSUE_TRACKER_ID; - issue.Priority.Id = UPDATED_ISSUE_PRIORITY_ID; - issue.Category.Id = UPDATED_ISSUE_CATEGORY_ID; - issue.AssignedTo.Id = UPDATED_ISSUE_ASSIGNED_TO_ID; - issue.ParentIssue.Id = UPDATED_ISSUE_PARENT_ISSUE_ID; - - if (issue.CustomFields != null) - issue.CustomFields.Add(new IssueCustomField { Id = UPDATED_ISSUE_CUSTOM_FIELD_ID, Values = new List { new CustomFieldValue { Info = UPDATED_ISSUE_CUSTOM_FIELD_VALUE } } }); - issue.EstimatedHours = UPDATED_ISSUE_ESTIMATED_HOURS; - issue.Notes = UPDATED_ISSUE_NOTES; - issue.PrivateNotes = UPDATED_ISSUE_PRIVATE_NOTES; - - fixture.RedmineManager.UpdateObject(UPDATED_ISSUE_ID, issue); - - var updatedIssue = fixture.RedmineManager.GetObject(UPDATED_ISSUE_ID, new NameValueCollection { { "include", "children,attachments,relations,changesets,journals,watchers" } }); - - Assert.NotNull(updatedIssue); - Assert.True(issue.Subject.Equals(updatedIssue.Subject), "Issue subject is invalid."); - - } - - [Fact, Order(99)] - public void Should_Delete_Issue() - { - const string DELETED_ISSUE_ID = "90"; - - RedmineException exception = (RedmineException)Record.Exception(() => fixture.RedmineManager.DeleteObject(DELETED_ISSUE_ID)); - Assert.Null (exception); - Assert.Throws(() => fixture.RedmineManager.GetObject(DELETED_ISSUE_ID, null)); - } - - [Fact, Order(13)] - public void Should_Add_Watcher_To_Issue() - { - fixture.RedmineManager.AddWatcherToIssue(WATCHER_ISSUE_ID, WATCHER_USER_ID); - - Issue issue = fixture.RedmineManager.GetObject(WATCHER_ISSUE_ID.ToString(), new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.WATCHERS } }); - - Assert.NotNull(issue); - Assert.NotNull(issue.Watchers); - Assert.True(((List)issue.Watchers).Find(w => w.Id == WATCHER_USER_ID) != null, "Watcher was not added."); - } - - [Fact, Order(14)] - public void Should_Remove_Watcher_From_Issue() - { - fixture.RedmineManager.RemoveWatcherFromIssue(WATCHER_ISSUE_ID, WATCHER_USER_ID); - - Issue issue = fixture.RedmineManager.GetObject(WATCHER_ISSUE_ID.ToString(), new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.WATCHERS } }); - - Assert.NotNull(issue); - Assert.True(issue.Watchers == null || ((List)issue.Watchers).Find(w => w.Id == WATCHER_USER_ID) == null, "Watcher was not removed."); - } - - [Fact, Order(15)] - public void Should_Clone_Issue() - { - const string ISSUE_TO_CLONE_SUBJECT = "Issue to clone"; - const int ISSUE_TO_CLONE_CUSTOM_FIELD_ID = 13; - const string ISSUE_TO_CLONE_CUSTOM_FIELD_VALUE = "Issue to clone custom field value"; - const int CLONED_ISSUE_CUSTOM_FIELD_ID = 13; - const string CLONED_ISSUE_CUSTOM_FIELD_VALUE = "Cloned issue custom field value"; - - var issueToClone = new Issue - { - Subject = ISSUE_TO_CLONE_SUBJECT, - CustomFields = new List - { - new IssueCustomField - { - Id = ISSUE_TO_CLONE_CUSTOM_FIELD_ID, - Values = - new List {new CustomFieldValue {Info = ISSUE_TO_CLONE_CUSTOM_FIELD_VALUE}} - } - } - }; - - - var clonedIssue = (Issue)issueToClone.Clone(); - clonedIssue.CustomFields.Add(new IssueCustomField - { - Id = CLONED_ISSUE_CUSTOM_FIELD_ID, - Values = new List { new CustomFieldValue { Info = CLONED_ISSUE_CUSTOM_FIELD_VALUE } } - }); - - Assert.True(issueToClone.CustomFields.Count != clonedIssue.CustomFields.Count); - } - - - [Fact] - public void Should_Get_Issue_With_Hours() - { - const string ISSUE_ID = "1"; - - var issue = fixture.RedmineManager.GetObject(ISSUE_ID, null); - - Assert.Equal(8.0f,issue.EstimatedHours); - Assert.Equal(8.0f,issue.TotalEstimatedHours); - Assert.Equal(5.0f,issue.TotalSpentHours); - Assert.Equal(5.0f,issue.SpentHours); - } - } + [Fact] + public void Should_Get_Issue_With_Hours() + { + const string ISSUE_ID = "1"; + + var issue = fixture.RedmineManager.GetObject(ISSUE_ID, null); + + Assert.Equal(8.0f, issue.EstimatedHours); + Assert.Equal(8.0f, issue.TotalEstimatedHours); + Assert.Equal(5.0f, issue.TotalSpentHours); + Assert.Equal(5.0f, issue.SpentHours); + } + } } \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Tests/Sync/NewsTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/NewsTests.cs index 1bacda60..5cb2bad0 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/NewsTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/NewsTests.cs @@ -15,12 +15,12 @@ limitations under the License. */ using System.Collections.Specialized; -using redmine.net.api.Tests.Infrastructure; +using Padi.RedmineApi.Tests.Infrastructure; using Redmine.Net.Api; using Redmine.Net.Api.Types; using Xunit; -namespace redmine.net.api.Tests.Tests.Sync +namespace Padi.RedmineApi.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "News")] #if !(NET20 || NET40) diff --git a/tests/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs index 898df782..767849f4 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs @@ -16,13 +16,13 @@ limitations under the License. using System.Collections.Generic; using System.Collections.Specialized; -using redmine.net.api.Tests.Infrastructure; +using Padi.RedmineApi.Tests.Infrastructure; using Redmine.Net.Api; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Types; using Xunit; -namespace redmine.net.api.Tests.Tests.Sync +namespace Padi.RedmineApi.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "ProjectMemberships")] #if !(NET20 || NET40) @@ -48,8 +48,8 @@ public void Should_Add_Project_Membership() var pm = new ProjectMembership { - User = new IdentifiableName {Id = NEW_PROJECT_MEMBERSHIP_USER_ID}, - Roles = new List {new MembershipRole {Id = NEW_PROJECT_MEMBERSHIP_ROLE_ID}} + User = IdentifiableName.Create(NEW_PROJECT_MEMBERSHIP_USER_ID), + Roles = new List { (MembershipRole)IdentifiableName.Create(NEW_PROJECT_MEMBERSHIP_ROLE_ID)} }; var createdPm = fixture.RedmineManager.CreateObject(pm, PROJECT_IDENTIFIER); @@ -110,7 +110,7 @@ public void Should_Update_Project_Membership() const int UPDATED_PROJECT_MEMBERSHIP_ROLE_ID = 4; var pm = fixture.RedmineManager.GetObject(UPDATED_PROJECT_MEMBERSHIP_ID, null); - pm.Roles.Add(new MembershipRole {Id = UPDATED_PROJECT_MEMBERSHIP_ROLE_ID}); + pm.Roles.Add((MembershipRole)IdentifiableName.Create(UPDATED_PROJECT_MEMBERSHIP_ROLE_ID)); fixture.RedmineManager.UpdateObject(UPDATED_PROJECT_MEMBERSHIP_ID, pm); diff --git a/tests/redmine-net-api.Tests/Tests/Sync/ProjectTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/ProjectTests.cs index eb8e3458..e1a79c98 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/ProjectTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/ProjectTests.cs @@ -16,13 +16,13 @@ limitations under the License. using System.Collections.Generic; using System.Collections.Specialized; -using redmine.net.api.Tests.Infrastructure; +using Padi.RedmineApi.Tests.Infrastructure; using Redmine.Net.Api; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Types; using Xunit; -namespace redmine.net.api.Tests.Tests.Sync +namespace Padi.RedmineApi.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "Projects")] #if !(NET20 || NET40) @@ -62,7 +62,6 @@ private static Project CreateTestProjectWithAllPropertiesSet() HomePage = "www.redminetest.com", IsPublic = true, InheritMembers = true, - Status = ProjectStatus.Active, EnabledModules = new List { new ProjectEnabledModule {Name = "issue_tracking"}, @@ -70,8 +69,8 @@ private static Project CreateTestProjectWithAllPropertiesSet() }, Trackers = new List { - new ProjectTracker {Id = 1}, - new ProjectTracker {Id = 2} + (ProjectTracker) IdentifiableName.Create( 1), + (ProjectTracker) IdentifiableName.Create(2) } }; @@ -86,8 +85,8 @@ private static Project CreateTestProjectWithInvalidTrackersId() Identifier = "rnaptit", Trackers = new List { - new ProjectTracker {Id = 999999}, - new ProjectTracker {Id = 999998} + (ProjectTracker) IdentifiableName.Create(999999), + (ProjectTracker) IdentifiableName.Create(999998) } }; @@ -100,24 +99,24 @@ private static Project CreateTestProjectWithParentSet(int parentId) { Name = "Redmine Net Api Project With Parent Set", Identifier = "rnapwps", - Parent = new IdentifiableName {Id = parentId} + Parent = IdentifiableName.Create(parentId) }; return project; } - [Fact, Order(0)] - public void Should_Create_Project_With_Required_Properties() - { - var savedProject = fixture.RedmineManager.CreateObject(CreateTestProjectWithRequiredPropertiesSet()); + [Fact, Order(0)] + public void Should_Create_Project_With_Required_Properties() + { + var savedProject = fixture.RedmineManager.CreateObject(CreateTestProjectWithRequiredPropertiesSet()); - Assert.NotNull(savedProject); - Assert.NotEqual(0, savedProject.Id); - Assert.True(savedProject.Name.Equals(PROJECT_NAME), "Project name is invalid."); - Assert.True(savedProject.Identifier.Equals(PROJECT_IDENTIFIER), "Project identifier is invalid."); - } + Assert.NotNull(savedProject); + Assert.NotEqual(0, savedProject.Id); + Assert.True(savedProject.Name.Equals(PROJECT_NAME), "Project name is invalid."); + Assert.True(savedProject.Identifier.Equals(PROJECT_IDENTIFIER), "Project identifier is invalid."); + } - [Fact, Order(1)] + [Fact, Order(1)] public void Should_Create_Project_With_All_Properties_Set() { var savedProject = fixture.RedmineManager.CreateObject(CreateTestProjectWithAllPropertiesSet()); @@ -133,7 +132,7 @@ public void Should_Create_Project_With_All_Properties_Set() public void Should_Create_Project_With_Parent() { var parentProject = - fixture.RedmineManager.CreateObject(new Project {Identifier = "parent-project", Name = "Parent project"}); + fixture.RedmineManager.CreateObject(new Project { Identifier = "parent-project", Name = "Parent project" }); var savedProject = fixture.RedmineManager.CreateObject(CreateTestProjectWithParentSet(parentProject.Id)); @@ -141,90 +140,90 @@ public void Should_Create_Project_With_Parent() Assert.True(savedProject.Parent.Id == parentProject.Id, "Parent project is invalid."); } - [Fact, Order(3)] - public void Should_Get_Redmine_Net_Api_Project_Test_Project() - { - var project = fixture.RedmineManager.GetObject(PROJECT_IDENTIFIER, null); - - Assert.NotNull(project); - Assert.IsType(project); - Assert.Equal(project.Identifier, PROJECT_IDENTIFIER); - Assert.Equal(project.Name, PROJECT_NAME); - } - - [Fact, Order(4)] - public void Should_Get_Test_Project_With_All_Properties_Set() - { - var project = fixture.RedmineManager.GetObject("rnaptap", new NameValueCollection - { - {RedmineKeys.INCLUDE, string.Join(",", RedmineKeys.TRACKERS, RedmineKeys.ENABLED_MODULES)} - }); - - Assert.NotNull(project); - Assert.IsType(project); - Assert.True(project.Name.Equals("Redmine Net Api Project Test All Properties"), "Project name not equal."); - Assert.True(project.Identifier.Equals("rnaptap"), "Project identifier not equal."); - Assert.True(project.Description.Equals("This is a test project."), "Project description not equal."); - Assert.True(project.HomePage.Equals("www.redminetest.com"), "Project homepage not equal."); - Assert.True(project.IsPublic.Equals(true), - "Project is_public not equal. (This property is available starting with 2.6.0)"); - - Assert.NotNull(project.Trackers); - Assert.True(project.Trackers.Count == 2, "Trackers count != " + 2); - - Assert.NotNull(project.EnabledModules); - Assert.True(project.EnabledModules.Count == 2, - "Enabled modules count (" + project.EnabledModules.Count + ") != " + 2); - } - - [Fact, Order(5)] - public void Should_Update_Redmine_Net_Api_Project_Test_Project() - { - const string UPDATED_PROJECT_NAME = "Project created using API updated"; - const string UPDATED_PROJECT_DESCRIPTION = "Test project description updated"; - const string UPDATED_PROJECT_HOMEPAGE = "http://redminetestsupdated.com/"; - const bool UPDATED_PROJECT_ISPUBLIC = true; - const bool UPDATED_PROJECT_INHERIT_MEMBERS = false; - - var project = fixture.RedmineManager.GetObject(PROJECT_IDENTIFIER, null); - project.Name = UPDATED_PROJECT_NAME; - project.Description = UPDATED_PROJECT_DESCRIPTION; - project.HomePage = UPDATED_PROJECT_HOMEPAGE; - project.IsPublic = UPDATED_PROJECT_ISPUBLIC; - project.InheritMembers = UPDATED_PROJECT_INHERIT_MEMBERS; - - var exception = - (RedmineException) - Record.Exception(() => fixture.RedmineManager.UpdateObject(PROJECT_IDENTIFIER, project)); - Assert.Null(exception); - - var updatedProject = fixture.RedmineManager.GetObject(PROJECT_IDENTIFIER, null); - - Assert.True(updatedProject.Name.Equals(UPDATED_PROJECT_NAME), "Project name was not updated."); - Assert.True(updatedProject.Description.Equals(UPDATED_PROJECT_DESCRIPTION), - "Project description was not updated."); - Assert.True(updatedProject.HomePage.Equals(UPDATED_PROJECT_HOMEPAGE), "Project homepage was not updated."); - Assert.True(updatedProject.IsPublic.Equals(UPDATED_PROJECT_ISPUBLIC), - "Project is_public was not updated. (This property is available starting with 2.6.0)"); - } - - [Fact, Order(7)] - public void Should_Throw_Exception_When_Create_Empty_Project() - { - Assert.Throws(() => fixture.RedmineManager.CreateObject(new Project())); - } - - [Fact, Order(8)] - public void Should_Throw_Exception_When_Project_Identifier_Is_Invalid() - { - Assert.Throws(() => fixture.RedmineManager.GetObject("99999999", null)); - } - - [Fact, Order(9)] + [Fact, Order(3)] + public void Should_Get_Redmine_Net_Api_Project_Test_Project() + { + var project = fixture.RedmineManager.GetObject(PROJECT_IDENTIFIER, null); + + Assert.NotNull(project); + Assert.IsType(project); + Assert.Equal(project.Identifier, PROJECT_IDENTIFIER); + Assert.Equal(project.Name, PROJECT_NAME); + } + + [Fact, Order(4)] + public void Should_Get_Test_Project_With_All_Properties_Set() + { + var project = fixture.RedmineManager.GetObject("rnaptap", new NameValueCollection + { + {RedmineKeys.INCLUDE, string.Join(",", RedmineKeys.TRACKERS, RedmineKeys.ENABLED_MODULES)} + }); + + Assert.NotNull(project); + Assert.IsType(project); + Assert.True(project.Name.Equals("Redmine Net Api Project Test All Properties"), "Project name not equal."); + Assert.True(project.Identifier.Equals("rnaptap"), "Project identifier not equal."); + Assert.True(project.Description.Equals("This is a test project."), "Project description not equal."); + Assert.True(project.HomePage.Equals("www.redminetest.com"), "Project homepage not equal."); + Assert.True(project.IsPublic.Equals(true), + "Project is_public not equal. (This property is available starting with 2.6.0)"); + + Assert.NotNull(project.Trackers); + Assert.True(project.Trackers.Count == 2, "Trackers count != " + 2); + + Assert.NotNull(project.EnabledModules); + Assert.True(project.EnabledModules.Count == 2, + "Enabled modules count (" + project.EnabledModules.Count + ") != " + 2); + } + + [Fact, Order(5)] + public void Should_Update_Redmine_Net_Api_Project_Test_Project() + { + const string UPDATED_PROJECT_NAME = "Project created using API updated"; + const string UPDATED_PROJECT_DESCRIPTION = "Test project description updated"; + const string UPDATED_PROJECT_HOMEPAGE = "http://redminetestsupdated.com/"; + const bool UPDATED_PROJECT_ISPUBLIC = true; + const bool UPDATED_PROJECT_INHERIT_MEMBERS = false; + + var project = fixture.RedmineManager.GetObject(PROJECT_IDENTIFIER, null); + project.Name = UPDATED_PROJECT_NAME; + project.Description = UPDATED_PROJECT_DESCRIPTION; + project.HomePage = UPDATED_PROJECT_HOMEPAGE; + project.IsPublic = UPDATED_PROJECT_ISPUBLIC; + project.InheritMembers = UPDATED_PROJECT_INHERIT_MEMBERS; + + var exception = + (RedmineException) + Record.Exception(() => fixture.RedmineManager.UpdateObject(PROJECT_IDENTIFIER, project)); + Assert.Null(exception); + + var updatedProject = fixture.RedmineManager.GetObject(PROJECT_IDENTIFIER, null); + + Assert.True(updatedProject.Name.Equals(UPDATED_PROJECT_NAME), "Project name was not updated."); + Assert.True(updatedProject.Description.Equals(UPDATED_PROJECT_DESCRIPTION), + "Project description was not updated."); + Assert.True(updatedProject.HomePage.Equals(UPDATED_PROJECT_HOMEPAGE), "Project homepage was not updated."); + Assert.True(updatedProject.IsPublic.Equals(UPDATED_PROJECT_ISPUBLIC), + "Project is_public was not updated. (This property is available starting with 2.6.0)"); + } + + [Fact, Order(7)] + public void Should_Throw_Exception_When_Create_Empty_Project() + { + Assert.Throws(() => fixture.RedmineManager.CreateObject(new Project())); + } + + [Fact, Order(8)] + public void Should_Throw_Exception_When_Project_Identifier_Is_Invalid() + { + Assert.Throws(() => fixture.RedmineManager.GetObject("99999999", null)); + } + + [Fact, Order(9)] public void Should_Delete_Project_And_Parent_Project() { var exception = - (RedmineException) Record.Exception(() => fixture.RedmineManager.DeleteObject("rnapwps")); + (RedmineException)Record.Exception(() => fixture.RedmineManager.DeleteObject("rnapwps")); Assert.Null(exception); Assert.Throws(() => fixture.RedmineManager.GetObject("rnapwps", null)); @@ -239,7 +238,7 @@ public void Should_Delete_Project_And_Parent_Project() public void Should_Delete_Project_With_All_Properties_Set() { var exception = - (RedmineException) Record.Exception(() => fixture.RedmineManager.DeleteObject("rnaptap")); + (RedmineException)Record.Exception(() => fixture.RedmineManager.DeleteObject("rnaptap")); Assert.Null(exception); Assert.Throws(() => fixture.RedmineManager.GetObject("rnaptap", null)); } diff --git a/tests/redmine-net-api.Tests/Tests/Sync/QueryTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/QueryTests.cs index 8297d5bb..3a472009 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/QueryTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/QueryTests.cs @@ -14,11 +14,11 @@ You may obtain a copy of the License at limitations under the License. */ -using redmine.net.api.Tests.Infrastructure; +using Padi.RedmineApi.Tests.Infrastructure; using Redmine.Net.Api.Types; using Xunit; -namespace redmine.net.api.Tests.Tests.Sync +namespace Padi.RedmineApi.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "Queries")] #if !(NET20 || NET40) diff --git a/tests/redmine-net-api.Tests/Tests/Sync/RoleTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/RoleTests.cs index 6495b953..7ef31761 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/RoleTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/RoleTests.cs @@ -14,11 +14,11 @@ You may obtain a copy of the License at limitations under the License. */ -using redmine.net.api.Tests.Infrastructure; +using Padi.RedmineApi.Tests.Infrastructure; using Redmine.Net.Api.Types; using Xunit; -namespace redmine.net.api.Tests.Tests.Sync +namespace Padi.RedmineApi.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "Roles")] #if !(NET20 || NET40) diff --git a/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryActivtiyTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryActivtiyTests.cs index 1e25dda4..451309bc 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryActivtiyTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryActivtiyTests.cs @@ -14,11 +14,11 @@ You may obtain a copy of the License at limitations under the License. */ -using redmine.net.api.Tests.Infrastructure; +using Padi.RedmineApi.Tests.Infrastructure; using Redmine.Net.Api.Types; using Xunit; -namespace redmine.net.api.Tests.Tests.Sync +namespace Padi.RedmineApi.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "TimeEntryActivities")] #if !(NET20 || NET40) diff --git a/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs index f798c7b4..8605e5c7 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs @@ -15,12 +15,12 @@ limitations under the License. */ using System; -using redmine.net.api.Tests.Infrastructure; +using Padi.RedmineApi.Tests.Infrastructure; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Types; using Xunit; -namespace redmine.net.api.Tests.Tests.Sync +namespace Padi.RedmineApi.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "TimeEntries")] #if !(NET20 || NET40) @@ -40,18 +40,18 @@ public void Should_Create_Time_Entry() { const int NEW_TIME_ENTRY_ISSUE_ID = 18; const int NEW_TIME_ENTRY_PROJECT_ID = 9; - DateTime NEW_TIME_ENTRY_DATE = DateTime.Now; + var newTimeEntryDate = DateTime.Now; const int NEW_TIME_ENTRY_HOURS = 1; const int NEW_TIME_ENTRY_ACTIVITY_ID = 16; const string NEW_TIME_ENTRY_COMMENTS = "Added time entry on project"; var timeEntry = new TimeEntry { - Issue = new IdentifiableName {Id = NEW_TIME_ENTRY_ISSUE_ID}, - Project = new IdentifiableName {Id = NEW_TIME_ENTRY_PROJECT_ID}, - SpentOn = NEW_TIME_ENTRY_DATE, + Issue = IdentifiableName.Create(NEW_TIME_ENTRY_ISSUE_ID), + Project = IdentifiableName.Create(NEW_TIME_ENTRY_PROJECT_ID), + SpentOn = newTimeEntryDate, Hours = NEW_TIME_ENTRY_HOURS, - Activity = new IdentifiableName {Id = NEW_TIME_ENTRY_ACTIVITY_ID}, + Activity = IdentifiableName.Create(NEW_TIME_ENTRY_ACTIVITY_ID), Comments = NEW_TIME_ENTRY_COMMENTS }; @@ -63,7 +63,7 @@ public void Should_Create_Time_Entry() Assert.NotNull(savedTimeEntry.Project); Assert.True(savedTimeEntry.Project.Id == NEW_TIME_ENTRY_PROJECT_ID, "Project id is invalid."); Assert.NotNull(savedTimeEntry.SpentOn); - Assert.True(DateTime.Compare(savedTimeEntry.SpentOn.Value.Date, NEW_TIME_ENTRY_DATE.Date) == 0, + Assert.True(DateTime.Compare(savedTimeEntry.SpentOn.Value.Date, newTimeEntryDate.Date) == 0, "Date is invalid."); Assert.True(savedTimeEntry.Hours == NEW_TIME_ENTRY_HOURS, "Hours value is not valid."); Assert.NotNull(savedTimeEntry.Activity); @@ -116,17 +116,16 @@ public void Should_Update_Time_Entry() const int UPDATED_TIME_ENTRY_HOURS = 3; const int UPDATED_TIME_ENTRY_ACTIVITY_ID = 17; const string UPDATED_TIME_ENTRY_COMMENTS = "Time entry updated"; - DateTime UPDATED_TIME_ENTRY_DATE = DateTime.Now.AddDays(-2); + var updatedTimeEntryDate = DateTime.Now.AddDays(-2); var timeEntry = fixture.RedmineManager.GetObject(UPDATED_TIME_ENTRY_ID, null); - timeEntry.Project.Id = UPDATED_TIME_ENTRY_PROJECT_ID; - timeEntry.Issue.Id = UPDATED_TIME_ENTRY_ISSUE_ID; - timeEntry.SpentOn = UPDATED_TIME_ENTRY_DATE; + timeEntry.Project = IdentifiableName.Create(UPDATED_TIME_ENTRY_PROJECT_ID); + timeEntry.Issue = IdentifiableName.Create(UPDATED_TIME_ENTRY_ISSUE_ID); + timeEntry.SpentOn = updatedTimeEntryDate; timeEntry.Hours = UPDATED_TIME_ENTRY_HOURS; timeEntry.Comments = UPDATED_TIME_ENTRY_COMMENTS; - if (timeEntry.Activity == null) timeEntry.Activity = new IdentifiableName(); - timeEntry.Activity.Id = UPDATED_TIME_ENTRY_ACTIVITY_ID; + if (timeEntry.Activity == null) timeEntry.Activity = IdentifiableName.Create(UPDATED_TIME_ENTRY_ACTIVITY_ID); fixture.RedmineManager.UpdateObject(UPDATED_TIME_ENTRY_ID, timeEntry); diff --git a/tests/redmine-net-api.Tests/Tests/Sync/TrackerTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/TrackerTests.cs index d941c902..847c7a4d 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/TrackerTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/TrackerTests.cs @@ -17,7 +17,7 @@ limitations under the License. using Redmine.Net.Api.Types; using Xunit; -namespace redmine.net.api.Tests.Tests.Sync +namespace Padi.RedmineApi.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "Trackers")] #if !(NET20 || NET40) diff --git a/tests/redmine-net-api.Tests/Tests/Sync/UserTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/UserTests.cs index 59fab743..569405a8 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/UserTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/UserTests.cs @@ -16,13 +16,13 @@ limitations under the License. using System.Collections.Specialized; using System.Globalization; -using redmine.net.api.Tests.Infrastructure; +using Padi.RedmineApi.Tests.Infrastructure; using Redmine.Net.Api; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Types; using Xunit; -namespace redmine.net.api.Tests.Tests.Sync +namespace Padi.RedmineApi.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "Users")] #if !(NET20 || NET40) @@ -43,8 +43,8 @@ public UserTests(RedmineFixture fixture) private const string USER_LAST_NAME = "One"; private const string USER_EMAIL = "testUser@mail.com"; - private static string CREATED_USER_ID; - private static string CREATED_USER_WITH_ALL_PROP_ID; + private static string createdUserId; + private static string createdUserWithAllPropId; private static User CreateTestUserWithRequiredPropertiesSet() { @@ -67,7 +67,7 @@ public void Should_Create_User_With_Required_Properties() Assert.NotNull(savedUser); Assert.NotEqual(0, savedUser.Id); - CREATED_USER_ID = savedUser.Id.ToString(); + createdUserId = savedUser.Id.ToString(); Assert.True(savedUser.Login.Equals(USER_LOGIN), "User login is invalid."); Assert.True(savedUser.FirstName.Equals(USER_FIRST_NAME), "User first name is invalid."); @@ -105,7 +105,7 @@ public void Should_Create_User_With_All_Properties_Set() Assert.NotNull(savedUser); Assert.NotEqual(0, savedUser.Id); - CREATED_USER_WITH_ALL_PROP_ID = savedUser.Id.ToString(); + createdUserWithAllPropId = savedUser.Id.ToString(); Assert.True(savedUser.Login.Equals(login), "User login is invalid."); Assert.True(savedUser.FirstName.Equals(firstName), "User first name is invalid."); @@ -116,7 +116,7 @@ public void Should_Create_User_With_All_Properties_Set() [Fact, Order(4)] public void Should_Get_Created_User_With_Required_Fields() { - var user = fixture.RedmineManager.GetObject(CREATED_USER_ID, null); + var user = fixture.RedmineManager.GetObject(createdUserId, null); Assert.NotNull(user); Assert.IsType(user); @@ -155,10 +155,10 @@ public void Should_Update_User() [Fact, Order(6)] public void Should_Not_Update_User_With_Invalid_Properties() { - var user = fixture.RedmineManager.GetObject(CREATED_USER_ID, null); + var user = fixture.RedmineManager.GetObject(createdUserId, null); user.FirstName = ""; - Assert.Throws(() => fixture.RedmineManager.UpdateObject(CREATED_USER_ID, user)); + Assert.Throws(() => fixture.RedmineManager.UpdateObject(createdUserId, user)); } [Fact, Order(7)] @@ -166,9 +166,9 @@ public void Should_Delete_User() { var exception = (RedmineException) - Record.Exception(() => fixture.RedmineManager.DeleteObject(CREATED_USER_ID)); + Record.Exception(() => fixture.RedmineManager.DeleteObject(createdUserId)); Assert.Null(exception); - Assert.Throws(() => fixture.RedmineManager.GetObject(CREATED_USER_ID, null)); + Assert.Throws(() => fixture.RedmineManager.GetObject(createdUserId, null)); } @@ -177,19 +177,19 @@ public void Should_Delete_User_Created_With_All_Properties_Set() { var exception = (RedmineException) - Record.Exception(() => fixture.RedmineManager.DeleteObject(CREATED_USER_WITH_ALL_PROP_ID)); + Record.Exception(() => fixture.RedmineManager.DeleteObject(createdUserWithAllPropId)); Assert.Null(exception); - Assert.Throws(() => fixture.RedmineManager.GetObject(CREATED_USER_WITH_ALL_PROP_ID, null)); + Assert.Throws(() => fixture.RedmineManager.GetObject(createdUserWithAllPropId, null)); } [Fact, Order(9)] public void Should_Get_Current_User() { - User currentUser = fixture.RedmineManager.GetCurrentUser(); + var currentUser = fixture.RedmineManager.GetCurrentUser(); Assert.NotNull(currentUser); - Assert.Equal(currentUser.ApiKey, Helper.ApiKey); + Assert.Equal(currentUser.ApiKey, fixture.Credentials.ApiKey); } [Fact, Order(10)] @@ -210,7 +210,7 @@ public void Should_Get_Users_By_State() { var users = fixture.RedmineManager.GetObjects(new NameValueCollection() { - {RedmineKeys.STATUS, ((int) UserStatus.STATUS_ACTIVE).ToString(CultureInfo.InvariantCulture)} + {RedmineKeys.STATUS, ((int) UserStatus.StatusActive).ToString(CultureInfo.InvariantCulture)} }); Assert.NotNull(users); diff --git a/tests/redmine-net-api.Tests/Tests/Sync/VersionTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/VersionTests.cs index 5f9660e6..34eda71b 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/VersionTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/VersionTests.cs @@ -16,14 +16,14 @@ limitations under the License. using System; using System.Collections.Specialized; +using Padi.RedmineApi.Tests.Infrastructure; using Redmine.Net.Api; using Redmine.Net.Api.Exceptions; -using redmine.net.api.Tests.Infrastructure; using Redmine.Net.Api.Types; using Xunit; using Version = Redmine.Net.Api.Types.Version; -namespace redmine.net.api.Tests.Tests.Sync +namespace Padi.RedmineApi.Tests.Tests.Sync { [Trait("Redmine-Net-Api", "Versions")] #if !(NET20 || NET40) @@ -47,7 +47,7 @@ public void Should_Create_Version() const string NEW_VERSION_NAME = "VersionTesting"; const VersionStatus NEW_VERSION_STATUS = VersionStatus.Locked; const VersionSharing NEW_VERSION_SHARING = VersionSharing.Hierarchy; - DateTime NEW_VERSION_DUE_DATE = DateTime.Now.AddDays(7); + DateTime newVersionDueDate = DateTime.Now.AddDays(7); const string NEW_VERSION_DESCRIPTION = "Version description"; var version = new Version @@ -55,7 +55,7 @@ public void Should_Create_Version() Name = NEW_VERSION_NAME, Status = NEW_VERSION_STATUS, Sharing = NEW_VERSION_SHARING, - DueDate = NEW_VERSION_DUE_DATE, + DueDate = newVersionDueDate, Description = NEW_VERSION_DESCRIPTION }; @@ -67,7 +67,7 @@ public void Should_Create_Version() Assert.True(savedVersion.Status.Equals(NEW_VERSION_STATUS), "Version status is invalid."); Assert.True(savedVersion.Sharing.Equals(NEW_VERSION_SHARING), "Version sharing is invalid."); Assert.NotNull(savedVersion.DueDate); - Assert.True(savedVersion.DueDate.Value.Date.Equals(NEW_VERSION_DUE_DATE.Date), "Version due date is invalid."); + Assert.True(savedVersion.DueDate.Value.Date.Equals(newVersionDueDate.Date), "Version due date is invalid."); Assert.True(savedVersion.Description.Equals(NEW_VERSION_DESCRIPTION), "Version description is invalid."); } @@ -119,13 +119,13 @@ public void Should_Update_Version() const VersionSharing UPDATED_VERSION_SHARING = VersionSharing.System; const string UPDATED_VERSION_DESCRIPTION = "Updated description"; - DateTime UPDATED_VERSION_DUE_DATE = DateTime.Now.AddMonths(1); + DateTime updatedVersionDueDate = DateTime.Now.AddMonths(1); var version = fixture.RedmineManager.GetObject(UPDATED_VERSION_ID, null); version.Name = UPDATED_VERSION_NAME; version.Status = UPDATED_VERSION_STATUS; version.Sharing = UPDATED_VERSION_SHARING; - version.DueDate = UPDATED_VERSION_DUE_DATE; + version.DueDate = updatedVersionDueDate; version.Description = UPDATED_VERSION_DESCRIPTION; fixture.RedmineManager.UpdateObject(UPDATED_VERSION_ID, version); diff --git a/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs index 809c4e03..43fc92b3 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs @@ -14,18 +14,17 @@ You may obtain a copy of the License at limitations under the License. */ -using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; -using redmine.net.api.Tests.Infrastructure; +using Padi.RedmineApi.Tests.Infrastructure; using Redmine.Net.Api; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Types; using Xunit; -namespace redmine.net.api.Tests.Tests.Sync +namespace Padi.RedmineApi.Tests.Tests.Sync { - [Trait("Redmine-Net-Api", "WikiPages")] + [Trait("Redmine-Net-Api", "WikiPages")] #if !(NET20 || NET40) [Collection("RedmineCollection")] #endif @@ -36,19 +35,19 @@ public WikiPageTests(RedmineFixture fixture) this.fixture = fixture; } - private readonly RedmineFixture fixture; + private readonly RedmineFixture fixture; - private const string PROJECT_ID = "redmine-net-api"; + private const string PROJECT_ID = "redmine-net-api"; private const string WIKI_PAGE_NAME = "Wiki"; [Fact, Order(1)] public void Should_Add_Or_Update_WikiPage() { - const string WIKI_PAGE_UPDATED_TEXT = "Updated again and again wiki page"; - const string WIKI_PAGE_COMMENT = "I did it through code"; + const string WIKI_PAGE_UPDATED_TEXT = "Updated again and again wiki page"; + const string WIKI_PAGE_COMMENT = "I did it through code"; - var page = fixture.RedmineManager.CreateOrUpdateWikiPage(PROJECT_ID, WIKI_PAGE_NAME, - new WikiPage {Text = WIKI_PAGE_UPDATED_TEXT, Comments = WIKI_PAGE_COMMENT}); + var page = fixture.RedmineManager.CreateOrUpdateWikiPage(PROJECT_ID, WIKI_PAGE_NAME, + new WikiPage { Text = WIKI_PAGE_UPDATED_TEXT, Comments = WIKI_PAGE_COMMENT }); Assert.NotNull(page); Assert.True(page.Title.Equals(WIKI_PAGE_NAME), "Wiki page name is invalid."); @@ -66,22 +65,21 @@ public void Should_Delete_Wiki_Page() [Fact, Order(2)] public void Should_Get_All_Wiki_Pages_By_Project_Id() { - const int NUMBER_OF_WIKI_PAGES = 2; + const int NUMBER_OF_WIKI_PAGES = 2; - var pages = (List) fixture.RedmineManager.GetAllWikiPages(PROJECT_ID); + var pages = fixture.RedmineManager.GetAllWikiPages(PROJECT_ID); Assert.NotNull(pages); Assert.True(pages.Count == NUMBER_OF_WIKI_PAGES, "Wiki pages count != " + NUMBER_OF_WIKI_PAGES); - Assert.True(pages.Exists(p => p.Title == WIKI_PAGE_NAME), - string.Format("Wiki page {0} does not exist", WIKI_PAGE_NAME)); + Assert.True(pages.Exists(p => p.Title == WIKI_PAGE_NAME), $"Wiki page {WIKI_PAGE_NAME} does not exist"); } [Fact, Order(3)] public void Should_Get_Wiki_Page_By_Title() { - const string WIKI_PAGE_TITLE = "Wiki2"; + const string WIKI_PAGE_TITLE = "Wiki2"; - var page = fixture.RedmineManager.GetWikiPage(PROJECT_ID, null, WIKI_PAGE_TITLE); + var page = fixture.RedmineManager.GetWikiPage(PROJECT_ID, null, WIKI_PAGE_TITLE); Assert.NotNull(page); Assert.True(page.Title.Equals(WIKI_PAGE_TITLE), "Wiki page title is invalid."); @@ -91,7 +89,7 @@ public void Should_Get_Wiki_Page_By_Title() public void Should_Get_Wiki_Page_By_Title_With_Attachments() { var page = fixture.RedmineManager.GetWikiPage(PROJECT_ID, - new NameValueCollection {{RedmineKeys.INCLUDE, RedmineKeys.ATTACHMENTS}}, WIKI_PAGE_NAME); + new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.ATTACHMENTS } }, WIKI_PAGE_NAME); Assert.NotNull(page); Assert.Equal(page.Title, WIKI_PAGE_NAME); @@ -101,8 +99,8 @@ public void Should_Get_Wiki_Page_By_Title_With_Attachments() [Fact, Order(5)] public void Should_Get_Wiki_Page_By_Version() { - const int WIKI_PAGE_VERSION = 1; - var oldPage = fixture.RedmineManager.GetWikiPage(PROJECT_ID, null, WIKI_PAGE_NAME, WIKI_PAGE_VERSION); + const int WIKI_PAGE_VERSION = 1; + var oldPage = fixture.RedmineManager.GetWikiPage(PROJECT_ID, null, WIKI_PAGE_NAME, WIKI_PAGE_VERSION); Assert.NotNull(oldPage); Assert.Equal(oldPage.Title, WIKI_PAGE_NAME); @@ -112,19 +110,16 @@ public void Should_Get_Wiki_Page_By_Version() [Fact] public void Should_Create_Wiki() { - var author = new IdentifiableName(); - author.Id = 1; - - var result = fixture.RedmineManager.CreateOrUpdateWikiPage("1","pagina2",new WikiPage - { - Text = "ana are mere multe si rosii!", - Comments = "asa", - Version = 1 + var result = fixture.RedmineManager.CreateOrUpdateWikiPage("1", "pagina2", new WikiPage + { + Text = "ana are mere multe si rosii!", + Comments = "asa", + Version = 1 }); - - Assert.NotNull(result); - + + Assert.NotNull(result); + } - + } } \ No newline at end of file From c8de7c1db8b54c20bb73f6b38ed5ee014eebb106 Mon Sep 17 00:00:00 2001 From: Zapadi Date: Fri, 17 Apr 2020 20:56:15 +0300 Subject: [PATCH 135/549] Add .editorconfig (Test project) --- tests/redmine-net-api.Tests/.editorconfig | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 tests/redmine-net-api.Tests/.editorconfig diff --git a/tests/redmine-net-api.Tests/.editorconfig b/tests/redmine-net-api.Tests/.editorconfig new file mode 100644 index 00000000..e45eade4 --- /dev/null +++ b/tests/redmine-net-api.Tests/.editorconfig @@ -0,0 +1,10 @@ +# To learn more about .editorconfig see https://aka.ms/editorconfigdocs +root = true + +# All files +[*] +indent_style = space + +# Xml files +[*.xml] +indent_size = 2 From dad5819c13cc5f517e1e655052c84fe2c1333ba7 Mon Sep 17 00:00:00 2001 From: Zapadi Date: Fri, 17 Apr 2020 20:57:13 +0300 Subject: [PATCH 136/549] Add usersecrets & fix redmine fixture --- tests/redmine-net-api.Tests/Helper.cs | 27 --- .../Properties/launchSettings.json | 11 ++ .../RedmineCredentials.cs | 10 + tests/redmine-net-api.Tests/RedmineFixture.cs | 27 +-- tests/redmine-net-api.Tests/TestHelper.cs | 40 ++++ .../Tests/RedmineTest.cs | 29 +-- tests/redmine-net-api.Tests/appsettings.json | 8 + .../redmine-net-api.Tests.csproj | 173 +++++++++++++++++- 8 files changed, 267 insertions(+), 58 deletions(-) delete mode 100644 tests/redmine-net-api.Tests/Helper.cs create mode 100644 tests/redmine-net-api.Tests/Properties/launchSettings.json create mode 100644 tests/redmine-net-api.Tests/RedmineCredentials.cs create mode 100644 tests/redmine-net-api.Tests/TestHelper.cs create mode 100644 tests/redmine-net-api.Tests/appsettings.json diff --git a/tests/redmine-net-api.Tests/Helper.cs b/tests/redmine-net-api.Tests/Helper.cs deleted file mode 100644 index b54b7cd3..00000000 --- a/tests/redmine-net-api.Tests/Helper.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Configuration; - -namespace redmine.net.api.Tests -{ - internal static class Helper - { - public static string Uri { get; private set; } - - public static string ApiKey { get; private set; } - - public static string Username { get; private set; } - - public static string Password { get; private set; } - - static Helper() - { - Uri = "http://192.168.1.53:8089/"; - - ApiKey = "a96e35d02bc6a6dbe655b83a2f6db57b82df2dff"; - - - Username = "zapadi"; - Password = "1qaz2wsx"; - } - } -} - diff --git a/tests/redmine-net-api.Tests/Properties/launchSettings.json b/tests/redmine-net-api.Tests/Properties/launchSettings.json new file mode 100644 index 00000000..18db8cac --- /dev/null +++ b/tests/redmine-net-api.Tests/Properties/launchSettings.json @@ -0,0 +1,11 @@ +{ + "profiles": { + "redmine-net-api.Tests": { + "commandName": "Project", + "environmentVariables": { + "BitVault410": "bitVault410", + "Local410": "local410" + } + } + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/RedmineCredentials.cs b/tests/redmine-net-api.Tests/RedmineCredentials.cs new file mode 100644 index 00000000..380def2b --- /dev/null +++ b/tests/redmine-net-api.Tests/RedmineCredentials.cs @@ -0,0 +1,10 @@ +namespace Padi.RedmineApi.Tests +{ + public sealed class RedmineCredentials + { + public string Uri { get; set; } + public string ApiKey { get; set; } + public string Username { get; set; } + public string Password { get; set; } + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/RedmineFixture.cs b/tests/redmine-net-api.Tests/RedmineFixture.cs index 7c8a5a39..89988e18 100644 --- a/tests/redmine-net-api.Tests/RedmineFixture.cs +++ b/tests/redmine-net-api.Tests/RedmineFixture.cs @@ -1,29 +1,30 @@ - -using System.Diagnostics; +using System.Diagnostics; using Redmine.Net.Api; -namespace redmine.net.api.Tests +namespace Padi.RedmineApi.Tests { public class RedmineFixture - { + { + public RedmineCredentials Credentials { get; private set; } public RedmineManager RedmineManager { get; set; } public RedmineFixture () - { - SetMimeTypeXML(); - SetMimeTypeJSON(); + { + Credentials = TestHelper.GetApplicationConfiguration(); + SetMimeTypeXml(); + SetMimeTypeJson(); } - [Conditional("JSON")] - private void SetMimeTypeJSON() + [Conditional("DEBUG_JSON")] + private void SetMimeTypeJson() { - RedmineManager = new RedmineManager(Helper.Uri, Helper.ApiKey, MimeFormat.Json); + RedmineManager = new RedmineManager(Credentials.Uri, Credentials.ApiKey, MimeFormat.Json); } - [Conditional("XML")] - private void SetMimeTypeXML() + [Conditional("DEBUG_XML")] + private void SetMimeTypeXml() { - RedmineManager = new RedmineManager(Helper.Uri, Helper.ApiKey); + RedmineManager = new RedmineManager(Credentials.Uri, Credentials.ApiKey); } } } \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/TestHelper.cs b/tests/redmine-net-api.Tests/TestHelper.cs new file mode 100644 index 00000000..377f9ab6 --- /dev/null +++ b/tests/redmine-net-api.Tests/TestHelper.cs @@ -0,0 +1,40 @@ +using System; +using System.IO; +using Microsoft.Extensions.Configuration; + +namespace Padi.RedmineApi.Tests +{ + internal static class TestHelper + { + public static IConfigurationRoot GetIConfigurationRoot(string outputPath) + { + var environment = Environment.GetEnvironmentVariable("Environment"); + + return new ConfigurationBuilder() + .SetBasePath(outputPath) + .AddJsonFile("appsettings.json", optional: true) + .AddJsonFile($"appsettings.{environment}.json", optional: true) + .AddUserSecrets("f8b9e946-b547-42f1-861c-f719dca00a84") + .AddEnvironmentVariables() + .Build(); + } + + public static RedmineCredentials GetApplicationConfiguration(string outputPath = "") + { + if (string.IsNullOrWhiteSpace(outputPath)) + { + outputPath = Directory.GetCurrentDirectory(); + } + + var credentials = new RedmineCredentials(); + + var iConfig = GetIConfigurationRoot(outputPath); + + iConfig + .GetSection("Credentials") + .Bind(credentials); + + return credentials; + } + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Tests/RedmineTest.cs b/tests/redmine-net-api.Tests/Tests/RedmineTest.cs index 98f4acff..83157397 100644 --- a/tests/redmine-net-api.Tests/Tests/RedmineTest.cs +++ b/tests/redmine-net-api.Tests/Tests/RedmineTest.cs @@ -1,54 +1,59 @@ - -using System; -using redmine.net.api.Tests.Infrastructure; +using Padi.RedmineApi.Tests.Infrastructure; using Redmine.Net.Api; using Redmine.Net.Api.Exceptions; using Xunit; -namespace redmine.net.api.Tests.Tests +namespace Padi.RedmineApi.Tests.Tests { [Trait("Redmine-api", "Credentials")] #if !(NET20 || NET40) [Collection("RedmineCollection")] #endif [Order(1)] - public class RedmineTest + public sealed class RedmineTest { + private static readonly RedmineCredentials Credentials; + + + static RedmineTest() + { + Credentials = TestHelper.GetApplicationConfiguration(); + } [Fact] public void Should_Throw_Redmine_Exception_When_Host_Is_Null() { - Assert.Throws(() => new RedmineManager(null, Helper.Username, Helper.Password)); + Assert.Throws(() => new RedmineManager(null, Credentials.Username, Credentials.Password)); } [Fact] public void Should_Throw_Redmine_Exception_When_Host_Is_Empty() { - Assert.Throws(() => new RedmineManager(string.Empty, Helper.Username, Helper.Password)); + Assert.Throws(() => new RedmineManager(string.Empty, Credentials.Username, Credentials.Password)); } [Fact] public void Should_Throw_Redmine_Exception_When_Host_Is_Invalid() { - Assert.Throws(() => new RedmineManager("invalid<>", Helper.Username, Helper.Password)); + Assert.Throws(() => new RedmineManager("invalid<>", Credentials.Username, Credentials.Password)); } [Fact] public void Should_Connect_With_Username_And_Password() { - var a = new RedmineManager(Helper.Uri, Helper.Username, Helper.Password); + var a = new RedmineManager(Credentials.Uri, Credentials.Username, Credentials.Password); var currentUser = a.GetCurrentUser(); Assert.NotNull(currentUser); - Assert.True(currentUser.Login.Equals(Helper.Username), "usernames not equals."); + Assert.True(currentUser.Login.Equals(Credentials.Username), "usernames not equals."); } [Fact] public void Should_Connect_With_Api_Key() { - var a = new RedmineManager(Helper.Uri, Helper.ApiKey); + var a = new RedmineManager(Credentials.Uri, Credentials.ApiKey); var currentUser = a.GetCurrentUser(); Assert.NotNull(currentUser); - Assert.True(currentUser.ApiKey.Equals(Helper.ApiKey),"api keys not equals."); + Assert.True(currentUser.ApiKey.Equals(Credentials.ApiKey),"api keys not equals."); } } } diff --git a/tests/redmine-net-api.Tests/appsettings.json b/tests/redmine-net-api.Tests/appsettings.json new file mode 100644 index 00000000..9b28a4ca --- /dev/null +++ b/tests/redmine-net-api.Tests/appsettings.json @@ -0,0 +1,8 @@ +{ + "Credentials": { + "Uri": "$Uri", + "ApiKey": "$ApiKey", + "Username": "$Username", + "Password": "$Password" + } +} diff --git a/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj index e809acc2..70a6b1d5 100644 --- a/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj +++ b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj @@ -3,13 +3,14 @@ - false + false net48 - net45;net451;net452;net46;net461;net462;net47;net471;net472;net48; + net451;net452;net46;net461;net462;net47;net471;net472;net48; false - redmine.net.api.Tests - redmine-net-api.Tests + Padi.RedmineApi.Tests + Padi.RedmineApi.Tests f8b9e946-b547-42f1-861c-f719dca00a84 + Release;Debug;DebugJson @@ -26,11 +27,11 @@ - NET45;NETFULL + DEBUG;NET45;NETFULL; - NET451;NETFULL + DEBUG;NET451;NETFULL;DEBUG_JSON @@ -66,6 +67,42 @@ NET48;NETFULL + + false + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE;DEBUG_XML + prompt + 4 + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE;DEBUG_JSON + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + @@ -73,6 +110,16 @@ + + + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -105,5 +152,119 @@ + + + PreserveNewest + + + + + + 1.1.0 + + + 1.1.0 + + + 1.1.0 + + + + + + 1.1.0 + + + 1.1.0 + + + 1.1.0 + + + + + + 1.1.0 + + + 1.1.0 + + + 1.1.0 + + + + + + 1.1.0 + + + 1.1.0 + + + 1.1.0 + + + + + + 1.1.0 + + + 1.1.0 + + + 1.1.0 + + + + + + 1.1.0 + + + 1.1.0 + + + 1.1.0 + + + + + + 1.1.0 + + + 1.1.0 + + + 1.1.0 + + + + + + 1.1.0 + + + 1.1.0 + + + 1.1.0 + + + + + + 1.1.0 + + + 1.1.0 + + + 1.1.0 + + + \ No newline at end of file From 045d62f63859fbdcbb0db9ed702eb6fb32f2b4e5 Mon Sep 17 00:00:00 2001 From: Zapadi Date: Sat, 18 Apr 2020 13:57:03 +0300 Subject: [PATCH 137/549] Remove unnecessary try/catch --- .../Internals/WebApiAsyncHelper.cs | 78 +++--------- src/redmine-net-api/Internals/WebApiHelper.cs | 119 +++++++++--------- src/redmine-net-api/RedirectType.cs | 21 ++++ src/redmine-net-api/RedmineWebClient.cs | 40 ++---- 4 files changed, 105 insertions(+), 153 deletions(-) create mode 100644 src/redmine-net-api/RedirectType.cs diff --git a/src/redmine-net-api/Internals/WebApiAsyncHelper.cs b/src/redmine-net-api/Internals/WebApiAsyncHelper.cs index 9b463cd9..0962c043 100644 --- a/src/redmine-net-api/Internals/WebApiAsyncHelper.cs +++ b/src/redmine-net-api/Internals/WebApiAsyncHelper.cs @@ -42,25 +42,16 @@ public static async Task ExecuteUpload(RedmineManager redmineManager, st { using (var wc = redmineManager.CreateWebClient(null)) { - try + if (actionType == HttpVerbs.POST || actionType == HttpVerbs.DELETE || actionType == HttpVerbs.PUT || + actionType == HttpVerbs.PATCH) { - if (actionType == HttpVerbs.POST || actionType == HttpVerbs.DELETE || actionType == HttpVerbs.PUT || - actionType == HttpVerbs.PATCH) - { - return await wc.UploadStringTaskAsync(address, actionType, data).ConfigureAwait(false); - } - } - catch (WebException webException) - { - webException.HandleWebException(redmineManager.Serializer); + return await wc.UploadStringTaskAsync(address, actionType, data).ConfigureAwait(false); } } return null; } - - /// /// Executes the download. /// @@ -75,16 +66,8 @@ public static async Task ExecuteDownload(RedmineManager redmineManager, st { using (var wc = redmineManager.CreateWebClient(parameters)) { - try - { - var response = await wc.DownloadStringTaskAsync(address).ConfigureAwait(false); - return redmineManager.Serializer.Deserialize(response); - } - catch (WebException webException) - { - webException.HandleWebException(redmineManager.Serializer); - } - return default(T); + var response = await wc.DownloadStringTaskAsync(address).ConfigureAwait(false); + return redmineManager.Serializer.Deserialize(response); } } @@ -97,21 +80,15 @@ public static async Task ExecuteDownload(RedmineManager redmineManager, st /// The parameters. /// public static async Task> ExecuteDownloadList(RedmineManager redmineManager, string address, - NameValueCollection parameters = null) where T : class, new() { using (var wc = redmineManager.CreateWebClient(parameters)) { - try - { - var response = await wc.DownloadStringTaskAsync(address).ConfigureAwait(false); - var result = redmineManager.Serializer.DeserializeToPagedResults(response); - if (result != null) - return new List(result.Items); - } - catch (WebException webException) + var response = await wc.DownloadStringTaskAsync(address).ConfigureAwait(false); + var result = redmineManager.Serializer.DeserializeToPagedResults(response); + if (result != null) { - webException.HandleWebException(redmineManager.Serializer); + return new List(result.Items); } return null; } @@ -127,21 +104,12 @@ public static async Task> ExecuteDownloadList(RedmineManager redmineM /// The parameters. /// public static async Task> ExecuteDownloadPaginatedList(RedmineManager redmineManager, string address, - NameValueCollection parameters = null) where T : class, new() { using (var wc = redmineManager.CreateWebClient(parameters)) { - try - { - var response = await wc.DownloadStringTaskAsync(address).ConfigureAwait(false); - return redmineManager.Serializer.DeserializeToPagedResults(response); - } - catch (WebException webException) - { - webException.HandleWebException(redmineManager.Serializer); - } - return null; + var response = await wc.DownloadStringTaskAsync(address).ConfigureAwait(false); + return redmineManager.Serializer.DeserializeToPagedResults(response); } } @@ -155,15 +123,7 @@ public static async Task ExecuteDownloadFile(RedmineManager redmineManag { using (var wc = redmineManager.CreateWebClient(null, true)) { - try - { - return await wc.DownloadDataTaskAsync(address).ConfigureAwait(false); - } - catch (WebException webException) - { - webException.HandleWebException(redmineManager.Serializer); - } - return null; + return await wc.DownloadDataTaskAsync(address).ConfigureAwait(false); } } @@ -178,17 +138,9 @@ public static async Task ExecuteUploadFile(RedmineManager redmineManager { using (var wc = redmineManager.CreateWebClient(null, true)) { - try - { - var response = await wc.UploadDataTaskAsync(address, data).ConfigureAwait(false); - var responseString = Encoding.ASCII.GetString(response); - return redmineManager.Serializer.Deserialize(responseString); - } - catch (WebException webException) - { - webException.HandleWebException(redmineManager.Serializer); - } - return null; + var response = await wc.UploadDataTaskAsync(address, data).ConfigureAwait(false); + var responseString = Encoding.ASCII.GetString(response); + return redmineManager.Serializer.Deserialize(responseString); } } } diff --git a/src/redmine-net-api/Internals/WebApiHelper.cs b/src/redmine-net-api/Internals/WebApiHelper.cs index fcb0284c..29766b27 100644 --- a/src/redmine-net-api/Internals/WebApiHelper.cs +++ b/src/redmine-net-api/Internals/WebApiHelper.cs @@ -14,9 +14,12 @@ You may obtain a copy of the License at limitations under the License. */ +using System; using System.Collections.Specialized; +using System.ComponentModel; using System.Net; using System.Text; +using System.Threading; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Types; @@ -41,17 +44,10 @@ public static void ExecuteUpload(RedmineManager redmineManager, string address, { using (var wc = redmineManager.CreateWebClient(parameters)) { - try + if (actionType == HttpVerbs.POST || actionType == HttpVerbs.DELETE || actionType == HttpVerbs.PUT || + actionType == HttpVerbs.PATCH) { - if (actionType == HttpVerbs.POST || actionType == HttpVerbs.DELETE || actionType == HttpVerbs.PUT || - actionType == HttpVerbs.PATCH) - { - wc.UploadString(address, actionType, data); - } - } - catch (WebException webException) - { - webException.HandleWebException(redmineManager.Serializer); + wc.UploadString(address, actionType, data); } } } @@ -70,20 +66,13 @@ public static T ExecuteUpload(RedmineManager redmineManager, string address, { using (var wc = redmineManager.CreateWebClient(null)) { - try + if (actionType == HttpVerbs.POST || actionType == HttpVerbs.DELETE || actionType == HttpVerbs.PUT || + actionType == HttpVerbs.PATCH) { - if (actionType == HttpVerbs.POST || actionType == HttpVerbs.DELETE || actionType == HttpVerbs.PUT || - actionType == HttpVerbs.PATCH) - { - var response = wc.UploadString(address, actionType, data); - return redmineManager.Serializer.Deserialize(response); - } + var response = wc.UploadString(address, actionType, data); + return redmineManager.Serializer.Deserialize(response); } - catch (WebException webException) - { - webException.HandleWebException(redmineManager.Serializer); - } - return default(T); + return default; } } @@ -101,17 +90,13 @@ public static T ExecuteDownload(RedmineManager redmineManager, string address { using (var wc = redmineManager.CreateWebClient(parameters)) { - try - { - var response = wc.DownloadString(address); - if (!string.IsNullOrEmpty(response)) - return redmineManager.Serializer.Deserialize(response); - } - catch (WebException webException) + var response = wc.DownloadString(address); + if (!string.IsNullOrEmpty(response)) { - webException.HandleWebException(redmineManager.Serializer); + return redmineManager.Serializer.Deserialize(response); } - return default(T); + + return default; } } @@ -128,16 +113,35 @@ public static PagedResults ExecuteDownloadList(RedmineManager redmineManag { using (var wc = redmineManager.CreateWebClient(parameters)) { - try - { - var response = wc.DownloadString(address); - return redmineManager.Serializer.DeserializeToPagedResults(response); - } - catch (WebException webException) + var response = wc.DownloadString(address); + return redmineManager.Serializer.DeserializeToPagedResults(response); + } + } + + /// + /// Executes the download file. + /// + /// The redmine manager. + /// The address. + /// The name of the file to be placed on the local computer. + /// + public static void ExecuteDownloadFile(RedmineManager redmineManager, string address, string filename) + { + using (var wc = redmineManager.CreateWebClient(null, true)) + { + wc.DownloadProgressChanged += HandleDownloadProgress; + wc.DownloadFileCompleted += HandleDownloadComplete; + + var syncObject = new object(); + lock (syncObject) { - webException.HandleWebException(redmineManager.Serializer); + wc.DownloadFileAsync(new Uri(address), filename, syncObject); + //This would block the thread until download completes + Monitor.Wait(syncObject); } - return null; + + wc.DownloadProgressChanged -= HandleDownloadProgress; + wc.DownloadFileCompleted -= HandleDownloadComplete; } } @@ -151,18 +155,23 @@ public static byte[] ExecuteDownloadFile(RedmineManager redmineManager, string a { using (var wc = redmineManager.CreateWebClient(null, true)) { - try - { - return wc.DownloadData(address); - } - catch (WebException webException) - { - webException.HandleWebException(redmineManager.Serializer); - } - return null; + return wc.DownloadData(address); } } + private static void HandleDownloadComplete(object sender, AsyncCompletedEventArgs e) + { + lock (e.UserState) + { + //releases blocked thread + Monitor.Pulse(e.UserState); + } + } + + private static void HandleDownloadProgress(object sender, DownloadProgressChangedEventArgs e) + { + } + /// /// Executes the upload file. /// @@ -174,17 +183,9 @@ public static Upload ExecuteUploadFile(RedmineManager redmineManager, string add { using (var wc = redmineManager.CreateWebClient(null, true)) { - try - { - var response = wc.UploadData(address, data); - var responseString = Encoding.ASCII.GetString(response); - return redmineManager.Serializer.Deserialize(responseString); - } - catch (WebException webException) - { - webException.HandleWebException(redmineManager.Serializer); - } - return null; + var response = wc.UploadData(address, data); + var responseString = Encoding.ASCII.GetString(response); + return redmineManager.Serializer.Deserialize(responseString); } } } diff --git a/src/redmine-net-api/RedirectType.cs b/src/redmine-net-api/RedirectType.cs new file mode 100644 index 00000000..24cbc1c2 --- /dev/null +++ b/src/redmine-net-api/RedirectType.cs @@ -0,0 +1,21 @@ +namespace Redmine.Net.Api +{ + /// + /// + /// + public enum RedirectType + { + /// + /// + /// + None, + /// + /// + /// + OnlyHost, + /// + /// + /// + All + }; +} \ No newline at end of file diff --git a/src/redmine-net-api/RedmineWebClient.cs b/src/redmine-net-api/RedmineWebClient.cs index 391cf841..12ae7278 100644 --- a/src/redmine-net-api/RedmineWebClient.cs +++ b/src/redmine-net-api/RedmineWebClient.cs @@ -130,8 +130,8 @@ protected override WebRequest GetWebRequest(Uri address) DecompressionMethods.None; httpWebRequest.PreAuthenticate = PreAuthenticate; httpWebRequest.KeepAlive = KeepAlive; - httpWebRequest.UseDefaultCredentials = UseDefaultCredentials; httpWebRequest.Credentials = Credentials; + httpWebRequest.UseDefaultCredentials = (httpWebRequest.Credentials == null); httpWebRequest.UserAgent = UA; httpWebRequest.CachePolicy = CachePolicy; @@ -171,15 +171,14 @@ protected override WebResponse GetWebResponse(WebRequest request) webException.HandleWebException(RedmineSerializer); } - if (response == null) + switch (response) { - return null; - } - - if (response is HttpWebResponse) - { - HandleRedirect(request, response); - HandleCookies(request, response); + case null: + return null; + case HttpWebResponse _: + HandleRedirect(request, response); + HandleCookies(request, response); + break; } return response; @@ -243,9 +242,7 @@ protected void HandleRedirect(WebRequest request, WebResponse response) redirectUrl = string.Empty; } } - - - + /// /// Handles additional cookies /// @@ -266,23 +263,4 @@ protected void HandleCookies(WebRequest request, WebResponse response) CookieContainer.Add(col); } } - - /// - /// - /// - public enum RedirectType - { - /// - /// - /// - None, - /// - /// - /// - OnlyHost, - /// - /// - /// - All - }; } \ No newline at end of file From 1cb050820ec8f4bba6a40605620c64e8eefa5408 Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 28 Apr 2020 15:19:21 +0300 Subject: [PATCH 138/549] Remove TargetFramework --- src/redmine-net-api/redmine-net-api.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 76c99e9c..333a6d42 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -1,8 +1,8 @@ - + - net48 + net20;net40;net45;net451;net452;net46;net461;net462;net47;net471;net472;net48; false Redmine.Net.Api From 208035ade9bf47635bea83b418d34656e2884852 Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 28 Apr 2020 15:19:54 +0300 Subject: [PATCH 139/549] Update Microsoft.CodeAnalysis.FxCopAnalyzers --- src/redmine-net-api/redmine-net-api.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 333a6d42..c443405a 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -1,4 +1,4 @@ - + @@ -108,7 +108,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From f669b90aea0323e5fc1e8fbde5eed8c86fb35b06 Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 28 Apr 2020 15:20:39 +0300 Subject: [PATCH 140/549] Fix #262 --- src/redmine-net-api/Types/IssueRelation.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/redmine-net-api/Types/IssueRelation.cs b/src/redmine-net-api/Types/IssueRelation.cs index a6bae1f1..9cec2cc4 100644 --- a/src/redmine-net-api/Types/IssueRelation.cs +++ b/src/redmine-net-api/Types/IssueRelation.cs @@ -123,7 +123,7 @@ public override void ReadXml(XmlReader reader) public override void WriteXml(XmlWriter writer) { writer.WriteElementString(RedmineKeys.ISSUE_TO_ID, IssueToId.ToString(CultureInfo.InvariantCulture)); - writer.WriteElementString(RedmineKeys.RELATION_TYPE, Type.ToString()); + writer.WriteElementString(RedmineKeys.RELATION_TYPE, Type.ToString().ToLowerInvariant()); if (Type == IssueRelationType.Precedes || Type == IssueRelationType.Follows) { writer.WriteValueOrEmpty(RedmineKeys.DELAY, Delay); @@ -141,7 +141,7 @@ public override void WriteJson(JsonWriter writer) using (new JsonObject(writer, RedmineKeys.RELATION)) { writer.WriteProperty(RedmineKeys.ISSUE_TO_ID, IssueToId); - writer.WriteProperty(RedmineKeys.RELATION_TYPE, Type); + writer.WriteProperty(RedmineKeys.RELATION_TYPE, Type.ToString().ToLowerInvariant()); if (Type == IssueRelationType.Precedes || Type == IssueRelationType.Follows) { writer.WriteValueOrEmpty(RedmineKeys.DELAY, Delay); From e84795dbba3be3a719976aa2d615296b442a7e6e Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 28 Apr 2020 15:45:07 +0300 Subject: [PATCH 141/549] Add issue and PR template markdown --- ISSUE_TEMPLATE.md | 6 ++++++ PULL_REQUEST_TEMPLATE.md | 0 redmine-net-api.sln | 2 ++ 3 files changed, 8 insertions(+) create mode 100644 ISSUE_TEMPLATE.md create mode 100644 PULL_REQUEST_TEMPLATE.md diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..9e322a34 --- /dev/null +++ b/ISSUE_TEMPLATE.md @@ -0,0 +1,6 @@ +When creating a new issue, please make sure the following information is part of your issue description (if applicable). + +- Which Redmine server version are you using +- Which Redmine.Net.Api version are you using +- Which serialization type (xml or json) are you using +- A list of steps or a gist or a github repository which can be easily used to reproduce your case. \ No newline at end of file diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..e69de29b diff --git a/redmine-net-api.sln b/redmine-net-api.sln index 07ba8903..65e353b8 100644 --- a/redmine-net-api.sln +++ b/redmine-net-api.sln @@ -20,6 +20,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionFolder", "SolutionF logo.png = logo.png README.md = README.md redmine-net-api.snk = redmine-net-api.snk + ISSUE_TEMPLATE.md = ISSUE_TEMPLATE.md + PULL_REQUEST_TEMPLATE.md = PULL_REQUEST_TEMPLATE.md EndProjectSection EndProject Global From b88d58ed2f7ed80fd2ff62bc625ba43e5279d0d0 Mon Sep 17 00:00:00 2001 From: Necati Meral Date: Tue, 28 Apr 2020 15:20:10 +0200 Subject: [PATCH 142/549] Fix #263 Consider generalizing deserialization of enums --- src/redmine-net-api/Types/IssueRelation.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/redmine-net-api/Types/IssueRelation.cs b/src/redmine-net-api/Types/IssueRelation.cs index a6bae1f1..244fb1ef 100644 --- a/src/redmine-net-api/Types/IssueRelation.cs +++ b/src/redmine-net-api/Types/IssueRelation.cs @@ -173,10 +173,20 @@ public override void ReadJson(JsonReader reader) case RedmineKeys.DELAY: Delay = reader.ReadAsInt32(); break; case RedmineKeys.ISSUE_ID: IssueId = reader.ReadAsInt(); break; case RedmineKeys.ISSUE_TO_ID: IssueToId = reader.ReadAsInt(); break; - case RedmineKeys.RELATION_TYPE: Type = (IssueRelationType)reader.ReadAsInt(); break; + case RedmineKeys.RELATION_TYPE: Type = ReadIssueRelationType(reader); break; } } } + + IssueRelationType ReadIssueRelationType(JsonReader reader) + { + var enumValue = reader.ReadAsString(); + if (short.TryParse(enumValue, out short enumId)) + { + return (IssueRelationType)enumId; + } + return (IssueRelationType)Enum.Parse(typeof(IssueRelationType), enumValue, true); + } #endregion #region Implementation of IEquatable From 3d5ffcc9ddae66a194f6594898a1d90b1a2bc73a Mon Sep 17 00:00:00 2001 From: Necati Meral Date: Tue, 28 Apr 2020 16:09:37 +0200 Subject: [PATCH 143/549] Fixed json serialization of `IssueRelation.Type` --- src/redmine-net-api/Types/IssueRelation.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/redmine-net-api/Types/IssueRelation.cs b/src/redmine-net-api/Types/IssueRelation.cs index 244fb1ef..0bd4c58b 100644 --- a/src/redmine-net-api/Types/IssueRelation.cs +++ b/src/redmine-net-api/Types/IssueRelation.cs @@ -141,7 +141,11 @@ public override void WriteJson(JsonWriter writer) using (new JsonObject(writer, RedmineKeys.RELATION)) { writer.WriteProperty(RedmineKeys.ISSUE_TO_ID, IssueToId); - writer.WriteProperty(RedmineKeys.RELATION_TYPE, Type); + +#pragma warning disable CA1308 // Redmine expects enum types as lower-case strings + writer.WriteProperty(RedmineKeys.RELATION_TYPE, Type.ToString().ToLowerInvariant()); +#pragma warning restore CA1308 + if (Type == IssueRelationType.Precedes || Type == IssueRelationType.Follows) { writer.WriteValueOrEmpty(RedmineKeys.DELAY, Delay); From a4dc08171d0442ad425d090da52e55bdf24e5b60 Mon Sep 17 00:00:00 2001 From: Necati Meral Date: Tue, 28 Apr 2020 16:52:11 +0200 Subject: [PATCH 144/549] Fixed: XML serialization should write relation_type as lower-case string Since xml deserialization handles empty string, I've added it to the json deserialization as well. --- src/redmine-net-api/Types/IssueRelation.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/redmine-net-api/Types/IssueRelation.cs b/src/redmine-net-api/Types/IssueRelation.cs index 0bd4c58b..1a492796 100644 --- a/src/redmine-net-api/Types/IssueRelation.cs +++ b/src/redmine-net-api/Types/IssueRelation.cs @@ -123,7 +123,11 @@ public override void ReadXml(XmlReader reader) public override void WriteXml(XmlWriter writer) { writer.WriteElementString(RedmineKeys.ISSUE_TO_ID, IssueToId.ToString(CultureInfo.InvariantCulture)); - writer.WriteElementString(RedmineKeys.RELATION_TYPE, Type.ToString()); + +#pragma warning disable CA1308 // Redmine expects enum types as lower-case strings + writer.WriteElementString(RedmineKeys.RELATION_TYPE, Type.ToString().ToLowerInvariant()); +#pragma warning restore CA1308 + if (Type == IssueRelationType.Precedes || Type == IssueRelationType.Follows) { writer.WriteValueOrEmpty(RedmineKeys.DELAY, Delay); @@ -185,11 +189,15 @@ public override void ReadJson(JsonReader reader) IssueRelationType ReadIssueRelationType(JsonReader reader) { var enumValue = reader.ReadAsString(); - if (short.TryParse(enumValue, out short enumId)) + if (!enumValue.IsNullOrWhiteSpace()) { - return (IssueRelationType)enumId; + if (short.TryParse(enumValue, out short enumId)) + { + return (IssueRelationType)enumId; + } + return (IssueRelationType)Enum.Parse(typeof(IssueRelationType), enumValue, true); } - return (IssueRelationType)Enum.Parse(typeof(IssueRelationType), enumValue, true); + return default; } #endregion From 5e27a1e80cf5862b4812fdfc8ff99ed432affd40 Mon Sep 17 00:00:00 2001 From: Necati Meral Date: Tue, 28 Apr 2020 18:08:40 +0200 Subject: [PATCH 145/549] Fix #265: Implemented missing properties to circumvent weird deserialization offsets This fix seems easier, since I need the default_status value anyway. bonus content would be a validation of unexpected `BeginObject` tokens. --- src/redmine-net-api/RedmineKeys.cs | 6 ++++++ src/redmine-net-api/Types/Tracker.cs | 16 +++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/redmine-net-api/RedmineKeys.cs b/src/redmine-net-api/RedmineKeys.cs index deb03f4d..4c3398d9 100644 --- a/src/redmine-net-api/RedmineKeys.cs +++ b/src/redmine-net-api/RedmineKeys.cs @@ -131,6 +131,12 @@ public static class RedmineKeys /// /// public const string CUSTOM_FIELDS = "custom_fields"; + + /// + /// + /// + public const string DEFAULT_STATUS = "default_status"; + /// /// /// diff --git a/src/redmine-net-api/Types/Tracker.cs b/src/redmine-net-api/Types/Tracker.cs index a63c7bc7..d08f5931 100644 --- a/src/redmine-net-api/Types/Tracker.cs +++ b/src/redmine-net-api/Types/Tracker.cs @@ -30,7 +30,17 @@ namespace Redmine.Net.Api.Types [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.TRACKER)] public class Tracker : IdentifiableName, IEquatable - { + { + /// + /// Gets the default (issue) status for this tracker. + /// + public IdentifiableName DefaultStatus { get; internal set; } + + /// + /// Gets the description of this tracker. + /// + public string Description { get; internal set; } + #region Implementation of IXmlSerialization /// /// Generates an object from its XML representation. @@ -51,6 +61,8 @@ public override void ReadXml(XmlReader reader) { case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; + case RedmineKeys.DEFAULT_STATUS: DefaultStatus = new IdentifiableName(reader); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadElementContentAsString(); break; default: reader.Read(); break; } } @@ -81,6 +93,8 @@ public override void ReadJson(JsonReader reader) { case RedmineKeys.ID: Id = reader.ReadAsInt(); break; case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + case RedmineKeys.DEFAULT_STATUS: DefaultStatus = new IdentifiableName(reader); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadAsString(); break; default: reader.Read(); break; } } From 84277d6d52db1f9803bfa8ce5be099d023ae5184 Mon Sep 17 00:00:00 2001 From: Necati Meral Date: Thu, 30 Apr 2020 16:46:52 +0200 Subject: [PATCH 146/549] Implemented a default value for `IssueRelationType` and added some error handling --- src/redmine-net-api/Types/IssueRelation.cs | 20 ++++++++++++++++++- .../Types/IssueRelationType.cs | 7 +++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/redmine-net-api/Types/IssueRelation.cs b/src/redmine-net-api/Types/IssueRelation.cs index 1a492796..ce06b95d 100644 --- a/src/redmine-net-api/Types/IssueRelation.cs +++ b/src/redmine-net-api/Types/IssueRelation.cs @@ -20,6 +20,7 @@ limitations under the License. using System.Xml; using System.Xml.Serialization; using Newtonsoft.Json; +using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; using Redmine.Net.Api.Serialization; @@ -122,6 +123,8 @@ public override void ReadXml(XmlReader reader) /// public override void WriteXml(XmlWriter writer) { + AssertValidIssueRelationType(); + writer.WriteElementString(RedmineKeys.ISSUE_TO_ID, IssueToId.ToString(CultureInfo.InvariantCulture)); #pragma warning disable CA1308 // Redmine expects enum types as lower-case strings @@ -142,6 +145,8 @@ public override void WriteXml(XmlWriter writer) /// public override void WriteJson(JsonWriter writer) { + AssertValidIssueRelationType(); + using (new JsonObject(writer, RedmineKeys.RELATION)) { writer.WriteProperty(RedmineKeys.ISSUE_TO_ID, IssueToId); @@ -186,6 +191,16 @@ public override void ReadJson(JsonReader reader) } } + void AssertValidIssueRelationType() + { +#pragma warning disable CS0618 // Use of internal enumeration value is allowed here for error handling + if (Type == IssueRelationType.Undefined) + { + throw new RedmineException($"The value `{nameof(IssueRelationType)}.`{nameof(IssueRelationType.Undefined)}` is not allowed to create relations!"); + } +#pragma warning restore CS0618 + } + IssueRelationType ReadIssueRelationType(JsonReader reader) { var enumValue = reader.ReadAsString(); @@ -197,7 +212,10 @@ IssueRelationType ReadIssueRelationType(JsonReader reader) } return (IssueRelationType)Enum.Parse(typeof(IssueRelationType), enumValue, true); } - return default; + +#pragma warning disable CS0618 // Use of internal enumeration value is allowed here to have a fallback + return IssueRelationType.Undefined; +#pragma warning restore CS0618 } #endregion diff --git a/src/redmine-net-api/Types/IssueRelationType.cs b/src/redmine-net-api/Types/IssueRelationType.cs index 1d80d177..fbb2223c 100644 --- a/src/redmine-net-api/Types/IssueRelationType.cs +++ b/src/redmine-net-api/Types/IssueRelationType.cs @@ -14,6 +14,8 @@ You may obtain a copy of the License at limitations under the License. */ +using System; + namespace Redmine.Net.Api.Types { /// @@ -21,6 +23,11 @@ namespace Redmine.Net.Api.Types /// public enum IssueRelationType { + /// + /// Fallback value for deserialization purposes in case the deserialization fails. Do not use to create new relations! + /// + [Obsolete("Fallback value for deserialization purposes in case the deserialization fails. Do not use to create new relations!")] + Undefined = 0, /// /// /// From 7e51ce816cd88e6dd654fef404cae1c3238207ad Mon Sep 17 00:00:00 2001 From: zapadi Date: Thu, 30 Apr 2020 18:34:40 +0300 Subject: [PATCH 147/549] Consistency --- src/redmine-net-api/Types/IssueRelation.cs | 43 ++++++------------- .../Types/IssueRelationType.cs | 5 ++- 2 files changed, 16 insertions(+), 32 deletions(-) diff --git a/src/redmine-net-api/Types/IssueRelation.cs b/src/redmine-net-api/Types/IssueRelation.cs index 5d5f333f..0c7125b8 100644 --- a/src/redmine-net-api/Types/IssueRelation.cs +++ b/src/redmine-net-api/Types/IssueRelation.cs @@ -126,15 +126,8 @@ public override void WriteXml(XmlWriter writer) AssertValidIssueRelationType(); writer.WriteElementString(RedmineKeys.ISSUE_TO_ID, IssueToId.ToString(CultureInfo.InvariantCulture)); -<<<<<<< HEAD - writer.WriteElementString(RedmineKeys.RELATION_TYPE, Type.ToString().ToLowerInvariant()); -======= + writer.WriteElementString(RedmineKeys.RELATION_TYPE, Type.ToString().ToLowerInv()); -#pragma warning disable CA1308 // Redmine expects enum types as lower-case strings - writer.WriteElementString(RedmineKeys.RELATION_TYPE, Type.ToString().ToLowerInvariant()); -#pragma warning restore CA1308 - ->>>>>>> master if (Type == IssueRelationType.Precedes || Type == IssueRelationType.Follows) { writer.WriteValueOrEmpty(RedmineKeys.DELAY, Delay); @@ -154,15 +147,8 @@ public override void WriteJson(JsonWriter writer) using (new JsonObject(writer, RedmineKeys.RELATION)) { writer.WriteProperty(RedmineKeys.ISSUE_TO_ID, IssueToId); -<<<<<<< HEAD - writer.WriteProperty(RedmineKeys.RELATION_TYPE, Type.ToString().ToLowerInvariant()); -======= - -#pragma warning disable CA1308 // Redmine expects enum types as lower-case strings - writer.WriteProperty(RedmineKeys.RELATION_TYPE, Type.ToString().ToLowerInvariant()); -#pragma warning restore CA1308 + writer.WriteProperty(RedmineKeys.RELATION_TYPE, Type.ToString().ToLowerInv()); ->>>>>>> master if (Type == IssueRelationType.Precedes || Type == IssueRelationType.Follows) { writer.WriteValueOrEmpty(RedmineKeys.DELAY, Delay); @@ -199,31 +185,28 @@ public override void ReadJson(JsonReader reader) } } - void AssertValidIssueRelationType() + private void AssertValidIssueRelationType() { -#pragma warning disable CS0618 // Use of internal enumeration value is allowed here for error handling if (Type == IssueRelationType.Undefined) { throw new RedmineException($"The value `{nameof(IssueRelationType)}.`{nameof(IssueRelationType.Undefined)}` is not allowed to create relations!"); } -#pragma warning restore CS0618 } - IssueRelationType ReadIssueRelationType(JsonReader reader) + private IssueRelationType ReadIssueRelationType(JsonReader reader) { var enumValue = reader.ReadAsString(); - if (!enumValue.IsNullOrWhiteSpace()) + if (enumValue.IsNullOrWhiteSpace()) { - if (short.TryParse(enumValue, out short enumId)) - { - return (IssueRelationType)enumId; - } - return (IssueRelationType)Enum.Parse(typeof(IssueRelationType), enumValue, true); + return IssueRelationType.Undefined; } - -#pragma warning disable CS0618 // Use of internal enumeration value is allowed here to have a fallback - return IssueRelationType.Undefined; -#pragma warning restore CS0618 + + if (short.TryParse(enumValue, out var enumId)) + { + return (IssueRelationType)enumId; + } + + return (IssueRelationType)Enum.Parse(typeof(IssueRelationType), enumValue, true); } #endregion diff --git a/src/redmine-net-api/Types/IssueRelationType.cs b/src/redmine-net-api/Types/IssueRelationType.cs index fbb2223c..79ba9fdb 100644 --- a/src/redmine-net-api/Types/IssueRelationType.cs +++ b/src/redmine-net-api/Types/IssueRelationType.cs @@ -23,11 +23,12 @@ namespace Redmine.Net.Api.Types /// public enum IssueRelationType { +#pragma warning disable CS0618 // Use of internal enumeration value is allowed here to have a fallback /// /// Fallback value for deserialization purposes in case the deserialization fails. Do not use to create new relations! /// - [Obsolete("Fallback value for deserialization purposes in case the deserialization fails. Do not use to create new relations!")] - Undefined = 0, + Undefined = 0, +#pragma warning restore CS0618 /// /// /// From 178cba584e1b98a167ac0bfb0db6768149801e2f Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 2 May 2020 19:07:55 +0300 Subject: [PATCH 148/549] Fix #225, #245 - Split CreateOrUpdate into separate methods. --- .../Async/RedmineManagerAsync.cs | 18 ++++++++- .../Async/RedmineManagerAsync40.cs | 9 ++++- .../Async/RedmineManagerAsync45.cs | 25 +++++++++++- src/redmine-net-api/IRedmineManager.cs | 13 ++++++- src/redmine-net-api/RedmineManager.cs | 27 ++++++++++++- .../Tests/Async/WikiPageAsyncTests.cs | 10 ++++- .../Tests/Sync/WikiPageTests.cs | 39 ++++++++----------- 7 files changed, 108 insertions(+), 33 deletions(-) diff --git a/src/redmine-net-api/Async/RedmineManagerAsync.cs b/src/redmine-net-api/Async/RedmineManagerAsync.cs index dbea12b0..441c6a3c 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync.cs @@ -44,10 +44,24 @@ public static Task GetCurrentUserAsync(this RedmineManager redmineManager, /// Name of the page. /// The wiki page. /// - public static Task CreateOrUpdateWikiPageAsync(this RedmineManager redmineManager, string projectId, + public static Task CreateWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage) { - return delegate { return redmineManager.CreateOrUpdateWikiPage(projectId, pageName, wikiPage); }; + return delegate { return redmineManager.CreateWikiPage(projectId, pageName, wikiPage); }; + } + + /// + /// + /// + /// + /// + /// + /// + /// + public static Task UpdateWikiPageAsync(this RedmineManager redmineManager, string projectId, + string pageName, WikiPage wikiPage) + { + return delegate { redmineManager.UpdateWikiPage(projectId, pageName, wikiPage); }; } /// diff --git a/src/redmine-net-api/Async/RedmineManagerAsync40.cs b/src/redmine-net-api/Async/RedmineManagerAsync40.cs index 303b2136..3d2a4f06 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync40.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync40.cs @@ -49,9 +49,14 @@ public static Task GetCurrentUserAsync(this RedmineManager redmineManager, /// Name of the page. /// The wiki page. /// - public static Task CreateOrUpdateWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage) + public static Task CreateWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage) { - return Task.Factory.StartNew(() => redmineManager.CreateOrUpdateWikiPage(projectId, pageName, wikiPage), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); + return Task.Factory.StartNew(() => redmineManager.CreateWikiPage(projectId, pageName, wikiPage), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); + } + + public static Task UpdateWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage) + { + return Task.Factory.StartNew(() => redmineManager.UpdateWikiPage(projectId, pageName, wikiPage), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); } /// diff --git a/src/redmine-net-api/Async/RedmineManagerAsync45.cs b/src/redmine-net-api/Async/RedmineManagerAsync45.cs index aab48dfb..e15cf3a0 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync45.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync45.cs @@ -54,7 +54,7 @@ public static async Task GetCurrentUserAsync(this RedmineManager redmineMa /// Name of the page. /// The wiki page. /// - public static async Task CreateOrUpdateWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage) + public static async Task CreateWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage) { var data = redmineManager.Serializer.Serialize(wikiPage); if (string.IsNullOrEmpty(data)) return null; @@ -67,6 +67,29 @@ public static async Task CreateOrUpdateWikiPageAsync(this RedmineManag return redmineManager.Serializer.Deserialize(response); } + /// + /// Creates the or update wiki page asynchronous. + /// + /// The redmine manager. + /// The project identifier. + /// Name of the page. + /// The wiki page. + /// + public static async Task UpdateWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage) + { + var data = redmineManager.Serializer.Serialize(wikiPage); + if (string.IsNullOrEmpty(data)) + { + return ; + } + + var url = UrlHelper.GetWikiCreateOrUpdaterUrl(redmineManager, projectId, pageName); + + url = Uri.EscapeUriString(url); + + var response = await WebApiAsyncHelper.ExecuteUpload(redmineManager, url, HttpVerbs.PUT, data).ConfigureAwait(false); + } + /// /// Deletes the wiki page asynchronous. /// diff --git a/src/redmine-net-api/IRedmineManager.cs b/src/redmine-net-api/IRedmineManager.cs index 5dcc2d04..41863803 100644 --- a/src/redmine-net-api/IRedmineManager.cs +++ b/src/redmine-net-api/IRedmineManager.cs @@ -89,7 +89,7 @@ public interface IRedmineManager /// /// void RemoveWatcherFromIssue(int issueId, int userId); - + /// /// /// @@ -97,7 +97,16 @@ public interface IRedmineManager /// /// /// - WikiPage CreateOrUpdateWikiPage(string projectId, string pageName, WikiPage wikiPage); + WikiPage CreateWikiPage(string projectId, string pageName, WikiPage wikiPage); + + /// + /// + /// + /// + /// + /// + void UpdateWikiPage(string projectId, string pageName, WikiPage wikiPage); + /// /// /// diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index 34ea7984..8652dd72 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -358,7 +358,30 @@ public void RemoveUserFromGroup(int groupId, int userId) /// The wiki page name. /// The wiki page to create or update. /// - public WikiPage CreateOrUpdateWikiPage(string projectId, string pageName, WikiPage wikiPage) + public void UpdateWikiPage(string projectId, string pageName, WikiPage wikiPage) + { + var result = Serializer.Serialize(wikiPage); + + if (string.IsNullOrEmpty(result)) + { + return; + } + + var url = UrlHelper.GetWikiCreateOrUpdaterUrl(this, projectId, pageName); + + url = Uri.EscapeUriString(url); + + WebApiHelper.ExecuteUpload(this, url, HttpVerbs.PUT, result); + } + + /// + /// + /// + /// + /// + /// + /// + public WikiPage CreateWikiPage(string projectId, string pageName, WikiPage wikiPage) { var result = Serializer.Serialize(wikiPage); diff --git a/tests/redmine-net-api.Tests/Tests/Async/WikiPageAsyncTests.cs b/tests/redmine-net-api.Tests/Tests/Async/WikiPageAsyncTests.cs index 08c85e30..586340c7 100644 --- a/tests/redmine-net-api.Tests/Tests/Async/WikiPageAsyncTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Async/WikiPageAsyncTests.cs @@ -26,13 +26,19 @@ public WikiPageAsyncTests(RedmineFixture fixture) } [Fact] - public async Task Should_Add_Or_Update_Page() + public async Task Should_Add_Wiki_Page() { - var page = await fixture.RedmineManager.CreateOrUpdateWikiPageAsync(PROJECT_ID, WIKI_PAGE_NAME, new WikiPage { Text = WIKI_PAGE_UPDATED_TEXT, Comments = WIKI_PAGE_COMMENT }); + var page = await fixture.RedmineManager.CreateWikiPageAsync(PROJECT_ID, WIKI_PAGE_NAME, new WikiPage { Text = WIKI_PAGE_UPDATED_TEXT, Comments = WIKI_PAGE_COMMENT }); Assert.NotNull(page); Assert.True(page.Title == WIKI_PAGE_NAME, "Wiki page " + WIKI_PAGE_NAME + " does not exist."); } + + [Fact] + public async Task Should_Update_Wiki_Page() + { + await fixture.RedmineManager.UpdateWikiPageAsync(PROJECT_ID, WIKI_PAGE_NAME, new WikiPage { Text = WIKI_PAGE_UPDATED_TEXT, Comments = WIKI_PAGE_COMMENT }); + } [Fact] public async Task Should_Get_All_Pages() diff --git a/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs index 43fc92b3..fefb48ad 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs @@ -37,23 +37,33 @@ public WikiPageTests(RedmineFixture fixture) private readonly RedmineFixture fixture; - private const string PROJECT_ID = "redmine-net-api"; + private const string PROJECT_ID = "redmine-net-api-project-test"; private const string WIKI_PAGE_NAME = "Wiki"; [Fact, Order(1)] - public void Should_Add_Or_Update_WikiPage() + public void Should_Add_WikiPage() { - const string WIKI_PAGE_UPDATED_TEXT = "Updated again and again wiki page"; + const string WIKI_PAGE_TEXT = "Create wiki page"; const string WIKI_PAGE_COMMENT = "I did it through code"; - var page = fixture.RedmineManager.CreateOrUpdateWikiPage(PROJECT_ID, WIKI_PAGE_NAME, - new WikiPage { Text = WIKI_PAGE_UPDATED_TEXT, Comments = WIKI_PAGE_COMMENT }); + var page = fixture.RedmineManager.CreateWikiPage(PROJECT_ID, "Wiki test page name", + new WikiPage { Text = WIKI_PAGE_TEXT, Comments = WIKI_PAGE_COMMENT }); Assert.NotNull(page); - Assert.True(page.Title.Equals(WIKI_PAGE_NAME), "Wiki page name is invalid."); - Assert.True(page.Text.Equals(WIKI_PAGE_UPDATED_TEXT), "Wiki page text is invalid."); + Assert.True(page.Title.Equals("Wiki test page name"), "Wiki page name is invalid."); + Assert.True(page.Text.Equals(WIKI_PAGE_TEXT), "Wiki page text is invalid."); Assert.True(page.Comments.Equals(WIKI_PAGE_COMMENT), "Wiki page comments are invalid."); } + + [Fact, Order(2)] + public void Should_Update_WikiPage() + { + const string WIKI_PAGE_UPDATED_TEXT = "Updated again and again wiki page and again"; + const string WIKI_PAGE_COMMENT = "I did it through code"; + + fixture.RedmineManager.UpdateWikiPage(PROJECT_ID, "Wiki test page name", + new WikiPage { Text = WIKI_PAGE_UPDATED_TEXT, Comments = WIKI_PAGE_COMMENT }); + } [Fact, Order(99)] public void Should_Delete_Wiki_Page() @@ -106,20 +116,5 @@ public void Should_Get_Wiki_Page_By_Version() Assert.Equal(oldPage.Title, WIKI_PAGE_NAME); Assert.True(oldPage.Version == WIKI_PAGE_VERSION, "Wiki page version is invalid."); } - - [Fact] - public void Should_Create_Wiki() - { - var result = fixture.RedmineManager.CreateOrUpdateWikiPage("1", "pagina2", new WikiPage - { - Text = "ana are mere multe si rosii!", - Comments = "asa", - Version = 1 - }); - - Assert.NotNull(result); - - } - } } \ No newline at end of file From 4b999b5c741be15b64c0fa164e03458148249c7b Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 2 May 2020 21:07:35 +0300 Subject: [PATCH 149/549] Add static Create of T to IdentifiableName --- src/redmine-net-api/Types/File.cs | 4 +- src/redmine-net-api/Types/Group.cs | 2 +- src/redmine-net-api/Types/IdentifiableName.cs | 14 +-- src/redmine-net-api/Types/Issue.cs | 4 +- src/redmine-net-api/Types/Project.cs | 2 +- .../Types/ProjectMembership.cs | 2 +- .../Tests/Async/AttachmentAsyncTests.cs | 22 ++--- .../Tests/Async/UserAsyncTests.cs | 2 +- .../Tests/Sync/AttachmentTests.cs | 2 +- .../Tests/Sync/GroupTests.cs | 4 +- .../Tests/Sync/IssueCategoryTests.cs | 4 +- .../Tests/Sync/IssueTests.cs | 91 ++++++++++--------- .../Tests/Sync/ProjectMembershipTests.cs | 6 +- .../Tests/Sync/ProjectTests.cs | 10 +- .../Tests/Sync/TimeEntryTests.cs | 12 +-- 15 files changed, 93 insertions(+), 88 deletions(-) diff --git a/src/redmine-net-api/Types/File.cs b/src/redmine-net-api/Types/File.cs index 5391565b..5094fa8e 100644 --- a/src/redmine-net-api/Types/File.cs +++ b/src/redmine-net-api/Types/File.cs @@ -123,7 +123,7 @@ public override void ReadXml(XmlReader reader) case RedmineKeys.FILE_SIZE: FileSize = reader.ReadElementContentAsInt(); break; case RedmineKeys.TOKEN: Token = reader.ReadElementContentAsString(); break; case RedmineKeys.VERSION: Version = new IdentifiableName(reader); break; - case RedmineKeys.VERSION_ID: Version = IdentifiableName.Create(reader.ReadElementContentAsInt()); break; + case RedmineKeys.VERSION_ID: Version = IdentifiableName.Create(reader.ReadElementContentAsInt()); break; default: reader.Read(); break; } } @@ -175,7 +175,7 @@ public override void ReadJson(JsonReader reader) case RedmineKeys.FILE_SIZE: FileSize = reader.ReadAsInt32().GetValueOrDefault(); break; case RedmineKeys.TOKEN: Token = reader.ReadAsString(); break; case RedmineKeys.VERSION: Version = new IdentifiableName(reader); break; - case RedmineKeys.VERSION_ID: Version = IdentifiableName.Create(reader.ReadAsInt32().GetValueOrDefault()); break; + case RedmineKeys.VERSION_ID: Version = IdentifiableName.Create(reader.ReadAsInt32().GetValueOrDefault()); break; default: reader.Read(); break; } } diff --git a/src/redmine-net-api/Types/Group.cs b/src/redmine-net-api/Types/Group.cs index 82d2b366..92e76776 100644 --- a/src/redmine-net-api/Types/Group.cs +++ b/src/redmine-net-api/Types/Group.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/redmine-net-api/Types/IdentifiableName.cs b/src/redmine-net-api/Types/IdentifiableName.cs index 271cefef..740f966c 100644 --- a/src/redmine-net-api/Types/IdentifiableName.cs +++ b/src/redmine-net-api/Types/IdentifiableName.cs @@ -29,17 +29,19 @@ namespace Redmine.Net.Api.Types /// [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] public class IdentifiableName : Identifiable + { /// /// /// /// /// - public static IdentifiableName Create(int id) - { - return new IdentifiableName {Id = id}; + public static T Create(int id) where T: IdentifiableName, new() + { + var t = new T (){Id = id}; + return t; } - + /// /// Initializes a new instance of the class. /// @@ -60,7 +62,7 @@ public IdentifiableName(XmlReader reader) /// public IdentifiableName(JsonReader reader) { - InitializeJsonReader(reader); + Initialize(reader); } private void Initialize(XmlReader reader) @@ -68,7 +70,7 @@ private void Initialize(XmlReader reader) ReadXml(reader); } - private void InitializeJsonReader(JsonReader reader) + private void Initialize(JsonReader reader) { ReadJson(reader); } diff --git a/src/redmine-net-api/Types/Issue.cs b/src/redmine-net-api/Types/Issue.cs index 858eece9..3abb361c 100644 --- a/src/redmine-net-api/Types/Issue.cs +++ b/src/redmine-net-api/Types/Issue.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2017 Adrian Popescu, Dorin Huzum. Licensed under the Apache License, Version 2.0 (the "License"); @@ -583,7 +583,7 @@ public object Clone() /// public IdentifiableName AsParent() { - return IdentifiableName.Create(Id); + return IdentifiableName.Create(Id); } diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs index c6ade8e9..55cb2e66 100644 --- a/src/redmine-net-api/Types/Project.cs +++ b/src/redmine-net-api/Types/Project.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/redmine-net-api/Types/ProjectMembership.cs b/src/redmine-net-api/Types/ProjectMembership.cs index db571eda..cb8b1406 100644 --- a/src/redmine-net-api/Types/ProjectMembership.cs +++ b/src/redmine-net-api/Types/ProjectMembership.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/tests/redmine-net-api.Tests/Tests/Async/AttachmentAsyncTests.cs b/tests/redmine-net-api.Tests/Tests/Async/AttachmentAsyncTests.cs index b7f91e12..f454a497 100644 --- a/tests/redmine-net-api.Tests/Tests/Async/AttachmentAsyncTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Async/AttachmentAsyncTests.cs @@ -50,21 +50,21 @@ public async Task Should_Upload_Attachment() attachments.Add(attachment); - var icf = (IssueCustomField)IdentifiableName.Create(13); + var icf = (IssueCustomField)IdentifiableName.Create(13); icf.Values = new List { new CustomFieldValue { Info = "Issue custom field completed" } }; var issue = new Issue { - Project = IdentifiableName.Create(9), - Tracker = IdentifiableName.Create(3), - Status = IdentifiableName.Create(6), - Priority = IdentifiableName.Create(9), + Project = IdentifiableName.Create(9), + Tracker = IdentifiableName.Create(3), + Status = IdentifiableName.Create(6), + Priority = IdentifiableName.Create(9), Subject = "Issue with attachments", Description = "Issue description...", - Category = IdentifiableName.Create(18), - FixedVersion = IdentifiableName.Create(9), - AssignedTo = IdentifiableName.Create(8), - ParentIssue = IdentifiableName.Create(96), + Category = IdentifiableName.Create(18), + FixedVersion = IdentifiableName.Create(9), + AssignedTo = IdentifiableName.Create(8), + ParentIssue = IdentifiableName.Create(96), CustomFields = new List {icf}, IsPrivate = true, EstimatedHours = 12, @@ -73,8 +73,8 @@ public async Task Should_Upload_Attachment() Uploads = attachments, Watchers = new List { - (Watcher) IdentifiableName.Create(8), - (Watcher) IdentifiableName.Create(2) + (Watcher) IdentifiableName.Create(8), + (Watcher) IdentifiableName.Create(2) } }; diff --git a/tests/redmine-net-api.Tests/Tests/Async/UserAsyncTests.cs b/tests/redmine-net-api.Tests/Tests/Async/UserAsyncTests.cs index 3570e5ae..f3690f44 100644 --- a/tests/redmine-net-api.Tests/Tests/Async/UserAsyncTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Async/UserAsyncTests.cs @@ -178,7 +178,7 @@ public async Task Should_Create_User() }; - var icf = (IssueCustomField)IdentifiableName.Create(4); + var icf = (IssueCustomField)IdentifiableName.Create(4); icf.Values = new List { new CustomFieldValue { Info = "userTestCustomField:" + DateTime.UtcNow } }; user.CustomFields = new List(); diff --git a/tests/redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs index b5debca6..014fd02a 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs @@ -89,7 +89,7 @@ public void Should_Upload_Attachment() var issue = new Issue { - Project = IdentifiableName.Create(PROJECT_ID ), + Project = IdentifiableName.Create(PROJECT_ID ), Subject = ISSUE_SUBJECT, Uploads = attachments }; diff --git a/tests/redmine-net-api.Tests/Tests/Sync/GroupTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/GroupTests.cs index 2129a0bf..eae41a6f 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/GroupTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/GroupTests.cs @@ -33,7 +33,7 @@ public void Should_Add_Group() var group = new Group(); group.Name = NEW_GROUP_NAME; - group.Users = new List { (GroupUser)IdentifiableName.Create(NEW_GROUP_USER_ID )}; + group.Users = new List { (GroupUser)IdentifiableName.Create(NEW_GROUP_USER_ID )}; Group savedGroup = null; var exception = @@ -54,7 +54,7 @@ public void Should_Update_Group() var group = fixture.RedmineManager.GetObject(UPDATED_GROUP_ID, new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.USERS } }); group.Name = UPDATED_GROUP_NAME; - group.Users.Add((GroupUser)IdentifiableName.Create(UPDATED_GROUP_USER_ID)); + group.Users.Add((GroupUser)IdentifiableName.Create(UPDATED_GROUP_USER_ID)); fixture.RedmineManager.UpdateObject(UPDATED_GROUP_ID, group); diff --git a/tests/redmine-net-api.Tests/Tests/Sync/IssueCategoryTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/IssueCategoryTests.cs index f0346abf..fa47d282 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/IssueCategoryTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/IssueCategoryTests.cs @@ -31,7 +31,7 @@ public void Should_Create_IssueCategory() var issueCategory = new IssueCategory { Name = NEW_ISSUE_CATEGORY_NAME, - AssignTo = IdentifiableName.Create(NEW_ISSUE_CATEGORY_ASIGNEE_ID) + AssignTo = IdentifiableName.Create(NEW_ISSUE_CATEGORY_ASIGNEE_ID) }; var savedIssueCategory = fixture.RedmineManager.CreateObject(issueCategory, PROJECT_ID); @@ -95,7 +95,7 @@ public void Should_Update_IssueCategory() var issueCategory = fixture.RedmineManager.GetObject(createdIssueCategoryId, null); issueCategory.Name = ISSUE_CATEGORY_NAME_TO_UPDATE; - issueCategory.AssignTo = IdentifiableName.Create(ISSUE_CATEGORY_ASIGNEE_ID_TO_UPDATE); + issueCategory.AssignTo = IdentifiableName.Create(ISSUE_CATEGORY_ASIGNEE_ID_TO_UPDATE); fixture.RedmineManager.UpdateObject(createdIssueCategoryId, issueCategory); diff --git a/tests/redmine-net-api.Tests/Tests/Sync/IssueTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/IssueTests.cs index 14601c11..ab3b58bd 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/IssueTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/IssueTests.cs @@ -138,9 +138,9 @@ public void Should_Add_Issue() { const bool NEW_ISSUE_IS_PRIVATE = true; - const int NEW_ISSUE_PROJECT_ID = 9; - const int NEW_ISSUE_TRACKER_ID = 3; - const int NEW_ISSUE_STATUS_ID = 6; + const int NEW_ISSUE_PROJECT_ID = 1; + const int NEW_ISSUE_TRACKER_ID = 1; + const int NEW_ISSUE_STATUS_ID = 1; const int NEW_ISSUE_PRIORITY_ID = 9; const int NEW_ISSUE_CATEGORY_ID = 18; const int NEW_ISSUE_FIXED_VERSION_ID = 9; @@ -158,39 +158,43 @@ public void Should_Add_Issue() var newIssueStartDate = DateTime.Now; var newIssueDueDate = DateTime.Now.AddDays(10); - var icf = (IssueCustomField)IdentifiableName.Create(NEW_ISSUE_CUSTOM_FIELD_ID); - icf.Values = new List { new CustomFieldValue { Info = NEW_ISSUE_CUSTOM_FIELD_VALUE } }; + var icf = IdentifiableName.Create(NEW_ISSUE_CUSTOM_FIELD_ID); + if (icf != null) + { + icf.Values = new List {new CustomFieldValue {Info = NEW_ISSUE_CUSTOM_FIELD_VALUE}}; + } var issue = new Issue - { - Project = IdentifiableName.Create(NEW_ISSUE_PROJECT_ID), - Tracker = IdentifiableName.Create(NEW_ISSUE_TRACKER_ID), - Status = IdentifiableName.Create(NEW_ISSUE_STATUS_ID), - Priority = IdentifiableName.Create(NEW_ISSUE_PRIORITY_ID), - Subject = NEW_ISSUE_SUBJECT, - Description = NEW_ISSUE_DESCRIPTION, - Category = IdentifiableName.Create(NEW_ISSUE_CATEGORY_ID), - FixedVersion = IdentifiableName.Create(NEW_ISSUE_FIXED_VERSION_ID), - AssignedTo = IdentifiableName.Create(NEW_ISSUE_ASSIGNED_TO_ID), - ParentIssue = IdentifiableName.Create(NEW_ISSUE_PARENT_ISSUE_ID), - - CustomFields = new List { icf }, - IsPrivate = NEW_ISSUE_IS_PRIVATE, - EstimatedHours = NEW_ISSUE_ESTIMATED_HOURS, - StartDate = newIssueStartDate, - DueDate = newIssueDueDate, - Watchers = new List { - (Watcher) IdentifiableName.Create(NEW_ISSUE_FIRST_WATCHER_ID), - (Watcher) IdentifiableName.Create(NEW_ISSUE_SECOND_WATCHER_ID) - } - }; - - var savedIssue = fixture.RedmineManager.CreateObject(issue); - - Assert.NotNull(savedIssue); - Assert.True(issue.Subject.Equals(savedIssue.Subject), "Issue subject is invalid."); - Assert.NotEqual(issue, savedIssue); + Project = IdentifiableName.Create(NEW_ISSUE_PROJECT_ID), + Tracker = IdentifiableName.Create(NEW_ISSUE_TRACKER_ID), + Status = IdentifiableName.Create(NEW_ISSUE_STATUS_ID), + Priority = IdentifiableName.Create(NEW_ISSUE_PRIORITY_ID), + Subject = NEW_ISSUE_SUBJECT, + Description = NEW_ISSUE_DESCRIPTION, + Category = IdentifiableName.Create(NEW_ISSUE_CATEGORY_ID), + FixedVersion = IdentifiableName.Create(NEW_ISSUE_FIXED_VERSION_ID), + AssignedTo = IdentifiableName.Create(NEW_ISSUE_ASSIGNED_TO_ID), + ParentIssue = IdentifiableName.Create(NEW_ISSUE_PARENT_ISSUE_ID), + + CustomFields = new List {icf}, + IsPrivate = NEW_ISSUE_IS_PRIVATE, + EstimatedHours = NEW_ISSUE_ESTIMATED_HOURS, + StartDate = newIssueStartDate, + DueDate = newIssueDueDate, + Watchers = new List + { + IdentifiableName.Create(NEW_ISSUE_FIRST_WATCHER_ID), + IdentifiableName.Create(NEW_ISSUE_SECOND_WATCHER_ID) + } + }; + + var savedIssue = fixture.RedmineManager.CreateObject(issue); + + Assert.NotNull(savedIssue); + Assert.True(issue.Subject.Equals(savedIssue.Subject), "Issue subject is invalid."); + Assert.NotEqual(issue, savedIssue); + } [Fact, Order(12)] @@ -224,14 +228,14 @@ public void Should_Update_Issue() issue.Description = UPDATED_ISSUE_DESCRIPTION; issue.StartDate = updatedIssueStartDate; issue.DueDate = updatedIssueDueDate; - issue.Project = IdentifiableName.Create(UPDATED_ISSUE_PROJECT_ID); - issue.Tracker = IdentifiableName.Create(UPDATED_ISSUE_TRACKER_ID); - issue.Priority = IdentifiableName.Create(UPDATED_ISSUE_PRIORITY_ID); - issue.Category = IdentifiableName.Create(UPDATED_ISSUE_CATEGORY_ID); - issue.AssignedTo = IdentifiableName.Create(UPDATED_ISSUE_ASSIGNED_TO_ID); - issue.ParentIssue = IdentifiableName.Create(UPDATED_ISSUE_PARENT_ISSUE_ID); - - var icf = (IssueCustomField)IdentifiableName.Create(UPDATED_ISSUE_CUSTOM_FIELD_ID); + issue.Project = IdentifiableName.Create(UPDATED_ISSUE_PROJECT_ID); + issue.Tracker = IdentifiableName.Create(UPDATED_ISSUE_TRACKER_ID); + issue.Priority = IdentifiableName.Create(UPDATED_ISSUE_PRIORITY_ID); + issue.Category = IdentifiableName.Create(UPDATED_ISSUE_CATEGORY_ID); + issue.AssignedTo = IdentifiableName.Create(UPDATED_ISSUE_ASSIGNED_TO_ID); + issue.ParentIssue = IdentifiableName.Create(UPDATED_ISSUE_PARENT_ISSUE_ID); + + var icf = (IssueCustomField)IdentifiableName.Create(UPDATED_ISSUE_CUSTOM_FIELD_ID); icf.Values = new List { new CustomFieldValue { Info = UPDATED_ISSUE_CUSTOM_FIELD_VALUE } }; issue.CustomFields?.Add(icf); @@ -293,7 +297,7 @@ public void Should_Clone_Issue() const int CLONED_ISSUE_CUSTOM_FIELD_ID = 13; const string CLONED_ISSUE_CUSTOM_FIELD_VALUE = "Cloned issue custom field value"; - var icfc = (IssueCustomField)IdentifiableName.Create(ISSUE_TO_CLONE_CUSTOM_FIELD_ID); + var icfc = (IssueCustomField)IdentifiableName.Create(ISSUE_TO_CLONE_CUSTOM_FIELD_ID); icfc.Values = new List { new CustomFieldValue { Info = ISSUE_TO_CLONE_CUSTOM_FIELD_VALUE } }; var issueToClone = new Issue @@ -304,7 +308,7 @@ public void Should_Clone_Issue() var clonedIssue = (Issue)issueToClone.Clone(); - var icf = (IssueCustomField)IdentifiableName.Create(CLONED_ISSUE_CUSTOM_FIELD_ID); + var icf = (IssueCustomField)IdentifiableName.Create(CLONED_ISSUE_CUSTOM_FIELD_ID); icf.Values = new List { new CustomFieldValue { Info = CLONED_ISSUE_CUSTOM_FIELD_VALUE } }; clonedIssue.CustomFields.Add(icf); @@ -312,7 +316,6 @@ public void Should_Clone_Issue() Assert.True(issueToClone.CustomFields.Count != clonedIssue.CustomFields.Count); } - [Fact] public void Should_Get_Issue_With_Hours() { diff --git a/tests/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs index 767849f4..85858563 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs @@ -48,8 +48,8 @@ public void Should_Add_Project_Membership() var pm = new ProjectMembership { - User = IdentifiableName.Create(NEW_PROJECT_MEMBERSHIP_USER_ID), - Roles = new List { (MembershipRole)IdentifiableName.Create(NEW_PROJECT_MEMBERSHIP_ROLE_ID)} + User = IdentifiableName.Create(NEW_PROJECT_MEMBERSHIP_USER_ID), + Roles = new List { (MembershipRole)IdentifiableName.Create(NEW_PROJECT_MEMBERSHIP_ROLE_ID)} }; var createdPm = fixture.RedmineManager.CreateObject(pm, PROJECT_IDENTIFIER); @@ -110,7 +110,7 @@ public void Should_Update_Project_Membership() const int UPDATED_PROJECT_MEMBERSHIP_ROLE_ID = 4; var pm = fixture.RedmineManager.GetObject(UPDATED_PROJECT_MEMBERSHIP_ID, null); - pm.Roles.Add((MembershipRole)IdentifiableName.Create(UPDATED_PROJECT_MEMBERSHIP_ROLE_ID)); + pm.Roles.Add((MembershipRole)IdentifiableName.Create(UPDATED_PROJECT_MEMBERSHIP_ROLE_ID)); fixture.RedmineManager.UpdateObject(UPDATED_PROJECT_MEMBERSHIP_ID, pm); diff --git a/tests/redmine-net-api.Tests/Tests/Sync/ProjectTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/ProjectTests.cs index e1a79c98..acee0878 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/ProjectTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/ProjectTests.cs @@ -69,8 +69,8 @@ private static Project CreateTestProjectWithAllPropertiesSet() }, Trackers = new List { - (ProjectTracker) IdentifiableName.Create( 1), - (ProjectTracker) IdentifiableName.Create(2) + (ProjectTracker) IdentifiableName.Create( 1), + (ProjectTracker) IdentifiableName.Create(2) } }; @@ -85,8 +85,8 @@ private static Project CreateTestProjectWithInvalidTrackersId() Identifier = "rnaptit", Trackers = new List { - (ProjectTracker) IdentifiableName.Create(999999), - (ProjectTracker) IdentifiableName.Create(999998) + (ProjectTracker) IdentifiableName.Create(999999), + (ProjectTracker) IdentifiableName.Create(999998) } }; @@ -99,7 +99,7 @@ private static Project CreateTestProjectWithParentSet(int parentId) { Name = "Redmine Net Api Project With Parent Set", Identifier = "rnapwps", - Parent = IdentifiableName.Create(parentId) + Parent = IdentifiableName.Create(parentId) }; return project; diff --git a/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs index 8605e5c7..e21458a3 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs @@ -47,11 +47,11 @@ public void Should_Create_Time_Entry() var timeEntry = new TimeEntry { - Issue = IdentifiableName.Create(NEW_TIME_ENTRY_ISSUE_ID), - Project = IdentifiableName.Create(NEW_TIME_ENTRY_PROJECT_ID), + Issue = IdentifiableName.Create(NEW_TIME_ENTRY_ISSUE_ID), + Project = IdentifiableName.Create(NEW_TIME_ENTRY_PROJECT_ID), SpentOn = newTimeEntryDate, Hours = NEW_TIME_ENTRY_HOURS, - Activity = IdentifiableName.Create(NEW_TIME_ENTRY_ACTIVITY_ID), + Activity = IdentifiableName.Create(NEW_TIME_ENTRY_ACTIVITY_ID), Comments = NEW_TIME_ENTRY_COMMENTS }; @@ -119,13 +119,13 @@ public void Should_Update_Time_Entry() var updatedTimeEntryDate = DateTime.Now.AddDays(-2); var timeEntry = fixture.RedmineManager.GetObject(UPDATED_TIME_ENTRY_ID, null); - timeEntry.Project = IdentifiableName.Create(UPDATED_TIME_ENTRY_PROJECT_ID); - timeEntry.Issue = IdentifiableName.Create(UPDATED_TIME_ENTRY_ISSUE_ID); + timeEntry.Project = IdentifiableName.Create(UPDATED_TIME_ENTRY_PROJECT_ID); + timeEntry.Issue = IdentifiableName.Create(UPDATED_TIME_ENTRY_ISSUE_ID); timeEntry.SpentOn = updatedTimeEntryDate; timeEntry.Hours = UPDATED_TIME_ENTRY_HOURS; timeEntry.Comments = UPDATED_TIME_ENTRY_COMMENTS; - if (timeEntry.Activity == null) timeEntry.Activity = IdentifiableName.Create(UPDATED_TIME_ENTRY_ACTIVITY_ID); + if (timeEntry.Activity == null) timeEntry.Activity = IdentifiableName.Create(UPDATED_TIME_ENTRY_ACTIVITY_ID); fixture.RedmineManager.UpdateObject(UPDATED_TIME_ENTRY_ID, timeEntry); From 193d614f338dad0008c533f5deb5a6e76ecd1141 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 2 May 2020 21:25:28 +0300 Subject: [PATCH 150/549] Small refactoring --- src/redmine-net-api/RedmineManager.cs | 13 ++++++-- src/redmine-net-api/RedmineWebClient.cs | 43 +++++++++---------------- 2 files changed, 26 insertions(+), 30 deletions(-) diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index 8652dd72..6c78e828 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -819,6 +819,8 @@ public byte[] DownloadFile(string address) return WebApiHelper.ExecuteDownloadFile(this, address); } + private const string UA = "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.163 Safari/535.1"; + /// /// Creates the Redmine web client. /// @@ -828,8 +830,8 @@ public byte[] DownloadFile(string address) /// public virtual RedmineWebClient CreateWebClient(NameValueCollection parameters, bool uploadFile = false) { - var webClient = new RedmineWebClient { Proxy = Proxy, Scheme = Scheme, RedmineSerializer = Serializer}; - + var webClient = new RedmineWebClient { Scheme = Scheme, RedmineSerializer = Serializer}; + webClient.UserAgent = UA; if (!uploadFile) { webClient.Headers.Add(HttpRequestHeader.ContentType, MimeFormat == MimeFormat.Xml @@ -866,6 +868,13 @@ public virtual RedmineWebClient CreateWebClient(NameValueCollection parameters, } } + if (Proxy != null) + { + Proxy.Credentials = cache; + webClient.Proxy = Proxy; + webClient.UseProxy = true; + } + if (!string.IsNullOrEmpty(ImpersonateUser)) { webClient.Headers.Add("X-Redmine-Switch-User", ImpersonateUser); diff --git a/src/redmine-net-api/RedmineWebClient.cs b/src/redmine-net-api/RedmineWebClient.cs index 12ae7278..4784ca00 100644 --- a/src/redmine-net-api/RedmineWebClient.cs +++ b/src/redmine-net-api/RedmineWebClient.cs @@ -26,16 +26,8 @@ namespace Redmine.Net.Api /// public class RedmineWebClient : WebClient { - private const string UA = "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.163 Safari/535.1"; private string redirectUrl = string.Empty; - /// - /// - /// - public RedmineWebClient() - { - UserAgent = UA; - } - + /// /// /// @@ -120,37 +112,24 @@ protected override WebRequest GetWebRequest(Uri address) return base.GetWebRequest(address); } + httpWebRequest.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | + DecompressionMethods.None; + if (UseCookies) { httpWebRequest.Headers.Add(HttpRequestHeader.Cookie, "redmineCookie"); httpWebRequest.CookieContainer = CookieContainer; } - - httpWebRequest.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | - DecompressionMethods.None; - httpWebRequest.PreAuthenticate = PreAuthenticate; + httpWebRequest.KeepAlive = KeepAlive; - httpWebRequest.Credentials = Credentials; - httpWebRequest.UseDefaultCredentials = (httpWebRequest.Credentials == null); - httpWebRequest.UserAgent = UA; httpWebRequest.CachePolicy = CachePolicy; - - if (UseProxy) - { - if (Proxy != null) - { - Proxy.Credentials = Credentials; - httpWebRequest.Proxy = Proxy; - } - } - + if (Timeout != null) { httpWebRequest.Timeout = Timeout.Value.Milliseconds; } - + return httpWebRequest; - } /// @@ -253,6 +232,9 @@ protected void HandleCookies(WebRequest request, WebResponse response) if (!(response is HttpWebResponse webResponse)) return; var webRequest = request as HttpWebRequest; + + if (webResponse.Cookies.Count <= 0) return; + var col = new CookieCollection(); foreach (Cookie c in webResponse.Cookies) @@ -260,6 +242,11 @@ protected void HandleCookies(WebRequest request, WebResponse response) col.Add(new Cookie(c.Name, c.Value, c.Path, webRequest?.Headers["Host"])); } + if (CookieContainer == null) + { + CookieContainer = new CookieContainer(); + } + CookieContainer.Add(col); } } From cbd51b985546f7b8a2f91382249e3e5b8379af8f Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 2 May 2020 21:33:47 +0300 Subject: [PATCH 151/549] Fix #260 --- src/redmine-net-api/RedmineManager.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index 6c78e828..14ec971c 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -579,7 +579,14 @@ public void DeleteWikiPage(string projectId, string pageName) /// Returns the complete list of objects. public List GetObjects(params string[] include) where T : class, new() { - return GetObjects(PageSize, 0, include); + var parameters = new NameValueCollection(); + + if (include != null && include.Length > 0) + { + parameters.Add(RedmineKeys.INCLUDE, string.Join(",", include)); + } + + return GetObjects(parameters); } /// From c337927ec800208f9c619213b0e31be8bfa76a93 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 2 May 2020 21:35:17 +0300 Subject: [PATCH 152/549] Change redmine & postgres version --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 78e1e2f4..e7a53c7d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: redmine: ports: - '8089:3000' - image: 'redmine:4.0.4' + image: 'redmine:4.1.1-alpine' container_name: 'redmine-web' depends_on: - db-postgres @@ -33,7 +33,7 @@ services: POSTGRES_USER: redmine-usr POSTGRES_PASSWORD: redmine-pswd container_name: 'redmine-db' - image: 'postgres:11.1' + image: 'postgres:11.1-alpine' healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 20s From 471c8dc440bd269e39bd58f3c96d34231657ad6b Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 3 May 2020 14:55:47 +0300 Subject: [PATCH 153/549] Add pragma CA1822 --- src/redmine-net-api/Serialization/JsonRedmineSerializer.cs | 4 +++- src/redmine-net-api/Serialization/XmlRedmineSerializer.cs | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/redmine-net-api/Serialization/JsonRedmineSerializer.cs b/src/redmine-net-api/Serialization/JsonRedmineSerializer.cs index ba98e836..5e5afde7 100644 --- a/src/redmine-net-api/Serialization/JsonRedmineSerializer.cs +++ b/src/redmine-net-api/Serialization/JsonRedmineSerializer.cs @@ -85,6 +85,7 @@ internal sealed class JsonRedmineSerializer : IRedmineSerializer } } + #pragma warning disable CA1822 public int Count(string jsonResponse) where T : class, new() { if (jsonResponse.IsNullOrWhiteSpace()) @@ -116,7 +117,8 @@ internal sealed class JsonRedmineSerializer : IRedmineSerializer } } } - + #pragma warning restore CA1822 + public string Type { get; } = "json"; public string Serialize(T entity) where T : class diff --git a/src/redmine-net-api/Serialization/XmlRedmineSerializer.cs b/src/redmine-net-api/Serialization/XmlRedmineSerializer.cs index 0bed6bef..99149526 100644 --- a/src/redmine-net-api/Serialization/XmlRedmineSerializer.cs +++ b/src/redmine-net-api/Serialization/XmlRedmineSerializer.cs @@ -50,6 +50,7 @@ public XmlRedmineSerializer(XmlWriterSettings xmlWriterSettings) } } +#pragma warning disable CA1822 public int Count(string xmlResponse) where T : class, new() { try @@ -62,6 +63,7 @@ public XmlRedmineSerializer(XmlWriterSettings xmlWriterSettings) throw new RedmineException(ex.Message, ex); } } +#pragma warning restore CA1822 public string Type { get; } = "xml"; From 3034579976588dc56728673c234afaa89101e50b Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 3 May 2020 14:54:14 +0300 Subject: [PATCH 154/549] Add props files. --- Directory.Build.props | 39 ++++++++++++++++++++++ redmine-net-api.sln | 5 +++ releasenotes.props | 24 +++++++++++++ signing.props | 6 ++++ src/redmine-net-api/redmine-net-api.csproj | 36 +------------------- version.props | 7 ++++ 6 files changed, 82 insertions(+), 35 deletions(-) create mode 100644 Directory.Build.props create mode 100644 releasenotes.props create mode 100644 signing.props create mode 100644 version.props diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 00000000..8ded1ea7 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,39 @@ + + + + + + Adrian Popescu + Redmine Api is a .NET rest client for Redmine. + p.adi + Adrian Popescu, 2011 - $([System.DateTime]::Now.Year.ToString()) + en-US + + redmine-api + redmine-api-signed + https://raw.githubusercontent.com/zapadi/redmine-net-api/master/logo.png + logo.png + LICENSE + Apache-2.0 + https://github.com/zapadi/redmine-net-api + true + Redmine; REST; API; Client; .NET; Adrian Popescu; + Redmine .NET API Client + git + https://github.com/zapadi/redmine-net-api + ... + Redmine .NET API Client + + + + + + false + $(SolutionDir)/.artifacts + + + + + + + \ No newline at end of file diff --git a/redmine-net-api.sln b/redmine-net-api.sln index 65e353b8..d81f37a3 100644 --- a/redmine-net-api.sln +++ b/redmine-net-api.sln @@ -22,6 +22,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionFolder", "SolutionF redmine-net-api.snk = redmine-net-api.snk ISSUE_TEMPLATE.md = ISSUE_TEMPLATE.md PULL_REQUEST_TEMPLATE.md = PULL_REQUEST_TEMPLATE.md + CHANGELOG.md = CHANGELOG.md + Directory.Build.props = Directory.Build.props + releasenotes.props = releasenotes.props + signing.props = signing.props + version.props = version.props EndProjectSection EndProject Global diff --git a/releasenotes.props b/releasenotes.props new file mode 100644 index 00000000..7a3e84ae --- /dev/null +++ b/releasenotes.props @@ -0,0 +1,24 @@ + + + + (id) in order to create identifiablename types with id. + +Features: +* Add support for .NET Standard 2.0 and 2.1 + +Fixes: +* Trackers - Cannot retreive List of trackers: Malformed objects (#265) (thanks NecatiMeral) +* IssueRelation - `relation_type` cannot be parsed (#263) (thanks NecatiMeral) +* Issue with 'relates" relation (#262) (thanks NecatiMeral) +* RedmineManager.GetObjects<>(params string[]) only retrieves 25 objects (#260) +* Unexpected ArgumentNullException in RedmineManager.cs:581 (#259) +]]> + + $(PackageReleaseNotes) + See $(PackageProjectUrl)/blob/master/CHANGELOG.md#v$(VersionPrefix.Replace('.','')) for more details. + + \ No newline at end of file diff --git a/signing.props b/signing.props new file mode 100644 index 00000000..1de15736 --- /dev/null +++ b/signing.props @@ -0,0 +1,6 @@ + + + true + ..\..\redmine-net-api.snk + + \ No newline at end of file diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index c443405a..c66c517b 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -1,4 +1,4 @@ - + @@ -25,40 +25,6 @@ - - Adrian Popescu - Redmine Api is a .NET rest client for Redmine. - p.adi - Adrian Popescu, 2011 - $([System.DateTime]::Now.Year.ToString()) - 1.0.0 - en-US - redmine-api - redmine-api-signed - https://raw.githubusercontent.com/zapadi/redmine-net-api/master/logo.png - logo.png - LICENSE - https://github.com/zapadi/redmine-net-api - true - - Add redmine-net-api.snk - Fix #242 - Invalid URI: The Uri scheme is too long - Fix package icon url. - - Redmine; REST; API; Client; .NET; Adrian Popescu; - Redmine .NET API Client - git - https://github.com/zapadi/redmine-net-api - ... - Redmine .NET API Client - 3.0.6 - 3.0.6.1 - - - - true - ..\..\redmine-net-api.snk - - NET20;NETFULL diff --git a/version.props b/version.props new file mode 100644 index 00000000..384aa58a --- /dev/null +++ b/version.props @@ -0,0 +1,7 @@ + + + 4.0.0 + $(VersionPrefix) + $(VersionPrefix)-$(VersionSuffix) + + \ No newline at end of file From ad3cc0a9e91f8f1e541ad7ed32db4954a053e7e5 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 3 May 2020 14:55:13 +0300 Subject: [PATCH 155/549] Add dotnetstandard 2.0 & 2.1 frameworks --- src/redmine-net-api/redmine-net-api.csproj | 26 ++++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index c66c517b..58333434 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -2,8 +2,7 @@ - - net20;net40;net45;net451;net452;net46;net461;net462;net47;net471;net472;net48; + net20;net40;net45;net451;net452;net46;net461;net462;net47;net471;net472;net48;netstandard2.0;netstandard2.1 false Redmine.Net.Api redmine-net-api @@ -12,6 +11,7 @@ TRACE Debug;Release;DebugJson PackageReference + 7.3 NU5105; CA1303; @@ -73,6 +73,18 @@ NET48;NETFULL + + NETSTANDARD13;NETSTANDARD + + + + NETSTANDARD20;NETSTANDARD + + + + NETSTANDARD21;NETSTANDARD + + all @@ -146,14 +158,14 @@ - - + + - redmine-net-api.snk - + redmine-net-api.snk + @@ -166,4 +178,4 @@ - + \ No newline at end of file From 8a6d286933fd7ae3f9481d849c39cda56c66b22d Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 3 May 2020 14:01:50 +0300 Subject: [PATCH 156/549] Add CHANGELOG.md --- src/CHANGELOG.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/CHANGELOG.md diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md new file mode 100644 index 00000000..9dc60cd4 --- /dev/null +++ b/src/CHANGELOG.md @@ -0,0 +1,28 @@ +# Changelog + +## [v4.0.0] + +Features: + +* Add support for .NET Standard 2.0 and 2.1 + +Fixes: + +* Trackers - Cannot retreive List of trackers: Malformed objects (#265) (thanks NecatiMeral) +* IssueRelation - `relation_type` cannot be parsed (#263) (thanks NecatiMeral) +* Issue with 'relates" relation (#262) (thanks NecatiMeral) +* RedmineManager.GetObjects<>(params string[]) only retrieves 25 objects (#260) +* Unexpected ArgumentNullException in RedmineManager.cs:581 (#259) +* Type Issue and its property ParentIssue have no matching base-type (#258) +* Cannot set the name of the project (#257) +* Cant create new issue (#256) +* Help me with create issues api redmine. Error is The property or indexer 'Identifiable.Id' cannot be used in this context because the set accessor is inaccessible (#254) +* Version 3.0.6.1 makes IssueCustomField.Info readonly breaking existing usage (#253) +* Empty response on CreateOrUpdateWikiPage (#245) +* Cannot set the status of a project (#255) +* Could not deserialize null!' When update WikiPage (#225) + +Breaking Changes: + +* Split CreateOrUpdateWikiPage into CreateWikiPage & UpdateWikiPage +* Add IdentifiableName.Create(id) in order to create identifiablename types with id. \ No newline at end of file From 0956529845b1df71c576c4a8a56ae5f253623b23 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 3 May 2020 15:04:30 +0300 Subject: [PATCH 157/549] Move CHANGELOG.md to solution folder --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..9dc60cd4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,28 @@ +# Changelog + +## [v4.0.0] + +Features: + +* Add support for .NET Standard 2.0 and 2.1 + +Fixes: + +* Trackers - Cannot retreive List of trackers: Malformed objects (#265) (thanks NecatiMeral) +* IssueRelation - `relation_type` cannot be parsed (#263) (thanks NecatiMeral) +* Issue with 'relates" relation (#262) (thanks NecatiMeral) +* RedmineManager.GetObjects<>(params string[]) only retrieves 25 objects (#260) +* Unexpected ArgumentNullException in RedmineManager.cs:581 (#259) +* Type Issue and its property ParentIssue have no matching base-type (#258) +* Cannot set the name of the project (#257) +* Cant create new issue (#256) +* Help me with create issues api redmine. Error is The property or indexer 'Identifiable.Id' cannot be used in this context because the set accessor is inaccessible (#254) +* Version 3.0.6.1 makes IssueCustomField.Info readonly breaking existing usage (#253) +* Empty response on CreateOrUpdateWikiPage (#245) +* Cannot set the status of a project (#255) +* Could not deserialize null!' When update WikiPage (#225) + +Breaking Changes: + +* Split CreateOrUpdateWikiPage into CreateWikiPage & UpdateWikiPage +* Add IdentifiableName.Create(id) in order to create identifiablename types with id. \ No newline at end of file From 5131740d7723e5f63c3be7b4b6a5743720cf0ff0 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 3 May 2020 16:14:40 +0300 Subject: [PATCH 158/549] Cleanup --- Directory.Build.props | 2 +- src/CHANGELOG.md | 28 ------------------- .../Async/RedmineManagerAsync40.cs | 8 ++++++ .../Extensions/CollectionExtensions.cs | 2 ++ src/redmine-net-api/RedmineManager.cs | 15 ++++++++-- src/redmine-net-api/Types/IssueRelation.cs | 2 +- tests/redmine-net-api.Tests/TestHelper.cs | 2 +- tests/redmine-net-api.Tests/appsettings.json | 6 ++++ .../redmine-net-api.Tests.csproj | 2 +- 9 files changed, 32 insertions(+), 35 deletions(-) delete mode 100644 src/CHANGELOG.md diff --git a/Directory.Build.props b/Directory.Build.props index 8ded1ea7..88b08f71 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -29,7 +29,7 @@ false - $(SolutionDir)/.artifacts + $(SolutionDir)/artifacts diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md deleted file mode 100644 index 9dc60cd4..00000000 --- a/src/CHANGELOG.md +++ /dev/null @@ -1,28 +0,0 @@ -# Changelog - -## [v4.0.0] - -Features: - -* Add support for .NET Standard 2.0 and 2.1 - -Fixes: - -* Trackers - Cannot retreive List of trackers: Malformed objects (#265) (thanks NecatiMeral) -* IssueRelation - `relation_type` cannot be parsed (#263) (thanks NecatiMeral) -* Issue with 'relates" relation (#262) (thanks NecatiMeral) -* RedmineManager.GetObjects<>(params string[]) only retrieves 25 objects (#260) -* Unexpected ArgumentNullException in RedmineManager.cs:581 (#259) -* Type Issue and its property ParentIssue have no matching base-type (#258) -* Cannot set the name of the project (#257) -* Cant create new issue (#256) -* Help me with create issues api redmine. Error is The property or indexer 'Identifiable.Id' cannot be used in this context because the set accessor is inaccessible (#254) -* Version 3.0.6.1 makes IssueCustomField.Info readonly breaking existing usage (#253) -* Empty response on CreateOrUpdateWikiPage (#245) -* Cannot set the status of a project (#255) -* Could not deserialize null!' When update WikiPage (#225) - -Breaking Changes: - -* Split CreateOrUpdateWikiPage into CreateWikiPage & UpdateWikiPage -* Add IdentifiableName.Create(id) in order to create identifiablename types with id. \ No newline at end of file diff --git a/src/redmine-net-api/Async/RedmineManagerAsync40.cs b/src/redmine-net-api/Async/RedmineManagerAsync40.cs index 3d2a4f06..5b69444d 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync40.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync40.cs @@ -54,6 +54,14 @@ public static Task CreateWikiPageAsync(this RedmineManager redmineMana return Task.Factory.StartNew(() => redmineManager.CreateWikiPage(projectId, pageName, wikiPage), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); } + /// + /// + /// + /// + /// + /// + /// + /// public static Task UpdateWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage) { return Task.Factory.StartNew(() => redmineManager.UpdateWikiPage(projectId, pageName, wikiPage), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); diff --git a/src/redmine-net-api/Extensions/CollectionExtensions.cs b/src/redmine-net-api/Extensions/CollectionExtensions.cs index 07d3b16b..22575397 100755 --- a/src/redmine-net-api/Extensions/CollectionExtensions.cs +++ b/src/redmine-net-api/Extensions/CollectionExtensions.cs @@ -38,7 +38,9 @@ public static IList Clone(this IList listToClone) where T : ICloneable if (listToClone == null) return null; IList clonedList = new List(); foreach (var item in listToClone) + { clonedList.Add((T) item.Clone()); + } return clonedList; } diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index 14ec971c..b5eb7573 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2019 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); @@ -599,7 +599,7 @@ public void DeleteWikiPage(string projectId, string pageName) /// public List GetObjects(NameValueCollection parameters) where T : class, new() { - int totalCount = 0, pageSize = 0, offset = 0; + int pageSize = 0, offset = 0; var isLimitSet = false; List resultList = null; @@ -623,6 +623,7 @@ public void DeleteWikiPage(string projectId, string pageName) var hasOffset = TypesWithOffset.ContainsKey(typeof(T)); if (hasOffset) { + var totalCount = 0; do { parameters.Set(RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)); @@ -901,7 +902,15 @@ public virtual RedmineWebClient CreateWebClient(NameValueCollection parameters, /// public virtual bool RemoteCertValidate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors sslPolicyErrors) { - return sslPolicyErrors == SslPolicyErrors.None; + const SslPolicyErrors ignoredErrors = + SslPolicyErrors.RemoteCertificateChainErrors | + SslPolicyErrors.RemoteCertificateNameMismatch; + + if ((sslPolicyErrors & ~ignoredErrors) == SslPolicyErrors.None) + { + return true; + } + return false; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/IssueRelation.cs b/src/redmine-net-api/Types/IssueRelation.cs index 0c7125b8..c9349001 100644 --- a/src/redmine-net-api/Types/IssueRelation.cs +++ b/src/redmine-net-api/Types/IssueRelation.cs @@ -193,7 +193,7 @@ private void AssertValidIssueRelationType() } } - private IssueRelationType ReadIssueRelationType(JsonReader reader) + private static IssueRelationType ReadIssueRelationType(JsonReader reader) { var enumValue = reader.ReadAsString(); if (enumValue.IsNullOrWhiteSpace()) diff --git a/tests/redmine-net-api.Tests/TestHelper.cs b/tests/redmine-net-api.Tests/TestHelper.cs index 377f9ab6..c49785c6 100644 --- a/tests/redmine-net-api.Tests/TestHelper.cs +++ b/tests/redmine-net-api.Tests/TestHelper.cs @@ -31,7 +31,7 @@ public static RedmineCredentials GetApplicationConfiguration(string outputPath = var iConfig = GetIConfigurationRoot(outputPath); iConfig - .GetSection("Credentials") + .GetSection("Credentials-Local") .Bind(credentials); return credentials; diff --git a/tests/redmine-net-api.Tests/appsettings.json b/tests/redmine-net-api.Tests/appsettings.json index 9b28a4ca..75421bd0 100644 --- a/tests/redmine-net-api.Tests/appsettings.json +++ b/tests/redmine-net-api.Tests/appsettings.json @@ -4,5 +4,11 @@ "ApiKey": "$ApiKey", "Username": "$Username", "Password": "$Password" + }, + "Credentials-Local":{ + "Uri": "$Uri", + "ApiKey": "$ApiKey", + "Username": "$Username", + "Password": "$Password" } } diff --git a/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj index 70a6b1d5..8fe1b797 100644 --- a/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj +++ b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj @@ -8,7 +8,7 @@ net451;net452;net46;net461;net462;net47;net471;net472;net48; false Padi.RedmineApi.Tests - Padi.RedmineApi.Tests + f8b9e946-b547-42f1-861c-f719dca00a84 Release;Debug;DebugJson From 46c25e525d78a6aa0ae8b2d010f7f76812013b60 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 3 May 2020 18:15:27 +0300 Subject: [PATCH 159/549] Fix nuget packageid issue --- redmine-net-api.sln | 10 ++++------ .../redmine-net-api.Tests/redmine-net-api.Tests.csproj | 3 +++ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/redmine-net-api.sln b/redmine-net-api.sln index d81f37a3..cd0dfab4 100644 --- a/redmine-net-api.sln +++ b/redmine-net-api.sln @@ -1,4 +1,3 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.29503.13 @@ -14,16 +13,16 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionFolder", "SolutionFolder", "{E8C35EC2-DD90-46E8-9B63-84EFD5F2FDE3}" ProjectSection(SolutionItems) = preProject appveyor.yml = appveyor.yml + CHANGELOG.md = CHANGELOG.md CONTRIBUTING.md = CONTRIBUTING.md + Directory.Build.props = Directory.Build.props docker-compose.yml = docker-compose.yml + ISSUE_TEMPLATE.md = ISSUE_TEMPLATE.md LICENSE = LICENSE logo.png = logo.png + PULL_REQUEST_TEMPLATE.md = PULL_REQUEST_TEMPLATE.md README.md = README.md redmine-net-api.snk = redmine-net-api.snk - ISSUE_TEMPLATE.md = ISSUE_TEMPLATE.md - PULL_REQUEST_TEMPLATE.md = PULL_REQUEST_TEMPLATE.md - CHANGELOG.md = CHANGELOG.md - Directory.Build.props = Directory.Build.props releasenotes.props = releasenotes.props signing.props = signing.props version.props = version.props @@ -47,7 +46,6 @@ Global {900EF0B3-0233-45DA-811F-4C59483E8452}.DebugJson|Any CPU.ActiveCfg = DebugJson|Any CPU {900EF0B3-0233-45DA-811F-4C59483E8452}.DebugJson|Any CPU.Build.0 = DebugJson|Any CPU {900EF0B3-0233-45DA-811F-4C59483E8452}.Release|Any CPU.ActiveCfg = Debug|Any CPU - {900EF0B3-0233-45DA-811F-4C59483E8452}.Release|Any CPU.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj index 8fe1b797..4a4eaf5c 100644 --- a/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj +++ b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj @@ -11,6 +11,9 @@ f8b9e946-b547-42f1-861c-f719dca00a84 Release;Debug;DebugJson + + redmine-api-test + redmine-api-test-signed From 88e360c6e7354aef77da481fbb719689ab3e0bdf Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 3 May 2020 18:38:13 +0300 Subject: [PATCH 160/549] Fix appveyor build --- appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 541dc970..5ba3cbdc 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -49,8 +49,8 @@ before_build: - ps: dotnet --version build_script: - - ps: dotnet build redmine-net-api.sln -c Release --version-suffix=$env:BUILD_SUFFIX - - ps: dotnet build redmine-net-api.sln -c Release --version-suffix=$env:BUILD_SUFFIX -p:Sign=true + - ps: dotnet build redmine-net-api.sln -c Release --version-suffix=$env:BUILD_SUFFIX src\redmine-net-api\redmine-net-api.csproj + - ps: dotnet build redmine-net-api.sln -c Release --version-suffix=$env:BUILD_SUFFIX -p:Sign=true src\redmine-net-api\redmine-net-api.csproj after_build: - ps: dotnet pack src\redmine-net-api\redmine-net-api.csproj -c Release --output .\artifacts --include-symbols -p:SymbolPackageFormat=snupkg --no-build $env:VERSION_SUFFIX From 3a389af64729953d4766ab504804681a33f801dd Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 3 May 2020 18:40:46 +0300 Subject: [PATCH 161/549] Fix appveyor build(for real) --- appveyor.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 5ba3cbdc..69df924a 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -49,8 +49,8 @@ before_build: - ps: dotnet --version build_script: - - ps: dotnet build redmine-net-api.sln -c Release --version-suffix=$env:BUILD_SUFFIX src\redmine-net-api\redmine-net-api.csproj - - ps: dotnet build redmine-net-api.sln -c Release --version-suffix=$env:BUILD_SUFFIX -p:Sign=true src\redmine-net-api\redmine-net-api.csproj + - ps: dotnet build src\redmine-net-api\redmine-net-api.csproj -c Release --version-suffix=$env:BUILD_SUFFIX + - ps: dotnet build src\redmine-net-api\redmine-net-api.csproj -c Release --version-suffix=$env:BUILD_SUFFIX -p:Sign=true after_build: - ps: dotnet pack src\redmine-net-api\redmine-net-api.csproj -c Release --output .\artifacts --include-symbols -p:SymbolPackageFormat=snupkg --no-build $env:VERSION_SUFFIX From 60b19eb89b7dd5960ebc252fe2d7e2d58a144a69 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Sat, 6 Jun 2020 13:54:40 +0300 Subject: [PATCH 162/549] Fix #271 (thanks muffmolch) --- src/redmine-net-api/Types/Upload.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/redmine-net-api/Types/Upload.cs b/src/redmine-net-api/Types/Upload.cs index 6074fcf1..0a0e448a 100644 --- a/src/redmine-net-api/Types/Upload.cs +++ b/src/redmine-net-api/Types/Upload.cs @@ -142,10 +142,12 @@ public void ReadJson(JsonReader reader) /// public void WriteJson(JsonWriter writer) { + writer.WriteStartObject(); writer.WriteProperty(RedmineKeys.TOKEN, Token); writer.WriteProperty(RedmineKeys.CONTENT_TYPE, ContentType); writer.WriteProperty(RedmineKeys.FILE_NAME, FileName); writer.WriteProperty(RedmineKeys.DESCRIPTION, Description); + writer.WriteEndObject(); } #endregion From 77dc3f25ce483aecd18f54656775d5ad572201a0 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Sat, 6 Jun 2020 14:39:32 +0300 Subject: [PATCH 163/549] Bump up version, update release notes --- CHANGELOG.md | 6 ++++++ releasenotes.props | 16 ++-------------- version.props | 2 +- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dc60cd4..8addcde0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [v4.0.1] + +Fixes: + +* JSON serialization exception for issues with uploads (missing WriteStart/EndObject calls) (#271) (thanks muffmolch) + ## [v4.0.0] Features: diff --git a/releasenotes.props b/releasenotes.props index 7a3e84ae..2bc9e283 100644 --- a/releasenotes.props +++ b/releasenotes.props @@ -1,21 +1,9 @@ - + (id) in order to create identifiablename types with id. - -Features: -* Add support for .NET Standard 2.0 and 2.1 - Fixes: -* Trackers - Cannot retreive List of trackers: Malformed objects (#265) (thanks NecatiMeral) -* IssueRelation - `relation_type` cannot be parsed (#263) (thanks NecatiMeral) -* Issue with 'relates" relation (#262) (thanks NecatiMeral) -* RedmineManager.GetObjects<>(params string[]) only retrieves 25 objects (#260) -* Unexpected ArgumentNullException in RedmineManager.cs:581 (#259) +* JSON serialization exception for issues with uploads (missing WriteStart/EndObject calls) (#271) (thanks muffmolch) ]]> $(PackageReleaseNotes) diff --git a/version.props b/version.props index 384aa58a..f3437e57 100644 --- a/version.props +++ b/version.props @@ -1,6 +1,6 @@ - 4.0.0 + 4.0.1 $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix) From fa2004b7b34131f6f9f63b1d1a0f2fa641054be8 Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 8 Jul 2020 10:12:02 +0300 Subject: [PATCH 164/549] Add fix #236 to the current version --- src/redmine-net-api/Types/User.cs | 51 ++++++++++++++++++++++++++----- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/src/redmine-net-api/Types/User.cs b/src/redmine-net-api/Types/User.cs index eeec1cec..dd220f56 100644 --- a/src/redmine-net-api/Types/User.cs +++ b/src/redmine-net-api/Types/User.cs @@ -179,12 +179,29 @@ public override void WriteXml(XmlWriter writer) writer.WriteElementString(RedmineKeys.FIRST_NAME, FirstName); writer.WriteElementString(RedmineKeys.LAST_NAME, LastName); writer.WriteElementString(RedmineKeys.MAIL, Email); - writer.WriteElementString(RedmineKeys.MAIL_NOTIFICATION, MailNotification); - writer.WriteElementString(RedmineKeys.PASSWORD, Password); - writer.WriteValueOrEmpty(RedmineKeys.AUTH_SOURCE_ID, AuthenticationModeId); + + if(!string.IsNullOrEmpty(MailNotification)) + { + writer.WriteElementString(RedmineKeys.MAIL_NOTIFICATION, MailNotification); + } + + if (!string.IsNullOrEmpty(Password)) + { + writer.WriteElementString(RedmineKeys.PASSWORD, Password); + } + + if(AuthenticationModeId.HasValue) + { + writer.WriteValueOrEmpty(RedmineKeys.AUTH_SOURCE_ID, AuthenticationModeId); + } + writer.WriteElementString(RedmineKeys.MUST_CHANGE_PASSWORD, MustChangePassword.ToString(CultureInfo.InvariantCulture).ToLowerInv()); writer.WriteElementString(RedmineKeys.STATUS, ((int)Status).ToString(CultureInfo.InvariantCulture)); - writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); + + if(CustomFields != null) + { + writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); + } } #endregion @@ -241,11 +258,29 @@ public override void WriteJson(JsonWriter writer) writer.WriteProperty(RedmineKeys.FIRST_NAME, FirstName); writer.WriteProperty(RedmineKeys.LAST_NAME, LastName); writer.WriteProperty(RedmineKeys.MAIL, Email); - writer.WriteProperty(RedmineKeys.MAIL_NOTIFICATION, MailNotification); - writer.WriteProperty(RedmineKeys.PASSWORD, Password); + + if(!string.IsNullOrEmpty(MailNotification)) + { + writer.WriteProperty(RedmineKeys.MAIL_NOTIFICATION, MailNotification); + } + + if (!string.IsNullOrEmpty(Password)) + { + writer.WriteProperty(RedmineKeys.PASSWORD, Password); + } + + if(AuthenticationModeId.HasValue) + { + writer.WriteValueOrEmpty(RedmineKeys.AUTH_SOURCE_ID, AuthenticationModeId); + } + writer.WriteProperty(RedmineKeys.MUST_CHANGE_PASSWORD, MustChangePassword.ToString(CultureInfo.InvariantCulture).ToLowerInv()); - writer.WriteValueOrEmpty(RedmineKeys.AUTH_SOURCE_ID, AuthenticationModeId); - writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); + writer.WriteProperty(RedmineKeys.STATUS, ((int)Status).ToString(CultureInfo.InvariantCulture)); + + if(CustomFields != null) + { + writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); + } } } #endregion From ce2d0e46f8f3b16be254fce37c017523548f86e8 Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 8 Jul 2020 10:17:09 +0300 Subject: [PATCH 165/549] Update version & change log --- CHANGELOG.md | 4 ++++ releasenotes.props | 4 ++-- version.props | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8addcde0..2e82ae71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [v4.0.2] + +Fixes: Add #236 to current version. + ## [v4.0.1] Fixes: diff --git a/releasenotes.props b/releasenotes.props index 2bc9e283..fd4d6f3f 100644 --- a/releasenotes.props +++ b/releasenotes.props @@ -1,9 +1,9 @@ - + $(PackageReleaseNotes) diff --git a/version.props b/version.props index f3437e57..c8a0b166 100644 --- a/version.props +++ b/version.props @@ -1,6 +1,6 @@ - 4.0.1 + 4.0.2 $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix) From 3f0d00c103bc0985978bd8ddc8152aa61cf1339d Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 26 Sep 2020 11:49:08 +0300 Subject: [PATCH 166/549] Fix #276 --- src/redmine-net-api/Types/Project.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs index 55cb2e66..73331225 100644 --- a/src/redmine-net-api/Types/Project.cs +++ b/src/redmine-net-api/Types/Project.cs @@ -122,12 +122,12 @@ public sealed class Project : IdentifiableName, IEquatable public IList EnabledModules { get; set; } /// - /// Gets the custom fields. + /// Gets or sets the custom fields. /// /// /// The custom fields. /// - public IList CustomFields { get; internal set; } + public IList CustomFields { get; set; } /// /// Gets the issue categories. From c8bad765f5b32d1953e813ffafa802fb1a1982dc Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 26 Sep 2020 11:49:56 +0300 Subject: [PATCH 167/549] Fix #274 --- src/redmine-net-api/Internals/UrlHelper.cs | 2 -- .../Tests/Async/WikiPageAsyncTests.cs | 20 +++++++++++++++++++ .../Tests/Sync/WikiPageTests.cs | 20 +++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/redmine-net-api/Internals/UrlHelper.cs b/src/redmine-net-api/Internals/UrlHelper.cs index 7a33d45e..cfce99c3 100644 --- a/src/redmine-net-api/Internals/UrlHelper.cs +++ b/src/redmine-net-api/Internals/UrlHelper.cs @@ -243,8 +243,6 @@ public static string GetWikisUrl(RedmineManager redmineManager, string projectId /// public static string GetWikiPageUrl(RedmineManager redmineManager, string projectId, string pageName, uint version = 0) { - pageName = Uri.EscapeUriString(pageName); - var uri = version == 0 ? string.Format(CultureInfo.InvariantCulture,WIKI_PAGE_FORMAT, redmineManager.Host, projectId, pageName, redmineManager.Format) diff --git a/tests/redmine-net-api.Tests/Tests/Async/WikiPageAsyncTests.cs b/tests/redmine-net-api.Tests/Tests/Async/WikiPageAsyncTests.cs index 586340c7..52d1cccb 100644 --- a/tests/redmine-net-api.Tests/Tests/Async/WikiPageAsyncTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Async/WikiPageAsyncTests.cs @@ -1,4 +1,5 @@ #if !(NET20 || NET40) +using System; using System.Collections.Specialized; using System.Threading.Tasks; using Redmine.Net.Api.Async; @@ -75,6 +76,25 @@ public async Task Should_Delete_WikiPage() await fixture.RedmineManager.DeleteWikiPageAsync(PROJECT_ID, WIKI_PAGE_NAME); await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetWikiPageAsync(PROJECT_ID, null, WIKI_PAGE_NAME)); } + + [Fact] + public async Task Should_Get_Wiki_Page_With_Special_Chars() + { + var wikiPageName = "some-page-with-umlauts-and-other-special-chars-äöüÄÖÜß"; + + var wikiPage = await fixture.RedmineManager.CreateWikiPageAsync(PROJECT_ID, wikiPageName, + new WikiPage { Text = "WIKI_PAGE_TEXT", Comments = "WIKI_PAGE_COMMENT" }); + + WikiPage page = await fixture.RedmineManager.GetWikiPageAsync + ( + PROJECT_ID, + null, + wikiPageName + ); + + Assert.NotNull(page); + Assert.True(string.Equals(page.Title,wikiPageName, StringComparison.OrdinalIgnoreCase),$"Wiki page {wikiPageName} does not exist."); + } } } diff --git a/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs index fefb48ad..4457b64b 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs @@ -14,6 +14,7 @@ You may obtain a copy of the License at limitations under the License. */ +using System; using System.Collections.Specialized; using System.Linq; using Padi.RedmineApi.Tests.Infrastructure; @@ -116,5 +117,24 @@ public void Should_Get_Wiki_Page_By_Version() Assert.Equal(oldPage.Title, WIKI_PAGE_NAME); Assert.True(oldPage.Version == WIKI_PAGE_VERSION, "Wiki page version is invalid."); } + + [Fact, Order(6)] + public void Should_Get_Wiki_Page_With_Special_Chars() + { + var wikiPageName = "some-page-with-umlauts-and-other-special-chars-äöüÄÖÜß"; + + var wikiPage = fixture.RedmineManager.CreateWikiPage(PROJECT_ID, wikiPageName, + new WikiPage { Text = "WIKI_PAGE_TEXT", Comments = "WIKI_PAGE_COMMENT" }); + + WikiPage page = fixture.RedmineManager.GetWikiPage + ( + PROJECT_ID, + null, + wikiPageName + ); + + Assert.NotNull(page); + Assert.True(string.Equals(page.Title,wikiPageName, StringComparison.OrdinalIgnoreCase),$"Wiki page {wikiPageName} does not exist."); + } } } \ No newline at end of file From 45f02b154b980e35a6b891d5ce1838426d9b8cf7 Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 30 Sep 2020 08:46:49 +0300 Subject: [PATCH 168/549] Update release notes --- CHANGELOG.md | 8 ++++++++ releasenotes.props | 5 +++-- version.props | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e82ae71..8dab1df8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [v4.1.0] + +Fixes: + +* Assigning IssueCustomFields to a project should be supported (#277) +* How to add a custom field to a project (#276) +* Wrong encoding of special characters in URLs causes 404 (#274) + ## [v4.0.2] Fixes: Add #236 to current version. diff --git a/releasenotes.props b/releasenotes.props index fd4d6f3f..8d154e1f 100644 --- a/releasenotes.props +++ b/releasenotes.props @@ -1,9 +1,10 @@ - + $(PackageReleaseNotes) diff --git a/version.props b/version.props index c8a0b166..4fe37a14 100644 --- a/version.props +++ b/version.props @@ -1,6 +1,6 @@ - 4.0.2 + 4.1.0 $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix) From 2aff74367895a7596a7ffd9a6b630146beff0b7c Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 30 Sep 2020 09:48:59 +0300 Subject: [PATCH 169/549] Auto stash before rebase of "4.1.0" --- appveyor.yml | 1 - src/redmine-net-api/RedmineManager.cs | 16 +++++----- src/redmine-net-api/RedmineWebClient.cs | 1 + .../Tests/Sync/WikiPageTests.cs | 32 +++++++++++++++++++ 4 files changed, 41 insertions(+), 9 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 69df924a..69a1e324 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -85,5 +85,4 @@ for: secure: iQKBODPsLcVrf7JQV5IR1jDHq01NiqEDmgj8N0Ahktuu76dKCs827tLggGMO9Mkd skip_symbols: true on: - branch: master APPVEYOR_REPO_TAG: true \ No newline at end of file diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index b5eb7573..09288930 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -362,7 +362,7 @@ public void UpdateWikiPage(string projectId, string pageName, WikiPage wikiPage) { var result = Serializer.Serialize(wikiPage); - if (string.IsNullOrEmpty(result)) + if (result.IsNullOrWhiteSpace()) { return; } @@ -385,7 +385,7 @@ public WikiPage CreateWikiPage(string projectId, string pageName, WikiPage wikiP { var result = Serializer.Serialize(wikiPage); - if (string.IsNullOrEmpty(result)) + if (result.IsNullOrWhiteSpace()) { return null; } @@ -451,15 +451,17 @@ public void DeleteWikiPage(string projectId, string pageName) /// public int Count(NameValueCollection parameters) where T : class, new() { - int totalCount = 0, pageSize = 1, offset = 0; + var totalCount = 0; + const int PAGE_SIZE = 1; + const int OFFSET = 0; if (parameters == null) { parameters = new NameValueCollection(); } - parameters.Set(RedmineKeys.LIMIT, pageSize.ToString(CultureInfo.InvariantCulture)); - parameters.Set(RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)); + parameters.Set(RedmineKeys.LIMIT, PAGE_SIZE.ToString(CultureInfo.InvariantCulture)); + parameters.Set(RedmineKeys.OFFSET, OFFSET.ToString(CultureInfo.InvariantCulture)); try { @@ -827,8 +829,6 @@ public byte[] DownloadFile(string address) return WebApiHelper.ExecuteDownloadFile(this, address); } - private const string UA = "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.163 Safari/535.1"; - /// /// Creates the Redmine web client. /// @@ -839,7 +839,7 @@ public byte[] DownloadFile(string address) public virtual RedmineWebClient CreateWebClient(NameValueCollection parameters, bool uploadFile = false) { var webClient = new RedmineWebClient { Scheme = Scheme, RedmineSerializer = Serializer}; - webClient.UserAgent = UA; + if (!uploadFile) { webClient.Headers.Add(HttpRequestHeader.ContentType, MimeFormat == MimeFormat.Xml diff --git a/src/redmine-net-api/RedmineWebClient.cs b/src/redmine-net-api/RedmineWebClient.cs index 4784ca00..1608bbbd 100644 --- a/src/redmine-net-api/RedmineWebClient.cs +++ b/src/redmine-net-api/RedmineWebClient.cs @@ -26,6 +26,7 @@ namespace Redmine.Net.Api /// public class RedmineWebClient : WebClient { + private const string UA = "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.163 Safari/535.1"; private string redirectUrl = string.Empty; /// diff --git a/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs index 4457b64b..a8dda8c3 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs @@ -17,6 +17,7 @@ limitations under the License. using System; using System.Collections.Specialized; using System.Linq; +using System.Net; using Padi.RedmineApi.Tests.Infrastructure; using Redmine.Net.Api; using Redmine.Net.Api.Exceptions; @@ -137,4 +138,35 @@ public void Should_Get_Wiki_Page_With_Special_Chars() Assert.True(string.Equals(page.Title,wikiPageName, StringComparison.OrdinalIgnoreCase),$"Wiki page {wikiPageName} does not exist."); } } + + [Trait("Redmine-Net-Api", "Download")] +#if !(NET20 || NET40) + [Collection("RedmineCollection")] +#endif + public class DownloadTests + { + private readonly RedmineFixture fixture; + + public DownloadTests(RedmineFixture fixture) + { + this.fixture = fixture; + } + + [Fact] + public void Should_Get_Gant_File() + { + using (WebClient client = fixture.RedmineManager.CreateWebClient(new NameValueCollection())) + { + var queryString = + "utf8=%E2%9C%93&set_filter=1&gant=1&f%5B%5D=status_id&f%5B%5D=project_id&f%5B%5D=&op%5Bstatus_id%5D=o&op%5Bproject_id%5D=%3D&v%5Bproject_id%5D%5B%5D=40&v%5Bproject_id%5D%5B%5D=6&v%5Bproject_id%5D%5B%5D=7&v%5Bproject_id%5D%5B%5D=13&v%5Bproject_id%5D%5B%5D=3&v%5Bproject_id%5D%5B%5D=14&v%5Bproject_id%5D%5B%5D=4&v%5Bproject_id%5D%5B%5D=8&query%5Bdraw_relations%5D=0&query%5Bdraw_relations%5D=1&query%5Bdraw_progress_line%5D=0&months=4&month=5&year=2017&zoom=4"; + + + var decode = Uri.UnescapeDataString(queryString); + + var address = $"{fixture.RedmineManager.Host}/issues/gantt.png?{queryString}"; + + // byte[] gantFile = client.DownloadData(address); + } + } + } } \ No newline at end of file From dd554f15a1fe44cd1bb66024d693df9afbc83e68 Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 30 Sep 2020 09:48:59 +0300 Subject: [PATCH 170/549] Revert "Auto stash before rebase of "4.1.0"" This reverts commit 2aff74367895a7596a7ffd9a6b630146beff0b7c. --- appveyor.yml | 1 + src/redmine-net-api/RedmineManager.cs | 16 +++++----- src/redmine-net-api/RedmineWebClient.cs | 1 - .../Tests/Sync/WikiPageTests.cs | 32 ------------------- 4 files changed, 9 insertions(+), 41 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 69a1e324..69df924a 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -85,4 +85,5 @@ for: secure: iQKBODPsLcVrf7JQV5IR1jDHq01NiqEDmgj8N0Ahktuu76dKCs827tLggGMO9Mkd skip_symbols: true on: + branch: master APPVEYOR_REPO_TAG: true \ No newline at end of file diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index 09288930..b5eb7573 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -362,7 +362,7 @@ public void UpdateWikiPage(string projectId, string pageName, WikiPage wikiPage) { var result = Serializer.Serialize(wikiPage); - if (result.IsNullOrWhiteSpace()) + if (string.IsNullOrEmpty(result)) { return; } @@ -385,7 +385,7 @@ public WikiPage CreateWikiPage(string projectId, string pageName, WikiPage wikiP { var result = Serializer.Serialize(wikiPage); - if (result.IsNullOrWhiteSpace()) + if (string.IsNullOrEmpty(result)) { return null; } @@ -451,17 +451,15 @@ public void DeleteWikiPage(string projectId, string pageName) /// public int Count(NameValueCollection parameters) where T : class, new() { - var totalCount = 0; - const int PAGE_SIZE = 1; - const int OFFSET = 0; + int totalCount = 0, pageSize = 1, offset = 0; if (parameters == null) { parameters = new NameValueCollection(); } - parameters.Set(RedmineKeys.LIMIT, PAGE_SIZE.ToString(CultureInfo.InvariantCulture)); - parameters.Set(RedmineKeys.OFFSET, OFFSET.ToString(CultureInfo.InvariantCulture)); + parameters.Set(RedmineKeys.LIMIT, pageSize.ToString(CultureInfo.InvariantCulture)); + parameters.Set(RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)); try { @@ -829,6 +827,8 @@ public byte[] DownloadFile(string address) return WebApiHelper.ExecuteDownloadFile(this, address); } + private const string UA = "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.163 Safari/535.1"; + /// /// Creates the Redmine web client. /// @@ -839,7 +839,7 @@ public byte[] DownloadFile(string address) public virtual RedmineWebClient CreateWebClient(NameValueCollection parameters, bool uploadFile = false) { var webClient = new RedmineWebClient { Scheme = Scheme, RedmineSerializer = Serializer}; - + webClient.UserAgent = UA; if (!uploadFile) { webClient.Headers.Add(HttpRequestHeader.ContentType, MimeFormat == MimeFormat.Xml diff --git a/src/redmine-net-api/RedmineWebClient.cs b/src/redmine-net-api/RedmineWebClient.cs index 1608bbbd..4784ca00 100644 --- a/src/redmine-net-api/RedmineWebClient.cs +++ b/src/redmine-net-api/RedmineWebClient.cs @@ -26,7 +26,6 @@ namespace Redmine.Net.Api /// public class RedmineWebClient : WebClient { - private const string UA = "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.163 Safari/535.1"; private string redirectUrl = string.Empty; /// diff --git a/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs index a8dda8c3..4457b64b 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs @@ -17,7 +17,6 @@ limitations under the License. using System; using System.Collections.Specialized; using System.Linq; -using System.Net; using Padi.RedmineApi.Tests.Infrastructure; using Redmine.Net.Api; using Redmine.Net.Api.Exceptions; @@ -138,35 +137,4 @@ public void Should_Get_Wiki_Page_With_Special_Chars() Assert.True(string.Equals(page.Title,wikiPageName, StringComparison.OrdinalIgnoreCase),$"Wiki page {wikiPageName} does not exist."); } } - - [Trait("Redmine-Net-Api", "Download")] -#if !(NET20 || NET40) - [Collection("RedmineCollection")] -#endif - public class DownloadTests - { - private readonly RedmineFixture fixture; - - public DownloadTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - [Fact] - public void Should_Get_Gant_File() - { - using (WebClient client = fixture.RedmineManager.CreateWebClient(new NameValueCollection())) - { - var queryString = - "utf8=%E2%9C%93&set_filter=1&gant=1&f%5B%5D=status_id&f%5B%5D=project_id&f%5B%5D=&op%5Bstatus_id%5D=o&op%5Bproject_id%5D=%3D&v%5Bproject_id%5D%5B%5D=40&v%5Bproject_id%5D%5B%5D=6&v%5Bproject_id%5D%5B%5D=7&v%5Bproject_id%5D%5B%5D=13&v%5Bproject_id%5D%5B%5D=3&v%5Bproject_id%5D%5B%5D=14&v%5Bproject_id%5D%5B%5D=4&v%5Bproject_id%5D%5B%5D=8&query%5Bdraw_relations%5D=0&query%5Bdraw_relations%5D=1&query%5Bdraw_progress_line%5D=0&months=4&month=5&year=2017&zoom=4"; - - - var decode = Uri.UnescapeDataString(queryString); - - var address = $"{fixture.RedmineManager.Host}/issues/gantt.png?{queryString}"; - - // byte[] gantFile = client.DownloadData(address); - } - } - } } \ No newline at end of file From 4095b149b9a7b79231a622bfbb4e9f5b1019264d Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 30 Sep 2020 09:49:58 +0300 Subject: [PATCH 171/549] Update appveyor.yml --- appveyor.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 69df924a..69a1e324 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -85,5 +85,4 @@ for: secure: iQKBODPsLcVrf7JQV5IR1jDHq01NiqEDmgj8N0Ahktuu76dKCs827tLggGMO9Mkd skip_symbols: true on: - branch: master APPVEYOR_REPO_TAG: true \ No newline at end of file From 194c661861a2fd865e7ccac8db6ac752731583f0 Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 30 Sep 2020 15:06:44 +0300 Subject: [PATCH 172/549] Small refactoring --- src/redmine-net-api/Extensions/WebExtensions.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/redmine-net-api/Extensions/WebExtensions.cs b/src/redmine-net-api/Extensions/WebExtensions.cs index 048f20d7..0d8920af 100644 --- a/src/redmine-net-api/Extensions/WebExtensions.cs +++ b/src/redmine-net-api/Extensions/WebExtensions.cs @@ -59,6 +59,7 @@ public static void HandleWebException(this WebException exception, IRedmineSeria throw new RedmineTimeoutException(nameof(WebExceptionStatus.Timeout), innerException); case WebExceptionStatus.NameResolutionFailure: throw new NameResolutionFailureException("Bad domain name.", innerException); + case WebExceptionStatus.ProtocolError: { var response = (HttpWebResponse)exception.Response; @@ -67,9 +68,6 @@ public static void HandleWebException(this WebException exception, IRedmineSeria case (int)HttpStatusCode.NotFound: throw new NotFoundException(response.StatusDescription, innerException); - case (int)HttpStatusCode.InternalServerError: - throw new InternalServerErrorException(response.StatusDescription, innerException); - case (int)HttpStatusCode.Unauthorized: throw new UnauthorizedException(response.StatusDescription, innerException); @@ -95,10 +93,12 @@ public static void HandleWebException(this WebException exception, IRedmineSeria case (int)HttpStatusCode.NotAcceptable: throw new NotAcceptableException(response.StatusDescription, innerException); + + default: + throw new RedmineException(response.StatusDescription, innerException); } } - break; - + default: throw new RedmineException(exception.Message, innerException); } From f7e30b3fdb44cfacd9e9dbc7c35ecf2f58efaf1a Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 30 Sep 2020 15:09:08 +0300 Subject: [PATCH 173/549] Update version & release notes --- CHANGELOG.md | 4 ++++ releasenotes.props | 6 ++---- version.props | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dab1df8..bc0ed261 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [v4.1.0] +* Small refactoring + +## [v4.1.0] + Fixes: * Assigning IssueCustomFields to a project should be supported (#277) diff --git a/releasenotes.props b/releasenotes.props index 8d154e1f..efad2ed7 100644 --- a/releasenotes.props +++ b/releasenotes.props @@ -1,10 +1,8 @@ - + $(PackageReleaseNotes) diff --git a/version.props b/version.props index 4fe37a14..031fa779 100644 --- a/version.props +++ b/version.props @@ -1,6 +1,6 @@ - 4.1.0 + 4.2.1 $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix) From ba9760f1980a38f1cadc083da7a68d3d52d9cea9 Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 30 Sep 2020 15:10:45 +0300 Subject: [PATCH 174/549] Fix version --- CHANGELOG.md | 2 +- releasenotes.props | 2 +- version.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc0ed261..1eab31bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [v4.1.0] +## [v4.2.0] * Small refactoring diff --git a/releasenotes.props b/releasenotes.props index efad2ed7..45611974 100644 --- a/releasenotes.props +++ b/releasenotes.props @@ -1,6 +1,6 @@ - + diff --git a/version.props b/version.props index 031fa779..38ea1fb2 100644 --- a/version.props +++ b/version.props @@ -1,6 +1,6 @@ - 4.2.1 + 4.2.0 $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix) From 59e7fc2cf8c7da05141696fa9f2404ceb1d2ea45 Mon Sep 17 00:00:00 2001 From: Padi Date: Tue, 13 Oct 2020 23:41:35 +0300 Subject: [PATCH 175/549] Update dotnetcore.yml --- .github/workflows/dotnetcore.yml | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml index 1dcfefd5..ae23ff25 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/dotnetcore.yml @@ -9,6 +9,7 @@ on: - LICENSE - tests/* pull_request: + workflow_dispatch: env: DOTNET_CLI_TELEMETRY_OPTOUT: 1 @@ -20,18 +21,33 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] - dotnet: [ '3.1.100' ] + dotnet: [ '3.1.301' ] name: OS ${{ matrix.os }} - dotnet ${{ matrix.dotnet }} steps: - uses: actions/checkout@v2 + - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: dotnet-version: ${{ matrix.dotnet }} + + - name: Install dependencies + run: dotnet restore redmine-net-api.sln + #- name: Get the version # id: get_version # run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} # ${{ steps.get_version.outputs.VERSION }} - - name: Build with dotnet - run: dotnet build redmine-net-api.sln --configuration Release + + - name: Build + run: dotnet build redmine-net-api.sln --configuration Release --no-restore + + #- name: Test + # run: dotnet test --no-restore --verbosity normal + + #- name: Generate a NuGet package + # run: dotnet pack --no-build -c Release -o . + + #- name: Push to GitHub package registry + # run: dotnet nuget push *.nupkg From bb5e880703215387e9f757c5a9babfcac5bce1ff Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 5 Dec 2020 22:15:50 +0200 Subject: [PATCH 176/549] Fix #280 --- src/redmine-net-api/Async/RedmineManagerAsync45.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/redmine-net-api/Async/RedmineManagerAsync45.cs b/src/redmine-net-api/Async/RedmineManagerAsync45.cs index e15cf3a0..53c60f6a 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync45.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync45.cs @@ -323,8 +323,10 @@ public static async Task> GetObjectsAsync(this RedmineManager redmine { if (resultList == null) { - resultList = new List(tempResult.Items); totalCount = tempResult.TotalItems; + resultList = totalCount > 0 + ? new List(tempResult.Items) + : new List(); } else { From 69c4163a1df728bebc0ecbc32b3251372d8f91bb Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 9 Dec 2020 14:33:30 +0200 Subject: [PATCH 177/549] Refactor GetObjectsAsync --- .../Async/RedmineManagerAsync45.cs | 39 ++++++++++++++----- src/redmine-net-api/RedmineManager.cs | 4 +- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/redmine-net-api/Async/RedmineManagerAsync45.cs b/src/redmine-net-api/Async/RedmineManagerAsync45.cs index 53c60f6a..24572b0d 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync45.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync45.cs @@ -299,13 +299,20 @@ public static async Task> GetPaginatedObjectsAsync(this Redmi public static async Task> GetObjectsAsync(this RedmineManager redmineManager, NameValueCollection parameters) where T : class, new() { - int totalCount = 0, pageSize, offset; + int pageSize = 0, offset = 0; + var isLimitSet = false; List resultList = null; - if (parameters == null) parameters = new NameValueCollection(); - - int.TryParse(parameters[RedmineKeys.LIMIT], out pageSize); - int.TryParse(parameters[RedmineKeys.OFFSET], out offset); + if (parameters == null) + { + parameters = new NameValueCollection(); + } + else + { + isLimitSet = int.TryParse(parameters[RedmineKeys.LIMIT], out pageSize); + int.TryParse(parameters[RedmineKeys.OFFSET], out offset); + } + if (pageSize == default(int)) { pageSize = redmineManager.PageSize > 0 @@ -315,18 +322,21 @@ public static async Task> GetObjectsAsync(this RedmineManager redmine } try { + var hasOffset = RedmineManager.TypesWithOffset.ContainsKey(typeof(T)); + if (hasOffset) + { + var totalCount = 0; do { parameters.Set(RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)); var tempResult = await redmineManager.GetPaginatedObjectsAsync(parameters).ConfigureAwait(false); - if (tempResult != null) + totalCount = isLimitSet ? pageSize : tempResult.TotalItems; + + if (tempResult?.Items != null) { if (resultList == null) { - totalCount = tempResult.TotalItems; - resultList = totalCount > 0 - ? new List(tempResult.Items) - : new List(); + resultList = new List(tempResult.Items); } else { @@ -335,6 +345,15 @@ public static async Task> GetObjectsAsync(this RedmineManager redmine } offset += pageSize; } while (offset < totalCount); + } + else + { + var result = await redmineManager.GetPaginatedObjectsAsync(parameters).ConfigureAwait(false); + if (result?.Items != null) + { + return new List(result.Items); + } + } } catch (WebException wex) { diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index b5eb7573..29165b29 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -66,14 +66,14 @@ public class RedmineManager : IRedmineManager {typeof(CustomField), "custom_fields"} }; - private static readonly Dictionary TypesWithOffset = new Dictionary{ + public static readonly Dictionary TypesWithOffset = new Dictionary{ {typeof(Issue), true}, {typeof(Project), true}, {typeof(User), true}, {typeof(News), true}, {typeof(Query), true}, {typeof(TimeEntry), true}, - {typeof(ProjectMembership), true}, + {typeof(ProjectMembership), true} }; private readonly string basicAuthorization; From 9c2b92d1deac7d9bbe472ffa17d7532c96974ddc Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 9 Dec 2020 14:39:37 +0200 Subject: [PATCH 178/549] Update changelog & version --- CHANGELOG.md | 10 ++++++++++ releasenotes.props | 2 +- version.props | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1eab31bf..17f0c777 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [v4.2.2] + +Fixes: + +* GetObjectsAsync raises ArgumentNullException when should return null (#280) + +## [v4.2.1] + +* Small fixes. + ## [v4.2.0] * Small refactoring diff --git a/releasenotes.props b/releasenotes.props index 45611974..de533cd9 100644 --- a/releasenotes.props +++ b/releasenotes.props @@ -1,6 +1,6 @@ - + diff --git a/version.props b/version.props index 38ea1fb2..d3d645fc 100644 --- a/version.props +++ b/version.props @@ -1,6 +1,6 @@ - 4.2.0 + 4.2.2 $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix) From df7abb96c601006fb182f1a4d3a75cdc0511947c Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 7 Feb 2021 22:25:40 +0200 Subject: [PATCH 179/549] Update workflow --- .github/workflows/dotnetcore.yml | 90 +++++++++++++++++++++++++++----- 1 file changed, 76 insertions(+), 14 deletions(-) diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml index ae23ff25..b5a7d094 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/dotnetcore.yml @@ -6,13 +6,32 @@ on: - '**/*.md' - '**/*.gif' - '**/*.png' + - '**/*.gitignore' + - '**/*.gitattributes' - LICENSE - tests/* + tags: + - v[1-9].[0-9]+.[0-9]+ pull_request: workflow_dispatch: + branches: + - main + path-ignore: + - '**/*.md' + - '**/*.gif' + - '**/*.png' + - '**/*.gitignore' + - '**/*.gitattributes' + - LICENSE + - tests/* env: DOTNET_CLI_TELEMETRY_OPTOUT: 1 + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 + DOTNET_NOLOGO: true + DOTNET_GENERATE_ASPNET_CERTIFICATE: false + DOTNET_ADD_GLOBAL_TOOLS_TO_PATH: false + DOTNET_MULTILEVEL_LOOKUP: 0 jobs: build: @@ -21,13 +40,13 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] - dotnet: [ '3.1.301' ] + dotnet: [ '3.1.x', '5.x' ] name: OS ${{ matrix.os }} - dotnet ${{ matrix.dotnet }} steps: - uses: actions/checkout@v2 - - name: Setup .NET Core + - name: Setup .NET Core SDK uses: actions/setup-dotnet@v1 with: dotnet-version: ${{ matrix.dotnet }} @@ -35,19 +54,62 @@ jobs: - name: Install dependencies run: dotnet restore redmine-net-api.sln - #- name: Get the version - # id: get_version - # run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} - # ${{ steps.get_version.outputs.VERSION }} + - name: Get the version + id: get_version + run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} + ${{ steps.get_version.outputs.VERSION }} - name: Build - run: dotnet build redmine-net-api.sln --configuration Release --no-restore + run: dotnet build redmine-net-api.sln --configuration Release --no-restore --version-suffix=${{ steps.get_version.outputs.VERSION }} + + - name: Build Signed + run: dotnet build redmine-net-api.sln --configuration Release --no-restore --version-suffix=${{ steps.get_version.outputs.VERSION }} -p:Sign=true #- name: Test - # run: dotnet test --no-restore --verbosity normal - - #- name: Generate a NuGet package - # run: dotnet pack --no-build -c Release -o . - - #- name: Push to GitHub package registry - # run: dotnet nuget push *.nupkg + # run: dotnet test redmine-net-api.sln --no-restore --verbosity normal + + - name: Pack + run: dotnet pack redmine-net-api.sln --configuration Release -o .\artifacts --include-symbols -p:SymbolPackageFormat=snupkg --no-build ${{ steps.get_version.outputs.VERSION }} + + - name: Pack Signed + run: dotnet pack redmine-net-api.sln --configuration Release -o .\artifacts --include-symbols -p:SymbolPackageFormat=snupkg --no-build ${{ steps.get_version.outputs.VERSION }} -p:Sign=true + + - name: Publish NuGet Packages + uses: actions/upload-artifact@master + with: + name: nupkg + path: .\artifacts\**\*.nupkg + + - name: Publish Symbol Packages + uses: actions/upload-artifact@master + with: + name: snupkg + path: .\artifacts\**\*.snupkg + + deploy: + needs: build + name: Deploy Packages + steps: + - name: Download Package artifact + uses: actions/download-artifact@master + with: + name: nupkg + - name: Download Package artifact + uses: actions/download-artifact@master + with: + name: snupkg + + - name: Setup NuGet + uses: NuGet/setup-nuget@v1.0.2 + with: + nuget-api-key: ${{ secrets.NUGET_API_KEY }} + nuget-version: latest + + - name: Setup .NET Core SDK + uses: actions/setup-dotnet@v1 + with: + dotnet-version: '3.1.x' + + - name: Push to NuGet + run: dotnet nuget push nupkg\*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://nuget.org + \ No newline at end of file From 8ecf59f3056a181be5644f67a829bc22c840886f Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 7 Feb 2021 20:28:24 +0000 Subject: [PATCH 180/549] Update dotnetcore.yml --- .github/workflows/dotnetcore.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml index b5a7d094..624d3090 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/dotnetcore.yml @@ -57,7 +57,7 @@ jobs: - name: Get the version id: get_version run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} - ${{ steps.get_version.outputs.VERSION }} + #${{ steps.get_version.outputs.VERSION }} - name: Build run: dotnet build redmine-net-api.sln --configuration Release --no-restore --version-suffix=${{ steps.get_version.outputs.VERSION }} @@ -112,4 +112,4 @@ jobs: - name: Push to NuGet run: dotnet nuget push nupkg\*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://nuget.org - \ No newline at end of file + From c11c63755399a891be4675dcd1c1b5343204db81 Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 7 Feb 2021 20:37:57 +0000 Subject: [PATCH 181/549] Update dotnetcore.yml --- .github/workflows/dotnetcore.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml index 624d3090..3ca5756e 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/dotnetcore.yml @@ -87,6 +87,7 @@ jobs: path: .\artifacts\**\*.snupkg deploy: + runs-on: macOS-latest needs: build name: Deploy Packages steps: From 5ef2b9d2a22f40fdc313dee64a04ff3faab8ba47 Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 7 Feb 2021 20:40:19 +0000 Subject: [PATCH 182/549] Update dotnetcore.yml --- .github/workflows/dotnetcore.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml index 3ca5756e..84309a6f 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/dotnetcore.yml @@ -40,7 +40,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] - dotnet: [ '3.1.x', '5.x' ] + dotnet: [ '3.1.x'] name: OS ${{ matrix.os }} - dotnet ${{ matrix.dotnet }} steps: From da2c8c1e6ea8417b4c82f360c72a7a6ef812e4dd Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 7 Feb 2021 20:52:54 +0000 Subject: [PATCH 183/549] Update dotnetcore.yml --- .github/workflows/dotnetcore.yml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml index 84309a6f..ff56f50f 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/dotnetcore.yml @@ -32,6 +32,7 @@ env: DOTNET_GENERATE_ASPNET_CERTIFICATE: false DOTNET_ADD_GLOBAL_TOOLS_TO_PATH: false DOTNET_MULTILEVEL_LOOKUP: 0 + VERSION: git.sha jobs: build: @@ -55,24 +56,25 @@ jobs: run: dotnet restore redmine-net-api.sln - name: Get the version - id: get_version - run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} + #id: get_version + #run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} #${{ steps.get_version.outputs.VERSION }} + run: echo "VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - name: Build - run: dotnet build redmine-net-api.sln --configuration Release --no-restore --version-suffix=${{ steps.get_version.outputs.VERSION }} + run: dotnet build redmine-net-api.sln --configuration Release --no-restore --version-suffix=${{ env.VERSION }} - name: Build Signed - run: dotnet build redmine-net-api.sln --configuration Release --no-restore --version-suffix=${{ steps.get_version.outputs.VERSION }} -p:Sign=true + run: dotnet build redmine-net-api.sln --configuration Release --no-restore --version-suffix=${{ env.VERSION }} -p:Sign=true #- name: Test # run: dotnet test redmine-net-api.sln --no-restore --verbosity normal - name: Pack - run: dotnet pack redmine-net-api.sln --configuration Release -o .\artifacts --include-symbols -p:SymbolPackageFormat=snupkg --no-build ${{ steps.get_version.outputs.VERSION }} + run: dotnet pack redmine-net-api.sln --configuration Release -o .\artifacts --include-symbols -p:SymbolPackageFormat=snupkg --no-build ${{ env.VERSION }} - name: Pack Signed - run: dotnet pack redmine-net-api.sln --configuration Release -o .\artifacts --include-symbols -p:SymbolPackageFormat=snupkg --no-build ${{ steps.get_version.outputs.VERSION }} -p:Sign=true + run: dotnet pack redmine-net-api.sln --configuration Release -o .\artifacts --include-symbols -p:SymbolPackageFormat=snupkg --no-build ${{ env.VERSION }} -p:Sign=true - name: Publish NuGet Packages uses: actions/upload-artifact@master From 92b67414af85edb9ad3a136cac1c0fdaa019e204 Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 7 Feb 2021 20:55:43 +0000 Subject: [PATCH 184/549] Update dotnetcore.yml --- .github/workflows/dotnetcore.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml index ff56f50f..4f7a2286 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/dotnetcore.yml @@ -32,7 +32,6 @@ env: DOTNET_GENERATE_ASPNET_CERTIFICATE: false DOTNET_ADD_GLOBAL_TOOLS_TO_PATH: false DOTNET_MULTILEVEL_LOOKUP: 0 - VERSION: git.sha jobs: build: @@ -60,7 +59,10 @@ jobs: #run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} #${{ steps.get_version.outputs.VERSION }} run: echo "VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - + - name: Test + run: | + echo $VERSION + echo ${{ env.VERSION }} - name: Build run: dotnet build redmine-net-api.sln --configuration Release --no-restore --version-suffix=${{ env.VERSION }} From f0b2e98c318081c02cc798ef4adccd3f97f05dc6 Mon Sep 17 00:00:00 2001 From: Padi Date: Mon, 8 Feb 2021 14:50:54 +0000 Subject: [PATCH 185/549] Update dotnetcore.yml --- .github/workflows/dotnetcore.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml index 4f7a2286..4d584342 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/dotnetcore.yml @@ -58,11 +58,19 @@ jobs: #id: get_version #run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} #${{ steps.get_version.outputs.VERSION }} - run: echo "VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + run: | + echo "VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + # Get the version number + XVERSION=$(dotnet minver -t v -v e -d preview) + + - name: Test run: | echo $VERSION echo ${{ env.VERSION }} + echo $XVERSION + echo $github.run_number + - name: Build run: dotnet build redmine-net-api.sln --configuration Release --no-restore --version-suffix=${{ env.VERSION }} @@ -74,9 +82,11 @@ jobs: - name: Pack run: dotnet pack redmine-net-api.sln --configuration Release -o .\artifacts --include-symbols -p:SymbolPackageFormat=snupkg --no-build ${{ env.VERSION }} + if: runner.os != 'Windows' - name: Pack Signed run: dotnet pack redmine-net-api.sln --configuration Release -o .\artifacts --include-symbols -p:SymbolPackageFormat=snupkg --no-build ${{ env.VERSION }} -p:Sign=true + if: runner.os != 'Windows' - name: Publish NuGet Packages uses: actions/upload-artifact@master From 455b4870b8bc393584457c082c9bebeb530247a3 Mon Sep 17 00:00:00 2001 From: Padi Date: Mon, 8 Feb 2021 14:59:07 +0000 Subject: [PATCH 186/549] Update dotnetcore.yml --- .github/workflows/dotnetcore.yml | 104 +++++++++++++++---------------- 1 file changed, 51 insertions(+), 53 deletions(-) diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml index 4d584342..d17aef8e 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/dotnetcore.yml @@ -50,7 +50,10 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: ${{ matrix.dotnet }} - + # Fetches all tags for the repo + - name: Fetch tags + run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* + - name: Install dependencies run: dotnet restore redmine-net-api.sln @@ -58,73 +61,68 @@ jobs: #id: get_version #run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} #${{ steps.get_version.outputs.VERSION }} - run: | - echo "VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - # Get the version number - XVERSION=$(dotnet minver -t v -v e -d preview) - + run: echo "VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - name: Test run: | echo $VERSION echo ${{ env.VERSION }} - echo $XVERSION echo $github.run_number - - name: Build - run: dotnet build redmine-net-api.sln --configuration Release --no-restore --version-suffix=${{ env.VERSION }} +# - name: Build +# run: dotnet build redmine-net-api.sln --configuration Release --no-restore --version-suffix=${{ env.VERSION }} - - name: Build Signed - run: dotnet build redmine-net-api.sln --configuration Release --no-restore --version-suffix=${{ env.VERSION }} -p:Sign=true +# - name: Build Signed +# run: dotnet build redmine-net-api.sln --configuration Release --no-restore --version-suffix=${{ env.VERSION }} -p:Sign=true - #- name: Test - # run: dotnet test redmine-net-api.sln --no-restore --verbosity normal +# #- name: Test +# # run: dotnet test redmine-net-api.sln --no-restore --verbosity normal - - name: Pack - run: dotnet pack redmine-net-api.sln --configuration Release -o .\artifacts --include-symbols -p:SymbolPackageFormat=snupkg --no-build ${{ env.VERSION }} - if: runner.os != 'Windows' +# - name: Pack +# run: dotnet pack redmine-net-api.sln --configuration Release -o .\artifacts --include-symbols -p:SymbolPackageFormat=snupkg --no-build ${{ env.VERSION }} +# if: runner.os != 'Windows' - - name: Pack Signed - run: dotnet pack redmine-net-api.sln --configuration Release -o .\artifacts --include-symbols -p:SymbolPackageFormat=snupkg --no-build ${{ env.VERSION }} -p:Sign=true - if: runner.os != 'Windows' +# - name: Pack Signed +# run: dotnet pack redmine-net-api.sln --configuration Release -o .\artifacts --include-symbols -p:SymbolPackageFormat=snupkg --no-build ${{ env.VERSION }} -p:Sign=true +# if: runner.os != 'Windows' - - name: Publish NuGet Packages - uses: actions/upload-artifact@master - with: - name: nupkg - path: .\artifacts\**\*.nupkg +# - name: Publish NuGet Packages +# uses: actions/upload-artifact@master +# with: +# name: nupkg +# path: .\artifacts\**\*.nupkg - - name: Publish Symbol Packages - uses: actions/upload-artifact@master - with: - name: snupkg - path: .\artifacts\**\*.snupkg +# - name: Publish Symbol Packages +# uses: actions/upload-artifact@master +# with: +# name: snupkg +# path: .\artifacts\**\*.snupkg - deploy: - runs-on: macOS-latest - needs: build - name: Deploy Packages - steps: - - name: Download Package artifact - uses: actions/download-artifact@master - with: - name: nupkg - - name: Download Package artifact - uses: actions/download-artifact@master - with: - name: snupkg +# deploy: +# runs-on: macOS-latest +# needs: build +# name: Deploy Packages +# steps: +# - name: Download Package artifact +# uses: actions/download-artifact@master +# with: +# name: nupkg +# - name: Download Package artifact +# uses: actions/download-artifact@master +# with: +# name: snupkg - - name: Setup NuGet - uses: NuGet/setup-nuget@v1.0.2 - with: - nuget-api-key: ${{ secrets.NUGET_API_KEY }} - nuget-version: latest +# - name: Setup NuGet +# uses: NuGet/setup-nuget@v1.0.2 +# with: +# nuget-api-key: ${{ secrets.NUGET_API_KEY }} +# nuget-version: latest - - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v1 - with: - dotnet-version: '3.1.x' +# - name: Setup .NET Core SDK +# uses: actions/setup-dotnet@v1 +# with: +# dotnet-version: '3.1.x' - - name: Push to NuGet - run: dotnet nuget push nupkg\*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://nuget.org +# - name: Push to NuGet +# run: dotnet nuget push nupkg\*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://nuget.org From 326ed514d2c62a957f255b3acf7e9e6c2c1d9b54 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 14 Feb 2021 14:46:45 +0200 Subject: [PATCH 187/549] Update appveyor --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 69a1e324..f5c6ee79 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -21,7 +21,7 @@ nuget: branches: only: - master - - /v\d*\.\d*\.\d*/ + - /\d*\.\d*\.\d*/ init: # Good practise, because Windows line endings are different from Unix/Linux ones From a9e2ecf9eaa42282a3f7ed67064f1bcd3426cc82 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 28 Feb 2021 20:38:28 +0200 Subject: [PATCH 188/549] Update appveyor --- appveyor.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index f5c6ee79..7927dc34 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -32,9 +32,9 @@ init: - ps: $buildNumber = $env:APPVEYOR_BUILD_NUMBER; - ps: $isRepoTag = $env:APPVEYOR_REPO_TAG; - ps: $revision = @{ $true = [string]::Empty; $false = "{0:00000}" -f [convert]::ToInt32("0" + $buildNumber, 10) }[$isRepoTag -eq "true"]; - - ps: $suffix = @{ $true = [string]::Empty; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "master" -and [string]::IsNullOrEmpty($revision)]; - - ps: $env:BUILD_SUFFIX = @{ $true = "$($suffix)-$($commitHash)"; $false = "$($branch)-$($commitHash)" }[ -not ([string]::IsNullOrEmpty($suffix))]; - - ps: $env:VERSION_SUFFIX = @{ $true = "--version-suffix=$($suffix)"; $false = ""}[ -not ([string]::IsNullOrEmpty($suffix))]; + - ps: $suffix = @{ $true = [string]::Empty; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))$([string]::IsNullOrEmpty($revision) ? [string]::Empty : -$revision)"}[$branch -eq "master" -and [string]::IsNullOrEmpty($revision)]; + - ps: $env:BUILD_SUFFIX = @{ $true = "$($branch)-$($commitHash)"; $false = "$($suffix)-$($commitHash)" }[[string]::IsNullOrEmpty($suffix)]; + - ps: $env:VERSION_SUFFIX = @{ $true = ""; $false = "--version-suffix=$($suffix)"}[[string]::IsNullOrEmpty($suffix)]; install: - ps: dotnet restore redmine-net-api.sln @@ -85,4 +85,5 @@ for: secure: iQKBODPsLcVrf7JQV5IR1jDHq01NiqEDmgj8N0Ahktuu76dKCs827tLggGMO9Mkd skip_symbols: true on: + branch: master APPVEYOR_REPO_TAG: true \ No newline at end of file From 59ee00ddb5473768fb4ae213ef898b00fbad4179 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 28 Feb 2021 20:44:48 +0200 Subject: [PATCH 189/549] Fix #284 --- src/redmine-net-api/RedmineWebClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/redmine-net-api/RedmineWebClient.cs b/src/redmine-net-api/RedmineWebClient.cs index 4784ca00..e1ba04a7 100644 --- a/src/redmine-net-api/RedmineWebClient.cs +++ b/src/redmine-net-api/RedmineWebClient.cs @@ -126,7 +126,7 @@ protected override WebRequest GetWebRequest(Uri address) if (Timeout != null) { - httpWebRequest.Timeout = Timeout.Value.Milliseconds; + httpWebRequest.Timeout = (int)Timeout.Value.TotalMilliseconds; } return httpWebRequest; From e463600a787a731e3b101d003d1e8a6003826455 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 28 Feb 2021 20:53:24 +0200 Subject: [PATCH 190/549] Add timeout option to ctor --- src/redmine-net-api/RedmineManager.cs | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index 29165b29..06fe12f3 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -91,20 +91,25 @@ public class RedmineManager : IRedmineManager /// The proxy. /// Use this parameter to specify a SecurityProtcolType. Note: it is recommended to leave this parameter at its default value as this setting also affects the calling application process. /// http or https. Default is https + /// /// /// Host is not defined! /// or /// The host is not valid! /// public RedmineManager(string host, MimeFormat mimeFormat = MimeFormat.Xml, bool verifyServerCert = true, - IWebProxy proxy = null, SecurityProtocolType securityProtocolType = default, string scheme = "https") + IWebProxy proxy = null, SecurityProtocolType securityProtocolType = default, string scheme = "https", TimeSpan? timeout = null) { - if (string.IsNullOrEmpty(host)) throw new RedmineException("Host is not defined!"); + if (string.IsNullOrEmpty(host)) + { + throw new RedmineException("Host is not defined!"); + } PageSize = 25; Scheme = scheme; Host = host; MimeFormat = mimeFormat; + Timeout = timeout; Proxy = proxy; if (mimeFormat == MimeFormat.Xml) @@ -118,7 +123,7 @@ public RedmineManager(string host, MimeFormat mimeFormat = MimeFormat.Xml, bool Serializer = new JsonRedmineSerializer(); } - if (default == securityProtocolType) + if (securityProtocolType == default) { securityProtocolType = ServicePointManager.SecurityProtocol; } @@ -155,8 +160,8 @@ public RedmineManager(string host, MimeFormat mimeFormat = MimeFormat.Xml, bool /// Use this parameter to specify a SecurityProtcolType. Note: it is recommended to leave this parameter at its default value as this setting also affects the calling application process. public RedmineManager(string host, string apiKey, MimeFormat mimeFormat = MimeFormat.Xml, bool verifyServerCert = true, IWebProxy proxy = null, - SecurityProtocolType securityProtocolType = default) - : this(host, mimeFormat, verifyServerCert, proxy, securityProtocolType) + SecurityProtocolType securityProtocolType = default, TimeSpan? timeout = null) + : this(host, mimeFormat, verifyServerCert, proxy, securityProtocolType, timeout: timeout) { ApiKey = apiKey; } @@ -184,8 +189,8 @@ public RedmineManager(string host, string apiKey, MimeFormat mimeFormat = MimeFo /// Use this parameter to specify a SecurityProtcolType. Note: it is recommended to leave this parameter at its default value as this setting also affects the calling application process. public RedmineManager(string host, string login, string password, MimeFormat mimeFormat = MimeFormat.Xml, bool verifyServerCert = true, IWebProxy proxy = null, - SecurityProtocolType securityProtocolType = default) - : this(host, mimeFormat, verifyServerCert, proxy, securityProtocolType) + SecurityProtocolType securityProtocolType = default, TimeSpan? timeout = null) + : this(host, mimeFormat, verifyServerCert, proxy, securityProtocolType, timeout: timeout) { cache = new CredentialCache { { new Uri(host), "Basic", new NetworkCredential(login, password) } }; @@ -210,6 +215,8 @@ public RedmineManager(string host, string login, string password, MimeFormat mim /// /// public string Scheme { get; private set; } + + public TimeSpan? Timeout { get; private set; } /// /// Gets the host. @@ -840,6 +847,7 @@ public virtual RedmineWebClient CreateWebClient(NameValueCollection parameters, { var webClient = new RedmineWebClient { Scheme = Scheme, RedmineSerializer = Serializer}; webClient.UserAgent = UA; + webClient.Timeout = Timeout; if (!uploadFile) { webClient.Headers.Add(HttpRequestHeader.ContentType, MimeFormat == MimeFormat.Xml From 63538d00622484639cf49df47add958968318363 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 28 Feb 2021 21:43:23 +0200 Subject: [PATCH 191/549] Update appveyor --- appveyor.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 7927dc34..d020e88e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -31,10 +31,10 @@ init: - ps: $branch = $env:APPVEYOR_REPO_BRANCH; - ps: $buildNumber = $env:APPVEYOR_BUILD_NUMBER; - ps: $isRepoTag = $env:APPVEYOR_REPO_TAG; - - ps: $revision = @{ $true = [string]::Empty; $false = "{0:00000}" -f [convert]::ToInt32("0" + $buildNumber, 10) }[$isRepoTag -eq "true"]; - - ps: $suffix = @{ $true = [string]::Empty; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))$([string]::IsNullOrEmpty($revision) ? [string]::Empty : -$revision)"}[$branch -eq "master" -and [string]::IsNullOrEmpty($revision)]; - - ps: $env:BUILD_SUFFIX = @{ $true = "$($branch)-$($commitHash)"; $false = "$($suffix)-$($commitHash)" }[[string]::IsNullOrEmpty($suffix)]; - - ps: $env:VERSION_SUFFIX = @{ $true = ""; $false = "--version-suffix=$($suffix)"}[[string]::IsNullOrEmpty($suffix)]; + - ps: $revision = $(If ($isRepoTag -eq "true") {[string]::Empty} Else {"{0:00000}" -f [convert]::ToInt32("0" + $buildNumber, 10)}); + - ps: $suffix = $(If ($branch -eq "master" -and [string]::IsNullOrEmpty($revision)) {[string]::Empty} Else {$branch.Substring(0, [math]::Min(10,$branch.Length))}); + - ps: $env:BUILD_SUFFIX = $(If ([string]::IsNullOrEmpty($suffix)) {"$branch-$commitHash"} Else {"$suffix-$commitHash"}); + - ps: $env:VERSION_SUFFIX = $(If ([string]::IsNullOrEmpty($suffix)) {[string]::Empty} Else {"--version-suffix=$suffix"}); install: - ps: dotnet restore redmine-net-api.sln From d8025adc679b51ba777642e3576934600fc5a755 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 28 Feb 2021 22:03:55 +0200 Subject: [PATCH 192/549] Update version & changelog & xml comments --- CHANGELOG.md | 5 +++ releasenotes.props | 5 --- src/redmine-net-api/RedmineManager.cs | 51 +++++++++++++++------------ version.props | 2 +- 4 files changed, 34 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17f0c777..256d1ff3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [v4.2.3] + +Fixes: +* The only milliseconds component is set to Timeout. (#284) + ## [v4.2.2] Fixes: diff --git a/releasenotes.props b/releasenotes.props index de533cd9..2538fad8 100644 --- a/releasenotes.props +++ b/releasenotes.props @@ -1,10 +1,5 @@ - - - $(PackageReleaseNotes) See $(PackageProjectUrl)/blob/master/CHANGELOG.md#v$(VersionPrefix.Replace('.','')) for more details. diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index 06fe12f3..91281903 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -66,6 +66,9 @@ public class RedmineManager : IRedmineManager {typeof(CustomField), "custom_fields"} }; + /// + /// + /// public static readonly Dictionary TypesWithOffset = new Dictionary{ {typeof(Issue), true}, {typeof(Project), true}, @@ -90,8 +93,8 @@ public class RedmineManager : IRedmineManager /// if set to true [verify server cert]. /// The proxy. /// Use this parameter to specify a SecurityProtcolType. Note: it is recommended to leave this parameter at its default value as this setting also affects the calling application process. - /// http or https. Default is https - /// + /// http or https. Default is https. + /// The webclient timeout. Default is 100 seconds. /// /// Host is not defined! /// or @@ -158,6 +161,7 @@ public RedmineManager(string host, MimeFormat mimeFormat = MimeFormat.Xml, bool /// if set to true [verify server cert]. /// The proxy. /// Use this parameter to specify a SecurityProtcolType. Note: it is recommended to leave this parameter at its default value as this setting also affects the calling application process. + /// The webclient timeout. Default is 100 seconds. public RedmineManager(string host, string apiKey, MimeFormat mimeFormat = MimeFormat.Xml, bool verifyServerCert = true, IWebProxy proxy = null, SecurityProtocolType securityProtocolType = default, TimeSpan? timeout = null) @@ -187,6 +191,7 @@ public RedmineManager(string host, string apiKey, MimeFormat mimeFormat = MimeFo /// if set to true [verify server cert]. /// The proxy. /// Use this parameter to specify a SecurityProtcolType. Note: it is recommended to leave this parameter at its default value as this setting also affects the calling application process. + /// The webclient timeout. Default is 100 seconds. public RedmineManager(string host, string login, string password, MimeFormat mimeFormat = MimeFormat.Xml, bool verifyServerCert = true, IWebProxy proxy = null, SecurityProtocolType securityProtocolType = default, TimeSpan? timeout = null) @@ -376,11 +381,11 @@ public void UpdateWikiPage(string projectId, string pageName, WikiPage wikiPage) var url = UrlHelper.GetWikiCreateOrUpdaterUrl(this, projectId, pageName); - url = Uri.EscapeUriString(url); + url = Uri.EscapeUriString(url); - WebApiHelper.ExecuteUpload(this, url, HttpVerbs.PUT, result); + WebApiHelper.ExecuteUpload(this, url, HttpVerbs.PUT, result); } - + /// /// /// @@ -444,9 +449,9 @@ public List GetAllWikiPages(string projectId) public void DeleteWikiPage(string projectId, string pageName) { var url = UrlHelper.GetDeleteWikiUrl(this, projectId, pageName); - + url = Uri.EscapeUriString(url); - + WebApiHelper.ExecuteUpload(this, url, HttpVerbs.DELETE, string.Empty); } @@ -471,7 +476,7 @@ public void DeleteWikiPage(string projectId, string pageName) try { var tempResult = GetPaginatedObjects(parameters); - + if (tempResult != null) { totalCount = tempResult.TotalItems; @@ -559,10 +564,10 @@ public void DeleteWikiPage(string projectId, string pageName) public List GetObjects(int limit, int offset, params string[] include) where T : class, new() { var parameters = new NameValueCollection(); - + parameters.Add(RedmineKeys.LIMIT, limit.ToString(CultureInfo.InvariantCulture)); parameters.Add(RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)); - + if (include != null && include.Length > 0) { parameters.Add(RedmineKeys.INCLUDE, string.Join(",", include)); @@ -587,7 +592,7 @@ public void DeleteWikiPage(string projectId, string pageName) public List GetObjects(params string[] include) where T : class, new() { var parameters = new NameValueCollection(); - + if (include != null && include.Length > 0) { parameters.Add(RedmineKeys.INCLUDE, string.Join(",", include)); @@ -644,7 +649,6 @@ public void DeleteWikiPage(string projectId, string pageName) if (resultList == null) { resultList = new List(tempResult.Items); - } else { @@ -653,8 +657,8 @@ public void DeleteWikiPage(string projectId, string pageName) } offset += pageSize; - - } while (offset < totalCount); + } + while (offset < totalCount); } else { @@ -718,9 +722,9 @@ public void DeleteWikiPage(string projectId, string pageName) public T CreateObject(T obj, string ownerId) where T : class, new() { var url = UrlHelper.GetCreateUrl(this, ownerId); - + var data = Serializer.Serialize(obj); - + return WebApiHelper.ExecuteUpload(this, url, HttpVerbs.POST, data); } @@ -757,11 +761,11 @@ public void DeleteWikiPage(string projectId, string pageName) public void UpdateObject(string id, T obj, string projectId) where T : class, new() { var url = UrlHelper.GetUploadUrl(this, id); - + var data = Serializer.Serialize(obj); - + data = Regex.Replace(data, @"\r\n|\r|\n", "\r\n"); - + WebApiHelper.ExecuteUpload(this, url, HttpVerbs.PUT, data); } @@ -808,9 +812,9 @@ public Upload UploadFile(byte[] data) public void UpdateAttachment(int issueId, Attachment attachment) { var address = UrlHelper.GetAttachmentUpdateUrl(this, issueId); - + var attachments = new Attachments { { attachment.Id, attachment } }; - + var data = Serializer.Serialize(attachments); WebApiHelper.ExecuteUpload(this, address, HttpVerbs.PATCH, data); @@ -835,7 +839,7 @@ public byte[] DownloadFile(string address) } private const string UA = "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.163 Safari/535.1"; - + /// /// Creates the Redmine web client. /// @@ -890,7 +894,7 @@ public virtual RedmineWebClient CreateWebClient(NameValueCollection parameters, webClient.Proxy = Proxy; webClient.UseProxy = true; } - + if (!string.IsNullOrEmpty(ImpersonateUser)) { webClient.Headers.Add("X-Redmine-Switch-User", ImpersonateUser); @@ -918,6 +922,7 @@ public virtual bool RemoteCertValidate(object sender, X509Certificate cert, X509 { return true; } + return false; } } diff --git a/version.props b/version.props index d3d645fc..c493f568 100644 --- a/version.props +++ b/version.props @@ -1,6 +1,6 @@ - 4.2.2 + 4.2.3 $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix) From 97274702d36d9a64637a2b5a6a9ca8b3f12fe8f3 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 28 Feb 2021 22:12:44 +0200 Subject: [PATCH 193/549] Fix appveyor --- appveyor.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index d020e88e..65e11969 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -32,7 +32,7 @@ init: - ps: $buildNumber = $env:APPVEYOR_BUILD_NUMBER; - ps: $isRepoTag = $env:APPVEYOR_REPO_TAG; - ps: $revision = $(If ($isRepoTag -eq "true") {[string]::Empty} Else {"{0:00000}" -f [convert]::ToInt32("0" + $buildNumber, 10)}); - - ps: $suffix = $(If ($branch -eq "master" -and [string]::IsNullOrEmpty($revision)) {[string]::Empty} Else {$branch.Substring(0, [math]::Min(10,$branch.Length))}); + - ps: $suffix = $(If ([string]::IsNullOrEmpty($revision)) {[string]::Empty} Else {$branch.Substring(0, [math]::Min(10,$branch.Length))}); - ps: $env:BUILD_SUFFIX = $(If ([string]::IsNullOrEmpty($suffix)) {"$branch-$commitHash"} Else {"$suffix-$commitHash"}); - ps: $env:VERSION_SUFFIX = $(If ([string]::IsNullOrEmpty($suffix)) {[string]::Empty} Else {"--version-suffix=$suffix"}); @@ -85,5 +85,4 @@ for: secure: iQKBODPsLcVrf7JQV5IR1jDHq01NiqEDmgj8N0Ahktuu76dKCs827tLggGMO9Mkd skip_symbols: true on: - branch: master APPVEYOR_REPO_TAG: true \ No newline at end of file From a2a29a691828e55e7c49c1ac55e26f791cfa7dee Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 28 Feb 2021 22:31:49 +0200 Subject: [PATCH 194/549] Update Nuget key --- appveyor.yml | 2 +- src/redmine-net-api/RedmineManager.cs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 65e11969..c4095dd5 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -82,7 +82,7 @@ for: - provider: NuGet name: production api_key: - secure: iQKBODPsLcVrf7JQV5IR1jDHq01NiqEDmgj8N0Ahktuu76dKCs827tLggGMO9Mkd + secure: fEZylRkHvyJqjgeQ+i9TfL/JOPjLKr43k+a8Oy5MIy54IkFC8ZECaEfskcWOyqcg skip_symbols: true on: APPVEYOR_REPO_TAG: true \ No newline at end of file diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index 91281903..dc3e5833 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -221,6 +221,9 @@ public RedmineManager(string host, string login, string password, MimeFormat mim /// public string Scheme { get; private set; } + /// + /// + /// public TimeSpan? Timeout { get; private set; } /// From 737d61325b2d6c487b10469eaaea95ea74e6da29 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 28 Feb 2021 22:45:46 +0200 Subject: [PATCH 195/549] Update copyright --- .../Async/RedmineManagerAsync40.cs | 2 +- .../Async/RedmineManagerAsync45.cs | 2 +- .../Exceptions/ConflictException.cs | 2 +- .../Exceptions/ForbiddenException.cs | 2 +- .../InternalServerErrorException.cs | 2 +- .../NameResolutionFailureException.cs | 2 +- .../Exceptions/NotAcceptableException.cs | 2 +- .../Exceptions/NotFoundException.cs | 2 +- .../Exceptions/RedmineException.cs | 2 +- .../Exceptions/RedmineTimeoutException.cs | 2 +- .../Exceptions/UnauthorizedException.cs | 2 +- .../Extensions/CollectionExtensions.cs | 2 +- .../Extensions/WebExtensions.cs | 2 +- .../Extensions/XmlReaderExtensions.cs | 2 +- .../Extensions/XmlWriterExtensions.cs | 2 +- src/redmine-net-api/HttpVerbs.cs | 2 +- src/redmine-net-api/IRedmineManager.cs | 2 +- src/redmine-net-api/IRedmineWebClient.cs | 2 +- src/redmine-net-api/Internals/DataHelper.cs | 2 +- src/redmine-net-api/Internals/Func.cs | 2 +- .../Internals/HashCodeHelper.cs | 2 +- src/redmine-net-api/Internals/UrlHelper.cs | 2 +- .../Internals/WebApiAsyncHelper.cs | 2 +- src/redmine-net-api/Internals/WebApiHelper.cs | 19 +++++++++++++------ src/redmine-net-api/MimeFormat.cs | 2 +- src/redmine-net-api/RedmineManager.cs | 10 ++++++---- src/redmine-net-api/RedmineWebClient.cs | 2 +- src/redmine-net-api/Types/Attachment.cs | 2 +- src/redmine-net-api/Types/Attachments.cs | 2 +- src/redmine-net-api/Types/ChangeSet.cs | 2 +- src/redmine-net-api/Types/CustomField.cs | 2 +- .../Types/CustomFieldPossibleValue.cs | 2 +- src/redmine-net-api/Types/CustomFieldRole.cs | 2 +- src/redmine-net-api/Types/CustomFieldValue.cs | 2 +- src/redmine-net-api/Types/Detail.cs | 2 +- src/redmine-net-api/Types/Error.cs | 2 +- src/redmine-net-api/Types/File.cs | 2 +- src/redmine-net-api/Types/Group.cs | 2 +- src/redmine-net-api/Types/GroupUser.cs | 2 +- src/redmine-net-api/Types/IValue.cs | 2 +- src/redmine-net-api/Types/Identifiable.cs | 2 +- src/redmine-net-api/Types/IdentifiableName.cs | 2 +- src/redmine-net-api/Types/IssueCategory.cs | 2 +- src/redmine-net-api/Types/IssueChild.cs | 2 +- src/redmine-net-api/Types/IssueCustomField.cs | 2 +- src/redmine-net-api/Types/IssuePriority.cs | 2 +- src/redmine-net-api/Types/IssueRelation.cs | 2 +- .../Types/IssueRelationType.cs | 2 +- src/redmine-net-api/Types/IssueStatus.cs | 2 +- src/redmine-net-api/Types/Journal.cs | 2 +- src/redmine-net-api/Types/Membership.cs | 2 +- src/redmine-net-api/Types/MembershipRole.cs | 2 +- src/redmine-net-api/Types/News.cs | 2 +- src/redmine-net-api/Types/Permission.cs | 2 +- src/redmine-net-api/Types/Project.cs | 2 +- .../Types/ProjectEnabledModule.cs | 2 +- .../Types/ProjectIssueCategory.cs | 2 +- .../Types/ProjectMembership.cs | 2 +- src/redmine-net-api/Types/ProjectStatus.cs | 2 +- src/redmine-net-api/Types/ProjectTracker.cs | 2 +- src/redmine-net-api/Types/Query.cs | 2 +- src/redmine-net-api/Types/Role.cs | 2 +- src/redmine-net-api/Types/TimeEntry.cs | 2 +- .../Types/TimeEntryActivity.cs | 2 +- src/redmine-net-api/Types/Tracker.cs | 2 +- .../Types/TrackerCustomField.cs | 2 +- src/redmine-net-api/Types/Upload.cs | 2 +- src/redmine-net-api/Types/User.cs | 2 +- src/redmine-net-api/Types/UserGroup.cs | 2 +- src/redmine-net-api/Types/UserStatus.cs | 2 +- src/redmine-net-api/Types/Version.cs | 2 +- src/redmine-net-api/Types/Watcher.cs | 2 +- src/redmine-net-api/Types/WikiPage.cs | 2 +- .../Tests/Sync/AttachmentTests.cs | 2 +- .../Tests/Sync/CustomFieldTests.cs | 2 +- .../Tests/Sync/IssuePriorityTests.cs | 2 +- .../Tests/Sync/IssueRelationTests.cs | 2 +- .../Tests/Sync/IssueStatusTests.cs | 2 +- .../Tests/Sync/NewsTests.cs | 2 +- .../Tests/Sync/ProjectMembershipTests.cs | 2 +- .../Tests/Sync/ProjectTests.cs | 2 +- .../Tests/Sync/QueryTests.cs | 2 +- .../Tests/Sync/RoleTests.cs | 2 +- .../Tests/Sync/TimeEntryActivtiyTests.cs | 2 +- .../Tests/Sync/TimeEntryTests.cs | 2 +- .../Tests/Sync/TrackerTests.cs | 2 +- .../Tests/Sync/UserTests.cs | 2 +- .../Tests/Sync/VersionTests.cs | 2 +- .../Tests/Sync/WikiPageTests.cs | 2 +- 89 files changed, 106 insertions(+), 97 deletions(-) diff --git a/src/redmine-net-api/Async/RedmineManagerAsync40.cs b/src/redmine-net-api/Async/RedmineManagerAsync40.cs index 5b69444d..3cf06d37 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync40.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync40.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Async/RedmineManagerAsync45.cs b/src/redmine-net-api/Async/RedmineManagerAsync45.cs index 24572b0d..afb27ea9 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync45.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync45.cs @@ -1,5 +1,5 @@ /* -Copyright 2011 - 2019 Adrian Popescu. +Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/ConflictException.cs b/src/redmine-net-api/Exceptions/ConflictException.cs index 43e4a5ad..86465c8d 100644 --- a/src/redmine-net-api/Exceptions/ConflictException.cs +++ b/src/redmine-net-api/Exceptions/ConflictException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/ForbiddenException.cs b/src/redmine-net-api/Exceptions/ForbiddenException.cs index de49d5f4..8c13b5a3 100644 --- a/src/redmine-net-api/Exceptions/ForbiddenException.cs +++ b/src/redmine-net-api/Exceptions/ForbiddenException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/InternalServerErrorException.cs b/src/redmine-net-api/Exceptions/InternalServerErrorException.cs index dea797e9..29704631 100644 --- a/src/redmine-net-api/Exceptions/InternalServerErrorException.cs +++ b/src/redmine-net-api/Exceptions/InternalServerErrorException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/NameResolutionFailureException.cs b/src/redmine-net-api/Exceptions/NameResolutionFailureException.cs index dd0e48c0..77781629 100644 --- a/src/redmine-net-api/Exceptions/NameResolutionFailureException.cs +++ b/src/redmine-net-api/Exceptions/NameResolutionFailureException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/NotAcceptableException.cs b/src/redmine-net-api/Exceptions/NotAcceptableException.cs index 90aee858..7e4a914e 100644 --- a/src/redmine-net-api/Exceptions/NotAcceptableException.cs +++ b/src/redmine-net-api/Exceptions/NotAcceptableException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/NotFoundException.cs b/src/redmine-net-api/Exceptions/NotFoundException.cs index dbd29178..328f1a6d 100644 --- a/src/redmine-net-api/Exceptions/NotFoundException.cs +++ b/src/redmine-net-api/Exceptions/NotFoundException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/RedmineException.cs b/src/redmine-net-api/Exceptions/RedmineException.cs index 0867b830..2f6ed3ef 100644 --- a/src/redmine-net-api/Exceptions/RedmineException.cs +++ b/src/redmine-net-api/Exceptions/RedmineException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/RedmineTimeoutException.cs b/src/redmine-net-api/Exceptions/RedmineTimeoutException.cs index 8f0da618..04af4c12 100644 --- a/src/redmine-net-api/Exceptions/RedmineTimeoutException.cs +++ b/src/redmine-net-api/Exceptions/RedmineTimeoutException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/UnauthorizedException.cs b/src/redmine-net-api/Exceptions/UnauthorizedException.cs index 4283b14c..edb6f1d0 100644 --- a/src/redmine-net-api/Exceptions/UnauthorizedException.cs +++ b/src/redmine-net-api/Exceptions/UnauthorizedException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Extensions/CollectionExtensions.cs b/src/redmine-net-api/Extensions/CollectionExtensions.cs index 22575397..1c70548e 100755 --- a/src/redmine-net-api/Extensions/CollectionExtensions.cs +++ b/src/redmine-net-api/Extensions/CollectionExtensions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Extensions/WebExtensions.cs b/src/redmine-net-api/Extensions/WebExtensions.cs index 0d8920af..0bbbbcaf 100644 --- a/src/redmine-net-api/Extensions/WebExtensions.cs +++ b/src/redmine-net-api/Extensions/WebExtensions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Extensions/XmlReaderExtensions.cs b/src/redmine-net-api/Extensions/XmlReaderExtensions.cs index 6002ca98..df17b0dc 100644 --- a/src/redmine-net-api/Extensions/XmlReaderExtensions.cs +++ b/src/redmine-net-api/Extensions/XmlReaderExtensions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Extensions/XmlWriterExtensions.cs b/src/redmine-net-api/Extensions/XmlWriterExtensions.cs index 9e24e63f..bdfb161d 100644 --- a/src/redmine-net-api/Extensions/XmlWriterExtensions.cs +++ b/src/redmine-net-api/Extensions/XmlWriterExtensions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/HttpVerbs.cs b/src/redmine-net-api/HttpVerbs.cs index b66ef937..29bd3194 100644 --- a/src/redmine-net-api/HttpVerbs.cs +++ b/src/redmine-net-api/HttpVerbs.cs @@ -1,5 +1,5 @@ /* -Copyright 2011 - 2019 Adrian Popescu. +Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/IRedmineManager.cs b/src/redmine-net-api/IRedmineManager.cs index 41863803..66b8a6ac 100644 --- a/src/redmine-net-api/IRedmineManager.cs +++ b/src/redmine-net-api/IRedmineManager.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/IRedmineWebClient.cs b/src/redmine-net-api/IRedmineWebClient.cs index f33e5088..a19679b7 100644 --- a/src/redmine-net-api/IRedmineWebClient.cs +++ b/src/redmine-net-api/IRedmineWebClient.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Internals/DataHelper.cs b/src/redmine-net-api/Internals/DataHelper.cs index dce5dd76..822082e2 100755 --- a/src/redmine-net-api/Internals/DataHelper.cs +++ b/src/redmine-net-api/Internals/DataHelper.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Internals/Func.cs b/src/redmine-net-api/Internals/Func.cs index d279f3eb..3c2a64e1 100644 --- a/src/redmine-net-api/Internals/Func.cs +++ b/src/redmine-net-api/Internals/Func.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Internals/HashCodeHelper.cs b/src/redmine-net-api/Internals/HashCodeHelper.cs index c33d5f28..ce667b11 100755 --- a/src/redmine-net-api/Internals/HashCodeHelper.cs +++ b/src/redmine-net-api/Internals/HashCodeHelper.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Internals/UrlHelper.cs b/src/redmine-net-api/Internals/UrlHelper.cs index cfce99c3..e5ac0e24 100644 --- a/src/redmine-net-api/Internals/UrlHelper.cs +++ b/src/redmine-net-api/Internals/UrlHelper.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Internals/WebApiAsyncHelper.cs b/src/redmine-net-api/Internals/WebApiAsyncHelper.cs index 0962c043..93a4ca63 100644 --- a/src/redmine-net-api/Internals/WebApiAsyncHelper.cs +++ b/src/redmine-net-api/Internals/WebApiAsyncHelper.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Internals/WebApiHelper.cs b/src/redmine-net-api/Internals/WebApiHelper.cs index 29766b27..7efc0d18 100644 --- a/src/redmine-net-api/Internals/WebApiHelper.cs +++ b/src/redmine-net-api/Internals/WebApiHelper.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -66,13 +66,20 @@ public static T ExecuteUpload(RedmineManager redmineManager, string address, { using (var wc = redmineManager.CreateWebClient(null)) { - if (actionType == HttpVerbs.POST || actionType == HttpVerbs.DELETE || actionType == HttpVerbs.PUT || - actionType == HttpVerbs.PATCH) + switch (actionType) { - var response = wc.UploadString(address, actionType, data); - return redmineManager.Serializer.Deserialize(response); + case HttpVerbs.POST: + case HttpVerbs.DELETE: + case HttpVerbs.PUT: + case HttpVerbs.PATCH: + { + var response = wc.UploadString(address, actionType, data); + return redmineManager.Serializer.Deserialize(response); + } + + default: + return default; } - return default; } } diff --git a/src/redmine-net-api/MimeFormat.cs b/src/redmine-net-api/MimeFormat.cs index b99bf3c6..d1ae8316 100755 --- a/src/redmine-net-api/MimeFormat.cs +++ b/src/redmine-net-api/MimeFormat.cs @@ -1,5 +1,5 @@ /* -Copyright 2011 - 2019 Adrian Popescu. +Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index dc3e5833..0cf5b6b1 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -857,9 +857,11 @@ public virtual RedmineWebClient CreateWebClient(NameValueCollection parameters, webClient.Timeout = Timeout; if (!uploadFile) { - webClient.Headers.Add(HttpRequestHeader.ContentType, MimeFormat == MimeFormat.Xml - ? "application/xml" - : "application/json"); + webClient.Headers.Add(HttpRequestHeader.ContentType, MimeFormat switch + { + MimeFormat.Xml => "application/xml", + _ => "application/json" + }); webClient.Encoding = Encoding.UTF8; } else diff --git a/src/redmine-net-api/RedmineWebClient.cs b/src/redmine-net-api/RedmineWebClient.cs index e1ba04a7..453f057e 100644 --- a/src/redmine-net-api/RedmineWebClient.cs +++ b/src/redmine-net-api/RedmineWebClient.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Attachment.cs b/src/redmine-net-api/Types/Attachment.cs index 9d8fb591..b99f4d51 100644 --- a/src/redmine-net-api/Types/Attachment.cs +++ b/src/redmine-net-api/Types/Attachment.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Attachments.cs b/src/redmine-net-api/Types/Attachments.cs index 6f4b0314..01ebb125 100644 --- a/src/redmine-net-api/Types/Attachments.cs +++ b/src/redmine-net-api/Types/Attachments.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/ChangeSet.cs b/src/redmine-net-api/Types/ChangeSet.cs index c84c237d..c364db85 100644 --- a/src/redmine-net-api/Types/ChangeSet.cs +++ b/src/redmine-net-api/Types/ChangeSet.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/CustomField.cs b/src/redmine-net-api/Types/CustomField.cs index 0c7b0afd..b455fe85 100644 --- a/src/redmine-net-api/Types/CustomField.cs +++ b/src/redmine-net-api/Types/CustomField.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/CustomFieldPossibleValue.cs b/src/redmine-net-api/Types/CustomFieldPossibleValue.cs index 4323c310..4225d9e1 100644 --- a/src/redmine-net-api/Types/CustomFieldPossibleValue.cs +++ b/src/redmine-net-api/Types/CustomFieldPossibleValue.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/CustomFieldRole.cs b/src/redmine-net-api/Types/CustomFieldRole.cs index 261eba5a..8567c5fe 100644 --- a/src/redmine-net-api/Types/CustomFieldRole.cs +++ b/src/redmine-net-api/Types/CustomFieldRole.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/CustomFieldValue.cs b/src/redmine-net-api/Types/CustomFieldValue.cs index 3d902482..9cdd1ac8 100644 --- a/src/redmine-net-api/Types/CustomFieldValue.cs +++ b/src/redmine-net-api/Types/CustomFieldValue.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Detail.cs b/src/redmine-net-api/Types/Detail.cs index 216ec805..24e5f40f 100644 --- a/src/redmine-net-api/Types/Detail.cs +++ b/src/redmine-net-api/Types/Detail.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Error.cs b/src/redmine-net-api/Types/Error.cs index c31be160..9f5e9ddc 100644 --- a/src/redmine-net-api/Types/Error.cs +++ b/src/redmine-net-api/Types/Error.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/File.cs b/src/redmine-net-api/Types/File.cs index 5094fa8e..1e7e8a69 100644 --- a/src/redmine-net-api/Types/File.cs +++ b/src/redmine-net-api/Types/File.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Group.cs b/src/redmine-net-api/Types/Group.cs index 92e76776..47bd7b58 100644 --- a/src/redmine-net-api/Types/Group.cs +++ b/src/redmine-net-api/Types/Group.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/GroupUser.cs b/src/redmine-net-api/Types/GroupUser.cs index 4d2dcffe..71992a96 100644 --- a/src/redmine-net-api/Types/GroupUser.cs +++ b/src/redmine-net-api/Types/GroupUser.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IValue.cs b/src/redmine-net-api/Types/IValue.cs index 27777057..6f92c86e 100755 --- a/src/redmine-net-api/Types/IValue.cs +++ b/src/redmine-net-api/Types/IValue.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Identifiable.cs b/src/redmine-net-api/Types/Identifiable.cs index 5fc2ec10..751b95da 100644 --- a/src/redmine-net-api/Types/Identifiable.cs +++ b/src/redmine-net-api/Types/Identifiable.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IdentifiableName.cs b/src/redmine-net-api/Types/IdentifiableName.cs index 740f966c..ec3b2894 100644 --- a/src/redmine-net-api/Types/IdentifiableName.cs +++ b/src/redmine-net-api/Types/IdentifiableName.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IssueCategory.cs b/src/redmine-net-api/Types/IssueCategory.cs index eb850ea6..7f21100a 100644 --- a/src/redmine-net-api/Types/IssueCategory.cs +++ b/src/redmine-net-api/Types/IssueCategory.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IssueChild.cs b/src/redmine-net-api/Types/IssueChild.cs index 5115ab33..63367ca9 100644 --- a/src/redmine-net-api/Types/IssueChild.cs +++ b/src/redmine-net-api/Types/IssueChild.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IssueCustomField.cs b/src/redmine-net-api/Types/IssueCustomField.cs index f30f8713..4f3ed6dd 100644 --- a/src/redmine-net-api/Types/IssueCustomField.cs +++ b/src/redmine-net-api/Types/IssueCustomField.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IssuePriority.cs b/src/redmine-net-api/Types/IssuePriority.cs index 8eabc866..1060101d 100644 --- a/src/redmine-net-api/Types/IssuePriority.cs +++ b/src/redmine-net-api/Types/IssuePriority.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IssueRelation.cs b/src/redmine-net-api/Types/IssueRelation.cs index c9349001..5f687c52 100644 --- a/src/redmine-net-api/Types/IssueRelation.cs +++ b/src/redmine-net-api/Types/IssueRelation.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IssueRelationType.cs b/src/redmine-net-api/Types/IssueRelationType.cs index 79ba9fdb..6cfd5e4c 100644 --- a/src/redmine-net-api/Types/IssueRelationType.cs +++ b/src/redmine-net-api/Types/IssueRelationType.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IssueStatus.cs b/src/redmine-net-api/Types/IssueStatus.cs index f6e28201..520ffb7f 100644 --- a/src/redmine-net-api/Types/IssueStatus.cs +++ b/src/redmine-net-api/Types/IssueStatus.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Journal.cs b/src/redmine-net-api/Types/Journal.cs index c7cd1950..3d9c2a7d 100644 --- a/src/redmine-net-api/Types/Journal.cs +++ b/src/redmine-net-api/Types/Journal.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Membership.cs b/src/redmine-net-api/Types/Membership.cs index 956651a9..2286bfbb 100644 --- a/src/redmine-net-api/Types/Membership.cs +++ b/src/redmine-net-api/Types/Membership.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/MembershipRole.cs b/src/redmine-net-api/Types/MembershipRole.cs index 0b16f7d5..77903ef2 100644 --- a/src/redmine-net-api/Types/MembershipRole.cs +++ b/src/redmine-net-api/Types/MembershipRole.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/News.cs b/src/redmine-net-api/Types/News.cs index 3e738072..7f28c0be 100644 --- a/src/redmine-net-api/Types/News.cs +++ b/src/redmine-net-api/Types/News.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Permission.cs b/src/redmine-net-api/Types/Permission.cs index 185157c1..7ee3c69b 100644 --- a/src/redmine-net-api/Types/Permission.cs +++ b/src/redmine-net-api/Types/Permission.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs index 73331225..0d71d1c8 100644 --- a/src/redmine-net-api/Types/Project.cs +++ b/src/redmine-net-api/Types/Project.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/ProjectEnabledModule.cs b/src/redmine-net-api/Types/ProjectEnabledModule.cs index d2a57d89..91ce379c 100644 --- a/src/redmine-net-api/Types/ProjectEnabledModule.cs +++ b/src/redmine-net-api/Types/ProjectEnabledModule.cs @@ -1,5 +1,5 @@ /* -Copyright 2011 - 2019 Adrian Popescu. +Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/ProjectIssueCategory.cs b/src/redmine-net-api/Types/ProjectIssueCategory.cs index 5af0986d..83916b01 100644 --- a/src/redmine-net-api/Types/ProjectIssueCategory.cs +++ b/src/redmine-net-api/Types/ProjectIssueCategory.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/ProjectMembership.cs b/src/redmine-net-api/Types/ProjectMembership.cs index cb8b1406..188b651a 100644 --- a/src/redmine-net-api/Types/ProjectMembership.cs +++ b/src/redmine-net-api/Types/ProjectMembership.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/ProjectStatus.cs b/src/redmine-net-api/Types/ProjectStatus.cs index 5c9dbd20..0955954f 100755 --- a/src/redmine-net-api/Types/ProjectStatus.cs +++ b/src/redmine-net-api/Types/ProjectStatus.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/ProjectTracker.cs b/src/redmine-net-api/Types/ProjectTracker.cs index b42c5468..74cafbf7 100644 --- a/src/redmine-net-api/Types/ProjectTracker.cs +++ b/src/redmine-net-api/Types/ProjectTracker.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Query.cs b/src/redmine-net-api/Types/Query.cs index 53e3c8e4..6cf9e5f2 100644 --- a/src/redmine-net-api/Types/Query.cs +++ b/src/redmine-net-api/Types/Query.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Role.cs b/src/redmine-net-api/Types/Role.cs index 3e6b905c..e11280f6 100644 --- a/src/redmine-net-api/Types/Role.cs +++ b/src/redmine-net-api/Types/Role.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/TimeEntry.cs b/src/redmine-net-api/Types/TimeEntry.cs index 29770f01..bf5f188c 100644 --- a/src/redmine-net-api/Types/TimeEntry.cs +++ b/src/redmine-net-api/Types/TimeEntry.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/TimeEntryActivity.cs b/src/redmine-net-api/Types/TimeEntryActivity.cs index 9fade0c3..82e7d1bd 100644 --- a/src/redmine-net-api/Types/TimeEntryActivity.cs +++ b/src/redmine-net-api/Types/TimeEntryActivity.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Tracker.cs b/src/redmine-net-api/Types/Tracker.cs index d08f5931..68ecacef 100644 --- a/src/redmine-net-api/Types/Tracker.cs +++ b/src/redmine-net-api/Types/Tracker.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/TrackerCustomField.cs b/src/redmine-net-api/Types/TrackerCustomField.cs index d20ae818..f1967a57 100644 --- a/src/redmine-net-api/Types/TrackerCustomField.cs +++ b/src/redmine-net-api/Types/TrackerCustomField.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Upload.cs b/src/redmine-net-api/Types/Upload.cs index 0a0e448a..b35301da 100644 --- a/src/redmine-net-api/Types/Upload.cs +++ b/src/redmine-net-api/Types/Upload.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/User.cs b/src/redmine-net-api/Types/User.cs index dd220f56..fa227276 100644 --- a/src/redmine-net-api/Types/User.cs +++ b/src/redmine-net-api/Types/User.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/UserGroup.cs b/src/redmine-net-api/Types/UserGroup.cs index 1de94606..7fde4bb8 100644 --- a/src/redmine-net-api/Types/UserGroup.cs +++ b/src/redmine-net-api/Types/UserGroup.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/UserStatus.cs b/src/redmine-net-api/Types/UserStatus.cs index 992d13b2..f2c541d6 100644 --- a/src/redmine-net-api/Types/UserStatus.cs +++ b/src/redmine-net-api/Types/UserStatus.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Version.cs b/src/redmine-net-api/Types/Version.cs index a5353243..87670a30 100644 --- a/src/redmine-net-api/Types/Version.cs +++ b/src/redmine-net-api/Types/Version.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Watcher.cs b/src/redmine-net-api/Types/Watcher.cs index 0980205c..8e9bb1b7 100644 --- a/src/redmine-net-api/Types/Watcher.cs +++ b/src/redmine-net-api/Types/Watcher.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/WikiPage.cs b/src/redmine-net-api/Types/WikiPage.cs index 9bc11a3b..c4293c45 100644 --- a/src/redmine-net-api/Types/WikiPage.cs +++ b/src/redmine-net-api/Types/WikiPage.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs index 014fd02a..24a644ad 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/CustomFieldTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/CustomFieldTests.cs index 100968a2..163cd1be 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/CustomFieldTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/CustomFieldTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/IssuePriorityTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/IssuePriorityTests.cs index 0e0a6fe5..f0aef0d0 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/IssuePriorityTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/IssuePriorityTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs index c9de2a44..c85d6139 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/IssueStatusTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/IssueStatusTests.cs index 59d32eaa..83dc2380 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/IssueStatusTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/IssueStatusTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/NewsTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/NewsTests.cs index 5cb2bad0..1fe7ffad 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/NewsTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/NewsTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs index 85858563..948a1c61 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/ProjectTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/ProjectTests.cs index acee0878..126542a3 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/ProjectTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/ProjectTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/QueryTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/QueryTests.cs index 3a472009..faad08df 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/QueryTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/QueryTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/RoleTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/RoleTests.cs index 7ef31761..1d5d5cc6 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/RoleTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/RoleTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryActivtiyTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryActivtiyTests.cs index 451309bc..28e2bbde 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryActivtiyTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryActivtiyTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs index e21458a3..e5424ebc 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/TrackerTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/TrackerTests.cs index 847c7a4d..cefbbd98 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/TrackerTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/TrackerTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/UserTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/UserTests.cs index 569405a8..624676f1 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/UserTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/UserTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/VersionTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/VersionTests.cs index 34eda71b..0c652581 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/VersionTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/VersionTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs index 4457b64b..68967b3d 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2019 Adrian Popescu. + Copyright 2011 - 2021 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From d9a88f559096677c26132a27605690845e2eb696 Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 1 Mar 2021 12:39:44 +0200 Subject: [PATCH 196/549] Revert test condition to c# 7.3 version --- src/redmine-net-api/RedmineManager.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index 0cf5b6b1..0bc375a2 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -857,11 +857,8 @@ public virtual RedmineWebClient CreateWebClient(NameValueCollection parameters, webClient.Timeout = Timeout; if (!uploadFile) { - webClient.Headers.Add(HttpRequestHeader.ContentType, MimeFormat switch - { - MimeFormat.Xml => "application/xml", - _ => "application/json" - }); + webClient.Headers.Add(HttpRequestHeader.ContentType, + MimeFormat is MimeFormat.Xml ? "application/xml" : "application/json"); webClient.Encoding = Encoding.UTF8; } else From ddf53070bda2a8a407474ed0a39cdd341c66df6b Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 7 Apr 2021 13:02:56 +0300 Subject: [PATCH 197/549] Fix #288 --- src/redmine-net-api/RedmineKeys.cs | 8 ++ src/redmine-net-api/Types/IssueRelation.cs | 42 ++++---- .../Types/IssueRelationType.cs | 95 ++++++++++++++++++- 3 files changed, 120 insertions(+), 25 deletions(-) diff --git a/src/redmine-net-api/RedmineKeys.cs b/src/redmine-net-api/RedmineKeys.cs index 4c3398d9..1ac7f942 100644 --- a/src/redmine-net-api/RedmineKeys.cs +++ b/src/redmine-net-api/RedmineKeys.cs @@ -107,6 +107,14 @@ public static class RedmineKeys /// /// /// + public const string COPIED_FROM = "copied_from"; + /// + /// + /// + public const string COPIED_TO = "copied_to"; + /// + /// + /// public const string CREATED_ON = "created_on"; /// diff --git a/src/redmine-net-api/Types/IssueRelation.cs b/src/redmine-net-api/Types/IssueRelation.cs index 5f687c52..ec50bda6 100644 --- a/src/redmine-net-api/Types/IssueRelation.cs +++ b/src/redmine-net-api/Types/IssueRelation.cs @@ -87,13 +87,7 @@ public override void ReadXml(XmlReader reader) case RedmineKeys.DELAY: Delay = reader.ReadAttributeAsNullableInt(attributeName); break; case RedmineKeys.ISSUE_ID: IssueId = reader.ReadAttributeAsInt(attributeName); break; case RedmineKeys.ISSUE_TO_ID: IssueToId = reader.ReadAttributeAsInt(attributeName); break; - case RedmineKeys.RELATION_TYPE: - var issueRelationType = reader.GetAttribute(attributeName); - if (!issueRelationType.IsNullOrWhiteSpace()) - { - Type = (IssueRelationType)Enum.Parse(typeof(IssueRelationType), issueRelationType, true); - } - break; + case RedmineKeys.RELATION_TYPE: Type = ReadIssueRelationType(reader.GetAttribute(attributeName)); break; } } return; @@ -105,13 +99,7 @@ public override void ReadXml(XmlReader reader) case RedmineKeys.DELAY: Delay = reader.ReadElementContentAsNullableInt(); break; case RedmineKeys.ISSUE_ID: IssueId = reader.ReadElementContentAsInt(); break; case RedmineKeys.ISSUE_TO_ID: IssueToId = reader.ReadElementContentAsInt(); break; - case RedmineKeys.RELATION_TYPE: - var issueRelationType = reader.ReadElementContentAsString(); - if (!issueRelationType.IsNullOrWhiteSpace()) - { - Type = (IssueRelationType)Enum.Parse(typeof(IssueRelationType), issueRelationType, true); - } - break; + case RedmineKeys.RELATION_TYPE: Type = ReadIssueRelationType(reader.ReadElementContentAsString()); break; default: reader.Read(); break; } } @@ -180,7 +168,7 @@ public override void ReadJson(JsonReader reader) case RedmineKeys.DELAY: Delay = reader.ReadAsInt32(); break; case RedmineKeys.ISSUE_ID: IssueId = reader.ReadAsInt(); break; case RedmineKeys.ISSUE_TO_ID: IssueToId = reader.ReadAsInt(); break; - case RedmineKeys.RELATION_TYPE: Type = ReadIssueRelationType(reader); break; + case RedmineKeys.RELATION_TYPE: Type = ReadIssueRelationType(reader.ReadAsString()); break; } } } @@ -192,22 +180,32 @@ private void AssertValidIssueRelationType() throw new RedmineException($"The value `{nameof(IssueRelationType)}.`{nameof(IssueRelationType.Undefined)}` is not allowed to create relations!"); } } - - private static IssueRelationType ReadIssueRelationType(JsonReader reader) + + private static IssueRelationType ReadIssueRelationType(string value) { - var enumValue = reader.ReadAsString(); - if (enumValue.IsNullOrWhiteSpace()) + if (value.IsNullOrWhiteSpace()) { return IssueRelationType.Undefined; } - if (short.TryParse(enumValue, out var enumId)) + if (short.TryParse(value, out var enumId)) { return (IssueRelationType)enumId; } - - return (IssueRelationType)Enum.Parse(typeof(IssueRelationType), enumValue, true); + + if (RedmineKeys.COPIED_TO.Equals(value, StringComparison.OrdinalIgnoreCase)) + { + return IssueRelationType.CopiedTo; + } + + if (RedmineKeys.COPIED_FROM.Equals(value, StringComparison.OrdinalIgnoreCase)) + { + return IssueRelationType.CopiedFrom; + } + + return (IssueRelationType)Enum.Parse(typeof(IssueRelationType), value, true); } + #endregion #region Implementation of IEquatable diff --git a/src/redmine-net-api/Types/IssueRelationType.cs b/src/redmine-net-api/Types/IssueRelationType.cs index 6cfd5e4c..c47bd96a 100644 --- a/src/redmine-net-api/Types/IssueRelationType.cs +++ b/src/redmine-net-api/Types/IssueRelationType.cs @@ -15,6 +15,10 @@ limitations under the License. */ using System; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using System.Runtime.Serialization; namespace Redmine.Net.Api.Types { @@ -23,47 +27,132 @@ namespace Redmine.Net.Api.Types /// public enum IssueRelationType { -#pragma warning disable CS0618 // Use of internal enumeration value is allowed here to have a fallback + #pragma warning disable CS0618 // Use of internal enumeration value is allowed here to have a fallback /// /// Fallback value for deserialization purposes in case the deserialization fails. Do not use to create new relations! /// - Undefined = 0, -#pragma warning restore CS0618 + Undefined = 0, + #pragma warning restore CS0618 /// /// /// Relates = 1, + /// /// /// Duplicates, + /// /// /// Duplicated, + /// /// /// Blocks, + /// /// /// Blocked, + /// /// /// Precedes, + /// /// /// Follows, + /// /// /// + + [XmlEnum("copied_to")] CopiedTo, + /// /// /// + [XmlEnum("copied_from")] CopiedFrom } + + // /// + // public class IssueRelationTypeConverter : JsonConverter + // { + // /// + // public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + // { + // IssueRelationType messageTransportResponseStatus = (IssueRelationType) value; + // + // switch (messageTransportResponseStatus) + // { + // case IssueRelationType.Undefined: + // break; + // case IssueRelationType.Relates: + // writer.WriteValue("relates"); + // break; + // case IssueRelationType.Duplicates: + // writer.WriteValue("duplicates"); + // break; + // case IssueRelationType.Duplicated: + // writer.WriteValue("duplicated"); + // break; + // case IssueRelationType.Blocks: + // writer.WriteValue("blocks"); + // break; + // case IssueRelationType.Blocked: + // writer.WriteValue("blocked"); + // break; + // case IssueRelationType.Precedes: + // writer.WriteValue("precedes"); + // break; + // case IssueRelationType.Follows: + // writer.WriteValue("follows"); + // break; + // case IssueRelationType.CopiedTo: + // writer.WriteValue("copied_to"); + // break; + // case IssueRelationType.CopiedFrom: + // writer.WriteValue("copied_from"); + // break; + // default: + // throw new ArgumentOutOfRangeException(); + // } + // } + // + // /// + // public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + // { + // var enumString = (string) reader.Value; + // switch (enumString) + // { + // case "relates": + // case "duplicates": + // case "duplicated": + // case "blocks": + // case "blocked": + // case "precedes": + // case "follows": + // return Enum.Parse(typeof(IssueRelationType), enumString, true); + // case "copied_to": + // return IssueRelationType.CopiedTo; + // case "copied_from": + // return IssueRelationType.CopiedFrom; + // default: + // throw new ArgumentOutOfRangeException(); + // } + // } + // + // /// + // public override bool CanConvert(Type objectType) + // { + // return objectType == typeof(string); + // } + // } } \ No newline at end of file From 457392c894d19344015e6b628523d36453a5b250 Mon Sep 17 00:00:00 2001 From: Necati Meral Date: Fri, 30 Apr 2021 10:24:04 +0200 Subject: [PATCH 198/549] Added serializer extension methods for `WriteBoolean` --- .../Extensions/JsonWriterExtensions.cs | 25 +++++++++---- .../Extensions/XmlWriterExtensions.cs | 35 +++++++++++++------ 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/src/redmine-net-api/Extensions/JsonWriterExtensions.cs b/src/redmine-net-api/Extensions/JsonWriterExtensions.cs index 3d00f176..063ff6f7 100644 --- a/src/redmine-net-api/Extensions/JsonWriterExtensions.cs +++ b/src/redmine-net-api/Extensions/JsonWriterExtensions.cs @@ -32,12 +32,12 @@ public static void WriteIdIfNotNull(this JsonWriter jsonWriter, string tag, Iden } /// - /// + /// Writes if not default or null. /// /// - /// - /// - /// + /// The writer. + /// The value. + /// The property name. public static void WriteIfNotDefaultOrNull(this JsonWriter writer, string elementName, T value) { if (EqualityComparer.Default.Equals(value, default)) @@ -48,10 +48,23 @@ public static void WriteIfNotDefaultOrNull(this JsonWriter writer, string ele if (value is bool) { writer.WriteProperty(elementName, value.ToString().ToLowerInv()); - return; } + else + { + writer.WriteProperty(elementName, value.ToString()); + } + } - writer.WriteProperty(elementName, string.Format(CultureInfo.InvariantCulture, "{0}", value.ToString())); + /// + /// Writes the boolean value + /// + /// + /// The writer. + /// The value. + /// The property name. + public static void WriteBoolean(this JsonWriter writer, string elementName, bool value) + { + writer.WriteProperty(elementName, value.ToString().ToLowerInv()); } /// diff --git a/src/redmine-net-api/Extensions/XmlWriterExtensions.cs b/src/redmine-net-api/Extensions/XmlWriterExtensions.cs index bdfb161d..42929452 100644 --- a/src/redmine-net-api/Extensions/XmlWriterExtensions.cs +++ b/src/redmine-net-api/Extensions/XmlWriterExtensions.cs @@ -30,11 +30,11 @@ namespace Redmine.Net.Api.Extensions public static partial class XmlExtensions { - #if !(NET20 || NET40 || NET45 || NET451 || NET452) +#if !(NET20 || NET40 || NET45 || NET451 || NET452) private static readonly Type[] EmptyTypeArray = Array.Empty(); - #else +#else private static readonly Type[] EmptyTypeArray = new Type[0]; - #endif +#endif private static readonly XmlAttributeOverrides XmlAttributeOverrides = new XmlAttributeOverrides(); /// @@ -63,7 +63,7 @@ public static void WriteArray(this XmlWriter writer, string elementName, IEnumer { return; } - + writer.WriteStartElement(elementName); writer.WriteAttributeString("type", "array"); @@ -115,12 +115,12 @@ public static void WriteArrayIds(this XmlWriter writer, string elementName, IEnu { return; } - + writer.WriteStartElement(elementName); writer.WriteAttributeString("type", "array"); var serializer = new XmlSerializer(type); - + foreach (var item in collection) { serializer.Serialize(writer, f.Invoke(item)); @@ -138,13 +138,13 @@ public static void WriteArrayIds(this XmlWriter writer, string elementName, IEnu /// The type. /// The root. /// The default namespace. - public static void WriteArray(this XmlWriter writer, string elementName, IEnumerable collection, Type type, string root, string defaultNamespace = null) + public static void WriteArray(this XmlWriter writer, string elementName, IEnumerable collection, Type type, string root, string defaultNamespace = null) { if (collection == null) { return; } - + writer.WriteStartElement(elementName); writer.WriteAttributeString("type", "array"); @@ -152,7 +152,7 @@ public static void WriteArray(this XmlWriter writer, string elementName, IEnumer var serializer = new XmlSerializer(type, XmlAttributeOverrides, EmptyTypeArray, rootAttribute, defaultNamespace); - + foreach (var item in collection) { serializer.Serialize(writer, item); @@ -252,10 +252,23 @@ public static void WriteIfNotDefaultOrNull(this XmlWriter writer, string elem if (value is bool) { writer.WriteElementString(elementName, value.ToString().ToLowerInv()); - return; } + else + { + writer.WriteElementString(elementName, value.ToString()); + } + } - writer.WriteElementString(elementName, value.ToString()); + /// + /// Writes the boolean value + /// + /// + /// The writer. + /// The value. + /// The tag. + public static void WriteBoolean(this XmlWriter writer, string elementName, bool value) + { + writer.WriteElementString(elementName, value.ToString().ToLowerInv()); } /// From e8d2d3094b9fad37ed54f48d102746698dca4c8f Mon Sep 17 00:00:00 2001 From: Necati Meral Date: Fri, 30 Apr 2021 10:24:34 +0200 Subject: [PATCH 199/549] Replaced serialization methods for boolean properties --- src/redmine-net-api/Types/Issue.cs | 6 +++--- src/redmine-net-api/Types/IssueCustomField.cs | 2 +- src/redmine-net-api/Types/Project.cs | 8 ++++---- src/redmine-net-api/Types/User.cs | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/redmine-net-api/Types/Issue.cs b/src/redmine-net-api/Types/Issue.cs index 3abb361c..65859599 100644 --- a/src/redmine-net-api/Types/Issue.cs +++ b/src/redmine-net-api/Types/Issue.cs @@ -322,7 +322,7 @@ public override void WriteXml(XmlWriter writer) if (Id != 0) { - writer.WriteElementString(RedmineKeys.PRIVATE_NOTES, PrivateNotes.ToString(CultureInfo.InvariantCulture).ToLowerInv()); + writer.WriteBoolean(RedmineKeys.PRIVATE_NOTES, PrivateNotes); } writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); @@ -423,10 +423,10 @@ public override void WriteJson(JsonWriter writer) if (Id != 0) { - writer.WriteProperty(RedmineKeys.PRIVATE_NOTES, PrivateNotes.ToString(CultureInfo.InvariantCulture).ToLowerInv()); + writer.WriteBoolean(RedmineKeys.PRIVATE_NOTES, PrivateNotes); } - writer.WriteProperty(RedmineKeys.IS_PRIVATE, IsPrivate.ToString(CultureInfo.InvariantCulture).ToLowerInv()); + writer.WriteBoolean(RedmineKeys.IS_PRIVATE, IsPrivate); writer.WriteIdIfNotNull(RedmineKeys.PROJECT_ID, Project); writer.WriteIdIfNotNull(RedmineKeys.PRIORITY_ID, Priority); writer.WriteIdIfNotNull(RedmineKeys.STATUS_ID, Status); diff --git a/src/redmine-net-api/Types/IssueCustomField.cs b/src/redmine-net-api/Types/IssueCustomField.cs index 4f3ed6dd..d2816c4a 100644 --- a/src/redmine-net-api/Types/IssueCustomField.cs +++ b/src/redmine-net-api/Types/IssueCustomField.cs @@ -155,7 +155,7 @@ public override void WriteJson(JsonWriter writer) } writer.WriteEndArray(); - writer.WriteProperty(RedmineKeys.MULTIPLE, Multiple.ToString(CultureInfo.InvariantCulture).ToLowerInv()); + writer.WriteBoolean(RedmineKeys.MULTIPLE, Multiple); } else { diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs index 0d71d1c8..682a7290 100644 --- a/src/redmine-net-api/Types/Project.cs +++ b/src/redmine-net-api/Types/Project.cs @@ -193,8 +193,8 @@ public override void WriteXml(XmlWriter writer) writer.WriteElementString(RedmineKeys.IDENTIFIER, Identifier); writer.WriteIfNotDefaultOrNull(RedmineKeys.DESCRIPTION, Description); - writer.WriteIfNotDefaultOrNull(RedmineKeys.INHERIT_MEMBERS, InheritMembers); - writer.WriteIfNotDefaultOrNull(RedmineKeys.IS_PUBLIC, IsPublic); + writer.WriteBoolean(RedmineKeys.INHERIT_MEMBERS, InheritMembers); + writer.WriteBoolean(RedmineKeys.IS_PUBLIC, IsPublic); writer.WriteIfNotDefaultOrNull(RedmineKeys.HOMEPAGE, HomePage); writer.WriteIdIfNotNull(RedmineKeys.PARENT_ID, Parent); @@ -268,8 +268,8 @@ public override void WriteJson(JsonWriter writer) writer.WriteProperty(RedmineKeys.IDENTIFIER, Identifier); writer.WriteIfNotDefaultOrNull(RedmineKeys.DESCRIPTION, Description); writer.WriteIfNotDefaultOrNull(RedmineKeys.HOMEPAGE, HomePage); - writer.WriteIfNotDefaultOrNull(RedmineKeys.INHERIT_MEMBERS, InheritMembers); - writer.WriteIfNotDefaultOrNull(RedmineKeys.IS_PUBLIC, IsPublic); + writer.WriteBoolean(RedmineKeys.INHERIT_MEMBERS, InheritMembers); + writer.WriteBoolean(RedmineKeys.IS_PUBLIC, IsPublic); writer.WriteIdIfNotNull(RedmineKeys.PARENT_ID, Parent); writer.WriteRepeatableElement(RedmineKeys.TRACKER_IDS, (IEnumerable)Trackers); writer.WriteRepeatableElement(RedmineKeys.ENABLED_MODULE_NAMES, (IEnumerable)EnabledModules); diff --git a/src/redmine-net-api/Types/User.cs b/src/redmine-net-api/Types/User.cs index fa227276..7b2403bb 100644 --- a/src/redmine-net-api/Types/User.cs +++ b/src/redmine-net-api/Types/User.cs @@ -195,7 +195,7 @@ public override void WriteXml(XmlWriter writer) writer.WriteValueOrEmpty(RedmineKeys.AUTH_SOURCE_ID, AuthenticationModeId); } - writer.WriteElementString(RedmineKeys.MUST_CHANGE_PASSWORD, MustChangePassword.ToString(CultureInfo.InvariantCulture).ToLowerInv()); + writer.WriteBoolean(RedmineKeys.MUST_CHANGE_PASSWORD, MustChangePassword); writer.WriteElementString(RedmineKeys.STATUS, ((int)Status).ToString(CultureInfo.InvariantCulture)); if(CustomFields != null) @@ -274,7 +274,7 @@ public override void WriteJson(JsonWriter writer) writer.WriteValueOrEmpty(RedmineKeys.AUTH_SOURCE_ID, AuthenticationModeId); } - writer.WriteProperty(RedmineKeys.MUST_CHANGE_PASSWORD, MustChangePassword.ToString(CultureInfo.InvariantCulture).ToLowerInv()); + writer.WriteBoolean(RedmineKeys.MUST_CHANGE_PASSWORD, MustChangePassword); writer.WriteProperty(RedmineKeys.STATUS, ((int)Status).ToString(CultureInfo.InvariantCulture)); if(CustomFields != null) From 5ed26667f1356fc4b4dc83f662dfbd1270876d4d Mon Sep 17 00:00:00 2001 From: Necati Meral Date: Fri, 30 Apr 2021 10:41:16 +0200 Subject: [PATCH 200/549] Fixed xml comment --- src/redmine-net-api/Extensions/JsonWriterExtensions.cs | 1 - src/redmine-net-api/Extensions/XmlWriterExtensions.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/src/redmine-net-api/Extensions/JsonWriterExtensions.cs b/src/redmine-net-api/Extensions/JsonWriterExtensions.cs index 063ff6f7..6cfbecb2 100644 --- a/src/redmine-net-api/Extensions/JsonWriterExtensions.cs +++ b/src/redmine-net-api/Extensions/JsonWriterExtensions.cs @@ -58,7 +58,6 @@ public static void WriteIfNotDefaultOrNull(this JsonWriter writer, string ele /// /// Writes the boolean value /// - /// /// The writer. /// The value. /// The property name. diff --git a/src/redmine-net-api/Extensions/XmlWriterExtensions.cs b/src/redmine-net-api/Extensions/XmlWriterExtensions.cs index 42929452..01d59b6a 100644 --- a/src/redmine-net-api/Extensions/XmlWriterExtensions.cs +++ b/src/redmine-net-api/Extensions/XmlWriterExtensions.cs @@ -262,7 +262,6 @@ public static void WriteIfNotDefaultOrNull(this XmlWriter writer, string elem /// /// Writes the boolean value /// - /// /// The writer. /// The value. /// The tag. From 861da4c596491972732d44d82064ce8c88fef432 Mon Sep 17 00:00:00 2001 From: Necati Meral Date: Fri, 30 Apr 2021 10:49:10 +0200 Subject: [PATCH 201/549] Removed hiding of `Name` property in inherited classes --- src/redmine-net-api/Types/CustomField.cs | 4 ---- src/redmine-net-api/Types/CustomFieldRole.cs | 8 +------- src/redmine-net-api/Types/Group.cs | 4 ---- src/redmine-net-api/Types/GroupUser.cs | 5 ----- src/redmine-net-api/Types/IdentifiableName.cs | 13 ++++++++++++- src/redmine-net-api/Types/IssueCustomField.cs | 4 ---- src/redmine-net-api/Types/Project.cs | 5 ----- src/redmine-net-api/Types/ProjectEnabledModule.cs | 5 ----- src/redmine-net-api/Types/ProjectIssueCategory.cs | 8 +------- .../Types/ProjectTimeEntryActivity.cs | 8 +------- src/redmine-net-api/Types/ProjectTracker.cs | 8 +------- src/redmine-net-api/Types/Role.cs | 4 ---- src/redmine-net-api/Types/TimeEntryActivity.cs | 9 +-------- src/redmine-net-api/Types/UserGroup.cs | 5 ----- src/redmine-net-api/Types/Watcher.cs | 6 ------ 15 files changed, 17 insertions(+), 79 deletions(-) diff --git a/src/redmine-net-api/Types/CustomField.cs b/src/redmine-net-api/Types/CustomField.cs index b455fe85..48afa037 100644 --- a/src/redmine-net-api/Types/CustomField.cs +++ b/src/redmine-net-api/Types/CustomField.cs @@ -37,10 +37,6 @@ public sealed class CustomField : IdentifiableName, IEquatable /// /// /// - public new string Name { get; set; } - /// - /// - /// public string CustomizedType { get; internal set; } /// diff --git a/src/redmine-net-api/Types/CustomFieldRole.cs b/src/redmine-net-api/Types/CustomFieldRole.cs index 8567c5fe..711e7fca 100644 --- a/src/redmine-net-api/Types/CustomFieldRole.cs +++ b/src/redmine-net-api/Types/CustomFieldRole.cs @@ -33,16 +33,10 @@ public sealed class CustomFieldRole : IdentifiableName public CustomFieldRole() { } internal CustomFieldRole(int id, string name) + : base(id, name) { - Id = id; - Name = name; } - /// - /// - /// - public new string Name { get; set; } - /// /// /// diff --git a/src/redmine-net-api/Types/Group.cs b/src/redmine-net-api/Types/Group.cs index 47bd7b58..d52fd8b6 100644 --- a/src/redmine-net-api/Types/Group.cs +++ b/src/redmine-net-api/Types/Group.cs @@ -49,10 +49,6 @@ public Group(string name) #region Properties /// - /// - /// - public new string Name { get; set; } - /// /// Represents the group's users. /// public IList Users { get; set; } diff --git a/src/redmine-net-api/Types/GroupUser.cs b/src/redmine-net-api/Types/GroupUser.cs index 71992a96..e6b8ac39 100644 --- a/src/redmine-net-api/Types/GroupUser.cs +++ b/src/redmine-net-api/Types/GroupUser.cs @@ -27,11 +27,6 @@ namespace Redmine.Net.Api.Types [XmlRoot(RedmineKeys.USER)] public sealed class GroupUser : IdentifiableName, IValue { - /// - /// - /// - public new string Name { get; set; } - #region Implementation of IValue /// /// diff --git a/src/redmine-net-api/Types/IdentifiableName.cs b/src/redmine-net-api/Types/IdentifiableName.cs index ec3b2894..37236d53 100644 --- a/src/redmine-net-api/Types/IdentifiableName.cs +++ b/src/redmine-net-api/Types/IdentifiableName.cs @@ -47,6 +47,17 @@ public class IdentifiableName : Identifiable /// public IdentifiableName() { } + /// + /// Initializes the class by using the given Id and Name. + /// + /// The Id. + /// The Name. + internal IdentifiableName(int id, string name) + { + Id = id; + Name = name; + } + /// /// Initializes a new instance of the class. /// @@ -79,7 +90,7 @@ private void Initialize(JsonReader reader) /// /// Gets or sets the name. /// - public string Name { get; internal set; } + public virtual string Name { get; set; } #endregion #region Implementation of IXmlSerializable diff --git a/src/redmine-net-api/Types/IssueCustomField.cs b/src/redmine-net-api/Types/IssueCustomField.cs index 4f3ed6dd..74330e0b 100644 --- a/src/redmine-net-api/Types/IssueCustomField.cs +++ b/src/redmine-net-api/Types/IssueCustomField.cs @@ -35,10 +35,6 @@ public sealed class IssueCustomField : IdentifiableName, IEquatable - /// - /// - public new string Name { get; set; } - /// /// Gets or sets the value. /// /// The value. diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs index 0d71d1c8..4b8d6f34 100644 --- a/src/redmine-net-api/Types/Project.cs +++ b/src/redmine-net-api/Types/Project.cs @@ -36,11 +36,6 @@ public sealed class Project : IdentifiableName, IEquatable { #region Properties - /// - /// - /// - public new string Name { get; set; } - /// /// Gets or sets the identifier. /// diff --git a/src/redmine-net-api/Types/ProjectEnabledModule.cs b/src/redmine-net-api/Types/ProjectEnabledModule.cs index 91ce379c..8ac0d753 100644 --- a/src/redmine-net-api/Types/ProjectEnabledModule.cs +++ b/src/redmine-net-api/Types/ProjectEnabledModule.cs @@ -50,11 +50,6 @@ public ProjectEnabledModule(string moduleName) #endregion - /// - /// - /// - public new string Name { get; set; } - #region Implementation of IValue /// /// diff --git a/src/redmine-net-api/Types/ProjectIssueCategory.cs b/src/redmine-net-api/Types/ProjectIssueCategory.cs index 83916b01..6f0c9e0c 100644 --- a/src/redmine-net-api/Types/ProjectIssueCategory.cs +++ b/src/redmine-net-api/Types/ProjectIssueCategory.cs @@ -32,16 +32,10 @@ public sealed class ProjectIssueCategory : IdentifiableName public ProjectIssueCategory() { } internal ProjectIssueCategory(int id, string name) + : base(id, name) { - Id = id; - Name = name; } - /// - /// - /// - public new string Name { get; set; } - /// /// /// diff --git a/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs b/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs index 9633f284..daa9cd78 100644 --- a/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs +++ b/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs @@ -16,16 +16,10 @@ public sealed class ProjectTimeEntryActivity : IdentifiableName public ProjectTimeEntryActivity() { } internal ProjectTimeEntryActivity(int id, string name) + : base(id, name) { - Id = id; - Name = name; } - /// - /// - /// - public new string Name { get; set; } - /// /// /// diff --git a/src/redmine-net-api/Types/ProjectTracker.cs b/src/redmine-net-api/Types/ProjectTracker.cs index 74cafbf7..e10bd823 100644 --- a/src/redmine-net-api/Types/ProjectTracker.cs +++ b/src/redmine-net-api/Types/ProjectTracker.cs @@ -38,9 +38,8 @@ public ProjectTracker() { } /// the tracker id: 1 for Bug, etc. /// public ProjectTracker(int trackerId, string name) + : base(trackerId, name) { - Id = trackerId; - Name = name; } /// @@ -52,11 +51,6 @@ internal ProjectTracker(int trackerId) Id = trackerId; } - /// - /// - /// - public new string Name { get; set; } - #region Implementation of IValue /// diff --git a/src/redmine-net-api/Types/Role.cs b/src/redmine-net-api/Types/Role.cs index e11280f6..6c4ac088 100644 --- a/src/redmine-net-api/Types/Role.cs +++ b/src/redmine-net-api/Types/Role.cs @@ -34,10 +34,6 @@ public sealed class Role : IdentifiableName, IEquatable { #region Properties /// - /// - /// - public new string Name { get; set; } - /// /// Gets the permissions. /// /// diff --git a/src/redmine-net-api/Types/TimeEntryActivity.cs b/src/redmine-net-api/Types/TimeEntryActivity.cs index 82e7d1bd..b7a145dc 100644 --- a/src/redmine-net-api/Types/TimeEntryActivity.cs +++ b/src/redmine-net-api/Types/TimeEntryActivity.cs @@ -38,18 +38,11 @@ public sealed class TimeEntryActivity : IdentifiableName, IEquatable - /// - /// - public new string Name { get; set; } - /// /// /// diff --git a/src/redmine-net-api/Types/UserGroup.cs b/src/redmine-net-api/Types/UserGroup.cs index 7fde4bb8..b5be7db6 100644 --- a/src/redmine-net-api/Types/UserGroup.cs +++ b/src/redmine-net-api/Types/UserGroup.cs @@ -26,11 +26,6 @@ namespace Redmine.Net.Api.Types [XmlRoot(RedmineKeys.GROUP)] public sealed class UserGroup : IdentifiableName { - /// - /// - /// - public new string Name { get; set; } - /// /// /// diff --git a/src/redmine-net-api/Types/Watcher.cs b/src/redmine-net-api/Types/Watcher.cs index 8e9bb1b7..0160da16 100644 --- a/src/redmine-net-api/Types/Watcher.cs +++ b/src/redmine-net-api/Types/Watcher.cs @@ -28,12 +28,6 @@ namespace Redmine.Net.Api.Types [XmlRoot(RedmineKeys.USER)] public sealed class Watcher : IdentifiableName, IValue, ICloneable { - /// - /// - /// - public new string Name { get; set; } - - #region Implementation of IValue /// /// From dc41a2be3aed9c84ca783589ef0cd3a0f9ef9189 Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 8 Jun 2021 13:57:37 +0300 Subject: [PATCH 202/549] Added WikiPageTitle, EstimatedHours & SpentHours to version --- src/redmine-net-api/RedmineKeys.cs | 6 +++- src/redmine-net-api/Types/Version.cs | 45 ++++++++++++++++++++++++---- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/redmine-net-api/RedmineKeys.cs b/src/redmine-net-api/RedmineKeys.cs index 1ac7f942..d471c9ea 100644 --- a/src/redmine-net-api/RedmineKeys.cs +++ b/src/redmine-net-api/RedmineKeys.cs @@ -702,7 +702,11 @@ public static class RedmineKeys /// /// /// - public const string WIKI_PAGE = "wiki_page"; + public const string WIKI_PAGE = "wiki_page"; + /// + /// + /// + public const string WIKI_PAGE_TITLE = "wiki_page_title"; /// /// /// diff --git a/src/redmine-net-api/Types/Version.cs b/src/redmine-net-api/Types/Version.cs index 87670a30..70b5ae5e 100644 --- a/src/redmine-net-api/Types/Version.cs +++ b/src/redmine-net-api/Types/Version.cs @@ -69,6 +69,21 @@ public sealed class Version : Identifiable /// /// The sharing. public VersionSharing Sharing { get; set; } + + /// + /// + /// + public string WikiPageTitle { get; set; } + + /// + /// + /// + public float? EstimatedHours { get; set; } + + /// + /// + /// + public float? SpentHours { get; set; } /// /// Gets the created on. @@ -111,6 +126,9 @@ public override void ReadXml(XmlReader reader) case RedmineKeys.SHARING: Sharing = (VersionSharing)Enum.Parse(typeof(VersionSharing), reader.ReadElementContentAsString(), true); break; case RedmineKeys.STATUS: Status = (VersionStatus)Enum.Parse(typeof(VersionStatus), reader.ReadElementContentAsString(), true); break; case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.WIKI_PAGE_TITLE: WikiPageTitle = reader.ReadElementContentAsString(); break; + case RedmineKeys.ESTIMATED_HOURS: EstimatedHours = reader.ReadElementContentAsNullableFloat(); break; + case RedmineKeys.SPENT_HOURS: SpentHours = reader.ReadElementContentAsNullableFloat(); break; default: reader.Read(); break; } } @@ -158,9 +176,14 @@ public override void ReadJson(JsonReader reader) case RedmineKeys.DUE_DATE: DueDate = reader.ReadAsDateTime(); break; case RedmineKeys.NAME: Name = reader.ReadAsString(); break; case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; - case RedmineKeys.SHARING: Sharing = (VersionSharing)Enum.Parse(typeof(VersionSharing), reader.ReadAsString(), true); break; - case RedmineKeys.STATUS: Status = (VersionStatus)Enum.Parse(typeof(VersionStatus), reader.ReadAsString(), true); break; + case RedmineKeys.SHARING: Sharing = (VersionSharing)Enum.Parse(typeof(VersionSharing), reader.ReadAsString() ?? string.Empty, true); break; + case RedmineKeys.STATUS: Status = (VersionStatus)Enum.Parse(typeof(VersionStatus), reader.ReadAsString() ?? string.Empty, true); break; case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.WIKI_PAGE_TITLE: WikiPageTitle = reader.ReadAsString(); break; + case RedmineKeys.ESTIMATED_HOURS: EstimatedHours = (float?)reader.ReadAsDouble(); break; + case RedmineKeys.SPENT_HOURS: SpentHours = (float?)reader.ReadAsDouble(); break; + + default: reader.Read(); break; } } @@ -200,7 +223,11 @@ public override bool Equals(Version other) && Sharing == other.Sharing && CreatedOn == other.CreatedOn && UpdatedOn == other.UpdatedOn - && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null); + && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null) + && string.Equals(WikiPageTitle,other.WikiPageTitle, StringComparison.InvariantCultureIgnoreCase) + && EstimatedHours == other.EstimatedHours + && SpentHours == other.SpentHours + ; } /// /// @@ -219,6 +246,9 @@ public override int GetHashCode() hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); hashCode = HashCodeHelper.GetHashCode(UpdatedOn, hashCode); hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); + hashCode = HashCodeHelper.GetHashCode(WikiPageTitle, hashCode); + hashCode = HashCodeHelper.GetHashCode(EstimatedHours, hashCode); + hashCode = HashCodeHelper.GetHashCode(SpentHours, hashCode); return hashCode; } } @@ -228,12 +258,17 @@ public override int GetHashCode() /// /// /// - private string DebuggerDisplay => $@"[{nameof(Version)}: {ToString()}, Project={Project}, Description={Description}, + private string DebuggerDisplay => $@"[{nameof(Version)}: {ToString()}, +Project={Project}, +Description={Description}, Status={Status:G}, - DueDate={DueDate?.ToString("u", CultureInfo.InvariantCulture)}, +DueDate={DueDate?.ToString("u", CultureInfo.InvariantCulture)}, Sharing={Sharing:G}, CreatedOn={CreatedOn?.ToString("u", CultureInfo.InvariantCulture)}, UpdatedOn={UpdatedOn?.ToString("u", CultureInfo.InvariantCulture)}, +EstimatedHours={EstimatedHours?.ToString("F", CultureInfo.InvariantCulture)}, +SpentHours={SpentHours?.ToString("F", CultureInfo.InvariantCulture)}, +WikiPageTitle={WikiPageTitle} CustomFields={CustomFields.Dump()}]"; } From 6fa4c61a28e0b6bc2ebc87011581696fdbe9e1cd Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 8 Jun 2021 14:00:37 +0300 Subject: [PATCH 203/549] Added IsAdmin, TwoFactorAuthenticationScheme, PasswordChangedOn, UpdatedOn to user --- src/redmine-net-api/RedmineKeys.cs | 8 +++++ src/redmine-net-api/Types/User.cs | 52 ++++++++++++++++++++++++++++-- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/src/redmine-net-api/RedmineKeys.cs b/src/redmine-net-api/RedmineKeys.cs index d471c9ea..02abae14 100644 --- a/src/redmine-net-api/RedmineKeys.cs +++ b/src/redmine-net-api/RedmineKeys.cs @@ -450,6 +450,10 @@ public static class RedmineKeys /// /// /// + public const string PASSWORD_CHANGED_ON = "passwd_changed_on"; + /// + /// + /// public const string PERMISSION = "permission"; /// /// @@ -638,6 +642,10 @@ public static class RedmineKeys /// /// /// + public const string TWO_FA_SCHEME = "twofa_scheme"; + /// + /// + /// public const string UPDATED_ON = "updated_on"; /// /// diff --git a/src/redmine-net-api/Types/User.cs b/src/redmine-net-api/Types/User.cs index 7b2403bb..9968bea4 100644 --- a/src/redmine-net-api/Types/User.cs +++ b/src/redmine-net-api/Types/User.cs @@ -65,6 +65,16 @@ public sealed class User : Identifiable /// The email. public string Email { get; set; } + /// + /// + /// + public bool IsAdmin { get; set; } + + /// + /// twofa_scheme + /// + public string TwoFactorAuthenticationScheme { get; set; } + /// /// Gets or sets the authentication mode id. /// @@ -74,7 +84,7 @@ public sealed class User : Identifiable public int? AuthenticationModeId { get; set; } /// - /// Gets or sets the created on. + /// Gets the created on. /// /// The created on. public DateTime? CreatedOn { get; internal set; } @@ -100,6 +110,17 @@ public sealed class User : Identifiable /// public bool MustChangePassword { get; set; } + + /// + /// + /// + public DateTime? PasswordChangedOn { get; set; } + + /// + /// + /// + public DateTime? UpdatedOn { get; set; } + /// /// Gets or sets the custom fields. /// @@ -150,6 +171,7 @@ public override void ReadXml(XmlReader reader) switch (reader.Name) { case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.ADMIN: IsAdmin = reader.ReadElementContentAsBoolean(); break; case RedmineKeys.API_KEY: ApiKey = reader.ReadElementContentAsString(); break; case RedmineKeys.AUTH_SOURCE_ID: AuthenticationModeId = reader.ReadElementContentAsNullableInt(); break; case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; @@ -163,7 +185,10 @@ public override void ReadXml(XmlReader reader) case RedmineKeys.MAIL_NOTIFICATION: MailNotification = reader.ReadElementContentAsString(); break; case RedmineKeys.MEMBERSHIPS: Memberships = reader.ReadElementContentAsCollection(); break; case RedmineKeys.MUST_CHANGE_PASSWORD: MustChangePassword = reader.ReadElementContentAsBoolean(); break; + case RedmineKeys.PASSWORD_CHANGED_ON: PasswordChangedOn = reader.ReadElementContentAsNullableDateTime(); break; case RedmineKeys.STATUS: Status = (UserStatus)reader.ReadElementContentAsInt(); break; + case RedmineKeys.TWO_FA_SCHEME: TwoFactorAuthenticationScheme = reader.ReadContentAsString(); break; + case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadElementContentAsNullableDateTime(); break; default: reader.Read(); break; } } @@ -227,6 +252,7 @@ public override void ReadJson(JsonReader reader) switch (reader.Value) { case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.ADMIN: IsAdmin = reader.ReadAsBool(); break; case RedmineKeys.API_KEY: ApiKey = reader.ReadAsString(); break; case RedmineKeys.AUTH_SOURCE_ID: AuthenticationModeId = reader.ReadAsInt32(); break; case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadAsDateTime(); break; @@ -240,7 +266,10 @@ public override void ReadJson(JsonReader reader) case RedmineKeys.MAIL_NOTIFICATION: MailNotification = reader.ReadAsString(); break; case RedmineKeys.MEMBERSHIPS: Memberships = reader.ReadAsCollection(); break; case RedmineKeys.MUST_CHANGE_PASSWORD: MustChangePassword = reader.ReadAsBool(); break; + case RedmineKeys.PASSWORD_CHANGED_ON: PasswordChangedOn = reader.ReadAsDateTime(); break; case RedmineKeys.STATUS: Status = (UserStatus)reader.ReadAsInt(); break; + case RedmineKeys.TWO_FA_SCHEME: TwoFactorAuthenticationScheme = reader.ReadAsString(); break; + case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadAsDateTime(); break; default: reader.Read(); break; } } @@ -308,7 +337,12 @@ public override bool Equals(User other) && MustChangePassword == other.MustChangePassword && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null) && (Memberships != null ? Memberships.Equals(other.Memberships) : other.Memberships == null) - && (Groups != null ? Groups.Equals(other.Groups) : other.Groups == null); + && (Groups != null ? Groups.Equals(other.Groups) : other.Groups == null) + && string.Equals(TwoFactorAuthenticationScheme,other.TwoFactorAuthenticationScheme, StringComparison.OrdinalIgnoreCase) + && IsAdmin == other.IsAdmin + && PasswordChangedOn == other.PasswordChangedOn + && UpdatedOn == other.UpdatedOn + ; } /// @@ -335,6 +369,10 @@ public override int GetHashCode() hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); hashCode = HashCodeHelper.GetHashCode(Memberships, hashCode); hashCode = HashCodeHelper.GetHashCode(Groups, hashCode); + hashCode = HashCodeHelper.GetHashCode(TwoFactorAuthenticationScheme, hashCode); + hashCode = HashCodeHelper.GetHashCode(IsAdmin, hashCode); + hashCode = HashCodeHelper.GetHashCode(PasswordChangedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(UpdatedOn, hashCode); return hashCode; } } @@ -345,10 +383,18 @@ public override int GetHashCode() /// /// private string DebuggerDisplay => - $@"[{nameof(User)}: {Groups}, Login={Login}, Password={Password}, FirstName={FirstName}, LastName={LastName}, Email={Email}, + $@"[{nameof(User)}: {Groups}, +Login={Login}, Password={Password}, +FirstName={FirstName}, +LastName={LastName}, +IsAdmin={IsAdmin.ToString(CultureInfo.InvariantCulture)}, +TwoFactorAuthenticationScheme={TwoFactorAuthenticationScheme} +Email={Email}, EmailNotification={MailNotification}, AuthenticationModeId={AuthenticationModeId?.ToString(CultureInfo.InvariantCulture)}, CreatedOn={CreatedOn?.ToString("u", CultureInfo.InvariantCulture)}, +UpdatedOn={UpdatedOn?.ToString("u", CultureInfo.InvariantCulture)} +PasswordChangedOn={PasswordChangedOn?.ToString("u", CultureInfo.InvariantCulture)} LastLoginOn={LastLoginOn?.ToString("u", CultureInfo.InvariantCulture)}, ApiKey={ApiKey}, Status={Status:G}, From 83243b26e5072041c6a8151d0b4ab893985f164a Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 8 Jun 2021 14:01:29 +0300 Subject: [PATCH 204/549] Added IsActive to time entry activity --- src/redmine-net-api/RedmineKeys.cs | 4 ++++ src/redmine-net-api/Types/TimeEntryActivity.cs | 12 ++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/redmine-net-api/RedmineKeys.cs b/src/redmine-net-api/RedmineKeys.cs index 02abae14..f1dafa5d 100644 --- a/src/redmine-net-api/RedmineKeys.cs +++ b/src/redmine-net-api/RedmineKeys.cs @@ -20,6 +20,10 @@ namespace Redmine.Net.Api /// public static class RedmineKeys { + /// + /// + /// + public const string ACTIVE = "active"; /// /// The activity /// diff --git a/src/redmine-net-api/Types/TimeEntryActivity.cs b/src/redmine-net-api/Types/TimeEntryActivity.cs index b7a145dc..299386fc 100644 --- a/src/redmine-net-api/Types/TimeEntryActivity.cs +++ b/src/redmine-net-api/Types/TimeEntryActivity.cs @@ -47,6 +47,11 @@ internal TimeEntryActivity(int id, string name) /// /// public bool IsDefault { get; internal set; } + + /// + /// + /// + public bool IsActive { get; internal set; } #endregion #region Implementation of IXmlSerializable @@ -71,6 +76,7 @@ public override void ReadXml(XmlReader reader) case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; case RedmineKeys.IS_DEFAULT: IsDefault = reader.ReadElementContentAsBoolean(); break; case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; + case RedmineKeys.ACTIVE: IsActive = reader.ReadElementContentAsBoolean(); break; default: reader.Read(); break; } } @@ -108,6 +114,7 @@ public override void ReadJson(JsonReader reader) case RedmineKeys.ID: Id = reader.ReadAsInt(); break; case RedmineKeys.IS_DEFAULT: IsDefault = reader.ReadAsBool(); break; case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + case RedmineKeys.ACTIVE: IsActive = reader.ReadAsBool(); break; default: reader.Read(); break; } } @@ -125,7 +132,7 @@ public bool Equals(TimeEntryActivity other) { if (other == null) return false; - return Id == other.Id && Name == other.Name && IsDefault == other.IsDefault; + return Id == other.Id && Name == other.Name && IsDefault == other.IsDefault && IsActive == other.IsActive; } /// @@ -153,6 +160,7 @@ public override int GetHashCode() hashCode = HashCodeHelper.GetHashCode(Id, hashCode); hashCode = HashCodeHelper.GetHashCode(Name, hashCode); hashCode = HashCodeHelper.GetHashCode(IsDefault, hashCode); + hashCode = HashCodeHelper.GetHashCode(IsActive, hashCode); return hashCode; } } @@ -163,7 +171,7 @@ public override int GetHashCode() /// /// /// - private string DebuggerDisplay => $"[{nameof(TimeEntryActivity)}:{ToString()}, IsDefault={IsDefault.ToString(CultureInfo.InvariantCulture)}]"; + private string DebuggerDisplay => $"[{nameof(TimeEntryActivity)}:{ToString()}, IsDefault={IsDefault.ToString(CultureInfo.InvariantCulture)}, IsActive={IsActive.ToString(CultureInfo.InvariantCulture)}]"; } } \ No newline at end of file From dcc8201b0458378767a67e3e8ba212cdb5013d2b Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 8 Jun 2021 14:03:42 +0300 Subject: [PATCH 205/549] Added IssuesVisibility, TimeEntriesVisibility, UsersVisibility & IsAssignable to role --- src/redmine-net-api/RedmineKeys.cs | 17 +++++++++++++++++ src/redmine-net-api/Types/Role.cs | 20 ++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/redmine-net-api/RedmineKeys.cs b/src/redmine-net-api/RedmineKeys.cs index f1dafa5d..f88c1382 100644 --- a/src/redmine-net-api/RedmineKeys.cs +++ b/src/redmine-net-api/RedmineKeys.cs @@ -55,6 +55,10 @@ public static class RedmineKeys /// /// /// + public const string ASSIGNABLE = "Assignable"; + /// + /// + /// public const string ATTACHMENT = "attachment"; /// /// @@ -323,6 +327,11 @@ public static class RedmineKeys /// /// public const string ISSUE_TO_ID = "issue_to_id"; + /// + /// + /// + public const string ISSUES_VISIBILITY = "issues_visibility"; + /// /// /// @@ -606,6 +615,10 @@ public static class RedmineKeys /// /// /// + public const string TIME_ENTRIES_VISIBILITY = "time_entries_visibility"; + /// + /// + /// public const string TITLE = "title"; /// /// @@ -678,6 +691,10 @@ public static class RedmineKeys /// /// /// + public const string USERS_VISIBILITY = "users_visibility"; + /// + /// + /// public const string VALUE = "value"; /// /// diff --git a/src/redmine-net-api/Types/Role.cs b/src/redmine-net-api/Types/Role.cs index 6c4ac088..8e049af2 100644 --- a/src/redmine-net-api/Types/Role.cs +++ b/src/redmine-net-api/Types/Role.cs @@ -40,6 +40,26 @@ public sealed class Role : IdentifiableName, IEquatable /// The issue relations. /// public IList Permissions { get; internal set; } + + /// + /// + /// + public string IssuesVisibility { get; set; } + + /// + /// + /// + public string TimeEntriesVisibility { get; set; } + + /// + /// + /// + public string UsersVisibility { get; set; } + + /// + /// + /// + public bool IsAssignable { get; set; } #endregion #region Implementation of IXmlSerialization From fda8f00a95b3bab135814ffb35f15a51290bb977 Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 8 Jun 2021 14:05:09 +0300 Subject: [PATCH 206/549] Added DefaultAssignee & DefaultVersion to project --- src/redmine-net-api/RedmineKeys.cs | 8 ++++++++ src/redmine-net-api/Types/Project.cs | 28 ++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/redmine-net-api/RedmineKeys.cs b/src/redmine-net-api/RedmineKeys.cs index f88c1382..d825a24c 100644 --- a/src/redmine-net-api/RedmineKeys.cs +++ b/src/redmine-net-api/RedmineKeys.cs @@ -148,6 +148,10 @@ public static class RedmineKeys /// public const string CUSTOM_FIELDS = "custom_fields"; + /// + /// + /// + public const string DEFAULT_ASSIGNEE = "default_assignee"; /// /// /// @@ -160,6 +164,10 @@ public static class RedmineKeys /// /// /// + public const string DEFAULT_VERSION = "default_version"; + /// + /// + /// public const string DELAY = "delay"; /// /// diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs index c4240c6d..8130fda4 100644 --- a/src/redmine-net-api/Types/Project.cs +++ b/src/redmine-net-api/Types/Project.cs @@ -138,6 +138,16 @@ public sealed class Project : IdentifiableName, IEquatable /// /// Available in Redmine starting with 3.4.0 version. public IList TimeEntryActivities { get; internal set; } + + /// + /// + /// + public IdentifiableName DefaultVersion { get; set; } + + /// + /// + /// + public IdentifiableName DefaultAssignee { get; set; } #endregion #region Implementation of IXmlSerializer @@ -174,6 +184,8 @@ public override void ReadXml(XmlReader reader) case RedmineKeys.TIME_ENTRY_ACTIVITIES: TimeEntryActivities = reader.ReadElementContentAsCollection(); break; case RedmineKeys.TRACKERS: Trackers = reader.ReadElementContentAsCollection(); break; case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.DEFAULT_ASSIGNEE: DefaultAssignee = new IdentifiableName(reader); break; + case RedmineKeys.DEFAULT_VERSION: DefaultVersion = new IdentifiableName(reader); break; default: reader.Read(); break; } } @@ -246,6 +258,8 @@ public override void ReadJson(JsonReader reader) case RedmineKeys.TIME_ENTRY_ACTIVITIES: TimeEntryActivities = reader.ReadAsCollection(); break; case RedmineKeys.TRACKERS: Trackers = reader.ReadAsCollection(); break; case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.DEFAULT_ASSIGNEE: DefaultAssignee = new IdentifiableName(reader); break; + case RedmineKeys.DEFAULT_VERSION: DefaultVersion = new IdentifiableName(reader); break; default: reader.Read(); break; } } @@ -307,7 +321,9 @@ public bool Equals(Project other) && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null) && (IssueCategories != null ? IssueCategories.Equals(other.IssueCategories) : other.IssueCategories == null) && (EnabledModules != null ? EnabledModules.Equals(other.EnabledModules) : other.EnabledModules == null) - && (TimeEntryActivities != null ? TimeEntryActivities.Equals(other.TimeEntryActivities) : other.TimeEntryActivities == null); + && (TimeEntryActivities != null ? TimeEntryActivities.Equals(other.TimeEntryActivities) : other.TimeEntryActivities == null) + && (DefaultAssignee != null ? DefaultAssignee.Equals(other.DefaultAssignee) : other.DefaultAssignee == null) + && (DefaultVersion != null ? DefaultVersion.Equals(other.DefaultVersion) : other.DefaultVersion == null); } /// @@ -333,6 +349,8 @@ public override int GetHashCode() hashCode = HashCodeHelper.GetHashCode(IssueCategories, hashCode); hashCode = HashCodeHelper.GetHashCode(EnabledModules, hashCode); hashCode = HashCodeHelper.GetHashCode(TimeEntryActivities, hashCode); + hashCode = HashCodeHelper.GetHashCode(DefaultAssignee, hashCode); + hashCode = HashCodeHelper.GetHashCode(DefaultVersion, hashCode); return hashCode; } @@ -344,12 +362,18 @@ public override int GetHashCode() /// /// private string DebuggerDisplay => - $@"[Project: {ToString()}, Identifier={Identifier}, Description={Description}, Parent={Parent}, HomePage={HomePage}, + $@"[Project: {ToString()}, +Identifier={Identifier}, +Description={Description}, +Parent={Parent}, +HomePage={HomePage}, CreatedOn={CreatedOn?.ToString("u", CultureInfo.InvariantCulture)}, UpdatedOn={UpdatedOn?.ToString("u", CultureInfo.InvariantCulture)}, Status={Status:G}, IsPublic={IsPublic.ToString(CultureInfo.InvariantCulture)}, InheritMembers={InheritMembers.ToString(CultureInfo.InvariantCulture)}, +DefaultAssignee={DefaultAssignee}, +DefaultVersion={DefaultVersion}, Trackers={Trackers.Dump()}, CustomFields={CustomFields.Dump()}, IssueCategories={IssueCategories.Dump()}, From f454e3070849609ecb9e4b2e8928f12353857f20 Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 8 Jun 2021 14:06:51 +0300 Subject: [PATCH 207/549] Added MyAccount type --- src/redmine-net-api/Internals/UrlHelper.cs | 9 +- src/redmine-net-api/RedmineManager.cs | 10 + src/redmine-net-api/Types/MyAccount.cs | 210 ++++++++++++++++++ .../Types/MyAccountCustomField.cs | 137 ++++++++++++ 4 files changed, 365 insertions(+), 1 deletion(-) create mode 100644 src/redmine-net-api/Types/MyAccount.cs create mode 100644 src/redmine-net-api/Types/MyAccountCustomField.cs diff --git a/src/redmine-net-api/Internals/UrlHelper.cs b/src/redmine-net-api/Internals/UrlHelper.cs index e5ac0e24..d6b33f2d 100644 --- a/src/redmine-net-api/Internals/UrlHelper.cs +++ b/src/redmine-net-api/Internals/UrlHelper.cs @@ -63,8 +63,10 @@ internal static class UrlHelper /// private const string FILE_URL_FORMAT = "{0}/projects/{1}/files.{2}"; + private const string MY_ACCOUNT_FORMAT = "{0}/my/account.{1}"; + - /// + /// /// private const string CURRENT_USER_URI = "current"; /// @@ -301,6 +303,11 @@ public static string GetCurrentUserUrl(RedmineManager redmineManager) redmineManager.Format); } + public static string GetMyAccountUrl(RedmineManager redmineManager) + { + return string.Format(CultureInfo.InvariantCulture,MY_ACCOUNT_FORMAT, redmineManager.Host, redmineManager.Format); + } + /// /// Gets the wiki create or updater URL. /// diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index 0bc375a2..7d92035e 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -322,6 +322,16 @@ public User GetCurrentUser(NameValueCollection parameters = null) return WebApiHelper.ExecuteDownload(this, url, parameters); } + /// + /// + /// + /// Returns the my account details. + public MyAccount GetMyAccount() + { + var url = UrlHelper.GetMyAccountUrl(this); + return WebApiHelper.ExecuteDownload(this, url); + } + /// /// Adds the watcher to issue. /// diff --git a/src/redmine-net-api/Types/MyAccount.cs b/src/redmine-net-api/Types/MyAccount.cs new file mode 100644 index 00000000..96a70bc5 --- /dev/null +++ b/src/redmine-net-api/Types/MyAccount.cs @@ -0,0 +1,210 @@ +/* + Copyright 2011 - 2021 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; + +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + /// Availability 4.1 + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [XmlRoot(RedmineKeys.USER)] + public sealed class MyAccount : Identifiable + { + #region Properties + + /// + /// Gets the user login. + /// + /// The login. + public string Login { get; internal set; } + + /// + /// Gets the first name. + /// + /// The first name. + public string FirstName { get; internal set; } + + /// + /// Gets the last name. + /// + /// The last name. + public string LastName { get; internal set; } + + /// + /// Gets the email. + /// + /// The email. + public string Email { get; internal set; } + + /// + /// Returns true if user is admin. + /// + /// + /// The authentication mode id. + /// + public bool IsAdmin { get; internal set; } + + /// + /// Gets the created on. + /// + /// The created on. + public DateTime? CreatedOn { get; internal set; } + + /// + /// Gets the last login on. + /// + /// The last login on. + public DateTime? LastLoginOn { get; internal set; } + + /// + /// Gets the API key + /// + public string ApiKey { get; internal set; } + + /// + /// Gets or sets the custom fields + /// + public List CustomFields { get; set; } + + #endregion + + #region Implementation of IXmlSerializable + + /// + public override void ReadXml(XmlReader reader) + { + reader.Read(); + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.ADMIN: IsAdmin = reader.ReadElementContentAsBoolean(); break; + case RedmineKeys.API_KEY: ApiKey = reader.ReadElementContentAsString(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.FIRST_NAME: FirstName = reader.ReadElementContentAsString(); break; + case RedmineKeys.LAST_LOGIN_ON: LastLoginOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.LAST_NAME: LastName = reader.ReadElementContentAsString(); break; + case RedmineKeys.LOGIN: Login = reader.ReadElementContentAsString(); break; + case RedmineKeys.MAIL: Email = reader.ReadElementContentAsString(); break; + default: reader.Read(); break; + } + } + } + + #endregion + + #region Implementation of IJsonSerializable + + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.ADMIN: IsAdmin = reader.ReadAsBool(); break; + case RedmineKeys.API_KEY: ApiKey = reader.ReadAsString(); break; + case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadAsCollection(); break; + case RedmineKeys.FIRST_NAME: FirstName = reader.ReadAsString(); break; + case RedmineKeys.LAST_LOGIN_ON: LastLoginOn = reader.ReadAsDateTime(); break; + case RedmineKeys.LAST_NAME: LastName = reader.ReadAsString(); break; + case RedmineKeys.LOGIN: Login = reader.ReadAsString(); break; + case RedmineKeys.MAIL: Email = reader.ReadAsString(); break; + default: reader.Read(); break; + } + } + } + + #endregion + + /// + public override bool Equals(MyAccount other) + { + if (other == null) return false; + return Id == other.Id + && string.Equals(Login, other.Login, StringComparison.OrdinalIgnoreCase) + && string.Equals(FirstName, other.FirstName, StringComparison.OrdinalIgnoreCase) + && string.Equals(LastName, other.LastName, StringComparison.OrdinalIgnoreCase) + && string.Equals(ApiKey, other.ApiKey, StringComparison.OrdinalIgnoreCase) + && IsAdmin == other.IsAdmin + && CreatedOn == other.CreatedOn + && LastLoginOn == other.LastLoginOn + && (CustomFields?.Equals(other.CustomFields) ?? other.CustomFields == null); + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Login, hashCode); + hashCode = HashCodeHelper.GetHashCode(FirstName, hashCode); + hashCode = HashCodeHelper.GetHashCode(LastName, hashCode); + hashCode = HashCodeHelper.GetHashCode(Email, hashCode); + hashCode = HashCodeHelper.GetHashCode(IsAdmin, hashCode); + hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(LastLoginOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(ApiKey, hashCode); + hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); + return hashCode; + } + } + + private string DebuggerDisplay => $@"[ {nameof(MyAccount)}: +Id={Id.ToString(CultureInfo.InvariantCulture)}, +Login={Login}, +ApiKey={ApiKey}, +FirstName={FirstName}, +LastName={LastName}, +Email={Email}, +IsAdmin={IsAdmin.ToString(CultureInfo.InvariantCulture).ToLowerInv()}, +CreatedOn={CreatedOn?.ToString("u", CultureInfo.InvariantCulture)}, +LastLoginOn={LastLoginOn?.ToString("u", CultureInfo.InvariantCulture)}, +CustomFields={CustomFields.Dump()}]"; + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/MyAccountCustomField.cs b/src/redmine-net-api/Types/MyAccountCustomField.cs new file mode 100644 index 00000000..08631c43 --- /dev/null +++ b/src/redmine-net-api/Types/MyAccountCustomField.cs @@ -0,0 +1,137 @@ +/* + Copyright 2011 - 2021 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System; +using System.Diagnostics; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; + +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [XmlRoot(RedmineKeys.CUSTOM_FIELD)] + public sealed class MyAccountCustomField : IdentifiableName + { + /// + /// Initializes a new instance of the class. + /// + /// Serialization + public MyAccountCustomField() { } + + + /// + /// + /// + public string Value { get; internal set; } + + internal MyAccountCustomField(int id, string name) + { + Id = id; + Name = name; + } + + /// + public override void ReadXml(XmlReader reader) + { + base.ReadXml(reader); + while (!reader.EOF) + { + if (reader.IsEmptyElement) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.VALUE: + Value = reader.ReadElementContentAsString(); + break; + + default: + reader.Read(); + break; + } + } + } + + /// + public override void WriteXml(XmlWriter writer) + { + } + + + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType == JsonToken.PropertyName) + { + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + case RedmineKeys.VALUE: Value = reader.ReadAsString(); break; + default: reader.Read(); break; + } + } + } + } + + /// + public override void WriteJson(JsonWriter writer) + { + } + + /// + public override bool Equals(IdentifiableName other) + { + var result = base.Equals(other); + + return result && string.Equals(Value,((MyAccountCustomField)other)?.Value, StringComparison.OrdinalIgnoreCase); + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Value, hashCode); + return hashCode; + } + } + + /// + /// + /// + /// + private string DebuggerDisplay => $"[{nameof(MyAccountCustomField)}: {ToString()}, Value: {Value}]"; + + } +} \ No newline at end of file From 85ece934a6fd2661aa40591f147cfff83fedc2f4 Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 8 Jun 2021 14:07:51 +0300 Subject: [PATCH 208/549] Added attachments & comments to news --- src/redmine-net-api/RedmineKeys.cs | 8 ++ src/redmine-net-api/Types/News.cs | 19 ++++ src/redmine-net-api/Types/NewsComment.cs | 124 +++++++++++++++++++++++ 3 files changed, 151 insertions(+) create mode 100644 src/redmine-net-api/Types/NewsComment.cs diff --git a/src/redmine-net-api/RedmineKeys.cs b/src/redmine-net-api/RedmineKeys.cs index d825a24c..c705cb62 100644 --- a/src/redmine-net-api/RedmineKeys.cs +++ b/src/redmine-net-api/RedmineKeys.cs @@ -99,6 +99,10 @@ public static class RedmineKeys /// /// /// + public const string COMMENT = "comment"; + /// + /// + /// public const string COMMENTS = "comments"; /// /// @@ -107,6 +111,10 @@ public static class RedmineKeys /// /// /// + public const string CONTENT = "content"; + /// + /// + /// public const string CONTENT_TYPE = "content_type"; /// /// diff --git a/src/redmine-net-api/Types/News.cs b/src/redmine-net-api/Types/News.cs index 7f28c0be..de25df19 100644 --- a/src/redmine-net-api/Types/News.cs +++ b/src/redmine-net-api/Types/News.cs @@ -15,6 +15,7 @@ limitations under the License. */ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Xml; @@ -68,6 +69,16 @@ public sealed class News : Identifiable /// /// The created on. public DateTime? CreatedOn { get; internal set; } + + /// + /// + /// + public List Attachments { get; internal set; } + + /// + /// + /// + public List Comments { get; internal set; } #endregion #region Implementation of IXmlSerialization @@ -95,6 +106,10 @@ public override void ReadXml(XmlReader reader) case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; case RedmineKeys.SUMMARY: Summary = reader.ReadElementContentAsString(); break; case RedmineKeys.TITLE: Title = reader.ReadElementContentAsString(); break; + case RedmineKeys.ATTACHMENTS: Attachments = reader.ReadElementContentAsCollection(); + break; + case RedmineKeys.COMMENTS: Comments = reader.ReadElementContentAsCollection(); + break; default: reader.Read(); break; } } @@ -129,6 +144,10 @@ public override void ReadJson(JsonReader reader) case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; case RedmineKeys.SUMMARY: Summary = reader.ReadAsString(); break; case RedmineKeys.TITLE: Title = reader.ReadAsString(); break; + case RedmineKeys.ATTACHMENTS: Attachments = reader.ReadAsCollection(); + break; + case RedmineKeys.COMMENTS: Comments = reader.ReadAsCollection(); + break; default: reader.Read(); break; } } diff --git a/src/redmine-net-api/Types/NewsComment.cs b/src/redmine-net-api/Types/NewsComment.cs new file mode 100644 index 00000000..3144765e --- /dev/null +++ b/src/redmine-net-api/Types/NewsComment.cs @@ -0,0 +1,124 @@ +/* + Copyright 2011 - 2021 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System.Diagnostics; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Internals; + +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [XmlRoot(RedmineKeys.COMMENT)] + public sealed class NewsComment: Identifiable + { + /// + /// + /// + public IdentifiableName Author { get; set; } + /// + /// + /// + public string Content { get; set; } + + /// + public override void ReadXml(XmlReader reader) + { + reader.Read(); + + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.AUTHOR: Author = new IdentifiableName(reader); break; + case RedmineKeys.CONTENT: Content = reader.ReadElementContentAsString(); break; + default: reader.Read(); break; + } + } + } + + /// + public override void WriteXml(XmlWriter writer) + { + } + + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: + Id = reader.ReadAsInt32().GetValueOrDefault(); + break; + case RedmineKeys.AUTHOR: Author = new IdentifiableName(reader); break; + case RedmineKeys.CONTENT: Content = reader.ReadAsString(); break; + default: reader.Read(); break; + } + } + } + + /// + public override void WriteJson(JsonWriter writer) + { + } + + /// + public override bool Equals(NewsComment other) + { + if (other == null) return false; + return Id == other.Id && Author == other.Author && Content == other.Content; + } + + /// + public override int GetHashCode() + { + var hashCode = base.GetHashCode(); + + hashCode = HashCodeHelper.GetHashCode(Author, hashCode); + hashCode = HashCodeHelper.GetHashCode(Content, hashCode); + + return hashCode; + } + + private string DebuggerDisplay => $@"[{nameof(IssueAllowedStatus)}: {ToString()}, +{nameof(NewsComment)}: {ToString()}, +Author={Author}, +CONTENT={Content}]"; + } +} \ No newline at end of file From 6b1ec8e0a724badd4545b51946515c0a9132b735 Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 8 Jun 2021 14:09:32 +0300 Subject: [PATCH 209/549] Added AllowedStatuses to issue --- src/redmine-net-api/RedmineKeys.cs | 4 +++ src/redmine-net-api/Types/Issue.cs | 7 +++++ .../Types/IssueAllowedStatus.cs | 31 +++++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 src/redmine-net-api/Types/IssueAllowedStatus.cs diff --git a/src/redmine-net-api/RedmineKeys.cs b/src/redmine-net-api/RedmineKeys.cs index c705cb62..809dcef7 100644 --- a/src/redmine-net-api/RedmineKeys.cs +++ b/src/redmine-net-api/RedmineKeys.cs @@ -43,6 +43,10 @@ public static class RedmineKeys /// /// /// + public const string ALLOWED_STATUSES = "allowed_statuses"; + /// + /// + /// public const string API_KEY = "api_key"; /// /// diff --git a/src/redmine-net-api/Types/Issue.cs b/src/redmine-net-api/Types/Issue.cs index 65859599..b10c2f29 100644 --- a/src/redmine-net-api/Types/Issue.cs +++ b/src/redmine-net-api/Types/Issue.cs @@ -253,6 +253,11 @@ public sealed class Issue : Identifiable, ICloneable /// /// public IList Watchers { get; set; } + + /// + /// + /// + public List AllowedStatuses { get; set; } #endregion #region Implementation of IXmlSerialization @@ -275,6 +280,7 @@ public override void ReadXml(XmlReader reader) switch (reader.Name) { case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.ALLOWED_STATUSES: AllowedStatuses = reader.ReadElementContentAsCollection(); break; case RedmineKeys.ASSIGNED_TO: AssignedTo = new IdentifiableName(reader); break; case RedmineKeys.ATTACHMENTS: Attachments = reader.ReadElementContentAsCollection(); break; case RedmineKeys.AUTHOR: Author = new IdentifiableName(reader); break; @@ -373,6 +379,7 @@ public override void ReadJson(JsonReader reader) switch (reader.Value) { case RedmineKeys.ID: Id = reader.ReadAsInt32().GetValueOrDefault(); break; + case RedmineKeys.ALLOWED_STATUSES: AllowedStatuses = reader.ReadAsCollection(); break; case RedmineKeys.ASSIGNED_TO: AssignedTo = new IdentifiableName(reader); break; case RedmineKeys.ATTACHMENTS: Attachments = reader.ReadAsCollection(); break; case RedmineKeys.AUTHOR: Author = new IdentifiableName(reader); break; diff --git a/src/redmine-net-api/Types/IssueAllowedStatus.cs b/src/redmine-net-api/Types/IssueAllowedStatus.cs new file mode 100644 index 00000000..23a2a3cc --- /dev/null +++ b/src/redmine-net-api/Types/IssueAllowedStatus.cs @@ -0,0 +1,31 @@ +/* + Copyright 2011 - 2021 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System.Diagnostics; +using System.Xml.Serialization; + +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [XmlRoot(RedmineKeys.STATUS)] + public sealed class IssueAllowedStatus : IdentifiableName + { + private string DebuggerDisplay => $"[{nameof(IssueAllowedStatus)}: {ToString()}]"; + } +} \ No newline at end of file From 604be0fa634fbeb5f40f2d4ad1901ce069bb9c47 Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 8 Jun 2021 14:10:56 +0300 Subject: [PATCH 210/549] Added search type --- src/redmine-net-api/RedmineKeys.cs | 18 ++++ src/redmine-net-api/Types/Search.cs | 157 ++++++++++++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 src/redmine-net-api/Types/Search.cs diff --git a/src/redmine-net-api/RedmineKeys.cs b/src/redmine-net-api/RedmineKeys.cs index 809dcef7..d96921cc 100644 --- a/src/redmine-net-api/RedmineKeys.cs +++ b/src/redmine-net-api/RedmineKeys.cs @@ -159,6 +159,11 @@ public static class RedmineKeys /// /// public const string CUSTOM_FIELDS = "custom_fields"; + + /// + /// + /// + public const string DATE_TIME = "datetime"; /// /// @@ -555,6 +560,10 @@ public static class RedmineKeys /// /// /// + public const string RESULT = "result"; + /// + /// + /// public const string REVISION = "revision"; /// /// @@ -676,6 +685,11 @@ public static class RedmineKeys /// /// public const string TRACKER_IDS = "tracker_ids"; + + /// + /// + /// + public const string TYPE = "type"; /// /// /// @@ -695,6 +709,10 @@ public static class RedmineKeys /// /// /// + public const string URL = "url"; + /// + /// + /// public const string USER = "user"; /// /// diff --git a/src/redmine-net-api/Types/Search.cs b/src/redmine-net-api/Types/Search.cs new file mode 100644 index 00000000..03786c82 --- /dev/null +++ b/src/redmine-net-api/Types/Search.cs @@ -0,0 +1,157 @@ +/* + Copyright 2011 - 2021 Adrian Popescu. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System; +using System.Diagnostics; +using System.Globalization; +using System.Xml; +using System.Xml.Schema; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization; + +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [XmlRoot(RedmineKeys.RESULT)] + public sealed class Search: IXmlSerializable, IJsonSerializable, IEquatable + { + /// + /// + /// + public int Id { get; set; } + /// + /// + /// + public string Title { get; set; } + /// + /// + /// + public string Type { get; set; } + /// + /// + /// + public string Url { get; set; } + /// + /// + /// + public string Description { get; set; } + /// + /// + /// + public DateTime? DateTime { get; set; } + + /// + public XmlSchema GetSchema() { return null; } + + /// + public void ReadXml(XmlReader reader) + { + reader.Read(); + while (!reader.EOF) + { + switch (reader.Name) + { + case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadElementContentAsString(); break; + case RedmineKeys.DATE_TIME: DateTime = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.URL: Url = reader.ReadElementContentAsString(); break; + case RedmineKeys.TYPE: Type = reader.ReadElementContentAsString(); break; + case RedmineKeys.TITLE: Title = reader.ReadElementContentAsString(); break; + default: reader.Read(); break; + } + } + } + + /// + public void WriteXml(XmlWriter writer) + { + } + + /// + public void WriteJson(JsonWriter writer) + { + } + + /// + public void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadAsString(); break; + case RedmineKeys.DATE_TIME: DateTime = reader.ReadAsDateTime(); break; + case RedmineKeys.URL: Url = reader.ReadAsString(); break; + case RedmineKeys.TYPE: Type = reader.ReadAsString(); break; + case RedmineKeys.TITLE: Title = reader.ReadAsString(); break; + default: reader.Read(); break; + } + } + } + + /// + public bool Equals(Search other) + { + if (other == null) return false; + return Id == other.Id && string.Equals(Title, other.Title, StringComparison.InvariantCultureIgnoreCase) + && string.Equals(Description, other.Description, StringComparison.InvariantCultureIgnoreCase) + && string.Equals(Url, other.Url, StringComparison.InvariantCultureIgnoreCase) + && string.Equals(Type, other.Type, StringComparison.InvariantCultureIgnoreCase) + && DateTime == other.DateTime; + } + + /// + public override bool Equals(object obj) + { + return Equals(obj as Search); + } + + /// + public override int GetHashCode() + { + unchecked + { + var hashCode = 397; + hashCode = HashCodeHelper.GetHashCode(Id, hashCode); + hashCode = HashCodeHelper.GetHashCode(Title, hashCode); + hashCode = HashCodeHelper.GetHashCode(Type, hashCode); + hashCode = HashCodeHelper.GetHashCode(Url, hashCode); + hashCode = HashCodeHelper.GetHashCode(Description, hashCode); + hashCode = HashCodeHelper.GetHashCode(DateTime, hashCode); + return hashCode; + } + } + + private string DebuggerDisplay => $@"[{nameof(Search)}:Id={Id.ToString(CultureInfo.InvariantCulture)},Title={Title},Type={Type},Url={Url},Description={Description}, DateTime={DateTime?.ToString("u", CultureInfo.InvariantCulture)}]"; + } +} \ No newline at end of file From 9742932ec368856f1da366b889308fe58e63e1a2 Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 8 Jun 2021 14:12:07 +0300 Subject: [PATCH 211/549] Cleanup --- src/redmine-net-api/Types/Attachment.cs | 4 ---- src/redmine-net-api/Types/CustomFieldPossibleValue.cs | 8 ++++---- src/redmine-net-api/Types/IdentifiableName.cs | 1 - 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/redmine-net-api/Types/Attachment.cs b/src/redmine-net-api/Types/Attachment.cs index b99f4d51..abc924ed 100644 --- a/src/redmine-net-api/Types/Attachment.cs +++ b/src/redmine-net-api/Types/Attachment.cs @@ -220,10 +220,6 @@ public override int GetHashCode() } #endregion - /// - /// - /// - /// private string DebuggerDisplay => $@"[{nameof(Attachment)}: {ToString()}, diff --git a/src/redmine-net-api/Types/CustomFieldPossibleValue.cs b/src/redmine-net-api/Types/CustomFieldPossibleValue.cs index 4225d9e1..e2f42a59 100644 --- a/src/redmine-net-api/Types/CustomFieldPossibleValue.cs +++ b/src/redmine-net-api/Types/CustomFieldPossibleValue.cs @@ -33,10 +33,10 @@ namespace Redmine.Net.Api.Types public sealed class CustomFieldPossibleValue : IXmlSerializable, IJsonSerializable, IEquatable { #region Properties - /// - /// - /// - public string Value { get; internal set; } + /// + /// + /// + public string Value { get; internal set; } /// /// diff --git a/src/redmine-net-api/Types/IdentifiableName.cs b/src/redmine-net-api/Types/IdentifiableName.cs index 37236d53..7446d1cb 100644 --- a/src/redmine-net-api/Types/IdentifiableName.cs +++ b/src/redmine-net-api/Types/IdentifiableName.cs @@ -29,7 +29,6 @@ namespace Redmine.Net.Api.Types /// [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] public class IdentifiableName : Identifiable - { /// /// From bb9475b437d6b581cb2538a8e61da2240727fd89 Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 8 Jun 2021 14:12:20 +0300 Subject: [PATCH 212/549] Update readme --- README.md | 43 +++++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index c3c8f8b3..9398bf40 100755 --- a/README.md +++ b/README.md @@ -16,26 +16,29 @@ redmine-net-api is a library for communicating with a Redmine project management * This API provides access and basic CRUD operations (create, read, update, delete) for the resources described below: |Resource | Read | Create | Update | Delete | -|:---------|:------:|:--------:|:--------:|:-------:| - Attachments|x|x|-|- - Custom Fields|x|-|-|- - Enumerations |x|-|-|- - Files |x|x|-|- - Groups|x|x|x|x - Issues |x|x|x|x - Issue Categories|x|x|x|x - Issue Relations|x|x|x|x - Issue Statuses|x|-|-|- - News|x|-|-|- - Projects|x|x|x|x - Project Memberships|x|x|x|x - Queries |x|-|-|- - Roles |x|-|-|- - Time Entries |x|x|x|x - Trackers |x|-|-|- - Users |x|x|x|x - Versions |x|x|x|x - Wiki Pages |x|x|x|x +|:---------|:------:|:----------:|:---------:|:-------:| + Attachments | ✓ | ✓ | ✗ | ✗| + Custom Fields | ✓ | ✗ | ✗ | ✗ + Enumerations | ✓ | ✗ | ✗ | ✗ + Files |✓|✓|✗|✗ + Groups |✓|✓|✓|✓ + Issues |✓|✓|✓|✓ + Issue Categories |✓|✓|✓|✓ + Issue Relations |✓|✓|✓|✓ + Issue Statuses |✓|✗|✗|✗ + My account |✓|✗|✓|✗ + News |✓|✗|✗|✗ + Projects |✓|✓|✓|✓ + Project Memberships|✓|✓|✓|✓ + Queries |✓|✗|✗|✗ + Roles |✓|✗|✗|✗ + Search | + Time Entries |✓|✓|✓|✓ + Trackers |✓|✗|✗|✗ + Users |✓|✓|✓|✓ + Versions |✓|✓|✓|✓ + Wiki Pages |✓|✓|✓|✓ + ## WIKI From 9ad5a03dd73f96d15939d6e623fd704a9da277b6 Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 8 Jun 2021 14:41:24 +0300 Subject: [PATCH 213/549] Updated version & changelog --- CHANGELOG.md | 17 +++++++++++++++++ version.props | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 256d1ff3..501cd134 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [v4.3.0] + +Added: +* Added WikiPageTitle, EstimatedHours & SpentHours to version +* Added IsAdmin, TwoFactorAuthenticationScheme, PasswordChangedOn, UpdatedOn to user +* Added IsActive to time entry activity +* Added IssuesVisibility, TimeEntriesVisibility, UsersVisibility & IsAssignable to role +* Added DefaultAssignee & DefaultVersion to project +* Added MyAccount type +* Added attachments & comments to news +* Added AllowedStatuses to issue +* Added search type + +Fixes: +* Issue Relations Read Error for Copied Issues (Relation Type : copied_to) (#288) + + ## [v4.2.3] Fixes: diff --git a/version.props b/version.props index c493f568..1a63b825 100644 --- a/version.props +++ b/version.props @@ -1,6 +1,6 @@ - 4.2.3 + 4.3.0 $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix) From 0a11d8bcc070bd1353406287d013e8b1f97a557d Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 22 Jan 2022 14:46:51 +0200 Subject: [PATCH 214/549] WIP: CI/CD action --- .github/workflows/act.yml | 93 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 .github/workflows/act.yml diff --git a/.github/workflows/act.yml b/.github/workflows/act.yml new file mode 100644 index 00000000..642cb981 --- /dev/null +++ b/.github/workflows/act.yml @@ -0,0 +1,93 @@ + +name: "CI/CD" +on: + push: + branches: + - "master" + pull_request: + branches: [ "master" ] + + +env: + # Disable the .NET logo in the console output. + DOTNET_NOLOGO: true + + # Stop wasting time caching packages + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + + # Disable sending usage data to Microsoft + DOTNET_CLI_TELEMETRY_OPTOUT: true + + # Project name to pack and publish + PROJECT_NAME: redmine-net-api + + # GitHub Packages Feed settings + GITHUB_FEED: https://nuget.pkg.github.com/redmine-net-api/ + GITHUB_USER: zapadi + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Official NuGet Feed settings + NUGET_FEED: https://api.nuget.org/v3/index.json + NUGET_KEY: ${{ secrets.NUGET_KEY }} + + # Set the build number in MinVer. + MINVERBUILDMETADATA: build.${{github.run_number}} + +jobs: + build: + name: Build-${{matrix.os}} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ ubuntu-latest, windows-latest, macos-latest ] + env: + PUSH_PACKAGES: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} + steps: + - name: Checkout + uses: actions/checkout@v2.4.0 + with: + lfs: true + fetch-depth: 0 + + - name: Setup .NET Core + uses: actions/setup-dotnet@v1.9.0 + with: + dotnet-version: | + 3.1.x + 5.0.x + + # - name: "Install .NET Core SDK" + # uses: actions/setup-dotnet@v1.9.0 + + - name: Restore + run: dotnet restore + + - name: Build + run: dotnet build -c Release --no-restore + + - name: Test + run: dotnet test -c Release --no-build + + - name: Create Release NuGet package + run: | + arrTag=(${GITHUB_REF//\// }) + VERSION="${arrTag[2]}" + VERSION="${VERSION//v}" + dotnet pack -v normal -c Release --include-symbols --include-source -p:PackageVersion=$VERSION -o nupkg src/$PROJECT_NAME/$PROJECT_NAME.*proj + + + - name: Update robots.txt for Testing Site + if: github.event_name == 'pull_request' + run: | + rm ./public/robots.txt + echo $'User-agent: *\nDisallow: /' >> ./public/robots.txt + + - name: Notify Slack + if: always() + uses: 8398a7/action-slack@v3 + with: + status: ${{ job.status }} + fields: repo,message,commit,author,action,eventName,ref,workflow,job,took # selectable (default: repo,message) + env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} # required + From 6729e7b554e2cc5ce4032e314a598a8f315b4a6e Mon Sep 17 00:00:00 2001 From: Padi Date: Mon, 24 Jan 2022 09:55:05 +0000 Subject: [PATCH 215/549] Create codeql-analysis.yml --- .github/workflows/codeql-analysis.yml | 70 +++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..e47ceb46 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,70 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '34 7 * * 6' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'csharp' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] + # Learn more about CodeQL language support at https://git.io/codeql-language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 From 7b735fac1a31db36b41683715adfed3b76405aea Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 5 Mar 2022 15:57:01 +0200 Subject: [PATCH 216/549] Update copyright --- .../Async/RedmineManagerAsync.cs | 17 +++++++++++++++++ .../Async/RedmineManagerAsync40.cs | 2 +- .../Async/RedmineManagerAsync45.cs | 2 +- .../Exceptions/ConflictException.cs | 2 +- .../Exceptions/ForbiddenException.cs | 2 +- .../Exceptions/InternalServerErrorException.cs | 2 +- .../NameResolutionFailureException.cs | 2 +- .../Exceptions/NotAcceptableException.cs | 2 +- .../Exceptions/NotFoundException.cs | 2 +- .../Exceptions/RedmineException.cs | 2 +- .../Exceptions/RedmineTimeoutException.cs | 2 +- .../Exceptions/UnauthorizedException.cs | 2 +- .../Extensions/CollectionExtensions.cs | 2 +- .../Extensions/ExtensionAttribute.cs | 2 +- .../Extensions/JsonReaderExtensions.cs | 18 +++++++++++++++++- .../Extensions/JsonWriterExtensions.cs | 16 ++++++++++++++++ .../NameValueCollectionExtensions.cs | 2 +- .../Extensions/StringExtensions.cs | 16 ++++++++++++++++ .../Extensions/WebExtensions.cs | 2 +- .../Extensions/XmlReaderExtensions.cs | 2 +- .../Extensions/XmlWriterExtensions.cs | 2 +- src/redmine-net-api/HttpVerbs.cs | 2 +- src/redmine-net-api/IRedmineManager.cs | 2 +- src/redmine-net-api/IRedmineWebClient.cs | 2 +- src/redmine-net-api/Internals/DataHelper.cs | 2 +- src/redmine-net-api/Internals/Func.cs | 2 +- .../Internals/HashCodeHelper.cs | 2 +- src/redmine-net-api/Internals/UrlHelper.cs | 2 +- .../Internals/WebApiAsyncHelper.cs | 2 +- src/redmine-net-api/Internals/WebApiHelper.cs | 2 +- .../Internals/XmlTextReaderBuilder.cs | 16 ++++++++++++++++ src/redmine-net-api/MimeFormat.cs | 2 +- src/redmine-net-api/RedirectType.cs | 16 ++++++++++++++++ src/redmine-net-api/RedmineKeys.cs | 2 +- src/redmine-net-api/RedmineManager.cs | 2 +- src/redmine-net-api/RedmineWebClient.cs | 2 +- .../Serialization/CacheKeyFactory.cs | 16 ++++++++++++++++ .../Serialization/IJsonSerializable.cs | 16 ++++++++++++++++ .../Serialization/ISerialization.cs | 16 ++++++++++++++++ .../Serialization/IXmlSerializerCache.cs | 16 ++++++++++++++++ .../Serialization/JsonObject.cs | 16 ++++++++++++++++ .../Serialization/JsonRedmineSerializer.cs | 16 ++++++++++++++++ .../Serialization/PagedResults.cs | 16 ++++++++++++++++ .../Serialization/XmlRedmineSerializer.cs | 16 ++++++++++++++++ .../Serialization/XmlSerializerCache.cs | 16 ++++++++++++++++ src/redmine-net-api/Types/Attachment.cs | 2 +- src/redmine-net-api/Types/Attachments.cs | 2 +- src/redmine-net-api/Types/ChangeSet.cs | 2 +- src/redmine-net-api/Types/CustomField.cs | 2 +- .../Types/CustomFieldPossibleValue.cs | 2 +- src/redmine-net-api/Types/CustomFieldRole.cs | 2 +- src/redmine-net-api/Types/CustomFieldValue.cs | 2 +- src/redmine-net-api/Types/Detail.cs | 2 +- src/redmine-net-api/Types/Error.cs | 2 +- src/redmine-net-api/Types/File.cs | 2 +- src/redmine-net-api/Types/Group.cs | 2 +- src/redmine-net-api/Types/GroupUser.cs | 2 +- src/redmine-net-api/Types/IValue.cs | 2 +- src/redmine-net-api/Types/Identifiable.cs | 2 +- src/redmine-net-api/Types/IdentifiableName.cs | 2 +- src/redmine-net-api/Types/Issue.cs | 2 +- .../Types/IssueAllowedStatus.cs | 2 +- src/redmine-net-api/Types/IssueCategory.cs | 2 +- src/redmine-net-api/Types/IssueChild.cs | 2 +- src/redmine-net-api/Types/IssueCustomField.cs | 2 +- src/redmine-net-api/Types/IssuePriority.cs | 2 +- src/redmine-net-api/Types/IssueRelation.cs | 2 +- src/redmine-net-api/Types/IssueRelationType.cs | 2 +- src/redmine-net-api/Types/IssueStatus.cs | 2 +- src/redmine-net-api/Types/Journal.cs | 2 +- src/redmine-net-api/Types/Membership.cs | 2 +- src/redmine-net-api/Types/MembershipRole.cs | 2 +- src/redmine-net-api/Types/MyAccount.cs | 2 +- .../Types/MyAccountCustomField.cs | 2 +- src/redmine-net-api/Types/News.cs | 2 +- src/redmine-net-api/Types/NewsComment.cs | 2 +- src/redmine-net-api/Types/Permission.cs | 2 +- src/redmine-net-api/Types/Project.cs | 2 +- .../Types/ProjectEnabledModule.cs | 2 +- .../Types/ProjectIssueCategory.cs | 2 +- src/redmine-net-api/Types/ProjectMembership.cs | 2 +- src/redmine-net-api/Types/ProjectStatus.cs | 2 +- .../Types/ProjectTimeEntryActivity.cs | 18 +++++++++++++++++- src/redmine-net-api/Types/ProjectTracker.cs | 2 +- src/redmine-net-api/Types/Query.cs | 2 +- src/redmine-net-api/Types/Role.cs | 2 +- src/redmine-net-api/Types/Search.cs | 2 +- src/redmine-net-api/Types/TimeEntry.cs | 2 +- src/redmine-net-api/Types/TimeEntryActivity.cs | 2 +- src/redmine-net-api/Types/Tracker.cs | 2 +- .../Types/TrackerCustomField.cs | 2 +- src/redmine-net-api/Types/Upload.cs | 2 +- src/redmine-net-api/Types/User.cs | 2 +- src/redmine-net-api/Types/UserGroup.cs | 2 +- src/redmine-net-api/Types/UserStatus.cs | 2 +- src/redmine-net-api/Types/Version.cs | 2 +- src/redmine-net-api/Types/VersionSharing.cs | 18 +++++++++++++++++- src/redmine-net-api/Types/VersionStatus.cs | 18 +++++++++++++++++- src/redmine-net-api/Types/Watcher.cs | 2 +- src/redmine-net-api/Types/WikiPage.cs | 2 +- .../Tests/Sync/AttachmentTests.cs | 2 +- .../Tests/Sync/CustomFieldTests.cs | 2 +- .../Tests/Sync/IssuePriorityTests.cs | 2 +- .../Tests/Sync/IssueRelationTests.cs | 2 +- .../Tests/Sync/IssueStatusTests.cs | 2 +- .../Tests/Sync/NewsTests.cs | 2 +- .../Tests/Sync/ProjectMembershipTests.cs | 2 +- .../Tests/Sync/ProjectTests.cs | 2 +- .../Tests/Sync/QueryTests.cs | 2 +- .../Tests/Sync/RoleTests.cs | 2 +- .../Tests/Sync/TimeEntryActivtiyTests.cs | 2 +- .../Tests/Sync/TimeEntryTests.cs | 2 +- .../Tests/Sync/TrackerTests.cs | 2 +- .../Tests/Sync/UserTests.cs | 2 +- .../Tests/Sync/VersionTests.cs | 2 +- .../Tests/Sync/WikiPageTests.cs | 2 +- 116 files changed, 391 insertions(+), 102 deletions(-) diff --git a/src/redmine-net-api/Async/RedmineManagerAsync.cs b/src/redmine-net-api/Async/RedmineManagerAsync.cs index 441c6a3c..bc76b936 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync.cs @@ -1,5 +1,22 @@ #if NET20 + +/* + Copyright 2011 - 2022 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + using System.Collections.Generic; using System.Collections.Specialized; using Redmine.Net.Api.Serialization; diff --git a/src/redmine-net-api/Async/RedmineManagerAsync40.cs b/src/redmine-net-api/Async/RedmineManagerAsync40.cs index 3cf06d37..09699ec3 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync40.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync40.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Async/RedmineManagerAsync45.cs b/src/redmine-net-api/Async/RedmineManagerAsync45.cs index afb27ea9..199e5e4a 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync45.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync45.cs @@ -1,5 +1,5 @@ /* -Copyright 2011 - 2021 Adrian Popescu. +Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/ConflictException.cs b/src/redmine-net-api/Exceptions/ConflictException.cs index 86465c8d..46faad5b 100644 --- a/src/redmine-net-api/Exceptions/ConflictException.cs +++ b/src/redmine-net-api/Exceptions/ConflictException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/ForbiddenException.cs b/src/redmine-net-api/Exceptions/ForbiddenException.cs index 8c13b5a3..b34dbbe9 100644 --- a/src/redmine-net-api/Exceptions/ForbiddenException.cs +++ b/src/redmine-net-api/Exceptions/ForbiddenException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/InternalServerErrorException.cs b/src/redmine-net-api/Exceptions/InternalServerErrorException.cs index 29704631..6d495faa 100644 --- a/src/redmine-net-api/Exceptions/InternalServerErrorException.cs +++ b/src/redmine-net-api/Exceptions/InternalServerErrorException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/NameResolutionFailureException.cs b/src/redmine-net-api/Exceptions/NameResolutionFailureException.cs index 77781629..27d648d2 100644 --- a/src/redmine-net-api/Exceptions/NameResolutionFailureException.cs +++ b/src/redmine-net-api/Exceptions/NameResolutionFailureException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/NotAcceptableException.cs b/src/redmine-net-api/Exceptions/NotAcceptableException.cs index 7e4a914e..b660bf3f 100644 --- a/src/redmine-net-api/Exceptions/NotAcceptableException.cs +++ b/src/redmine-net-api/Exceptions/NotAcceptableException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/NotFoundException.cs b/src/redmine-net-api/Exceptions/NotFoundException.cs index 328f1a6d..fbc320c5 100644 --- a/src/redmine-net-api/Exceptions/NotFoundException.cs +++ b/src/redmine-net-api/Exceptions/NotFoundException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/RedmineException.cs b/src/redmine-net-api/Exceptions/RedmineException.cs index 2f6ed3ef..50be65f7 100644 --- a/src/redmine-net-api/Exceptions/RedmineException.cs +++ b/src/redmine-net-api/Exceptions/RedmineException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/RedmineTimeoutException.cs b/src/redmine-net-api/Exceptions/RedmineTimeoutException.cs index 04af4c12..9b615dff 100644 --- a/src/redmine-net-api/Exceptions/RedmineTimeoutException.cs +++ b/src/redmine-net-api/Exceptions/RedmineTimeoutException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/UnauthorizedException.cs b/src/redmine-net-api/Exceptions/UnauthorizedException.cs index edb6f1d0..2599d7c3 100644 --- a/src/redmine-net-api/Exceptions/UnauthorizedException.cs +++ b/src/redmine-net-api/Exceptions/UnauthorizedException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Extensions/CollectionExtensions.cs b/src/redmine-net-api/Extensions/CollectionExtensions.cs index 1c70548e..4246c267 100755 --- a/src/redmine-net-api/Extensions/CollectionExtensions.cs +++ b/src/redmine-net-api/Extensions/CollectionExtensions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Extensions/ExtensionAttribute.cs b/src/redmine-net-api/Extensions/ExtensionAttribute.cs index e9542cc7..fa070874 100755 --- a/src/redmine-net-api/Extensions/ExtensionAttribute.cs +++ b/src/redmine-net-api/Extensions/ExtensionAttribute.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2016 Adrian Popescu + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Extensions/JsonReaderExtensions.cs b/src/redmine-net-api/Extensions/JsonReaderExtensions.cs index 7cf3cfd2..bebf9925 100644 --- a/src/redmine-net-api/Extensions/JsonReaderExtensions.cs +++ b/src/redmine-net-api/Extensions/JsonReaderExtensions.cs @@ -1,4 +1,20 @@ -using System; +/* + Copyright 2011 - 2022 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System; using System.Collections.Generic; using Newtonsoft.Json; using Redmine.Net.Api.Exceptions; diff --git a/src/redmine-net-api/Extensions/JsonWriterExtensions.cs b/src/redmine-net-api/Extensions/JsonWriterExtensions.cs index 6cfbecb2..e48d5b27 100644 --- a/src/redmine-net-api/Extensions/JsonWriterExtensions.cs +++ b/src/redmine-net-api/Extensions/JsonWriterExtensions.cs @@ -1,3 +1,19 @@ +/* + Copyright 2011 - 2022 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + using System; using System.Collections; using System.Collections.Generic; diff --git a/src/redmine-net-api/Extensions/NameValueCollectionExtensions.cs b/src/redmine-net-api/Extensions/NameValueCollectionExtensions.cs index 3157ae68..28264d82 100644 --- a/src/redmine-net-api/Extensions/NameValueCollectionExtensions.cs +++ b/src/redmine-net-api/Extensions/NameValueCollectionExtensions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2017 Adrian Popescu + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Extensions/StringExtensions.cs b/src/redmine-net-api/Extensions/StringExtensions.cs index 2ba360ed..10308d2e 100644 --- a/src/redmine-net-api/Extensions/StringExtensions.cs +++ b/src/redmine-net-api/Extensions/StringExtensions.cs @@ -1,3 +1,19 @@ +/* + Copyright 2011 - 2022 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + using System; using System.Diagnostics.CodeAnalysis; using System.Security; diff --git a/src/redmine-net-api/Extensions/WebExtensions.cs b/src/redmine-net-api/Extensions/WebExtensions.cs index 0bbbbcaf..59ca73ab 100644 --- a/src/redmine-net-api/Extensions/WebExtensions.cs +++ b/src/redmine-net-api/Extensions/WebExtensions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Extensions/XmlReaderExtensions.cs b/src/redmine-net-api/Extensions/XmlReaderExtensions.cs index df17b0dc..57be87d9 100644 --- a/src/redmine-net-api/Extensions/XmlReaderExtensions.cs +++ b/src/redmine-net-api/Extensions/XmlReaderExtensions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Extensions/XmlWriterExtensions.cs b/src/redmine-net-api/Extensions/XmlWriterExtensions.cs index 01d59b6a..cddbd9b9 100644 --- a/src/redmine-net-api/Extensions/XmlWriterExtensions.cs +++ b/src/redmine-net-api/Extensions/XmlWriterExtensions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/HttpVerbs.cs b/src/redmine-net-api/HttpVerbs.cs index 29bd3194..09abd93a 100644 --- a/src/redmine-net-api/HttpVerbs.cs +++ b/src/redmine-net-api/HttpVerbs.cs @@ -1,5 +1,5 @@ /* -Copyright 2011 - 2021 Adrian Popescu. +Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/IRedmineManager.cs b/src/redmine-net-api/IRedmineManager.cs index 66b8a6ac..4456c725 100644 --- a/src/redmine-net-api/IRedmineManager.cs +++ b/src/redmine-net-api/IRedmineManager.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/IRedmineWebClient.cs b/src/redmine-net-api/IRedmineWebClient.cs index a19679b7..ef328929 100644 --- a/src/redmine-net-api/IRedmineWebClient.cs +++ b/src/redmine-net-api/IRedmineWebClient.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Internals/DataHelper.cs b/src/redmine-net-api/Internals/DataHelper.cs index 822082e2..821996a1 100755 --- a/src/redmine-net-api/Internals/DataHelper.cs +++ b/src/redmine-net-api/Internals/DataHelper.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Internals/Func.cs b/src/redmine-net-api/Internals/Func.cs index 3c2a64e1..5adb6ef8 100644 --- a/src/redmine-net-api/Internals/Func.cs +++ b/src/redmine-net-api/Internals/Func.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Internals/HashCodeHelper.cs b/src/redmine-net-api/Internals/HashCodeHelper.cs index ce667b11..87ebdc09 100755 --- a/src/redmine-net-api/Internals/HashCodeHelper.cs +++ b/src/redmine-net-api/Internals/HashCodeHelper.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Internals/UrlHelper.cs b/src/redmine-net-api/Internals/UrlHelper.cs index d6b33f2d..e3d9db16 100644 --- a/src/redmine-net-api/Internals/UrlHelper.cs +++ b/src/redmine-net-api/Internals/UrlHelper.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Internals/WebApiAsyncHelper.cs b/src/redmine-net-api/Internals/WebApiAsyncHelper.cs index 93a4ca63..deaa7255 100644 --- a/src/redmine-net-api/Internals/WebApiAsyncHelper.cs +++ b/src/redmine-net-api/Internals/WebApiAsyncHelper.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Internals/WebApiHelper.cs b/src/redmine-net-api/Internals/WebApiHelper.cs index 7efc0d18..37e5e890 100644 --- a/src/redmine-net-api/Internals/WebApiHelper.cs +++ b/src/redmine-net-api/Internals/WebApiHelper.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs b/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs index ec7d29ec..3fafc243 100644 --- a/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs +++ b/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs @@ -1,3 +1,19 @@ +/* + Copyright 2011 - 2022 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + using System.IO; using System.Xml; diff --git a/src/redmine-net-api/MimeFormat.cs b/src/redmine-net-api/MimeFormat.cs index d1ae8316..5f0e4ff4 100755 --- a/src/redmine-net-api/MimeFormat.cs +++ b/src/redmine-net-api/MimeFormat.cs @@ -1,5 +1,5 @@ /* -Copyright 2011 - 2021 Adrian Popescu. +Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/RedirectType.cs b/src/redmine-net-api/RedirectType.cs index 24cbc1c2..d3157bcf 100644 --- a/src/redmine-net-api/RedirectType.cs +++ b/src/redmine-net-api/RedirectType.cs @@ -1,3 +1,19 @@ +/* + Copyright 2011 - 2022 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + namespace Redmine.Net.Api { /// diff --git a/src/redmine-net-api/RedmineKeys.cs b/src/redmine-net-api/RedmineKeys.cs index d96921cc..2fe8f586 100644 --- a/src/redmine-net-api/RedmineKeys.cs +++ b/src/redmine-net-api/RedmineKeys.cs @@ -1,5 +1,5 @@ /* - Copyright 2016 - 2017 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index 7d92035e..ec43f705 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/RedmineWebClient.cs b/src/redmine-net-api/RedmineWebClient.cs index 453f057e..3ecaa47b 100644 --- a/src/redmine-net-api/RedmineWebClient.cs +++ b/src/redmine-net-api/RedmineWebClient.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Serialization/CacheKeyFactory.cs b/src/redmine-net-api/Serialization/CacheKeyFactory.cs index d6244b1b..3e0d3c21 100644 --- a/src/redmine-net-api/Serialization/CacheKeyFactory.cs +++ b/src/redmine-net-api/Serialization/CacheKeyFactory.cs @@ -1,3 +1,19 @@ +/* + Copyright 2011 - 2022 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + using System; using System.Globalization; using System.Text; diff --git a/src/redmine-net-api/Serialization/IJsonSerializable.cs b/src/redmine-net-api/Serialization/IJsonSerializable.cs index efb4ec2f..ac0f6086 100644 --- a/src/redmine-net-api/Serialization/IJsonSerializable.cs +++ b/src/redmine-net-api/Serialization/IJsonSerializable.cs @@ -1,3 +1,19 @@ +/* + Copyright 2011 - 2022 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + using Newtonsoft.Json; namespace Redmine.Net.Api.Serialization diff --git a/src/redmine-net-api/Serialization/ISerialization.cs b/src/redmine-net-api/Serialization/ISerialization.cs index d571939d..0a65b9a4 100644 --- a/src/redmine-net-api/Serialization/ISerialization.cs +++ b/src/redmine-net-api/Serialization/ISerialization.cs @@ -1,3 +1,19 @@ +/* + Copyright 2011 - 2022 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + namespace Redmine.Net.Api.Serialization { internal interface IRedmineSerializer diff --git a/src/redmine-net-api/Serialization/IXmlSerializerCache.cs b/src/redmine-net-api/Serialization/IXmlSerializerCache.cs index 72e458bf..50e80d25 100644 --- a/src/redmine-net-api/Serialization/IXmlSerializerCache.cs +++ b/src/redmine-net-api/Serialization/IXmlSerializerCache.cs @@ -1,3 +1,19 @@ +/* + Copyright 2011 - 2022 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + using System; using System.Xml.Serialization; diff --git a/src/redmine-net-api/Serialization/JsonObject.cs b/src/redmine-net-api/Serialization/JsonObject.cs index a42c4316..ff5b3b8a 100644 --- a/src/redmine-net-api/Serialization/JsonObject.cs +++ b/src/redmine-net-api/Serialization/JsonObject.cs @@ -1,3 +1,19 @@ +/* + Copyright 2011 - 2022 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + using System; using Newtonsoft.Json; using Redmine.Net.Api.Extensions; diff --git a/src/redmine-net-api/Serialization/JsonRedmineSerializer.cs b/src/redmine-net-api/Serialization/JsonRedmineSerializer.cs index 5e5afde7..1357e6c0 100644 --- a/src/redmine-net-api/Serialization/JsonRedmineSerializer.cs +++ b/src/redmine-net-api/Serialization/JsonRedmineSerializer.cs @@ -1,3 +1,19 @@ +/* + Copyright 2011 - 2022 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + using System; using System.Collections.Generic; using System.IO; diff --git a/src/redmine-net-api/Serialization/PagedResults.cs b/src/redmine-net-api/Serialization/PagedResults.cs index a6900f7c..56c40d1d 100644 --- a/src/redmine-net-api/Serialization/PagedResults.cs +++ b/src/redmine-net-api/Serialization/PagedResults.cs @@ -1,3 +1,19 @@ +/* + Copyright 2011 - 2022 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + using System.Collections.Generic; namespace Redmine.Net.Api.Serialization diff --git a/src/redmine-net-api/Serialization/XmlRedmineSerializer.cs b/src/redmine-net-api/Serialization/XmlRedmineSerializer.cs index 99149526..f8ef1f00 100644 --- a/src/redmine-net-api/Serialization/XmlRedmineSerializer.cs +++ b/src/redmine-net-api/Serialization/XmlRedmineSerializer.cs @@ -1,3 +1,19 @@ +/* + Copyright 2011 - 2022 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + using System; using System.IO; using System.Xml; diff --git a/src/redmine-net-api/Serialization/XmlSerializerCache.cs b/src/redmine-net-api/Serialization/XmlSerializerCache.cs index cadbd6a9..d85e31f2 100644 --- a/src/redmine-net-api/Serialization/XmlSerializerCache.cs +++ b/src/redmine-net-api/Serialization/XmlSerializerCache.cs @@ -1,3 +1,19 @@ +/* + Copyright 2011 - 2022 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + using System; using System.Collections.Generic; using System.Diagnostics; diff --git a/src/redmine-net-api/Types/Attachment.cs b/src/redmine-net-api/Types/Attachment.cs index abc924ed..937c78c3 100644 --- a/src/redmine-net-api/Types/Attachment.cs +++ b/src/redmine-net-api/Types/Attachment.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Attachments.cs b/src/redmine-net-api/Types/Attachments.cs index 01ebb125..439955bf 100644 --- a/src/redmine-net-api/Types/Attachments.cs +++ b/src/redmine-net-api/Types/Attachments.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/ChangeSet.cs b/src/redmine-net-api/Types/ChangeSet.cs index c364db85..89210dce 100644 --- a/src/redmine-net-api/Types/ChangeSet.cs +++ b/src/redmine-net-api/Types/ChangeSet.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/CustomField.cs b/src/redmine-net-api/Types/CustomField.cs index 48afa037..6e0fe955 100644 --- a/src/redmine-net-api/Types/CustomField.cs +++ b/src/redmine-net-api/Types/CustomField.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/CustomFieldPossibleValue.cs b/src/redmine-net-api/Types/CustomFieldPossibleValue.cs index e2f42a59..ab994251 100644 --- a/src/redmine-net-api/Types/CustomFieldPossibleValue.cs +++ b/src/redmine-net-api/Types/CustomFieldPossibleValue.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/CustomFieldRole.cs b/src/redmine-net-api/Types/CustomFieldRole.cs index 711e7fca..c0d910d1 100644 --- a/src/redmine-net-api/Types/CustomFieldRole.cs +++ b/src/redmine-net-api/Types/CustomFieldRole.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/CustomFieldValue.cs b/src/redmine-net-api/Types/CustomFieldValue.cs index 9cdd1ac8..9463d000 100644 --- a/src/redmine-net-api/Types/CustomFieldValue.cs +++ b/src/redmine-net-api/Types/CustomFieldValue.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Detail.cs b/src/redmine-net-api/Types/Detail.cs index 24e5f40f..c2bc5ae0 100644 --- a/src/redmine-net-api/Types/Detail.cs +++ b/src/redmine-net-api/Types/Detail.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Error.cs b/src/redmine-net-api/Types/Error.cs index 9f5e9ddc..0a86d6b1 100644 --- a/src/redmine-net-api/Types/Error.cs +++ b/src/redmine-net-api/Types/Error.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/File.cs b/src/redmine-net-api/Types/File.cs index 1e7e8a69..9e07b06c 100644 --- a/src/redmine-net-api/Types/File.cs +++ b/src/redmine-net-api/Types/File.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Group.cs b/src/redmine-net-api/Types/Group.cs index d52fd8b6..70a7892d 100644 --- a/src/redmine-net-api/Types/Group.cs +++ b/src/redmine-net-api/Types/Group.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/GroupUser.cs b/src/redmine-net-api/Types/GroupUser.cs index e6b8ac39..8fa1775a 100644 --- a/src/redmine-net-api/Types/GroupUser.cs +++ b/src/redmine-net-api/Types/GroupUser.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IValue.cs b/src/redmine-net-api/Types/IValue.cs index 6f92c86e..52fb0065 100755 --- a/src/redmine-net-api/Types/IValue.cs +++ b/src/redmine-net-api/Types/IValue.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Identifiable.cs b/src/redmine-net-api/Types/Identifiable.cs index 751b95da..8dddd6cd 100644 --- a/src/redmine-net-api/Types/Identifiable.cs +++ b/src/redmine-net-api/Types/Identifiable.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IdentifiableName.cs b/src/redmine-net-api/Types/IdentifiableName.cs index 7446d1cb..4a57363a 100644 --- a/src/redmine-net-api/Types/IdentifiableName.cs +++ b/src/redmine-net-api/Types/IdentifiableName.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Issue.cs b/src/redmine-net-api/Types/Issue.cs index b10c2f29..4bcbc1b0 100644 --- a/src/redmine-net-api/Types/Issue.cs +++ b/src/redmine-net-api/Types/Issue.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2017 Adrian Popescu, Dorin Huzum. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IssueAllowedStatus.cs b/src/redmine-net-api/Types/IssueAllowedStatus.cs index 23a2a3cc..9b37bb2d 100644 --- a/src/redmine-net-api/Types/IssueAllowedStatus.cs +++ b/src/redmine-net-api/Types/IssueAllowedStatus.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IssueCategory.cs b/src/redmine-net-api/Types/IssueCategory.cs index 7f21100a..c489e118 100644 --- a/src/redmine-net-api/Types/IssueCategory.cs +++ b/src/redmine-net-api/Types/IssueCategory.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IssueChild.cs b/src/redmine-net-api/Types/IssueChild.cs index 63367ca9..90ec83cd 100644 --- a/src/redmine-net-api/Types/IssueChild.cs +++ b/src/redmine-net-api/Types/IssueChild.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IssueCustomField.cs b/src/redmine-net-api/Types/IssueCustomField.cs index 95c1a8c6..8ea5a7b6 100644 --- a/src/redmine-net-api/Types/IssueCustomField.cs +++ b/src/redmine-net-api/Types/IssueCustomField.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IssuePriority.cs b/src/redmine-net-api/Types/IssuePriority.cs index 1060101d..155d98ec 100644 --- a/src/redmine-net-api/Types/IssuePriority.cs +++ b/src/redmine-net-api/Types/IssuePriority.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IssueRelation.cs b/src/redmine-net-api/Types/IssueRelation.cs index ec50bda6..eddab5fa 100644 --- a/src/redmine-net-api/Types/IssueRelation.cs +++ b/src/redmine-net-api/Types/IssueRelation.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IssueRelationType.cs b/src/redmine-net-api/Types/IssueRelationType.cs index c47bd96a..006492e3 100644 --- a/src/redmine-net-api/Types/IssueRelationType.cs +++ b/src/redmine-net-api/Types/IssueRelationType.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IssueStatus.cs b/src/redmine-net-api/Types/IssueStatus.cs index 520ffb7f..7091ad2d 100644 --- a/src/redmine-net-api/Types/IssueStatus.cs +++ b/src/redmine-net-api/Types/IssueStatus.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Journal.cs b/src/redmine-net-api/Types/Journal.cs index 3d9c2a7d..49ed2647 100644 --- a/src/redmine-net-api/Types/Journal.cs +++ b/src/redmine-net-api/Types/Journal.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Membership.cs b/src/redmine-net-api/Types/Membership.cs index 2286bfbb..59add538 100644 --- a/src/redmine-net-api/Types/Membership.cs +++ b/src/redmine-net-api/Types/Membership.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/MembershipRole.cs b/src/redmine-net-api/Types/MembershipRole.cs index 77903ef2..a9764a4d 100644 --- a/src/redmine-net-api/Types/MembershipRole.cs +++ b/src/redmine-net-api/Types/MembershipRole.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/MyAccount.cs b/src/redmine-net-api/Types/MyAccount.cs index 96a70bc5..e86c78e0 100644 --- a/src/redmine-net-api/Types/MyAccount.cs +++ b/src/redmine-net-api/Types/MyAccount.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/MyAccountCustomField.cs b/src/redmine-net-api/Types/MyAccountCustomField.cs index 08631c43..ebbbf4a8 100644 --- a/src/redmine-net-api/Types/MyAccountCustomField.cs +++ b/src/redmine-net-api/Types/MyAccountCustomField.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/News.cs b/src/redmine-net-api/Types/News.cs index de25df19..f7702bb2 100644 --- a/src/redmine-net-api/Types/News.cs +++ b/src/redmine-net-api/Types/News.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/NewsComment.cs b/src/redmine-net-api/Types/NewsComment.cs index 3144765e..4cb7520e 100644 --- a/src/redmine-net-api/Types/NewsComment.cs +++ b/src/redmine-net-api/Types/NewsComment.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Permission.cs b/src/redmine-net-api/Types/Permission.cs index 7ee3c69b..3abc6495 100644 --- a/src/redmine-net-api/Types/Permission.cs +++ b/src/redmine-net-api/Types/Permission.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs index 8130fda4..4d83d910 100644 --- a/src/redmine-net-api/Types/Project.cs +++ b/src/redmine-net-api/Types/Project.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/ProjectEnabledModule.cs b/src/redmine-net-api/Types/ProjectEnabledModule.cs index 8ac0d753..2ec6796c 100644 --- a/src/redmine-net-api/Types/ProjectEnabledModule.cs +++ b/src/redmine-net-api/Types/ProjectEnabledModule.cs @@ -1,5 +1,5 @@ /* -Copyright 2011 - 2021 Adrian Popescu. +Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/ProjectIssueCategory.cs b/src/redmine-net-api/Types/ProjectIssueCategory.cs index 6f0c9e0c..fcfe2ead 100644 --- a/src/redmine-net-api/Types/ProjectIssueCategory.cs +++ b/src/redmine-net-api/Types/ProjectIssueCategory.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/ProjectMembership.cs b/src/redmine-net-api/Types/ProjectMembership.cs index 188b651a..f91508a4 100644 --- a/src/redmine-net-api/Types/ProjectMembership.cs +++ b/src/redmine-net-api/Types/ProjectMembership.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/ProjectStatus.cs b/src/redmine-net-api/Types/ProjectStatus.cs index 0955954f..f0092025 100755 --- a/src/redmine-net-api/Types/ProjectStatus.cs +++ b/src/redmine-net-api/Types/ProjectStatus.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs b/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs index daa9cd78..d9bb4a50 100644 --- a/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs +++ b/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs @@ -1,4 +1,20 @@ -using System.Diagnostics; +/* + Copyright 2011 - 2022 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System.Diagnostics; using System.Xml.Serialization; namespace Redmine.Net.Api.Types diff --git a/src/redmine-net-api/Types/ProjectTracker.cs b/src/redmine-net-api/Types/ProjectTracker.cs index e10bd823..55d71cfe 100644 --- a/src/redmine-net-api/Types/ProjectTracker.cs +++ b/src/redmine-net-api/Types/ProjectTracker.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Query.cs b/src/redmine-net-api/Types/Query.cs index 6cf9e5f2..7adea21c 100644 --- a/src/redmine-net-api/Types/Query.cs +++ b/src/redmine-net-api/Types/Query.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Role.cs b/src/redmine-net-api/Types/Role.cs index 8e049af2..7af6c8b0 100644 --- a/src/redmine-net-api/Types/Role.cs +++ b/src/redmine-net-api/Types/Role.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Search.cs b/src/redmine-net-api/Types/Search.cs index 03786c82..74bea91a 100644 --- a/src/redmine-net-api/Types/Search.cs +++ b/src/redmine-net-api/Types/Search.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/TimeEntry.cs b/src/redmine-net-api/Types/TimeEntry.cs index bf5f188c..2fef1f18 100644 --- a/src/redmine-net-api/Types/TimeEntry.cs +++ b/src/redmine-net-api/Types/TimeEntry.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/TimeEntryActivity.cs b/src/redmine-net-api/Types/TimeEntryActivity.cs index 299386fc..1ef81689 100644 --- a/src/redmine-net-api/Types/TimeEntryActivity.cs +++ b/src/redmine-net-api/Types/TimeEntryActivity.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Tracker.cs b/src/redmine-net-api/Types/Tracker.cs index 68ecacef..e9fca1d3 100644 --- a/src/redmine-net-api/Types/Tracker.cs +++ b/src/redmine-net-api/Types/Tracker.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/TrackerCustomField.cs b/src/redmine-net-api/Types/TrackerCustomField.cs index f1967a57..2f63a7e8 100644 --- a/src/redmine-net-api/Types/TrackerCustomField.cs +++ b/src/redmine-net-api/Types/TrackerCustomField.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Upload.cs b/src/redmine-net-api/Types/Upload.cs index b35301da..9c8800d7 100644 --- a/src/redmine-net-api/Types/Upload.cs +++ b/src/redmine-net-api/Types/Upload.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/User.cs b/src/redmine-net-api/Types/User.cs index 9968bea4..a93cdb6b 100644 --- a/src/redmine-net-api/Types/User.cs +++ b/src/redmine-net-api/Types/User.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/UserGroup.cs b/src/redmine-net-api/Types/UserGroup.cs index b5be7db6..e79e09d7 100644 --- a/src/redmine-net-api/Types/UserGroup.cs +++ b/src/redmine-net-api/Types/UserGroup.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/UserStatus.cs b/src/redmine-net-api/Types/UserStatus.cs index f2c541d6..7ff5ab48 100644 --- a/src/redmine-net-api/Types/UserStatus.cs +++ b/src/redmine-net-api/Types/UserStatus.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Version.cs b/src/redmine-net-api/Types/Version.cs index 70b5ae5e..c7199e9d 100644 --- a/src/redmine-net-api/Types/Version.cs +++ b/src/redmine-net-api/Types/Version.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/VersionSharing.cs b/src/redmine-net-api/Types/VersionSharing.cs index a4c0a155..636853ed 100644 --- a/src/redmine-net-api/Types/VersionSharing.cs +++ b/src/redmine-net-api/Types/VersionSharing.cs @@ -1,4 +1,20 @@ -namespace Redmine.Net.Api.Types +/* + Copyright 2011 - 2022 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +namespace Redmine.Net.Api.Types { /// /// diff --git a/src/redmine-net-api/Types/VersionStatus.cs b/src/redmine-net-api/Types/VersionStatus.cs index ee86fedf..df070fed 100644 --- a/src/redmine-net-api/Types/VersionStatus.cs +++ b/src/redmine-net-api/Types/VersionStatus.cs @@ -1,4 +1,20 @@ -namespace Redmine.Net.Api.Types +/* + Copyright 2011 - 2022 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +namespace Redmine.Net.Api.Types { /// /// diff --git a/src/redmine-net-api/Types/Watcher.cs b/src/redmine-net-api/Types/Watcher.cs index 0160da16..212e42bb 100644 --- a/src/redmine-net-api/Types/Watcher.cs +++ b/src/redmine-net-api/Types/Watcher.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/WikiPage.cs b/src/redmine-net-api/Types/WikiPage.cs index c4293c45..e2d8e16f 100644 --- a/src/redmine-net-api/Types/WikiPage.cs +++ b/src/redmine-net-api/Types/WikiPage.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs index 24a644ad..a4fd925e 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/CustomFieldTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/CustomFieldTests.cs index 163cd1be..799b0dde 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/CustomFieldTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/CustomFieldTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/IssuePriorityTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/IssuePriorityTests.cs index f0aef0d0..90b13829 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/IssuePriorityTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/IssuePriorityTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs index c85d6139..ed621c23 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/IssueStatusTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/IssueStatusTests.cs index 83dc2380..0616e192 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/IssueStatusTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/IssueStatusTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/NewsTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/NewsTests.cs index 1fe7ffad..b6ab8383 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/NewsTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/NewsTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs index 948a1c61..2a25a49a 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/ProjectTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/ProjectTests.cs index 126542a3..6d716190 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/ProjectTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/ProjectTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/QueryTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/QueryTests.cs index faad08df..249ee322 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/QueryTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/QueryTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/RoleTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/RoleTests.cs index 1d5d5cc6..2bfe2d53 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/RoleTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/RoleTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryActivtiyTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryActivtiyTests.cs index 28e2bbde..7d069c8f 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryActivtiyTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryActivtiyTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs index e5424ebc..3ef9065e 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/TrackerTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/TrackerTests.cs index cefbbd98..1a7af4b8 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/TrackerTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/TrackerTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/UserTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/UserTests.cs index 624676f1..f784ba3d 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/UserTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/UserTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/VersionTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/VersionTests.cs index 0c652581..e8db33ef 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/VersionTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/VersionTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs index 68967b3d..c9c1baf6 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2021 Adrian Popescu. + Copyright 2011 - 2022 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 2b1ce069f54374c84d002d18a777c429d4384d27 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 5 Mar 2022 17:25:26 +0200 Subject: [PATCH 217/549] Bump Microsoft.NETFramework.ReferenceAssemblies from 1.0.0 to 1.0.2 --- src/redmine-net-api/redmine-net-api.csproj | 2 +- tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 58333434..fa5a04d1 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -90,7 +90,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj index 4a4eaf5c..b4255e60 100644 --- a/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj +++ b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj @@ -123,7 +123,11 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + all runtime; build; native; contentfiles; analyzers; buildtransitive From 07879f1c486640c1b2d7a335186e8dda37f746cf Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 5 Mar 2022 17:26:50 +0200 Subject: [PATCH 218/549] Bump Netwonsoft.Json from 12.0.3 to 13.0.1 --- src/redmine-net-api/redmine-net-api.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index fa5a04d1..d033eea5 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -94,7 +94,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + From 93e964bbe79765f9679dd50027ef49a383f18bda Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 5 Mar 2022 17:27:42 +0200 Subject: [PATCH 219/549] Bump xunit.runner.visualstudio from 2.4.1 to 2.4.3 --- tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj index b4255e60..5ee231fe 100644 --- a/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj +++ b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj @@ -128,6 +128,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -143,10 +144,6 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - From 1345c9dc8d1f00ccfb745e9a2110d1cf9791ec75 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 5 Mar 2022 17:28:20 +0200 Subject: [PATCH 220/549] Replace Microsoft.CodeAnalysis.FxCopAnalyzers with Microsoft.CodeAnalysis.NetAnalyzers --- src/redmine-net-api/redmine-net-api.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index d033eea5..c97520fd 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -86,7 +86,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 8a71cea6b344f456c22a4f4227a4861e93669244 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 5 Mar 2022 18:41:16 +0200 Subject: [PATCH 221/549] Rename workflow from act to ci-cd --- .github/workflows/act.yml | 93 ---------------------------- .github/workflows/ci-cd.yml | 120 ++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 93 deletions(-) delete mode 100644 .github/workflows/act.yml create mode 100644 .github/workflows/ci-cd.yml diff --git a/.github/workflows/act.yml b/.github/workflows/act.yml deleted file mode 100644 index 642cb981..00000000 --- a/.github/workflows/act.yml +++ /dev/null @@ -1,93 +0,0 @@ - -name: "CI/CD" -on: - push: - branches: - - "master" - pull_request: - branches: [ "master" ] - - -env: - # Disable the .NET logo in the console output. - DOTNET_NOLOGO: true - - # Stop wasting time caching packages - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true - - # Disable sending usage data to Microsoft - DOTNET_CLI_TELEMETRY_OPTOUT: true - - # Project name to pack and publish - PROJECT_NAME: redmine-net-api - - # GitHub Packages Feed settings - GITHUB_FEED: https://nuget.pkg.github.com/redmine-net-api/ - GITHUB_USER: zapadi - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - # Official NuGet Feed settings - NUGET_FEED: https://api.nuget.org/v3/index.json - NUGET_KEY: ${{ secrets.NUGET_KEY }} - - # Set the build number in MinVer. - MINVERBUILDMETADATA: build.${{github.run_number}} - -jobs: - build: - name: Build-${{matrix.os}} - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ ubuntu-latest, windows-latest, macos-latest ] - env: - PUSH_PACKAGES: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} - steps: - - name: Checkout - uses: actions/checkout@v2.4.0 - with: - lfs: true - fetch-depth: 0 - - - name: Setup .NET Core - uses: actions/setup-dotnet@v1.9.0 - with: - dotnet-version: | - 3.1.x - 5.0.x - - # - name: "Install .NET Core SDK" - # uses: actions/setup-dotnet@v1.9.0 - - - name: Restore - run: dotnet restore - - - name: Build - run: dotnet build -c Release --no-restore - - - name: Test - run: dotnet test -c Release --no-build - - - name: Create Release NuGet package - run: | - arrTag=(${GITHUB_REF//\// }) - VERSION="${arrTag[2]}" - VERSION="${VERSION//v}" - dotnet pack -v normal -c Release --include-symbols --include-source -p:PackageVersion=$VERSION -o nupkg src/$PROJECT_NAME/$PROJECT_NAME.*proj - - - - name: Update robots.txt for Testing Site - if: github.event_name == 'pull_request' - run: | - rm ./public/robots.txt - echo $'User-agent: *\nDisallow: /' >> ./public/robots.txt - - - name: Notify Slack - if: always() - uses: 8398a7/action-slack@v3 - with: - status: ${{ job.status }} - fields: repo,message,commit,author,action,eventName,ref,workflow,job,took # selectable (default: repo,message) - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} # required - diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml new file mode 100644 index 00000000..06c03936 --- /dev/null +++ b/.github/workflows/ci-cd.yml @@ -0,0 +1,120 @@ + +name: "CI/CD" +on: + workflow_dispatch: + push: + branches: [ master ] + paths-ignore: + - '**/*.md' + - '**/*.gif' + - '**/*.png' + - '**/*.gitignore' + - '**/*.gitattributes' + - LICENSE + - tests/* + tags: + - v[1-9].[0-9]+.[0-9]+ + pull_request: + branches: [ master ] + paths-ignore: + - '**/*.md' + - '**/*.gif' + - '**/*.png' + - '**/*.gitignore' + - '**/*.gitattributes' + - LICENSE + - tests/* + +env: + # Disable the .NET logo in the console output. + DOTNET_NOLOGO: true + + # Stop wasting time caching packages + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + + # Disable sending usage data to Microsoft + DOTNET_CLI_TELEMETRY_OPTOUT: true + + DOTNET_ADD_GLOBAL_TOOLS_TO_PATH: false + + DOTNET_MULTILEVEL_LOOKUP: 0 + + # Project name to pack and publish + PROJECT_NAME: redmine-net-api + + BUILD_CONFIGURATION: Release + + # Set the build number in MinVer. + MINVERBUILDMETADATA: build.${{github.run_number}} + +jobs: + build: + name: OS ${{ matrix.os }} - dotnet ${{ matrix.dotnet }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ ubuntu-latest, windows-latest, macos-latest ] + dotnet: [ '3.1.x', '5.x.x', '6.x.x' ] + + steps: + - name: Checkout + uses: actions/checkout@v2.4.0 + with: + lfs: true + fetch-depth: 0 + + - name: Setup .NET Core + uses: actions/setup-dotnet@v1.9.0 + with: + dotnet-version: ${{ matrix.dotnet }} + + - name: Get Version + #run: echo "VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV + run: | + arrTag=(${GITHUB_REF//\// }) + VERSION="${arrTag[2]}" + echo "1. Version = ${VERSION}" + echo "VERSION="${VERSION//v}"" >> $GITHUB_ENV + + - name: Restore + run: dotnet restore + + - name: Build + run: dotnet build --no-restore --configuration $BUILD_CONFIGURATION --version-suffix=${{ env.VERSION }} + + - name: Build Signed + if: runner.os == 'Ubuntu' + run: dotnet build redmine-net-api.sln --no-restore --configuration $BUILD_CONFIGURATION --version-suffix=${{ env.VERSION }} -p:Sign=true + + - name: Test + run: dotnet test --no-restore --no-build --configuration $BUILD_CONFIGURATION + + - name: Pack + if: runner.os == 'Ubuntu' + run: | + dotnet pack --output ./artifacts --configuration $BUILD_CONFIG --version-suffix $VERSION --include-symbols --include-source + + - name: Pack Signed + if: runner.os == 'Ubuntu' + run: | + dotnet pack --output ./artifacts --configuration $BUILD_CONFIG --version-suffix $VERSION --include-symbols --include-source -p:Sign=true + + - uses: actions/upload-artifact@v1 + if: runner.os == 'Ubuntu' + with: + name: artifacts + path: ./artifacts + + deploy: + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/v') + needs: build + name: Deploy Packages + steps: + - uses: actions/download-artifact@v1 + with: + name: artifacts + path: ./artifacts + + - name: Publish packages + run: dotnet nuget push ./artifacts/**.nupkg --source nuget.org --api-key ${{secrets.NUGET_TOKEN}} \ No newline at end of file From dee9ac3fca0fd915f28f37067e565f5b1daa79df Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 5 Mar 2022 18:41:48 +0200 Subject: [PATCH 222/549] Change branch name main in master --- .github/workflows/dotnetcore.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml index d17aef8e..3214bad2 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/dotnetcore.yml @@ -15,7 +15,7 @@ on: pull_request: workflow_dispatch: branches: - - main + - master path-ignore: - '**/*.md' - '**/*.gif' @@ -40,7 +40,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] - dotnet: [ '3.1.x'] + dotnet: [ '3.1.x', '5.x.x', '6.x.x'] name: OS ${{ matrix.os }} - dotnet ${{ matrix.dotnet }} steps: @@ -50,6 +50,7 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: ${{ matrix.dotnet }} + # Fetches all tags for the repo - name: Fetch tags run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* From a686533bfa16115a666ec151553c028084b3be59 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 5 Mar 2022 19:16:35 +0200 Subject: [PATCH 223/549] Comment out VersionPrefix & PackageVersion --- version.props | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/version.props b/version.props index 1a63b825..05e950d9 100644 --- a/version.props +++ b/version.props @@ -1,7 +1,7 @@ - 4.3.0 - $(VersionPrefix) - $(VersionPrefix)-$(VersionSuffix) + + + \ No newline at end of file From 7f35136ee21c023e368ae26fb44d8dee3fde397f Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 5 Mar 2022 19:21:42 +0200 Subject: [PATCH 224/549] Change tags regex --- .github/workflows/ci-cd.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 06c03936..f918705a 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -13,7 +13,7 @@ on: - LICENSE - tests/* tags: - - v[1-9].[0-9]+.[0-9]+ + - [1-9].[0-9]+.[0-9]+ pull_request: branches: [ master ] paths-ignore: @@ -69,12 +69,8 @@ jobs: dotnet-version: ${{ matrix.dotnet }} - name: Get Version - #run: echo "VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV run: | - arrTag=(${GITHUB_REF//\// }) - VERSION="${arrTag[2]}" - echo "1. Version = ${VERSION}" - echo "VERSION="${VERSION//v}"" >> $GITHUB_ENV + echo "VERSION = $(git describe --tags --dirty)" >> $GITHUB_ENV - name: Restore run: dotnet restore From 436367d60938ba5bff03297f6c05346e612bc9c6 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 5 Mar 2022 19:28:36 +0200 Subject: [PATCH 225/549] Fix tags regex --- .github/workflows/ci-cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index f918705a..820fdf66 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -13,7 +13,7 @@ on: - LICENSE - tests/* tags: - - [1-9].[0-9]+.[0-9]+ + - '[0-9]+.[0-9]+.[0-9]+' pull_request: branches: [ master ] paths-ignore: From 9de4c1aaf0fdecbdebf86276a15024527df93da3 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 5 Mar 2022 19:49:37 +0200 Subject: [PATCH 226/549] Fix configuration build & version parameters --- .github/workflows/ci-cd.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 820fdf66..61aeb894 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -76,11 +76,11 @@ jobs: run: dotnet restore - name: Build - run: dotnet build --no-restore --configuration $BUILD_CONFIGURATION --version-suffix=${{ env.VERSION }} + run: dotnet build --no-restore --configuration $BUILD_CONFIGURATION --version-suffix $VERSION - name: Build Signed if: runner.os == 'Ubuntu' - run: dotnet build redmine-net-api.sln --no-restore --configuration $BUILD_CONFIGURATION --version-suffix=${{ env.VERSION }} -p:Sign=true + run: dotnet build redmine-net-api.sln --no-restore --configuration $BUILD_CONFIGURATION --version-suffix $VERSION -p:Sign=true - name: Test run: dotnet test --no-restore --no-build --configuration $BUILD_CONFIGURATION @@ -88,12 +88,12 @@ jobs: - name: Pack if: runner.os == 'Ubuntu' run: | - dotnet pack --output ./artifacts --configuration $BUILD_CONFIG --version-suffix $VERSION --include-symbols --include-source + dotnet pack --output ./artifacts --configuration $BUILD_CONFIGURATION --version-suffix $VERSION --include-symbols --include-source - name: Pack Signed if: runner.os == 'Ubuntu' run: | - dotnet pack --output ./artifacts --configuration $BUILD_CONFIG --version-suffix $VERSION --include-symbols --include-source -p:Sign=true + dotnet pack --output ./artifacts --configuration $BUILD_CONFIGURATION --version-suffix $VERSION --include-symbols --include-source -p:Sign=true - uses: actions/upload-artifact@v1 if: runner.os == 'Ubuntu' @@ -103,7 +103,7 @@ jobs: deploy: runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/v') + if: startsWith(github.ref, 'refs/tags') needs: build name: Deploy Packages steps: From 370cbf5daee7353dc6ed23d30eb9c01b7060fb98 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 5 Mar 2022 19:56:16 +0200 Subject: [PATCH 227/549] Another fix --- .github/workflows/ci-cd.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 61aeb894..cbdadc63 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -76,11 +76,11 @@ jobs: run: dotnet restore - name: Build - run: dotnet build --no-restore --configuration $BUILD_CONFIGURATION --version-suffix $VERSION + run: dotnet build --no-restore --configuration $BUILD_CONFIGURATION --version-suffix ${{env.VERSION}} - name: Build Signed if: runner.os == 'Ubuntu' - run: dotnet build redmine-net-api.sln --no-restore --configuration $BUILD_CONFIGURATION --version-suffix $VERSION -p:Sign=true + run: dotnet build redmine-net-api.sln --no-restore --configuration $BUILD_CONFIGURATION --version-suffix ${{env.VERSION}} -p:Sign=true - name: Test run: dotnet test --no-restore --no-build --configuration $BUILD_CONFIGURATION @@ -88,12 +88,12 @@ jobs: - name: Pack if: runner.os == 'Ubuntu' run: | - dotnet pack --output ./artifacts --configuration $BUILD_CONFIGURATION --version-suffix $VERSION --include-symbols --include-source + dotnet pack --output ./artifacts --configuration $BUILD_CONFIGURATION --version-suffix ${{env.VERSION}} --include-symbols --include-source - name: Pack Signed if: runner.os == 'Ubuntu' run: | - dotnet pack --output ./artifacts --configuration $BUILD_CONFIGURATION --version-suffix $VERSION --include-symbols --include-source -p:Sign=true + dotnet pack --output ./artifacts --configuration $BUILD_CONFIGURATION --version-suffix ${{env.VERSION}} --include-symbols --include-source -p:Sign=true - uses: actions/upload-artifact@v1 if: runner.os == 'Ubuntu' From 4cd2748e43d53c39f36d54cf9dfa6a912e6fffaa Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 5 Mar 2022 20:32:29 +0200 Subject: [PATCH 228/549] Update ci-cd.yml --- .github/workflows/ci-cd.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index cbdadc63..31067605 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -70,30 +70,30 @@ jobs: - name: Get Version run: | - echo "VERSION = $(git describe --tags --dirty)" >> $GITHUB_ENV + echo "VERSION = $(git describe --abbrev=0)" >> $GITHUB_ENV - name: Restore run: dotnet restore - name: Build - run: dotnet build --no-restore --configuration $BUILD_CONFIGURATION --version-suffix ${{env.VERSION}} + run: dotnet build --no-restore --configuration $BUILD_CONFIGURATION /p:Version="${{env.VERSION}}" - name: Build Signed if: runner.os == 'Ubuntu' - run: dotnet build redmine-net-api.sln --no-restore --configuration $BUILD_CONFIGURATION --version-suffix ${{env.VERSION}} -p:Sign=true + run: dotnet build redmine-net-api.sln --no-restore --configuration $BUILD_CONFIGURATION /p:Version="${{env.VERSION}}" -p:Sign=true - name: Test run: dotnet test --no-restore --no-build --configuration $BUILD_CONFIGURATION - name: Pack - if: runner.os == 'Ubuntu' + if: runner.os == 'Ubuntu' && startsWith(github.ref, 'refs/tags') run: | - dotnet pack --output ./artifacts --configuration $BUILD_CONFIGURATION --version-suffix ${{env.VERSION}} --include-symbols --include-source + dotnet pack --output ./artifacts --configuration $BUILD_CONFIGURATION /p:Version="${{env.VERSION}}" --include-symbols --include-source - - name: Pack Signed + - name: Pack Signed && startsWith(github.ref, 'refs/tags') if: runner.os == 'Ubuntu' run: | - dotnet pack --output ./artifacts --configuration $BUILD_CONFIGURATION --version-suffix ${{env.VERSION}} --include-symbols --include-source -p:Sign=true + dotnet pack --output ./artifacts --configuration $BUILD_CONFIGURATION /p:Version="${{env.VERSION}}" --include-symbols --include-source -p:Sign=true - uses: actions/upload-artifact@v1 if: runner.os == 'Ubuntu' From 48f40bea2a2dd3d89bd84a48c0be22777d53ed32 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 5 Mar 2022 20:38:16 +0200 Subject: [PATCH 229/549] Update ci-cd.yml --- .github/workflows/ci-cd.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 31067605..07dc1385 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -71,16 +71,17 @@ jobs: - name: Get Version run: | echo "VERSION = $(git describe --abbrev=0)" >> $GITHUB_ENV + echo $VERSION - name: Restore run: dotnet restore - name: Build - run: dotnet build --no-restore --configuration $BUILD_CONFIGURATION /p:Version="${{env.VERSION}}" + run: dotnet build --no-restore --configuration $BUILD_CONFIGURATION -p:Version=$VERSION - name: Build Signed if: runner.os == 'Ubuntu' - run: dotnet build redmine-net-api.sln --no-restore --configuration $BUILD_CONFIGURATION /p:Version="${{env.VERSION}}" -p:Sign=true + run: dotnet build redmine-net-api.sln --no-restore --configuration $BUILD_CONFIGURATION -p:Version=$VERSION -p:Sign=true - name: Test run: dotnet test --no-restore --no-build --configuration $BUILD_CONFIGURATION @@ -88,12 +89,12 @@ jobs: - name: Pack if: runner.os == 'Ubuntu' && startsWith(github.ref, 'refs/tags') run: | - dotnet pack --output ./artifacts --configuration $BUILD_CONFIGURATION /p:Version="${{env.VERSION}}" --include-symbols --include-source + dotnet pack --output ./artifacts --configuration $BUILD_CONFIGURATION -p:Version=$VERSION --include-symbols --include-source - name: Pack Signed && startsWith(github.ref, 'refs/tags') if: runner.os == 'Ubuntu' run: | - dotnet pack --output ./artifacts --configuration $BUILD_CONFIGURATION /p:Version="${{env.VERSION}}" --include-symbols --include-source -p:Sign=true + dotnet pack --output ./artifacts --configuration $BUILD_CONFIGURATION -p:Version=$VERSION --include-symbols --include-source -p:Sign=true - uses: actions/upload-artifact@v1 if: runner.os == 'Ubuntu' From 8e8859212bc90e96705463b4a88cd8896604b76b Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 6 Mar 2022 12:45:41 +0200 Subject: [PATCH 230/549] Update ci-cd.yml --- .github/workflows/ci-cd.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 07dc1385..c2d80305 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -70,8 +70,8 @@ jobs: - name: Get Version run: | - echo "VERSION = $(git describe --abbrev=0)" >> $GITHUB_ENV - echo $VERSION + echo "VERSION = ${git describe --abbrev=0}" >> $GITHUB_ENV + echo "$VERSION" - name: Restore run: dotnet restore From 030c75bd4c7b63c8aefecc3c9ffcd3ad9e01146e Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 6 Mar 2022 12:47:47 +0200 Subject: [PATCH 231/549] Update ci-cd.yml --- .github/workflows/ci-cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index c2d80305..6049072f 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -70,7 +70,7 @@ jobs: - name: Get Version run: | - echo "VERSION = ${git describe --abbrev=0}" >> $GITHUB_ENV + echo "VERSION = ${(git describe --abbrev=0)}" >> $GITHUB_ENV echo "$VERSION" - name: Restore From ace668fceec7b1ea766c2d2ac57d450bcdb07b3e Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 6 Mar 2022 12:49:09 +0200 Subject: [PATCH 232/549] Fix substitution --- .github/workflows/ci-cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 6049072f..b4d9d96c 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -70,7 +70,7 @@ jobs: - name: Get Version run: | - echo "VERSION = ${(git describe --abbrev=0)}" >> $GITHUB_ENV + echo "VERSION = $(git describe --abbrev=0)" >> $GITHUB_ENV echo "$VERSION" - name: Restore From 0a0503f414c942128447dce51bd073f40c6614f2 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 6 Mar 2022 12:58:59 +0200 Subject: [PATCH 233/549] Update ci-cd.yml --- .github/workflows/ci-cd.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index b4d9d96c..742087f6 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -71,13 +71,14 @@ jobs: - name: Get Version run: | echo "VERSION = $(git describe --abbrev=0)" >> $GITHUB_ENV - echo "$VERSION" + + - run: echo ${{ env.VERSION }} - name: Restore run: dotnet restore - name: Build - run: dotnet build --no-restore --configuration $BUILD_CONFIGURATION -p:Version=$VERSION + run: dotnet build --no-restore --configuration $BUILD_CONFIGURATION -p:Version=${{env.VERSION}} - name: Build Signed if: runner.os == 'Ubuntu' From a0dc51b2dc4c73ca8fb773ca2523e6a61af132b3 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 6 Mar 2022 13:25:08 +0200 Subject: [PATCH 234/549] Update ci-cd.yml --- .github/workflows/ci-cd.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 742087f6..aedd74c2 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -71,14 +71,20 @@ jobs: - name: Get Version run: | echo "VERSION = $(git describe --abbrev=0)" >> $GITHUB_ENV - - - run: echo ${{ env.VERSION }} + + - name: Display VERSION env variable (shell) + run: echo "Version=$VERSION" + + - name: Display VERSION env variable + run: echo "Version = ${{ env.VERSION }}" - name: Restore run: dotnet restore - name: Build - run: dotnet build --no-restore --configuration $BUILD_CONFIGURATION -p:Version=${{env.VERSION}} + run: | + echo "Version=$VERSION" + dotnet build --no-restore --configuration $BUILD_CONFIGURATION -p:Version=${{env.VERSION}} - name: Build Signed if: runner.os == 'Ubuntu' From ac6f3b40de68a5f82f9fe7a0d359b4c8636cbcac Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 6 Mar 2022 13:49:47 +0200 Subject: [PATCH 235/549] Update ci-cd.yml --- .github/workflows/ci-cd.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index aedd74c2..126435a6 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -53,8 +53,8 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ ubuntu-latest, windows-latest, macos-latest ] - dotnet: [ '3.1.x', '5.x.x', '6.x.x' ] + os: [ ubuntu-latest]#, windows-latest, macos-latest ] + dotnet: [ '3.1.x']#, '5.x.x', '6.x.x' ] steps: - name: Checkout @@ -70,13 +70,13 @@ jobs: - name: Get Version run: | - echo "VERSION = $(git describe --abbrev=0)" >> $GITHUB_ENV + echo "VERSION=$(git describe --abbrev=0)" >> $GITHUB_ENV - name: Display VERSION env variable (shell) run: echo "Version=$VERSION" - name: Display VERSION env variable - run: echo "Version = ${{ env.VERSION }}" + run: echo "Version=${{ env.VERSION }}" - name: Restore run: dotnet restore From 1e3296316bf20f58177177b784ad4b325cb78f33 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 6 Mar 2022 13:55:21 +0200 Subject: [PATCH 236/549] Clean test steps & fix runner.os context --- .github/workflows/ci-cd.yml | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 126435a6..bba599c8 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -72,39 +72,32 @@ jobs: run: | echo "VERSION=$(git describe --abbrev=0)" >> $GITHUB_ENV - - name: Display VERSION env variable (shell) - run: echo "Version=$VERSION" - - - name: Display VERSION env variable - run: echo "Version=${{ env.VERSION }}" - - name: Restore run: dotnet restore - name: Build run: | - echo "Version=$VERSION" dotnet build --no-restore --configuration $BUILD_CONFIGURATION -p:Version=${{env.VERSION}} - name: Build Signed - if: runner.os == 'Ubuntu' + if: runner.os == 'Linux' run: dotnet build redmine-net-api.sln --no-restore --configuration $BUILD_CONFIGURATION -p:Version=$VERSION -p:Sign=true - name: Test run: dotnet test --no-restore --no-build --configuration $BUILD_CONFIGURATION - name: Pack - if: runner.os == 'Ubuntu' && startsWith(github.ref, 'refs/tags') + if: runner.os == 'Linux' && startsWith(github.ref, 'refs/tags') run: | dotnet pack --output ./artifacts --configuration $BUILD_CONFIGURATION -p:Version=$VERSION --include-symbols --include-source - name: Pack Signed && startsWith(github.ref, 'refs/tags') - if: runner.os == 'Ubuntu' + if: runner.os == 'Linux' run: | dotnet pack --output ./artifacts --configuration $BUILD_CONFIGURATION -p:Version=$VERSION --include-symbols --include-source -p:Sign=true - uses: actions/upload-artifact@v1 - if: runner.os == 'Ubuntu' + if: runner.os == 'Linux' with: name: artifacts path: ./artifacts From 347f5c074ef66b99f320ab5011234727e4d9ad15 Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 7 Mar 2022 18:09:41 +0200 Subject: [PATCH 237/549] Add SearchFilterBuilder --- src/redmine-net-api/SearchFilterBuilder.cs | 204 +++++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 src/redmine-net-api/SearchFilterBuilder.cs diff --git a/src/redmine-net-api/SearchFilterBuilder.cs b/src/redmine-net-api/SearchFilterBuilder.cs new file mode 100644 index 00000000..e4a0b294 --- /dev/null +++ b/src/redmine-net-api/SearchFilterBuilder.cs @@ -0,0 +1,204 @@ +using System; +using System.Collections.Specialized; +using System.Globalization; +using Redmine.Net.Api.Extensions; + +namespace Redmine.Net.Api +{ + /// + /// + /// + public sealed class SearchFilterBuilder + { + /// + /// search scope condition + /// + /// + public SearchScope? Scope + { + get => _scope; + set + { + _scope = value; + if (_scope != null) + { + switch (_scope) + { + case SearchScope.All: + _internalScope = "all"; + break; + case SearchScope.MyProject: + _internalScope = "my_project"; + break; + case SearchScope.SubProjects: + _internalScope = "subprojects"; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + } + + /// + /// + /// + public bool? AllWords { get; set; } + + /// + /// + /// + public bool? TitlesOnly { get; set; } + + /// + /// + /// + public bool? IncludeIssues{ get; set; } + + /// + /// + /// + public bool? IncludeNews{ get; set; } + + /// + /// + /// + public bool? IncludeDocuments{ get; set; } + + /// + /// + /// + public bool? IncludeChangeSets{ get; set; } + + /// + /// + /// + public bool? IncludeWikiPages{ get; set; } + + /// + /// + /// + public bool? IncludeMessages{ get; set; } + + /// + /// + /// + public bool? IncludeProjects{ get; set; } + + /// + /// filtered by open issues. + /// + public bool? OpenIssues{ get; set; } + + + /// + /// + public SearchAttachment? Attachments + { + get => _attachments; + set + { + _attachments = value; + if (_attachments != null) + { + switch (_attachments) + { + case SearchAttachment.OnlyInAttachment: + _internalAttachments = "only"; + break; + + case SearchAttachment.InDescription: + _internalAttachments = "0"; + break; + case SearchAttachment.InDescriptionAndAttachment: + _internalAttachments = "1"; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + } + } + + private string _internalScope; + private string _internalAttachments; + private SearchAttachment? _attachments; + private SearchScope? _scope; + + /// + /// + /// + public NameValueCollection Build(NameValueCollection sb) + { + AddIfNotNull(sb,"scope",_internalScope); + AddIfNotNull(sb,"projects",IncludeProjects); + AddIfNotNull(sb,"open_issues",OpenIssues); + AddIfNotNull(sb,"messages",IncludeMessages); + AddIfNotNull(sb,"wiki_pages",IncludeWikiPages); + AddIfNotNull(sb,"changesets",IncludeChangeSets); + AddIfNotNull(sb,"documents",IncludeDocuments); + AddIfNotNull(sb,"news",IncludeNews); + AddIfNotNull(sb,"issues",IncludeIssues); + AddIfNotNull(sb,"titles_only",TitlesOnly); + AddIfNotNull(sb,"all_words", AllWords); + AddIfNotNull(sb,"attachments", _internalAttachments); + + return sb; + } + + private static void AddIfNotNull(NameValueCollection nameValueCollection, string key, bool? value) + { + if (value.HasValue) + { + nameValueCollection.Add(key, value.Value.ToString(CultureInfo.InvariantCulture)); + } + } + + private static void AddIfNotNull(NameValueCollection nameValueCollection, string key, string value) + { + if (!value.IsNullOrWhiteSpace()) + { + nameValueCollection.Add(key, value); + } + } + + } + + /// + /// + /// + public enum SearchScope + { + /// + /// all projects + /// + All, + /// + /// assigned projects + /// + MyProject, + /// + /// include subproject + /// + SubProjects + } + + /// + /// + /// + public enum SearchAttachment + { + /// + /// search only in description + /// + InDescription = 0, + /// + /// search by description and attachment + /// + InDescriptionAndAttachment, + /// + /// search only in attachment + /// + OnlyInAttachment + } +} \ No newline at end of file From 8e2e26f34980c0e97b08b99acb78a9656b598427 Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 7 Mar 2022 18:10:05 +0200 Subject: [PATCH 238/549] Add Q keyword --- src/redmine-net-api/RedmineKeys.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/redmine-net-api/RedmineKeys.cs b/src/redmine-net-api/RedmineKeys.cs index 2fe8f586..5bbb3b14 100644 --- a/src/redmine-net-api/RedmineKeys.cs +++ b/src/redmine-net-api/RedmineKeys.cs @@ -536,6 +536,10 @@ public static class RedmineKeys /// /// /// + public const string Q = "q"; + /// + /// + /// public const string QUERY = "query"; /// /// From fe4a7a85ab08969dfe43a8f20f15023f69a17d1f Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 7 Mar 2022 18:21:21 +0200 Subject: [PATCH 239/549] Add Search method --- .../Async/RedmineManagerAsync.cs | 37 ++++++++++++++++ .../Async/RedmineManagerAsync40.cs | 37 ++++++++++++++++ .../Async/RedmineManagerAsync45.cs | 34 +++++++++++++++ src/redmine-net-api/IRedmineManager.cs | 14 +++++++ src/redmine-net-api/RedmineManager.cs | 42 ++++++++++++++++++- 5 files changed, 162 insertions(+), 2 deletions(-) diff --git a/src/redmine-net-api/Async/RedmineManagerAsync.cs b/src/redmine-net-api/Async/RedmineManagerAsync.cs index bc76b936..9f29ceeb 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync.cs @@ -17,8 +17,11 @@ You may obtain a copy of the License at limitations under the License. */ +using System; using System.Collections.Generic; using System.Collections.Specialized; +using System.Globalization; +using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Types; @@ -281,6 +284,40 @@ public static Task DownloadFileAsync(this RedmineManager redmineManager, { return delegate { return redmineManager.DownloadFile(address); }; } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static Task> SearchAsync(this RedmineManager redmineManager, string q, int limit = RedmineManager.DEFAULT_PAGE_SIZE_VALUE, int offset = 0, SearchFilterBuilder searchFilter = null) + { + if (q.IsNullOrWhiteSpace()) + { + throw new ArgumentNullException(nameof(q)); + } + + var parameters = new NameValueCollection + { + {RedmineKeys.Q, q}, + {RedmineKeys.LIMIT, limit.ToString(CultureInfo.InvariantCulture)}, + {RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)}, + }; + + if (searchFilter != null) + { + parameters = searchFilter.Build(parameters); + } + + var result = redmineManager.GetPaginatedObjectsAsync(parameters); + + return result; + } } } #endif \ No newline at end of file diff --git a/src/redmine-net-api/Async/RedmineManagerAsync40.cs b/src/redmine-net-api/Async/RedmineManagerAsync40.cs index 09699ec3..09253578 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync40.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync40.cs @@ -16,10 +16,13 @@ limitations under the License. #if NET40 +using System; using System.Collections.Generic; using System.Collections.Specialized; +using System.Globalization; using System.Threading; using System.Threading.Tasks; +using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Types; using Redmine.Net.Api.Serialization; @@ -286,6 +289,40 @@ public static Task DownloadFileAsync(this RedmineManager redmineManager, { return Task.Factory.StartNew(() => redmineManager.DownloadFile(address), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static Task> SearchAsync(this RedmineManager redmineManager, string q, int limit = RedmineManager.DEFAULT_PAGE_SIZE_VALUE, int offset = 0, SearchFilterBuilder searchFilter = null) + { + if (q.IsNullOrWhiteSpace()) + { + throw new ArgumentNullException(nameof(q)); + } + + var parameters = new NameValueCollection + { + {RedmineKeys.Q, q}, + {RedmineKeys.LIMIT, limit.ToString(CultureInfo.InvariantCulture)}, + {RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)}, + }; + + if (searchFilter != null) + { + parameters = searchFilter.Build(parameters); + } + + var result = redmineManager.GetPaginatedObjectsAsync(parameters); + + return result; + } } } #endif \ No newline at end of file diff --git a/src/redmine-net-api/Async/RedmineManagerAsync45.cs b/src/redmine-net-api/Async/RedmineManagerAsync45.cs index 199e5e4a..59c492cc 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync45.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync45.cs @@ -439,6 +439,40 @@ public static async Task DeleteObjectAsync(this RedmineManager redmineManager var uri = UrlHelper.GetDeleteUrl(redmineManager, id); await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.DELETE, string.Empty).ConfigureAwait(false); } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static async Task> SearchAsync(this RedmineManager redmineManager, string q, int limit = RedmineManager.DEFAULT_PAGE_SIZE_VALUE, int offset = 0, SearchFilterBuilder searchFilter = null) + { + if (q.IsNullOrWhiteSpace()) + { + throw new ArgumentNullException(nameof(q)); + } + + var parameters = new NameValueCollection + { + {RedmineKeys.Q, q}, + {RedmineKeys.LIMIT, limit.ToString(CultureInfo.InvariantCulture)}, + {RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)}, + }; + + if (searchFilter != null) + { + parameters = searchFilter.Build(parameters); + } + + var result = await redmineManager.GetPaginatedObjectsAsync(parameters).ConfigureAwait(false); + + return result; + } } } #endif \ No newline at end of file diff --git a/src/redmine-net-api/IRedmineManager.cs b/src/redmine-net-api/IRedmineManager.cs index 4456c725..bb647f54 100644 --- a/src/redmine-net-api/IRedmineManager.cs +++ b/src/redmine-net-api/IRedmineManager.cs @@ -141,6 +141,20 @@ public interface IRedmineManager /// /// void UpdateAttachment(int issueId, Attachment attachment); + + /// + /// + /// + /// query strings. enable to specify multiple values separated by a space " ". + /// number of results in response. + /// skip this number of results in response + /// Optional filters. + /// + /// Returns the search results by the specified condition parameters. + /// + PagedResults Search(string q, int limit , int offset = 0, + SearchFilterBuilder searchFilter = null); + /// /// /// diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index ec43f705..f07f385c 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -63,7 +63,8 @@ public class RedmineManager : IRedmineManager {typeof(IssuePriority), "enumerations/issue_priorities"}, {typeof(Watcher), "watchers"}, {typeof(IssueCustomField), "custom_fields"}, - {typeof(CustomField), "custom_fields"} + {typeof(CustomField), "custom_fields"}, + {typeof(Search), "search"} }; /// @@ -76,7 +77,8 @@ public class RedmineManager : IRedmineManager {typeof(News), true}, {typeof(Query), true}, {typeof(TimeEntry), true}, - {typeof(ProjectMembership), true} + {typeof(ProjectMembership), true}, + {typeof(Search), true} }; private readonly string basicAuthorization; @@ -851,6 +853,42 @@ public byte[] DownloadFile(string address) return WebApiHelper.ExecuteDownloadFile(this, address); } + + /// + /// + /// + /// query strings. enable to specify multiple values separated by a space " ". + /// number of results in response. + /// skip this number of results in response + /// Optional filters. + /// + /// Returns the search results by the specified condition parameters. + /// + /// + public PagedResults Search(string q, int limit = DEFAULT_PAGE_SIZE_VALUE, int offset = 0, SearchFilterBuilder searchFilter = null) + { + if (q.IsNullOrWhiteSpace()) + { + throw new ArgumentNullException(nameof(q)); + } + + var parameters = new NameValueCollection + { + {RedmineKeys.Q, q}, + {RedmineKeys.LIMIT, limit.ToString(CultureInfo.InvariantCulture)}, + {RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)}, + }; + + if (searchFilter != null) + { + parameters = searchFilter.Build(parameters); + } + + var result = GetPaginatedObjects(parameters); + + return result; + } + private const string UA = "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.163 Safari/535.1"; /// From f16ecdb7bc05dcd808d2515ce517c852f5dae290 Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 28 Feb 2023 20:39:57 +0200 Subject: [PATCH 240/549] Updated copyrigth --- src/redmine-net-api/Async/RedmineManagerAsync.cs | 2 +- .../Async/RedmineManagerAsync40.cs | 2 +- .../Async/RedmineManagerAsync45.cs | 2 +- .../Exceptions/ConflictException.cs | 2 +- .../Exceptions/ForbiddenException.cs | 2 +- .../Exceptions/InternalServerErrorException.cs | 2 +- .../Exceptions/NameResolutionFailureException.cs | 2 +- .../Exceptions/NotAcceptableException.cs | 2 +- .../Exceptions/NotFoundException.cs | 2 +- .../Exceptions/RedmineException.cs | 2 +- .../Exceptions/RedmineTimeoutException.cs | 2 +- .../Exceptions/UnauthorizedException.cs | 2 +- .../Extensions/CollectionExtensions.cs | 2 +- .../Extensions/ExtensionAttribute.cs | 2 +- .../Extensions/JsonReaderExtensions.cs | 2 +- .../Extensions/JsonWriterExtensions.cs | 2 +- .../Extensions/NameValueCollectionExtensions.cs | 2 +- .../Extensions/StringExtensions.cs | 2 +- src/redmine-net-api/Extensions/WebExtensions.cs | 2 +- .../Extensions/XmlReaderExtensions.cs | 2 +- .../Extensions/XmlWriterExtensions.cs | 2 +- src/redmine-net-api/HttpVerbs.cs | 2 +- src/redmine-net-api/IRedmineManager.cs | 2 +- src/redmine-net-api/IRedmineWebClient.cs | 2 +- src/redmine-net-api/Internals/DataHelper.cs | 2 +- src/redmine-net-api/Internals/Func.cs | 2 +- src/redmine-net-api/Internals/HashCodeHelper.cs | 2 +- src/redmine-net-api/Internals/UrlHelper.cs | 2 +- .../Internals/WebApiAsyncHelper.cs | 2 +- src/redmine-net-api/Internals/WebApiHelper.cs | 2 +- .../Internals/XmlTextReaderBuilder.cs | 2 +- src/redmine-net-api/MimeFormat.cs | 2 +- src/redmine-net-api/RedirectType.cs | 2 +- src/redmine-net-api/RedmineKeys.cs | 2 +- src/redmine-net-api/RedmineManager.cs | 2 +- src/redmine-net-api/RedmineWebClient.cs | 2 +- src/redmine-net-api/SearchFilterBuilder.cs | 16 ++++++++++++++++ .../Serialization/CacheKeyFactory.cs | 2 +- .../Serialization/IJsonSerializable.cs | 2 +- .../Serialization/ISerialization.cs | 2 +- .../Serialization/IXmlSerializerCache.cs | 2 +- src/redmine-net-api/Serialization/JsonObject.cs | 2 +- .../Serialization/JsonRedmineSerializer.cs | 2 +- .../Serialization/PagedResults.cs | 2 +- .../Serialization/XmlRedmineSerializer.cs | 2 +- .../Serialization/XmlSerializerCache.cs | 2 +- src/redmine-net-api/Types/Attachment.cs | 2 +- src/redmine-net-api/Types/Attachments.cs | 2 +- src/redmine-net-api/Types/ChangeSet.cs | 2 +- src/redmine-net-api/Types/CustomField.cs | 2 +- .../Types/CustomFieldPossibleValue.cs | 2 +- src/redmine-net-api/Types/CustomFieldRole.cs | 2 +- src/redmine-net-api/Types/CustomFieldValue.cs | 2 +- src/redmine-net-api/Types/Detail.cs | 2 +- src/redmine-net-api/Types/Error.cs | 2 +- src/redmine-net-api/Types/File.cs | 2 +- src/redmine-net-api/Types/Group.cs | 2 +- src/redmine-net-api/Types/GroupUser.cs | 2 +- src/redmine-net-api/Types/IValue.cs | 2 +- src/redmine-net-api/Types/Identifiable.cs | 2 +- src/redmine-net-api/Types/IdentifiableName.cs | 2 +- src/redmine-net-api/Types/Issue.cs | 2 +- src/redmine-net-api/Types/IssueAllowedStatus.cs | 2 +- src/redmine-net-api/Types/IssueCategory.cs | 2 +- src/redmine-net-api/Types/IssueChild.cs | 2 +- src/redmine-net-api/Types/IssueCustomField.cs | 2 +- src/redmine-net-api/Types/IssuePriority.cs | 2 +- src/redmine-net-api/Types/IssueRelation.cs | 2 +- src/redmine-net-api/Types/IssueRelationType.cs | 2 +- src/redmine-net-api/Types/IssueStatus.cs | 2 +- src/redmine-net-api/Types/Journal.cs | 2 +- src/redmine-net-api/Types/Membership.cs | 2 +- src/redmine-net-api/Types/MembershipRole.cs | 2 +- src/redmine-net-api/Types/MyAccount.cs | 2 +- .../Types/MyAccountCustomField.cs | 2 +- src/redmine-net-api/Types/News.cs | 2 +- src/redmine-net-api/Types/NewsComment.cs | 2 +- src/redmine-net-api/Types/Permission.cs | 2 +- src/redmine-net-api/Types/Project.cs | 2 +- .../Types/ProjectEnabledModule.cs | 2 +- .../Types/ProjectIssueCategory.cs | 2 +- src/redmine-net-api/Types/ProjectMembership.cs | 2 +- src/redmine-net-api/Types/ProjectStatus.cs | 2 +- .../Types/ProjectTimeEntryActivity.cs | 2 +- src/redmine-net-api/Types/ProjectTracker.cs | 2 +- src/redmine-net-api/Types/Query.cs | 2 +- src/redmine-net-api/Types/Role.cs | 2 +- src/redmine-net-api/Types/Search.cs | 2 +- src/redmine-net-api/Types/TimeEntry.cs | 2 +- src/redmine-net-api/Types/TimeEntryActivity.cs | 2 +- src/redmine-net-api/Types/Tracker.cs | 2 +- src/redmine-net-api/Types/TrackerCustomField.cs | 2 +- src/redmine-net-api/Types/Upload.cs | 2 +- src/redmine-net-api/Types/User.cs | 2 +- src/redmine-net-api/Types/UserGroup.cs | 2 +- src/redmine-net-api/Types/UserStatus.cs | 2 +- src/redmine-net-api/Types/Version.cs | 2 +- src/redmine-net-api/Types/VersionSharing.cs | 2 +- src/redmine-net-api/Types/VersionStatus.cs | 2 +- src/redmine-net-api/Types/Watcher.cs | 2 +- src/redmine-net-api/Types/WikiPage.cs | 2 +- .../Tests/Sync/AttachmentTests.cs | 2 +- .../Tests/Sync/CustomFieldTests.cs | 2 +- .../Tests/Sync/IssuePriorityTests.cs | 2 +- .../Tests/Sync/IssueRelationTests.cs | 2 +- .../Tests/Sync/IssueStatusTests.cs | 2 +- .../Tests/Sync/NewsTests.cs | 2 +- .../Tests/Sync/ProjectMembershipTests.cs | 2 +- .../Tests/Sync/ProjectTests.cs | 2 +- .../Tests/Sync/QueryTests.cs | 2 +- .../Tests/Sync/RoleTests.cs | 2 +- .../Tests/Sync/TimeEntryActivtiyTests.cs | 2 +- .../Tests/Sync/TimeEntryTests.cs | 2 +- .../Tests/Sync/TrackerTests.cs | 2 +- .../Tests/Sync/UserTests.cs | 2 +- .../Tests/Sync/VersionTests.cs | 2 +- .../Tests/Sync/WikiPageTests.cs | 2 +- 117 files changed, 132 insertions(+), 116 deletions(-) diff --git a/src/redmine-net-api/Async/RedmineManagerAsync.cs b/src/redmine-net-api/Async/RedmineManagerAsync.cs index 9f29ceeb..124c2b45 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync.cs @@ -2,7 +2,7 @@ #if NET20 /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Async/RedmineManagerAsync40.cs b/src/redmine-net-api/Async/RedmineManagerAsync40.cs index 09253578..93845ccc 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync40.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync40.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Async/RedmineManagerAsync45.cs b/src/redmine-net-api/Async/RedmineManagerAsync45.cs index 59c492cc..1cdc2725 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync45.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync45.cs @@ -1,5 +1,5 @@ /* -Copyright 2011 - 2022 Adrian Popescu +Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/ConflictException.cs b/src/redmine-net-api/Exceptions/ConflictException.cs index 46faad5b..1cb1dcf9 100644 --- a/src/redmine-net-api/Exceptions/ConflictException.cs +++ b/src/redmine-net-api/Exceptions/ConflictException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/ForbiddenException.cs b/src/redmine-net-api/Exceptions/ForbiddenException.cs index b34dbbe9..6821eb19 100644 --- a/src/redmine-net-api/Exceptions/ForbiddenException.cs +++ b/src/redmine-net-api/Exceptions/ForbiddenException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/InternalServerErrorException.cs b/src/redmine-net-api/Exceptions/InternalServerErrorException.cs index 6d495faa..bf6dda64 100644 --- a/src/redmine-net-api/Exceptions/InternalServerErrorException.cs +++ b/src/redmine-net-api/Exceptions/InternalServerErrorException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/NameResolutionFailureException.cs b/src/redmine-net-api/Exceptions/NameResolutionFailureException.cs index 27d648d2..3651d6db 100644 --- a/src/redmine-net-api/Exceptions/NameResolutionFailureException.cs +++ b/src/redmine-net-api/Exceptions/NameResolutionFailureException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/NotAcceptableException.cs b/src/redmine-net-api/Exceptions/NotAcceptableException.cs index b660bf3f..6b1ef5ea 100644 --- a/src/redmine-net-api/Exceptions/NotAcceptableException.cs +++ b/src/redmine-net-api/Exceptions/NotAcceptableException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/NotFoundException.cs b/src/redmine-net-api/Exceptions/NotFoundException.cs index fbc320c5..71722e93 100644 --- a/src/redmine-net-api/Exceptions/NotFoundException.cs +++ b/src/redmine-net-api/Exceptions/NotFoundException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/RedmineException.cs b/src/redmine-net-api/Exceptions/RedmineException.cs index 50be65f7..9ce2af67 100644 --- a/src/redmine-net-api/Exceptions/RedmineException.cs +++ b/src/redmine-net-api/Exceptions/RedmineException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/RedmineTimeoutException.cs b/src/redmine-net-api/Exceptions/RedmineTimeoutException.cs index 9b615dff..0f4d89c4 100644 --- a/src/redmine-net-api/Exceptions/RedmineTimeoutException.cs +++ b/src/redmine-net-api/Exceptions/RedmineTimeoutException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/UnauthorizedException.cs b/src/redmine-net-api/Exceptions/UnauthorizedException.cs index 2599d7c3..7e063c58 100644 --- a/src/redmine-net-api/Exceptions/UnauthorizedException.cs +++ b/src/redmine-net-api/Exceptions/UnauthorizedException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Extensions/CollectionExtensions.cs b/src/redmine-net-api/Extensions/CollectionExtensions.cs index 4246c267..e01f1e57 100755 --- a/src/redmine-net-api/Extensions/CollectionExtensions.cs +++ b/src/redmine-net-api/Extensions/CollectionExtensions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Extensions/ExtensionAttribute.cs b/src/redmine-net-api/Extensions/ExtensionAttribute.cs index fa070874..cdb7b66b 100755 --- a/src/redmine-net-api/Extensions/ExtensionAttribute.cs +++ b/src/redmine-net-api/Extensions/ExtensionAttribute.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Extensions/JsonReaderExtensions.cs b/src/redmine-net-api/Extensions/JsonReaderExtensions.cs index bebf9925..ad54ba31 100644 --- a/src/redmine-net-api/Extensions/JsonReaderExtensions.cs +++ b/src/redmine-net-api/Extensions/JsonReaderExtensions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Extensions/JsonWriterExtensions.cs b/src/redmine-net-api/Extensions/JsonWriterExtensions.cs index e48d5b27..2bbca7dd 100644 --- a/src/redmine-net-api/Extensions/JsonWriterExtensions.cs +++ b/src/redmine-net-api/Extensions/JsonWriterExtensions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Extensions/NameValueCollectionExtensions.cs b/src/redmine-net-api/Extensions/NameValueCollectionExtensions.cs index 28264d82..2fd504b8 100644 --- a/src/redmine-net-api/Extensions/NameValueCollectionExtensions.cs +++ b/src/redmine-net-api/Extensions/NameValueCollectionExtensions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Extensions/StringExtensions.cs b/src/redmine-net-api/Extensions/StringExtensions.cs index 10308d2e..d0856246 100644 --- a/src/redmine-net-api/Extensions/StringExtensions.cs +++ b/src/redmine-net-api/Extensions/StringExtensions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Extensions/WebExtensions.cs b/src/redmine-net-api/Extensions/WebExtensions.cs index 59ca73ab..493a07c9 100644 --- a/src/redmine-net-api/Extensions/WebExtensions.cs +++ b/src/redmine-net-api/Extensions/WebExtensions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Extensions/XmlReaderExtensions.cs b/src/redmine-net-api/Extensions/XmlReaderExtensions.cs index 57be87d9..87b2b528 100644 --- a/src/redmine-net-api/Extensions/XmlReaderExtensions.cs +++ b/src/redmine-net-api/Extensions/XmlReaderExtensions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Extensions/XmlWriterExtensions.cs b/src/redmine-net-api/Extensions/XmlWriterExtensions.cs index cddbd9b9..8791c6b1 100644 --- a/src/redmine-net-api/Extensions/XmlWriterExtensions.cs +++ b/src/redmine-net-api/Extensions/XmlWriterExtensions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/HttpVerbs.cs b/src/redmine-net-api/HttpVerbs.cs index 09abd93a..83c65de5 100644 --- a/src/redmine-net-api/HttpVerbs.cs +++ b/src/redmine-net-api/HttpVerbs.cs @@ -1,5 +1,5 @@ /* -Copyright 2011 - 2022 Adrian Popescu +Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/IRedmineManager.cs b/src/redmine-net-api/IRedmineManager.cs index bb647f54..57f8a423 100644 --- a/src/redmine-net-api/IRedmineManager.cs +++ b/src/redmine-net-api/IRedmineManager.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/IRedmineWebClient.cs b/src/redmine-net-api/IRedmineWebClient.cs index ef328929..91cf5bb2 100644 --- a/src/redmine-net-api/IRedmineWebClient.cs +++ b/src/redmine-net-api/IRedmineWebClient.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Internals/DataHelper.cs b/src/redmine-net-api/Internals/DataHelper.cs index 821996a1..7686e2fa 100755 --- a/src/redmine-net-api/Internals/DataHelper.cs +++ b/src/redmine-net-api/Internals/DataHelper.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Internals/Func.cs b/src/redmine-net-api/Internals/Func.cs index 5adb6ef8..0b6a38f2 100644 --- a/src/redmine-net-api/Internals/Func.cs +++ b/src/redmine-net-api/Internals/Func.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Internals/HashCodeHelper.cs b/src/redmine-net-api/Internals/HashCodeHelper.cs index 87ebdc09..5f8982c8 100755 --- a/src/redmine-net-api/Internals/HashCodeHelper.cs +++ b/src/redmine-net-api/Internals/HashCodeHelper.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Internals/UrlHelper.cs b/src/redmine-net-api/Internals/UrlHelper.cs index e3d9db16..a1867e3b 100644 --- a/src/redmine-net-api/Internals/UrlHelper.cs +++ b/src/redmine-net-api/Internals/UrlHelper.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Internals/WebApiAsyncHelper.cs b/src/redmine-net-api/Internals/WebApiAsyncHelper.cs index deaa7255..6f0b3f16 100644 --- a/src/redmine-net-api/Internals/WebApiAsyncHelper.cs +++ b/src/redmine-net-api/Internals/WebApiAsyncHelper.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Internals/WebApiHelper.cs b/src/redmine-net-api/Internals/WebApiHelper.cs index 37e5e890..27406f2c 100644 --- a/src/redmine-net-api/Internals/WebApiHelper.cs +++ b/src/redmine-net-api/Internals/WebApiHelper.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs b/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs index 3fafc243..2d24a598 100644 --- a/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs +++ b/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/MimeFormat.cs b/src/redmine-net-api/MimeFormat.cs index 5f0e4ff4..8bc2b2ae 100755 --- a/src/redmine-net-api/MimeFormat.cs +++ b/src/redmine-net-api/MimeFormat.cs @@ -1,5 +1,5 @@ /* -Copyright 2011 - 2022 Adrian Popescu +Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/RedirectType.cs b/src/redmine-net-api/RedirectType.cs index d3157bcf..ead5ed4a 100644 --- a/src/redmine-net-api/RedirectType.cs +++ b/src/redmine-net-api/RedirectType.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/RedmineKeys.cs b/src/redmine-net-api/RedmineKeys.cs index 5bbb3b14..081ba422 100644 --- a/src/redmine-net-api/RedmineKeys.cs +++ b/src/redmine-net-api/RedmineKeys.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu. + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index f07f385c..f89d8989 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/RedmineWebClient.cs b/src/redmine-net-api/RedmineWebClient.cs index 3ecaa47b..6db3ee4d 100644 --- a/src/redmine-net-api/RedmineWebClient.cs +++ b/src/redmine-net-api/RedmineWebClient.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/SearchFilterBuilder.cs b/src/redmine-net-api/SearchFilterBuilder.cs index e4a0b294..d4c6e9fd 100644 --- a/src/redmine-net-api/SearchFilterBuilder.cs +++ b/src/redmine-net-api/SearchFilterBuilder.cs @@ -1,3 +1,19 @@ +/* + Copyright 2011 - 2023 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + using System; using System.Collections.Specialized; using System.Globalization; diff --git a/src/redmine-net-api/Serialization/CacheKeyFactory.cs b/src/redmine-net-api/Serialization/CacheKeyFactory.cs index 3e0d3c21..f8a4fa72 100644 --- a/src/redmine-net-api/Serialization/CacheKeyFactory.cs +++ b/src/redmine-net-api/Serialization/CacheKeyFactory.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Serialization/IJsonSerializable.cs b/src/redmine-net-api/Serialization/IJsonSerializable.cs index ac0f6086..c325545f 100644 --- a/src/redmine-net-api/Serialization/IJsonSerializable.cs +++ b/src/redmine-net-api/Serialization/IJsonSerializable.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Serialization/ISerialization.cs b/src/redmine-net-api/Serialization/ISerialization.cs index 0a65b9a4..52a0c317 100644 --- a/src/redmine-net-api/Serialization/ISerialization.cs +++ b/src/redmine-net-api/Serialization/ISerialization.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Serialization/IXmlSerializerCache.cs b/src/redmine-net-api/Serialization/IXmlSerializerCache.cs index 50e80d25..daa3afed 100644 --- a/src/redmine-net-api/Serialization/IXmlSerializerCache.cs +++ b/src/redmine-net-api/Serialization/IXmlSerializerCache.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Serialization/JsonObject.cs b/src/redmine-net-api/Serialization/JsonObject.cs index ff5b3b8a..7c8624e4 100644 --- a/src/redmine-net-api/Serialization/JsonObject.cs +++ b/src/redmine-net-api/Serialization/JsonObject.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Serialization/JsonRedmineSerializer.cs b/src/redmine-net-api/Serialization/JsonRedmineSerializer.cs index 1357e6c0..2e09c76b 100644 --- a/src/redmine-net-api/Serialization/JsonRedmineSerializer.cs +++ b/src/redmine-net-api/Serialization/JsonRedmineSerializer.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Serialization/PagedResults.cs b/src/redmine-net-api/Serialization/PagedResults.cs index 56c40d1d..b6b446e5 100644 --- a/src/redmine-net-api/Serialization/PagedResults.cs +++ b/src/redmine-net-api/Serialization/PagedResults.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Serialization/XmlRedmineSerializer.cs b/src/redmine-net-api/Serialization/XmlRedmineSerializer.cs index f8ef1f00..15d585d0 100644 --- a/src/redmine-net-api/Serialization/XmlRedmineSerializer.cs +++ b/src/redmine-net-api/Serialization/XmlRedmineSerializer.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Serialization/XmlSerializerCache.cs b/src/redmine-net-api/Serialization/XmlSerializerCache.cs index d85e31f2..bb90a370 100644 --- a/src/redmine-net-api/Serialization/XmlSerializerCache.cs +++ b/src/redmine-net-api/Serialization/XmlSerializerCache.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Attachment.cs b/src/redmine-net-api/Types/Attachment.cs index 937c78c3..b6e188a6 100644 --- a/src/redmine-net-api/Types/Attachment.cs +++ b/src/redmine-net-api/Types/Attachment.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Attachments.cs b/src/redmine-net-api/Types/Attachments.cs index 439955bf..7797e7e7 100644 --- a/src/redmine-net-api/Types/Attachments.cs +++ b/src/redmine-net-api/Types/Attachments.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/ChangeSet.cs b/src/redmine-net-api/Types/ChangeSet.cs index 89210dce..9c9cd711 100644 --- a/src/redmine-net-api/Types/ChangeSet.cs +++ b/src/redmine-net-api/Types/ChangeSet.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/CustomField.cs b/src/redmine-net-api/Types/CustomField.cs index 6e0fe955..28573116 100644 --- a/src/redmine-net-api/Types/CustomField.cs +++ b/src/redmine-net-api/Types/CustomField.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/CustomFieldPossibleValue.cs b/src/redmine-net-api/Types/CustomFieldPossibleValue.cs index ab994251..0934e711 100644 --- a/src/redmine-net-api/Types/CustomFieldPossibleValue.cs +++ b/src/redmine-net-api/Types/CustomFieldPossibleValue.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/CustomFieldRole.cs b/src/redmine-net-api/Types/CustomFieldRole.cs index c0d910d1..8bbf192b 100644 --- a/src/redmine-net-api/Types/CustomFieldRole.cs +++ b/src/redmine-net-api/Types/CustomFieldRole.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/CustomFieldValue.cs b/src/redmine-net-api/Types/CustomFieldValue.cs index 9463d000..0a0f1033 100644 --- a/src/redmine-net-api/Types/CustomFieldValue.cs +++ b/src/redmine-net-api/Types/CustomFieldValue.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Detail.cs b/src/redmine-net-api/Types/Detail.cs index c2bc5ae0..298e93b7 100644 --- a/src/redmine-net-api/Types/Detail.cs +++ b/src/redmine-net-api/Types/Detail.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Error.cs b/src/redmine-net-api/Types/Error.cs index 0a86d6b1..ef6b7b7e 100644 --- a/src/redmine-net-api/Types/Error.cs +++ b/src/redmine-net-api/Types/Error.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/File.cs b/src/redmine-net-api/Types/File.cs index 9e07b06c..2afc20e1 100644 --- a/src/redmine-net-api/Types/File.cs +++ b/src/redmine-net-api/Types/File.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Group.cs b/src/redmine-net-api/Types/Group.cs index 70a7892d..9db8382a 100644 --- a/src/redmine-net-api/Types/Group.cs +++ b/src/redmine-net-api/Types/Group.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/GroupUser.cs b/src/redmine-net-api/Types/GroupUser.cs index 8fa1775a..90b77899 100644 --- a/src/redmine-net-api/Types/GroupUser.cs +++ b/src/redmine-net-api/Types/GroupUser.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IValue.cs b/src/redmine-net-api/Types/IValue.cs index 52fb0065..ae430755 100755 --- a/src/redmine-net-api/Types/IValue.cs +++ b/src/redmine-net-api/Types/IValue.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Identifiable.cs b/src/redmine-net-api/Types/Identifiable.cs index 8dddd6cd..799355b8 100644 --- a/src/redmine-net-api/Types/Identifiable.cs +++ b/src/redmine-net-api/Types/Identifiable.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IdentifiableName.cs b/src/redmine-net-api/Types/IdentifiableName.cs index 4a57363a..277e6929 100644 --- a/src/redmine-net-api/Types/IdentifiableName.cs +++ b/src/redmine-net-api/Types/IdentifiableName.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Issue.cs b/src/redmine-net-api/Types/Issue.cs index 4bcbc1b0..7864a2c5 100644 --- a/src/redmine-net-api/Types/Issue.cs +++ b/src/redmine-net-api/Types/Issue.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IssueAllowedStatus.cs b/src/redmine-net-api/Types/IssueAllowedStatus.cs index 9b37bb2d..2df5b42a 100644 --- a/src/redmine-net-api/Types/IssueAllowedStatus.cs +++ b/src/redmine-net-api/Types/IssueAllowedStatus.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IssueCategory.cs b/src/redmine-net-api/Types/IssueCategory.cs index c489e118..c7fb203e 100644 --- a/src/redmine-net-api/Types/IssueCategory.cs +++ b/src/redmine-net-api/Types/IssueCategory.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IssueChild.cs b/src/redmine-net-api/Types/IssueChild.cs index 90ec83cd..f0821625 100644 --- a/src/redmine-net-api/Types/IssueChild.cs +++ b/src/redmine-net-api/Types/IssueChild.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IssueCustomField.cs b/src/redmine-net-api/Types/IssueCustomField.cs index 8ea5a7b6..d278d5fc 100644 --- a/src/redmine-net-api/Types/IssueCustomField.cs +++ b/src/redmine-net-api/Types/IssueCustomField.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IssuePriority.cs b/src/redmine-net-api/Types/IssuePriority.cs index 155d98ec..80539b90 100644 --- a/src/redmine-net-api/Types/IssuePriority.cs +++ b/src/redmine-net-api/Types/IssuePriority.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IssueRelation.cs b/src/redmine-net-api/Types/IssueRelation.cs index eddab5fa..7f7e8381 100644 --- a/src/redmine-net-api/Types/IssueRelation.cs +++ b/src/redmine-net-api/Types/IssueRelation.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IssueRelationType.cs b/src/redmine-net-api/Types/IssueRelationType.cs index 006492e3..35a34519 100644 --- a/src/redmine-net-api/Types/IssueRelationType.cs +++ b/src/redmine-net-api/Types/IssueRelationType.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IssueStatus.cs b/src/redmine-net-api/Types/IssueStatus.cs index 7091ad2d..80164ece 100644 --- a/src/redmine-net-api/Types/IssueStatus.cs +++ b/src/redmine-net-api/Types/IssueStatus.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Journal.cs b/src/redmine-net-api/Types/Journal.cs index 49ed2647..7cee01df 100644 --- a/src/redmine-net-api/Types/Journal.cs +++ b/src/redmine-net-api/Types/Journal.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Membership.cs b/src/redmine-net-api/Types/Membership.cs index 59add538..b819c91b 100644 --- a/src/redmine-net-api/Types/Membership.cs +++ b/src/redmine-net-api/Types/Membership.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/MembershipRole.cs b/src/redmine-net-api/Types/MembershipRole.cs index a9764a4d..25af2590 100644 --- a/src/redmine-net-api/Types/MembershipRole.cs +++ b/src/redmine-net-api/Types/MembershipRole.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/MyAccount.cs b/src/redmine-net-api/Types/MyAccount.cs index e86c78e0..06189301 100644 --- a/src/redmine-net-api/Types/MyAccount.cs +++ b/src/redmine-net-api/Types/MyAccount.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/MyAccountCustomField.cs b/src/redmine-net-api/Types/MyAccountCustomField.cs index ebbbf4a8..a09a6bbe 100644 --- a/src/redmine-net-api/Types/MyAccountCustomField.cs +++ b/src/redmine-net-api/Types/MyAccountCustomField.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/News.cs b/src/redmine-net-api/Types/News.cs index f7702bb2..8e037189 100644 --- a/src/redmine-net-api/Types/News.cs +++ b/src/redmine-net-api/Types/News.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/NewsComment.cs b/src/redmine-net-api/Types/NewsComment.cs index 4cb7520e..48496829 100644 --- a/src/redmine-net-api/Types/NewsComment.cs +++ b/src/redmine-net-api/Types/NewsComment.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Permission.cs b/src/redmine-net-api/Types/Permission.cs index 3abc6495..f912a2e9 100644 --- a/src/redmine-net-api/Types/Permission.cs +++ b/src/redmine-net-api/Types/Permission.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs index 4d83d910..44dd0c0d 100644 --- a/src/redmine-net-api/Types/Project.cs +++ b/src/redmine-net-api/Types/Project.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/ProjectEnabledModule.cs b/src/redmine-net-api/Types/ProjectEnabledModule.cs index 2ec6796c..5128a5c0 100644 --- a/src/redmine-net-api/Types/ProjectEnabledModule.cs +++ b/src/redmine-net-api/Types/ProjectEnabledModule.cs @@ -1,5 +1,5 @@ /* -Copyright 2011 - 2022 Adrian Popescu +Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/ProjectIssueCategory.cs b/src/redmine-net-api/Types/ProjectIssueCategory.cs index fcfe2ead..d9181e17 100644 --- a/src/redmine-net-api/Types/ProjectIssueCategory.cs +++ b/src/redmine-net-api/Types/ProjectIssueCategory.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/ProjectMembership.cs b/src/redmine-net-api/Types/ProjectMembership.cs index f91508a4..8ed99f79 100644 --- a/src/redmine-net-api/Types/ProjectMembership.cs +++ b/src/redmine-net-api/Types/ProjectMembership.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/ProjectStatus.cs b/src/redmine-net-api/Types/ProjectStatus.cs index f0092025..355a3092 100755 --- a/src/redmine-net-api/Types/ProjectStatus.cs +++ b/src/redmine-net-api/Types/ProjectStatus.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs b/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs index d9bb4a50..44e23cd5 100644 --- a/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs +++ b/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/ProjectTracker.cs b/src/redmine-net-api/Types/ProjectTracker.cs index 55d71cfe..bf96390d 100644 --- a/src/redmine-net-api/Types/ProjectTracker.cs +++ b/src/redmine-net-api/Types/ProjectTracker.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Query.cs b/src/redmine-net-api/Types/Query.cs index 7adea21c..3c68cf8e 100644 --- a/src/redmine-net-api/Types/Query.cs +++ b/src/redmine-net-api/Types/Query.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Role.cs b/src/redmine-net-api/Types/Role.cs index 7af6c8b0..366ce69e 100644 --- a/src/redmine-net-api/Types/Role.cs +++ b/src/redmine-net-api/Types/Role.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Search.cs b/src/redmine-net-api/Types/Search.cs index 74bea91a..d9355764 100644 --- a/src/redmine-net-api/Types/Search.cs +++ b/src/redmine-net-api/Types/Search.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/TimeEntry.cs b/src/redmine-net-api/Types/TimeEntry.cs index 2fef1f18..1a088178 100644 --- a/src/redmine-net-api/Types/TimeEntry.cs +++ b/src/redmine-net-api/Types/TimeEntry.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/TimeEntryActivity.cs b/src/redmine-net-api/Types/TimeEntryActivity.cs index 1ef81689..762cc5e8 100644 --- a/src/redmine-net-api/Types/TimeEntryActivity.cs +++ b/src/redmine-net-api/Types/TimeEntryActivity.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Tracker.cs b/src/redmine-net-api/Types/Tracker.cs index e9fca1d3..157b75aa 100644 --- a/src/redmine-net-api/Types/Tracker.cs +++ b/src/redmine-net-api/Types/Tracker.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/TrackerCustomField.cs b/src/redmine-net-api/Types/TrackerCustomField.cs index 2f63a7e8..875d93b7 100644 --- a/src/redmine-net-api/Types/TrackerCustomField.cs +++ b/src/redmine-net-api/Types/TrackerCustomField.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Upload.cs b/src/redmine-net-api/Types/Upload.cs index 9c8800d7..42b25a75 100644 --- a/src/redmine-net-api/Types/Upload.cs +++ b/src/redmine-net-api/Types/Upload.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/User.cs b/src/redmine-net-api/Types/User.cs index a93cdb6b..7fcfac3f 100644 --- a/src/redmine-net-api/Types/User.cs +++ b/src/redmine-net-api/Types/User.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/UserGroup.cs b/src/redmine-net-api/Types/UserGroup.cs index e79e09d7..88a173ca 100644 --- a/src/redmine-net-api/Types/UserGroup.cs +++ b/src/redmine-net-api/Types/UserGroup.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/UserStatus.cs b/src/redmine-net-api/Types/UserStatus.cs index 7ff5ab48..ae00c930 100644 --- a/src/redmine-net-api/Types/UserStatus.cs +++ b/src/redmine-net-api/Types/UserStatus.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Version.cs b/src/redmine-net-api/Types/Version.cs index c7199e9d..b7c8b8d9 100644 --- a/src/redmine-net-api/Types/Version.cs +++ b/src/redmine-net-api/Types/Version.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/VersionSharing.cs b/src/redmine-net-api/Types/VersionSharing.cs index 636853ed..1fbf63b0 100644 --- a/src/redmine-net-api/Types/VersionSharing.cs +++ b/src/redmine-net-api/Types/VersionSharing.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/VersionStatus.cs b/src/redmine-net-api/Types/VersionStatus.cs index df070fed..60a4684b 100644 --- a/src/redmine-net-api/Types/VersionStatus.cs +++ b/src/redmine-net-api/Types/VersionStatus.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Watcher.cs b/src/redmine-net-api/Types/Watcher.cs index 212e42bb..7dbc2b8d 100644 --- a/src/redmine-net-api/Types/Watcher.cs +++ b/src/redmine-net-api/Types/Watcher.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/WikiPage.cs b/src/redmine-net-api/Types/WikiPage.cs index e2d8e16f..f26c7b25 100644 --- a/src/redmine-net-api/Types/WikiPage.cs +++ b/src/redmine-net-api/Types/WikiPage.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs index a4fd925e..0a5d2f3f 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/CustomFieldTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/CustomFieldTests.cs index 799b0dde..de6db017 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/CustomFieldTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/CustomFieldTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/IssuePriorityTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/IssuePriorityTests.cs index 90b13829..6d82c763 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/IssuePriorityTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/IssuePriorityTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs index ed621c23..e8e74148 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/IssueStatusTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/IssueStatusTests.cs index 0616e192..1ae6b7c1 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/IssueStatusTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/IssueStatusTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/NewsTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/NewsTests.cs index b6ab8383..e7a4482c 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/NewsTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/NewsTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs index 2a25a49a..0b9e7ae8 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/ProjectTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/ProjectTests.cs index 6d716190..b07e0065 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/ProjectTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/ProjectTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/QueryTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/QueryTests.cs index 249ee322..87f5c4b0 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/QueryTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/QueryTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/RoleTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/RoleTests.cs index 2bfe2d53..6c44ea55 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/RoleTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/RoleTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryActivtiyTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryActivtiyTests.cs index 7d069c8f..77d7150b 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryActivtiyTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryActivtiyTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs index 3ef9065e..f52808ec 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/TrackerTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/TrackerTests.cs index 1a7af4b8..b5061e55 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/TrackerTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/TrackerTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/UserTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/UserTests.cs index f784ba3d..bf771e3e 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/UserTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/UserTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/VersionTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/VersionTests.cs index e8db33ef..f629df86 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/VersionTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/VersionTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs index c9c1baf6..c5788f0e 100644 --- a/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs +++ b/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2022 Adrian Popescu + Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 0a101550e369e28370bbe8977afd1414e71b53df Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 28 Feb 2023 20:56:21 +0200 Subject: [PATCH 241/549] Fixed #311 --- src/redmine-net-api/Types/ChangeSet.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/redmine-net-api/Types/ChangeSet.cs b/src/redmine-net-api/Types/ChangeSet.cs index 9c9cd711..5a362d68 100644 --- a/src/redmine-net-api/Types/ChangeSet.cs +++ b/src/redmine-net-api/Types/ChangeSet.cs @@ -38,7 +38,7 @@ public sealed class ChangeSet : IXmlSerializable, IJsonSerializable, IEquatable< /// /// /// - public int Revision { get; internal set; } + public string Revision { get; internal set; } /// /// @@ -78,7 +78,7 @@ public void ReadXml(XmlReader reader) continue; } - Revision = reader.ReadAttributeAsInt(RedmineKeys.REVISION); + Revision = reader.GetAttribute(RedmineKeys.REVISION); switch (reader.Name) { @@ -125,7 +125,7 @@ public void ReadJson(JsonReader reader) case RedmineKeys.COMMITTED_ON: CommittedOn = reader.ReadAsDateTime(); break; - case RedmineKeys.REVISION: Revision = reader.ReadAsInt(); break; + case RedmineKeys.REVISION: Revision = reader.ReadAsString(); break; case RedmineKeys.USER: User = new IdentifiableName(reader); break; From cef39aff155d7ba61b49b2fcfae82ab8e0342cbc Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 1 Mar 2023 22:23:13 +0200 Subject: [PATCH 242/549] Update appveyor nuget api key --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index c4095dd5..fcb1ad07 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -82,7 +82,7 @@ for: - provider: NuGet name: production api_key: - secure: fEZylRkHvyJqjgeQ+i9TfL/JOPjLKr43k+a8Oy5MIy54IkFC8ZECaEfskcWOyqcg + secure: W38N2nYNrxoik84zDowE+ShuVYKUyPA/fl4/8nYMBEXwcG+pSHVkt/2r6xQvQOaC skip_symbols: true on: APPVEYOR_REPO_TAG: true \ No newline at end of file From f2ff58786b8a7ff166901fca95950f82a5dbb454 Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 1 Mar 2023 23:25:39 +0200 Subject: [PATCH 243/549] Add parent title to wiki page --- src/redmine-net-api/Types/WikiPage.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/redmine-net-api/Types/WikiPage.cs b/src/redmine-net-api/Types/WikiPage.cs index f26c7b25..bba1fcf7 100644 --- a/src/redmine-net-api/Types/WikiPage.cs +++ b/src/redmine-net-api/Types/WikiPage.cs @@ -39,7 +39,12 @@ public sealed class WikiPage : Identifiable /// Gets the title. /// public string Title { get; internal set; } - + + /// + /// + /// + public string ParentTitle { get; internal set; } + /// /// Gets or sets the text. /// @@ -118,6 +123,7 @@ public override void ReadXml(XmlReader reader) case RedmineKeys.TITLE: Title = reader.ReadElementContentAsString(); break; case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadElementContentAsNullableDateTime(); break; case RedmineKeys.VERSION: Version = reader.ReadElementContentAsInt(); break; + case RedmineKeys.PARENT: ParentTitle = reader.GetAttribute(RedmineKeys.PARENT); break; default: reader.Read(); break; } } @@ -167,6 +173,7 @@ public override void ReadJson(JsonReader reader) case RedmineKeys.TITLE: Title = reader.ReadAsString(); break; case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadAsDateTime(); break; case RedmineKeys.VERSION: Version = reader.ReadAsInt(); break; + case RedmineKeys.PARENT: ParentTitle = reader.ReadAsString(); break; default: reader.Read(); break; } } From 676c78c67eebc55adf555702f539054b88307138 Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 1 Mar 2023 23:38:17 +0200 Subject: [PATCH 244/549] Updated CHANGELOG.md --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 501cd134..a423cf3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [v4.4.0] + +Added: +* Added ParentTitle to wiki page + +Breaking Changes: + +* Changed ChangeSet revision type from int to string + ## [v4.3.0] Added: From a5fa39f5181279a0649f9645b3fcf94537f7abbd Mon Sep 17 00:00:00 2001 From: zapadi Date: Thu, 2 Mar 2023 00:38:46 +0200 Subject: [PATCH 245/549] Upgrade Newtonsoft.Json from 13.0.1 to 13.0.2 --- src/redmine-net-api/redmine-net-api.csproj | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index c97520fd..811bb8c3 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -86,15 +86,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - + From d58cc8b19886682d48552241cc927eae05e0f541 Mon Sep 17 00:00:00 2001 From: zapadi Date: Thu, 2 Mar 2023 00:40:51 +0200 Subject: [PATCH 246/549] Set actions version to 3 --- .github/workflows/ci-cd.yml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index bba599c8..aa8eea53 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -54,17 +54,17 @@ jobs: strategy: matrix: os: [ ubuntu-latest]#, windows-latest, macos-latest ] - dotnet: [ '3.1.x']#, '5.x.x', '6.x.x' ] + dotnet: [ '7.x.x' ] steps: - name: Checkout - uses: actions/checkout@v2.4.0 + uses: actions/checkout@v3 with: lfs: true fetch-depth: 0 - name: Setup .NET Core - uses: actions/setup-dotnet@v1.9.0 + uses: actions/setup-dotnet@v3 with: dotnet-version: ${{ matrix.dotnet }} @@ -77,7 +77,7 @@ jobs: - name: Build run: | - dotnet build --no-restore --configuration $BUILD_CONFIGURATION -p:Version=${{env.VERSION}} + dotnet build --no-restore --configuration $BUILD_CONFIGURATION -p:Version=$VERSION - name: Build Signed if: runner.os == 'Linux' @@ -86,17 +86,17 @@ jobs: - name: Test run: dotnet test --no-restore --no-build --configuration $BUILD_CONFIGURATION - - name: Pack - if: runner.os == 'Linux' && startsWith(github.ref, 'refs/tags') + - name: Pack && startsWith(github.ref, 'refs/tags') + if: runner.os == 'Linux' run: | - dotnet pack --output ./artifacts --configuration $BUILD_CONFIGURATION -p:Version=$VERSION --include-symbols --include-source + dotnet pack --output ./artifacts --configuration $BUILD_CONFIGURATION -p:Version=$VERSION --include-symbols --include-source -p:SymbolPackageFormat=snupkg - name: Pack Signed && startsWith(github.ref, 'refs/tags') if: runner.os == 'Linux' run: | - dotnet pack --output ./artifacts --configuration $BUILD_CONFIGURATION -p:Version=$VERSION --include-symbols --include-source -p:Sign=true + dotnet pack --output ./artifacts --configuration $BUILD_CONFIGURATION -p:Version=$VERSION --include-symbols --include-source -p:Sign=true -p:SymbolPackageFormat=snupkg - - uses: actions/upload-artifact@v1 + - uses: actions/upload-artifact@v3 if: runner.os == 'Linux' with: name: artifacts @@ -108,10 +108,10 @@ jobs: needs: build name: Deploy Packages steps: - - uses: actions/download-artifact@v1 + - uses: actions/download-artifact@v3 with: name: artifacts path: ./artifacts - name: Publish packages - run: dotnet nuget push ./artifacts/**.nupkg --source nuget.org --api-key ${{secrets.NUGET_TOKEN}} \ No newline at end of file + run: dotnet nuget push ./artifacts/**.nupkg --source nuget.org -k ${{secrets.NUGET_TOKEN}} \ No newline at end of file From 84d207aac4a1178bf52d1cf61ef01ca67f2d9615 Mon Sep 17 00:00:00 2001 From: zapadi Date: Thu, 2 Mar 2023 00:53:11 +0200 Subject: [PATCH 247/549] Added --tags --- .github/workflows/ci-cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index aa8eea53..8352668f 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -70,7 +70,7 @@ jobs: - name: Get Version run: | - echo "VERSION=$(git describe --abbrev=0)" >> $GITHUB_ENV + echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV - name: Restore run: dotnet restore From 46e2d739690cfee4ee22462a647abdbc1c358588 Mon Sep 17 00:00:00 2001 From: zapadi Date: Thu, 2 Mar 2023 01:07:28 +0200 Subject: [PATCH 248/549] Changed pack output with --property:PackageOutputPath --- .github/workflows/ci-cd.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 8352668f..abc4ef9a 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -89,12 +89,12 @@ jobs: - name: Pack && startsWith(github.ref, 'refs/tags') if: runner.os == 'Linux' run: | - dotnet pack --output ./artifacts --configuration $BUILD_CONFIGURATION -p:Version=$VERSION --include-symbols --include-source -p:SymbolPackageFormat=snupkg + dotnet pack --property:PackageOutputPath ./artifacts --configuration $BUILD_CONFIGURATION -p:Version=$VERSION --include-symbols --include-source -p:SymbolPackageFormat=snupkg - name: Pack Signed && startsWith(github.ref, 'refs/tags') if: runner.os == 'Linux' run: | - dotnet pack --output ./artifacts --configuration $BUILD_CONFIGURATION -p:Version=$VERSION --include-symbols --include-source -p:Sign=true -p:SymbolPackageFormat=snupkg + dotnet pack --property:PackageOutputPath ./artifacts --configuration $BUILD_CONFIGURATION -p:Version=$VERSION --include-symbols --include-source -p:Sign=true -p:SymbolPackageFormat=snupkg - uses: actions/upload-artifact@v3 if: runner.os == 'Linux' From 1879269599dc6717edf228de910341479d11deae Mon Sep 17 00:00:00 2001 From: zapadi Date: Thu, 2 Mar 2023 01:14:03 +0200 Subject: [PATCH 249/549] Revert --- .github/workflows/ci-cd.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index abc4ef9a..78a99fed 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -89,12 +89,12 @@ jobs: - name: Pack && startsWith(github.ref, 'refs/tags') if: runner.os == 'Linux' run: | - dotnet pack --property:PackageOutputPath ./artifacts --configuration $BUILD_CONFIGURATION -p:Version=$VERSION --include-symbols --include-source -p:SymbolPackageFormat=snupkg + dotnet pack ./src/redmine-net-api/redmine-net-api.csproj -o ./artifacts --configuration $BUILD_CONFIGURATION -p:Version=$VERSION --include-symbols --include-source -p:SymbolPackageFormat=snupkg - name: Pack Signed && startsWith(github.ref, 'refs/tags') if: runner.os == 'Linux' run: | - dotnet pack --property:PackageOutputPath ./artifacts --configuration $BUILD_CONFIGURATION -p:Version=$VERSION --include-symbols --include-source -p:Sign=true -p:SymbolPackageFormat=snupkg + dotnet pack ./src/redmine-net-api/redmine-net-api.csproj -o ./artifacts --configuration $BUILD_CONFIGURATION -p:Version=$VERSION --include-symbols --include-source -p:Sign=true -p:SymbolPackageFormat=snupkg - uses: actions/upload-artifact@v3 if: runner.os == 'Linux' From 4e83b1c53540dd4414254da62f0704c634a79390 Mon Sep 17 00:00:00 2001 From: zapadi Date: Thu, 2 Mar 2023 01:22:09 +0200 Subject: [PATCH 250/549] Fixed nuget api key env name --- .github/workflows/ci-cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 78a99fed..029bbc5f 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -114,4 +114,4 @@ jobs: path: ./artifacts - name: Publish packages - run: dotnet nuget push ./artifacts/**.nupkg --source nuget.org -k ${{secrets.NUGET_TOKEN}} \ No newline at end of file + run: dotnet nuget push ./artifacts/**.nupkg --source nuget.org -k ${{secrets.NUGET_API_KEY}} \ No newline at end of file From 5ea0f6294fdd2640b30f4672e1cb429d8fc6e1e6 Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 7 Mar 2023 12:48:43 +0100 Subject: [PATCH 251/549] Add & retrieve project news --- README.md | 53 ++++++++------- .../Extensions/RedmineManagerExtensions.cs | 64 +++++++++++++++++++ src/redmine-net-api/Types/News.cs | 42 ++++++++++++ 3 files changed, 132 insertions(+), 27 deletions(-) create mode 100644 src/redmine-net-api/Extensions/RedmineManagerExtensions.cs diff --git a/README.md b/README.md index 9398bf40..06420b3d 100755 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -![Nuget](https://img.shields.io/nuget/dt/redmine-net-api) -![Redmine .NET Api](https://github.com/zapadi/redmine-net-api/workflows/Redmine%20.NET%20Api/badge.svg?branch=master) +![Redmine .NET Api](https://github.com/zapadi/redmine-net-api/workflows/CI%2FCD/badge.svg?branch=master) ![Appveyor last build status](https://ci.appveyor.com/api/projects/status/github/zapadi/redmine-net-api?branch=master&svg=true&passingText=master%20-%20OK&failingText=ups...) -[![NuGet package](https://img.shields.io/nuget/v/redmine-api.svg)](https://www.nuget.org/packages/redmine-api) +[![NuGet package](https://img.shields.io/nuget/v/redmine-api.svg)](https://www.nuget.org/packages/redmine-api) +![Nuget](https://img.shields.io/nuget/dt/redmine-net-api) Buy Me A Coffee @@ -15,30 +15,29 @@ redmine-net-api is a library for communicating with a Redmine project management * Supports GZipped responses from servers. * This API provides access and basic CRUD operations (create, read, update, delete) for the resources described below: -|Resource | Read | Create | Update | Delete | -|:---------|:------:|:----------:|:---------:|:-------:| - Attachments | ✓ | ✓ | ✗ | ✗| - Custom Fields | ✓ | ✗ | ✗ | ✗ - Enumerations | ✓ | ✗ | ✗ | ✗ - Files |✓|✓|✗|✗ - Groups |✓|✓|✓|✓ - Issues |✓|✓|✓|✓ - Issue Categories |✓|✓|✓|✓ - Issue Relations |✓|✓|✓|✓ - Issue Statuses |✓|✗|✗|✗ - My account |✓|✗|✓|✗ - News |✓|✗|✗|✗ - Projects |✓|✓|✓|✓ - Project Memberships|✓|✓|✓|✓ - Queries |✓|✗|✗|✗ - Roles |✓|✗|✗|✗ - Search | - Time Entries |✓|✓|✓|✓ - Trackers |✓|✗|✗|✗ - Users |✓|✓|✓|✓ - Versions |✓|✓|✓|✓ - Wiki Pages |✓|✓|✓|✓ - +| Resource | Read | Create | Update | Delete | +|:--------------------|:-------:|:-------:|:-------:|:-------:| +| Attachments | ✓ | ✓ | ✗ | ✗ | +| Custom Fields | ✓ | ✗ | ✗ | ✗ | +| Enumerations | ✓ | ✗ | ✗ | ✗ | +| Files | ✓ | ✓ | ✗ | ✗ | +| Groups | ✓ | ✓ | ✓ | ✓ | +| Issues | ✓ | ✓ | ✓ | ✓ | +| Issue Categories | ✓ | ✓ | ✓ | ✓ | +| Issue Relations | ✓ | ✓ | ✓ | ✓ | +| Issue Statuses | ✓ | ✗ | ✗ | ✗ | +| My account | ✓ | ✗ | ✓ | ✗ | +| News | ✓ | ✓ | ✓ | ✓ | +| Projects | ✓ | ✓ | ✓ | ✓ | +| Project Memberships | ✓ | ✓ | ✓ | ✓ | +| Queries | ✓ | ✗ | ✗ | ✗ | +| Roles | ✓ | ✗ | ✗ | ✗ | +| Search | ✓ | | | | +| Time Entries | ✓ | ✓ | ✓ | ✓ | +| Trackers | ✓ | ✗ | ✗ | ✗ | +| Users | ✓ | ✓ | ✓ | ✓ | +| Versions | ✓ | ✓ | ✓ | ✓ | +| Wiki Pages | ✓ | ✓ | ✓ | ✓ | ## WIKI diff --git a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs new file mode 100644 index 00000000..e62f61a0 --- /dev/null +++ b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Specialized; +using System.Globalization; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Types; +using Version = Redmine.Net.Api.Types.Version; + +namespace Redmine.Net.Api.Extensions +{ + /// + /// + /// + public static class RedmineManagerExtensions + { + /// + /// + /// + /// + /// + /// + /// + public static PagedResults GetProjectNews(this RedmineManager redmineManager, string projectIdentifier, NameValueCollection nameValueCollection) + { + if (projectIdentifier.IsNullOrWhiteSpace()) + { + throw new RedmineException("Argument 'projectIdentifier' is null"); + } + + return WebApiHelper.ExecuteDownloadList(redmineManager, Uri.EscapeUriString($"{redmineManager.Host}/project/{projectIdentifier}/news.{redmineManager.Format}"), nameValueCollection); + } + + /// + /// + /// + /// + /// + /// + /// + /// + public static News AddProjectNews(this RedmineManager redmineManager, string projectIdentifier, News news) + { + if (projectIdentifier.IsNullOrWhiteSpace()) + { + throw new RedmineException("Argument 'projectIdentifier' is null"); + } + + if (news == null) + { + throw new RedmineException("Argument news is null"); + } + + if (news.Title.IsNullOrWhiteSpace()) + { + throw new RedmineException("Title cannot be blank"); + } + + var data = redmineManager.Serializer.Serialize(news); + + return WebApiHelper.ExecuteUpload(redmineManager, Uri.EscapeUriString($"{redmineManager.Host}/project/{projectIdentifier}/news.{redmineManager.Format}"), HttpVerbs.POST, data); + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/News.cs b/src/redmine-net-api/Types/News.cs index 8e037189..e5fbb3b4 100644 --- a/src/redmine-net-api/Types/News.cs +++ b/src/redmine-net-api/Types/News.cs @@ -23,6 +23,7 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api.Types { @@ -79,6 +80,12 @@ public sealed class News : Identifiable /// /// public List Comments { get; internal set; } + + /// + /// + /// + public List Uploads { get; set; } + #endregion #region Implementation of IXmlSerialization @@ -114,6 +121,22 @@ public override void ReadXml(XmlReader reader) } } } + + /// + /// + /// + /// + public override void WriteXml(XmlWriter writer) + { + writer.WriteElementString(RedmineKeys.TITLE, Title); + writer.WriteElementString(RedmineKeys.SUMMARY, Summary); + writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); + if (Uploads != null) + { + writer.WriteArray(RedmineKeys.UPLOADS, Uploads); + } + } + #endregion #region Implementation of IJsonSerialization @@ -152,6 +175,25 @@ public override void ReadJson(JsonReader reader) } } } + + /// + /// + /// + /// + public override void WriteJson(JsonWriter writer) + { + using (new JsonObject(writer, RedmineKeys.NEWS)) + { + writer.WriteProperty(RedmineKeys.TITLE, Title); + writer.WriteProperty(RedmineKeys.SUMMARY, Summary); + writer.WriteProperty(RedmineKeys.DESCRIPTION, Description); + if (Uploads != null) + { + writer.WriteArray(RedmineKeys.UPLOADS, Uploads); + } + } + } + #endregion #region Implementation of IEquatable From a4df0e6aa478c5a70b45b70489981f618c01fb42 Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 28 Mar 2023 08:56:38 +0200 Subject: [PATCH 252/549] Added GetProjectMemberships extension --- .../Extensions/RedmineManagerExtensions.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs index e62f61a0..5a1cbfce 100644 --- a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs +++ b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs @@ -60,5 +60,15 @@ public static News AddProjectNews(this RedmineManager redmineManager, string pro return WebApiHelper.ExecuteUpload(redmineManager, Uri.EscapeUriString($"{redmineManager.Host}/project/{projectIdentifier}/news.{redmineManager.Format}"), HttpVerbs.POST, data); } + + public static PagedResults GetProjectMemberships(this RedmineManager redmineManager, string projectIdentifier, NameValueCollection nameValueCollection) + { + if (projectIdentifier.IsNullOrWhiteSpace()) + { + throw new RedmineException($"Argument '{nameof(projectIdentifier)}' is null"); + } + + return WebApiHelper.ExecuteDownloadList(redmineManager, Uri.EscapeUriString($"{redmineManager.Host}/project/{projectIdentifier}/memberships.{redmineManager.Format}"), nameValueCollection); + } } } \ No newline at end of file From f129d109a0396b09f775b24b0cae32ecc359fcb7 Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 28 Mar 2023 08:58:11 +0200 Subject: [PATCH 253/549] Enabled schema set --- src/redmine-net-api/RedmineManager.cs | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index f89d8989..50bd7f60 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -105,7 +105,7 @@ public class RedmineManager : IRedmineManager public RedmineManager(string host, MimeFormat mimeFormat = MimeFormat.Xml, bool verifyServerCert = true, IWebProxy proxy = null, SecurityProtocolType securityProtocolType = default, string scheme = "https", TimeSpan? timeout = null) { - if (string.IsNullOrEmpty(host)) + if (host.IsNullOrWhiteSpace()) { throw new RedmineException("Host is not defined!"); } @@ -163,11 +163,12 @@ public RedmineManager(string host, MimeFormat mimeFormat = MimeFormat.Xml, bool /// if set to true [verify server cert]. /// The proxy. /// Use this parameter to specify a SecurityProtcolType. Note: it is recommended to leave this parameter at its default value as this setting also affects the calling application process. + /// /// The webclient timeout. Default is 100 seconds. public RedmineManager(string host, string apiKey, MimeFormat mimeFormat = MimeFormat.Xml, - bool verifyServerCert = true, IWebProxy proxy = null, - SecurityProtocolType securityProtocolType = default, TimeSpan? timeout = null) - : this(host, mimeFormat, verifyServerCert, proxy, securityProtocolType, timeout: timeout) + bool verifyServerCert = true, IWebProxy proxy = null, + SecurityProtocolType securityProtocolType = default, string scheme = "https", TimeSpan? timeout = null) + : this(host, mimeFormat, verifyServerCert, proxy, securityProtocolType, scheme, timeout: timeout) { ApiKey = apiKey; } @@ -193,11 +194,13 @@ public RedmineManager(string host, string apiKey, MimeFormat mimeFormat = MimeFo /// if set to true [verify server cert]. /// The proxy. /// Use this parameter to specify a SecurityProtcolType. Note: it is recommended to leave this parameter at its default value as this setting also affects the calling application process. + /// /// The webclient timeout. Default is 100 seconds. public RedmineManager(string host, string login, string password, MimeFormat mimeFormat = MimeFormat.Xml, - bool verifyServerCert = true, IWebProxy proxy = null, - SecurityProtocolType securityProtocolType = default, TimeSpan? timeout = null) - : this(host, mimeFormat, verifyServerCert, proxy, securityProtocolType, timeout: timeout) + bool verifyServerCert = true, IWebProxy proxy = null, + SecurityProtocolType securityProtocolType = default, string scheme = "https", TimeSpan? timeout = null) + : this(host, mimeFormat, verifyServerCert, proxy, securityProtocolType, scheme, timeout: timeout) + { cache = new CredentialCache { { new Uri(host), "Basic", new NetworkCredential(login, password) } }; @@ -244,6 +247,11 @@ private set if (Uri.TryCreate(host, UriKind.Absolute, out Uri uriResult) && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps)) { + if (Scheme.IsNullOrWhiteSpace()) + { + Scheme = uriResult.Scheme; + } + return; } From f057ceb8800d185c2ac18e7f877e1667feae0a0f Mon Sep 17 00:00:00 2001 From: Elvaron Date: Tue, 27 Jun 2023 12:09:50 +0200 Subject: [PATCH 254/549] UpdateObject not changing Custom Fields in Version objects #301 (#323) --- src/redmine-net-api/Types/Version.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/redmine-net-api/Types/Version.cs b/src/redmine-net-api/Types/Version.cs index b7c8b8d9..e094ee27 100644 --- a/src/redmine-net-api/Types/Version.cs +++ b/src/redmine-net-api/Types/Version.cs @@ -145,6 +145,10 @@ public override void WriteXml(XmlWriter writer) writer.WriteElementString(RedmineKeys.SHARING, Sharing.ToString().ToLowerInv()); writer.WriteDateOrEmpty(RedmineKeys.DUE_DATE, DueDate); writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); + if (CustomFields != null) + { + writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); + } } #endregion @@ -272,4 +276,4 @@ public override int GetHashCode() CustomFields={CustomFields.Dump()}]"; } -} \ No newline at end of file +} From 49a9eb69afb097d436f26be651677f91e8f0cb66 Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 27 Aug 2023 18:53:21 +0000 Subject: [PATCH 255/549] #324 fix (#327) --- src/redmine-net-api/Types/User.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/redmine-net-api/Types/User.cs b/src/redmine-net-api/Types/User.cs index 7fcfac3f..7a93bfcf 100644 --- a/src/redmine-net-api/Types/User.cs +++ b/src/redmine-net-api/Types/User.cs @@ -187,7 +187,7 @@ public override void ReadXml(XmlReader reader) case RedmineKeys.MUST_CHANGE_PASSWORD: MustChangePassword = reader.ReadElementContentAsBoolean(); break; case RedmineKeys.PASSWORD_CHANGED_ON: PasswordChangedOn = reader.ReadElementContentAsNullableDateTime(); break; case RedmineKeys.STATUS: Status = (UserStatus)reader.ReadElementContentAsInt(); break; - case RedmineKeys.TWO_FA_SCHEME: TwoFactorAuthenticationScheme = reader.ReadContentAsString(); break; + case RedmineKeys.TWO_FA_SCHEME: TwoFactorAuthenticationScheme = reader.ReadElementContentAsString(); break; case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadElementContentAsNullableDateTime(); break; default: reader.Read(); break; } From 85fec9b7a0e2f5de9d7b309e720e1efc84c0684f Mon Sep 17 00:00:00 2001 From: Padi Date: Tue, 29 Aug 2023 08:25:01 +0000 Subject: [PATCH 256/549] #328 Fix - Getting role ends with exception (#329) --- .../Serialization/XmlRedmineSerializer.cs | 18 ++++++++++-------- src/redmine-net-api/Types/Permission.cs | 14 ++------------ 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/src/redmine-net-api/Serialization/XmlRedmineSerializer.cs b/src/redmine-net-api/Serialization/XmlRedmineSerializer.cs index 15d585d0..be1764a0 100644 --- a/src/redmine-net-api/Serialization/XmlRedmineSerializer.cs +++ b/src/redmine-net-api/Serialization/XmlRedmineSerializer.cs @@ -50,7 +50,7 @@ public XmlRedmineSerializer(XmlWriterSettings xmlWriterSettings) } catch (Exception ex) { - throw new RedmineException(ex.Message, ex); + throw new RedmineException(ex.GetBaseException().Message, ex); } } @@ -62,7 +62,7 @@ public XmlRedmineSerializer(XmlWriterSettings xmlWriterSettings) } catch (Exception ex) { - throw new RedmineException(ex.Message, ex); + throw new RedmineException(ex.GetBaseException().Message, ex); } } @@ -76,7 +76,7 @@ public XmlRedmineSerializer(XmlWriterSettings xmlWriterSettings) } catch (Exception ex) { - throw new RedmineException(ex.Message, ex); + throw new RedmineException(ex.GetBaseException().Message, ex); } } #pragma warning restore CA1822 @@ -91,7 +91,7 @@ public string Serialize(T entity) where T : class } catch (Exception ex) { - throw new RedmineException(ex.Message, ex); + throw new RedmineException(ex.GetBaseException().Message, ex); } } @@ -109,12 +109,14 @@ public string Serialize(T entity) where T : class throw new ArgumentNullException(nameof(xmlResponse), $"Could not deserialize null or empty input for type '{typeof(T).Name}'."); } - using (TextReader stringReader = new StringReader(xmlResponse)) + using (var stringReader = new StringReader(xmlResponse)) { - using (var xmlReader = XmlReader.Create(stringReader)) + using (var xmlReader = XmlTextReaderBuilder.Create(stringReader)) { - xmlReader.Read(); - xmlReader.Read(); + while (xmlReader.NodeType == XmlNodeType.None || xmlReader.NodeType == XmlNodeType.XmlDeclaration) + { + xmlReader.Read(); + } var totalItems = xmlReader.ReadAttributeAsInt(RedmineKeys.TOTAL_COUNT); diff --git a/src/redmine-net-api/Types/Permission.cs b/src/redmine-net-api/Types/Permission.cs index f912a2e9..13fb4e9d 100644 --- a/src/redmine-net-api/Types/Permission.cs +++ b/src/redmine-net-api/Types/Permission.cs @@ -54,19 +54,9 @@ public sealed class Permission : IXmlSerializable, IJsonSerializable, IEquatable public void ReadXml(XmlReader reader) { reader.Read(); - while (!reader.EOF) + if (reader.NodeType == XmlNodeType.Text) { - if (reader.IsEmptyElement && !reader.HasAttributes) - { - reader.Read(); - continue; - } - - switch (reader.Name) - { - case RedmineKeys.PERMISSION: Info = reader.ReadElementContentAsString(); break; - default: reader.Read(); break; - } + Info = reader.Value; } } From b4319206d8cfe991dfb292d000c8cc3d2ef97cbe Mon Sep 17 00:00:00 2001 From: Padi Date: Tue, 3 Oct 2023 08:22:09 +0000 Subject: [PATCH 257/549] [Journal] Add support for notes update (#335) --- src/redmine-net-api/RedmineManager.cs | 3 ++- src/redmine-net-api/Types/Journal.cs | 18 +++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index 50bd7f60..df24603c 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -64,7 +64,8 @@ public class RedmineManager : IRedmineManager {typeof(Watcher), "watchers"}, {typeof(IssueCustomField), "custom_fields"}, {typeof(CustomField), "custom_fields"}, - {typeof(Search), "search"} + {typeof(Search), "search"}, + {typeof(Journal), "journals"} }; /// diff --git a/src/redmine-net-api/Types/Journal.cs b/src/redmine-net-api/Types/Journal.cs index 7cee01df..9558fa8a 100644 --- a/src/redmine-net-api/Types/Journal.cs +++ b/src/redmine-net-api/Types/Journal.cs @@ -48,7 +48,9 @@ public sealed class Journal : Identifiable /// /// The notes. /// - public string Notes { get; internal set; } + /// Setting Notes to string.empty or null will destroy the journal + /// + public string Notes { get; set; } /// /// Gets or sets the created on. @@ -101,6 +103,13 @@ public override void ReadXml(XmlReader reader) } } } + + /// + public override void WriteXml(XmlWriter writer) + { + writer.WriteElementString(RedmineKeys.NOTES, Notes); + } + #endregion #region Implementation of IJsonSerialization @@ -136,6 +145,13 @@ public override void ReadJson(JsonReader reader) } } } + + /// + public override void WriteJson(JsonWriter writer) + { + writer.WriteProperty(RedmineKeys.NOTES, Notes); + } + #endregion #region Implementation of IEquatable From 30d9ca2f93ba7cc8623c7670dcb66d323864a560 Mon Sep 17 00:00:00 2001 From: Padi Date: Tue, 3 Oct 2023 10:16:50 +0000 Subject: [PATCH 258/549] [New][RedmineManagerExtension] Add GetProjectFiles (#336) --- .../Extensions/RedmineManagerExtensions.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs index 5a1cbfce..987bc928 100644 --- a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs +++ b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs @@ -61,6 +61,14 @@ public static News AddProjectNews(this RedmineManager redmineManager, string pro return WebApiHelper.ExecuteUpload(redmineManager, Uri.EscapeUriString($"{redmineManager.Host}/project/{projectIdentifier}/news.{redmineManager.Format}"), HttpVerbs.POST, data); } + /// + /// + /// + /// + /// + /// + /// + /// public static PagedResults GetProjectMemberships(this RedmineManager redmineManager, string projectIdentifier, NameValueCollection nameValueCollection) { if (projectIdentifier.IsNullOrWhiteSpace()) @@ -70,5 +78,23 @@ public static PagedResults GetProjectMemberships(this Redmine return WebApiHelper.ExecuteDownloadList(redmineManager, Uri.EscapeUriString($"{redmineManager.Host}/project/{projectIdentifier}/memberships.{redmineManager.Format}"), nameValueCollection); } + + /// + /// + /// + /// + /// + /// + /// + /// + public static PagedResults GetProjectFiles(this RedmineManager redmineManager, string projectIdentifier, NameValueCollection nameValueCollection) + { + if (projectIdentifier.IsNullOrWhiteSpace()) + { + throw new RedmineException($"Argument '{nameof(projectIdentifier)}' is null"); + } + + return WebApiHelper.ExecuteDownloadList(redmineManager, Uri.EscapeUriString($"{redmineManager.Host}/project/{projectIdentifier}/files.{redmineManager.Format}"), nameValueCollection); + } } } \ No newline at end of file From 3876d0210598a7060d9ca23874bc589a1c4b3cc2 Mon Sep 17 00:00:00 2001 From: Padi Date: Fri, 24 Nov 2023 12:35:28 +0000 Subject: [PATCH 259/549] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 06420b3d..28ec1021 100755 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![Redmine .NET Api](https://github.com/zapadi/redmine-net-api/workflows/CI%2FCD/badge.svg?branch=master) ![Appveyor last build status](https://ci.appveyor.com/api/projects/status/github/zapadi/redmine-net-api?branch=master&svg=true&passingText=master%20-%20OK&failingText=ups...) [![NuGet package](https://img.shields.io/nuget/v/redmine-api.svg)](https://www.nuget.org/packages/redmine-api) -![Nuget](https://img.shields.io/nuget/dt/redmine-net-api) +![Nuget](https://img.shields.io/nuget/dt/redmine-net) Buy Me A Coffee From a1267eef1e5eff4486a70e5b086b295159608b63 Mon Sep 17 00:00:00 2001 From: Padi Date: Fri, 24 Nov 2023 12:36:31 +0000 Subject: [PATCH 260/549] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 28ec1021..5d79c737 100755 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![Redmine .NET Api](https://github.com/zapadi/redmine-net-api/workflows/CI%2FCD/badge.svg?branch=master) ![Appveyor last build status](https://ci.appveyor.com/api/projects/status/github/zapadi/redmine-net-api?branch=master&svg=true&passingText=master%20-%20OK&failingText=ups...) [![NuGet package](https://img.shields.io/nuget/v/redmine-api.svg)](https://www.nuget.org/packages/redmine-api) -![Nuget](https://img.shields.io/nuget/dt/redmine-net) +![Nuget](https://img.shields.io/nuget/dt/redmine-api) Buy Me A Coffee From 8c961a403703151c42b4c5592d8240160e4e2367 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 26 Nov 2023 02:16:29 +0200 Subject: [PATCH 261/549] [Csproj] Clean up --- Directory.Build.props | 10 +- src/redmine-net-api/redmine-net-api.csproj | 142 +-------- tests/redmine-net-api.Tests/TestHelper.cs | 1 - .../redmine-net-api.Tests.csproj | 296 ++++-------------- 4 files changed, 81 insertions(+), 368 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 88b08f71..60f98518 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -26,12 +26,16 @@ - - + true + embedded false $(SolutionDir)/artifacts - + + + + + diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 811bb8c3..0684adee 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -2,10 +2,10 @@ - net20;net40;net45;net451;net452;net46;net461;net462;net47;net471;net472;net48;netstandard2.0;netstandard2.1 - false Redmine.Net.Api redmine-net-api + net20;net40;net45;net451;net452;net46;net461;net462;net47;net471;net472;net48;net481;net60;net70; + false False true TRACE @@ -22,137 +22,33 @@ CA1724; CA1806; CA2227; + CS0612; + CS0618; + CA1002; - - - NET20;NETFULL - - - - NET40;NETFULL - - - - NET45;NETFULL - - - - NET451;NETFULL - - - - NET452;NETFULL - - - - NET46;NETFULL - - - - NET461;NETFULL - - - - NET462;NETFULL - - - - NET47;NETFULL - - - - NET471;NETFULL - - - - NET472;NETFULL - - - - NET48;NETFULL - - - - NETSTANDARD13;NETSTANDARD - - - - NETSTANDARD20;NETSTANDARD - - - - NETSTANDARD21;NETSTANDARD + + + true + true + AllEnabledByDefault + latest - + - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -167,11 +63,7 @@ - - <_Parameter1 Condition="'$(Sign)' == '' OR '$(Sign)' == 'false'">$(MSBuildProjectName).Tests - <_Parameter1 Condition="'$(Sign)' == 'true'">$(MSBuildProjectName).Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100678670c10a958cde6b74892d5207885bd2ab375255b85fd7794d60ff01ba1cf81aaff13f54d8a08a8f8c7816ef4fc0138de7941031e47b5b0c5d51f58cbfe6c5652e11cfa0865e2d0a860f47f73b701e6758e3e381665f7664f938462c9eb9bdc17312621e984981227fd9d38dbec5288e269d42836b9c8fc4c8ebd0282ca4d3 - - + \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/TestHelper.cs b/tests/redmine-net-api.Tests/TestHelper.cs index c49785c6..785a5607 100644 --- a/tests/redmine-net-api.Tests/TestHelper.cs +++ b/tests/redmine-net-api.Tests/TestHelper.cs @@ -15,7 +15,6 @@ public static IConfigurationRoot GetIConfigurationRoot(string outputPath) .AddJsonFile("appsettings.json", optional: true) .AddJsonFile($"appsettings.{environment}.json", optional: true) .AddUserSecrets("f8b9e946-b547-42f1-861c-f719dca00a84") - .AddEnvironmentVariables() .Build(); } diff --git a/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj index 5ee231fe..3de7e771 100644 --- a/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj +++ b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj @@ -1,79 +1,21 @@ + + Padi.DotNet.RedmineAPI.Tests + $(AssemblyName) + false + net481 + net451;net452;net46;net461;net462;net47;net471;net472;net48;net481; + false + f8b9e946-b547-42f1-861c-f719dca00a84 + Release;Debug;DebugJson + - - false - net48 - net451;net452;net46;net461;net462;net47;net471;net472;net48; - false - Padi.RedmineApi.Tests - - f8b9e946-b547-42f1-861c-f719dca00a84 - Release;Debug;DebugJson - - redmine-api-test - redmine-api-test-signed - - - - true - ..\..\redmine-net-api.snk - - - - NET20;NETFULL - - - - NET40;NETFULL - - - - DEBUG;NET45;NETFULL; - - - - DEBUG;NET451;NETFULL;DEBUG_JSON - - - - NET452;NETFULL - - - - NET46;NETFULL - - - - NET461;NETFULL - - - - NET462;NETFULL - - - - - NET47;NETFULL - - - - NET471;NETFULL - - - - NET472;NETFULL - - - - NET48;NETFULL - - - - false - - + + |net45|net451|net452|net46|net461| + + AnyCPU true @@ -84,7 +26,7 @@ prompt 4 - + AnyCPU true @@ -95,6 +37,7 @@ prompt 4 + AnyCPU pdbonly @@ -105,170 +48,45 @@ 4 - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - - - redmine-net-api.snk - - - - - - PreserveNewest - - - - - - 1.1.0 - - - 1.1.0 - - - 1.1.0 - - - - - - 1.1.0 - - - 1.1.0 - - - 1.1.0 - - - - - - 1.1.0 - - - 1.1.0 - - - 1.1.0 - - - - - - 1.1.0 - - - 1.1.0 - - - 1.1.0 - - - - - - 1.1.0 - - - 1.1.0 - - - 1.1.0 - - - - - - 1.1.0 - - - 1.1.0 - - - 1.1.0 - - - - - - 1.1.0 - - - 1.1.0 - - - 1.1.0 - - - - - - 1.1.0 - - - 1.1.0 - - - 1.1.0 - - - - - - 1.1.0 - - - 1.1.0 - - - 1.1.0 - - - - + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + PreserveNewest + + + \ No newline at end of file From 8f0f839a1499c84da24d12c498628b7472b1f3c7 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 26 Nov 2023 18:56:44 +0200 Subject: [PATCH 262/549] [LangVersion] Set to 11 --- Directory.Build.props | 2 ++ src/redmine-net-api/redmine-net-api.csproj | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 60f98518..d0f44311 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -26,6 +26,8 @@ + 11 + strict true embedded false diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 0684adee..d6eff592 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -11,7 +11,6 @@ TRACE Debug;Release;DebugJson PackageReference - 7.3 NU5105; CA1303; From b5a7e818449b52723f3423c5dd03eb753433809b Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 26 Nov 2023 18:56:13 +0200 Subject: [PATCH 263/549] [Replace] DataHelper with SerializationHelper --- .../Async/RedmineManagerAsync45.cs | 4 +- src/redmine-net-api/Internals/DataHelper.cs | 37 ------------------- src/redmine-net-api/RedmineManager.cs | 4 +- .../Serialization/SerializationHelper.cs | 23 ++++++++++++ 4 files changed, 27 insertions(+), 41 deletions(-) delete mode 100755 src/redmine-net-api/Internals/DataHelper.cs create mode 100644 src/redmine-net-api/Serialization/SerializationHelper.cs diff --git a/src/redmine-net-api/Async/RedmineManagerAsync45.cs b/src/redmine-net-api/Async/RedmineManagerAsync45.cs index 1cdc2725..2020dee6 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync45.cs +++ b/src/redmine-net-api/Async/RedmineManagerAsync45.cs @@ -172,7 +172,7 @@ public static async Task> GetAllWikiPagesAsync(this RedmineManage /// public static async Task AddUserToGroupAsync(this RedmineManager redmineManager, int groupId, int userId) { - var data = DataHelper.UserData(userId, redmineManager.MimeFormat); + var data = SerializationHelper.SerializeUserId(userId, redmineManager.MimeFormat); var uri = UrlHelper.GetAddUserToGroupUrl(redmineManager, groupId); await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.POST, data).ConfigureAwait(false); @@ -200,7 +200,7 @@ public static async Task RemoveUserFromGroupAsync(this RedmineManager redmineMan /// public static async Task AddWatcherToIssueAsync(this RedmineManager redmineManager, int issueId, int userId) { - var data = DataHelper.UserData(userId, redmineManager.MimeFormat); + var data = SerializationHelper.SerializeUserId(userId, redmineManager.MimeFormat); var uri = UrlHelper.GetAddWatcherUrl(redmineManager, issueId); await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.POST, data).ConfigureAwait(false); diff --git a/src/redmine-net-api/Internals/DataHelper.cs b/src/redmine-net-api/Internals/DataHelper.cs deleted file mode 100755 index 7686e2fa..00000000 --- a/src/redmine-net-api/Internals/DataHelper.cs +++ /dev/null @@ -1,37 +0,0 @@ -/* - Copyright 2011 - 2023 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -namespace Redmine.Net.Api.Internals -{ - /// - /// - /// - internal static class DataHelper - { - /// - /// Users the data. - /// - /// The user identifier. - /// The MIME format. - /// - public static string UserData(int userId, MimeFormat mimeFormat) - { - return mimeFormat == MimeFormat.Xml - ? $"{userId}" - : $"{{\"user_id\":\"{userId}\"}}"; - } - } -} \ No newline at end of file diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index df24603c..4b732745 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -351,7 +351,7 @@ public MyAccount GetMyAccount() public void AddWatcherToIssue(int issueId, int userId) { var url = UrlHelper.GetAddWatcherUrl(this, issueId); - WebApiHelper.ExecuteUpload(this, url, HttpVerbs.POST, DataHelper.UserData(userId, MimeFormat)); + WebApiHelper.ExecuteUpload(this, url, HttpVerbs.POST, SerializationHelper.SerializeUserId(userId, MimeFormat)); } /// @@ -373,7 +373,7 @@ public void RemoveWatcherFromIssue(int issueId, int userId) public void AddUserToGroup(int groupId, int userId) { var url = UrlHelper.GetAddUserToGroupUrl(this, groupId); - WebApiHelper.ExecuteUpload(this, url, HttpVerbs.POST, DataHelper.UserData(userId, MimeFormat)); + WebApiHelper.ExecuteUpload(this, url, HttpVerbs.POST, SerializationHelper.SerializeUserId(userId, MimeFormat)); } /// diff --git a/src/redmine-net-api/Serialization/SerializationHelper.cs b/src/redmine-net-api/Serialization/SerializationHelper.cs new file mode 100644 index 00000000..678b22c7 --- /dev/null +++ b/src/redmine-net-api/Serialization/SerializationHelper.cs @@ -0,0 +1,23 @@ +using System.Globalization; + +namespace Redmine.Net.Api +{ + /// + /// + /// + internal static class SerializationHelper + { + /// + /// + /// + /// + /// + /// + public static string SerializeUserId(int userId, MimeFormat mimeFormat) + { + return mimeFormat == MimeFormat.Xml + ? $"{userId.ToString(CultureInfo.InvariantCulture)}" + : $"{{\"user_id\":\"{userId.ToString(CultureInfo.InvariantCulture)}\"}}"; + } + } +} \ No newline at end of file From 3585478c223d0c72609006e0a63838d6bccf3d87 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 26 Nov 2023 19:28:31 +0200 Subject: [PATCH 264/549] [Rearrange] Serialization files --- .../{ => Serialization/Json}/Extensions/JsonReaderExtensions.cs | 0 .../{ => Serialization/Json}/Extensions/JsonWriterExtensions.cs | 0 .../Serialization/{ => Json}/IJsonSerializable.cs | 0 src/redmine-net-api/Serialization/{ => Json}/JsonObject.cs | 0 .../Serialization/{ => Json}/JsonRedmineSerializer.cs | 0 src/redmine-net-api/{ => Serialization}/MimeFormat.cs | 1 - src/redmine-net-api/Serialization/{ => Xml}/CacheKeyFactory.cs | 0 .../{ => Serialization/Xml}/Extensions/XmlReaderExtensions.cs | 0 .../{ => Serialization/Xml}/Extensions/XmlWriterExtensions.cs | 0 .../Serialization/{ => Xml}/IXmlSerializerCache.cs | 0 .../Serialization/{ => Xml}/XmlRedmineSerializer.cs | 0 .../Serialization/{ => Xml}/XmlSerializerCache.cs | 0 .../{Internals => Serialization/Xml}/XmlTextReaderBuilder.cs | 0 13 files changed, 1 deletion(-) rename src/redmine-net-api/{ => Serialization/Json}/Extensions/JsonReaderExtensions.cs (100%) rename src/redmine-net-api/{ => Serialization/Json}/Extensions/JsonWriterExtensions.cs (100%) rename src/redmine-net-api/Serialization/{ => Json}/IJsonSerializable.cs (100%) rename src/redmine-net-api/Serialization/{ => Json}/JsonObject.cs (100%) rename src/redmine-net-api/Serialization/{ => Json}/JsonRedmineSerializer.cs (100%) rename src/redmine-net-api/{ => Serialization}/MimeFormat.cs (99%) rename src/redmine-net-api/Serialization/{ => Xml}/CacheKeyFactory.cs (100%) rename src/redmine-net-api/{ => Serialization/Xml}/Extensions/XmlReaderExtensions.cs (100%) rename src/redmine-net-api/{ => Serialization/Xml}/Extensions/XmlWriterExtensions.cs (100%) rename src/redmine-net-api/Serialization/{ => Xml}/IXmlSerializerCache.cs (100%) rename src/redmine-net-api/Serialization/{ => Xml}/XmlRedmineSerializer.cs (100%) rename src/redmine-net-api/Serialization/{ => Xml}/XmlSerializerCache.cs (100%) rename src/redmine-net-api/{Internals => Serialization/Xml}/XmlTextReaderBuilder.cs (100%) diff --git a/src/redmine-net-api/Extensions/JsonReaderExtensions.cs b/src/redmine-net-api/Serialization/Json/Extensions/JsonReaderExtensions.cs similarity index 100% rename from src/redmine-net-api/Extensions/JsonReaderExtensions.cs rename to src/redmine-net-api/Serialization/Json/Extensions/JsonReaderExtensions.cs diff --git a/src/redmine-net-api/Extensions/JsonWriterExtensions.cs b/src/redmine-net-api/Serialization/Json/Extensions/JsonWriterExtensions.cs similarity index 100% rename from src/redmine-net-api/Extensions/JsonWriterExtensions.cs rename to src/redmine-net-api/Serialization/Json/Extensions/JsonWriterExtensions.cs diff --git a/src/redmine-net-api/Serialization/IJsonSerializable.cs b/src/redmine-net-api/Serialization/Json/IJsonSerializable.cs similarity index 100% rename from src/redmine-net-api/Serialization/IJsonSerializable.cs rename to src/redmine-net-api/Serialization/Json/IJsonSerializable.cs diff --git a/src/redmine-net-api/Serialization/JsonObject.cs b/src/redmine-net-api/Serialization/Json/JsonObject.cs similarity index 100% rename from src/redmine-net-api/Serialization/JsonObject.cs rename to src/redmine-net-api/Serialization/Json/JsonObject.cs diff --git a/src/redmine-net-api/Serialization/JsonRedmineSerializer.cs b/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs similarity index 100% rename from src/redmine-net-api/Serialization/JsonRedmineSerializer.cs rename to src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs diff --git a/src/redmine-net-api/MimeFormat.cs b/src/redmine-net-api/Serialization/MimeFormat.cs similarity index 99% rename from src/redmine-net-api/MimeFormat.cs rename to src/redmine-net-api/Serialization/MimeFormat.cs index 8bc2b2ae..5f755688 100755 --- a/src/redmine-net-api/MimeFormat.cs +++ b/src/redmine-net-api/Serialization/MimeFormat.cs @@ -14,7 +14,6 @@ You may obtain a copy of the License at limitations under the License. */ - namespace Redmine.Net.Api { /// diff --git a/src/redmine-net-api/Serialization/CacheKeyFactory.cs b/src/redmine-net-api/Serialization/Xml/CacheKeyFactory.cs similarity index 100% rename from src/redmine-net-api/Serialization/CacheKeyFactory.cs rename to src/redmine-net-api/Serialization/Xml/CacheKeyFactory.cs diff --git a/src/redmine-net-api/Extensions/XmlReaderExtensions.cs b/src/redmine-net-api/Serialization/Xml/Extensions/XmlReaderExtensions.cs similarity index 100% rename from src/redmine-net-api/Extensions/XmlReaderExtensions.cs rename to src/redmine-net-api/Serialization/Xml/Extensions/XmlReaderExtensions.cs diff --git a/src/redmine-net-api/Extensions/XmlWriterExtensions.cs b/src/redmine-net-api/Serialization/Xml/Extensions/XmlWriterExtensions.cs similarity index 100% rename from src/redmine-net-api/Extensions/XmlWriterExtensions.cs rename to src/redmine-net-api/Serialization/Xml/Extensions/XmlWriterExtensions.cs diff --git a/src/redmine-net-api/Serialization/IXmlSerializerCache.cs b/src/redmine-net-api/Serialization/Xml/IXmlSerializerCache.cs similarity index 100% rename from src/redmine-net-api/Serialization/IXmlSerializerCache.cs rename to src/redmine-net-api/Serialization/Xml/IXmlSerializerCache.cs diff --git a/src/redmine-net-api/Serialization/XmlRedmineSerializer.cs b/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs similarity index 100% rename from src/redmine-net-api/Serialization/XmlRedmineSerializer.cs rename to src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs diff --git a/src/redmine-net-api/Serialization/XmlSerializerCache.cs b/src/redmine-net-api/Serialization/Xml/XmlSerializerCache.cs similarity index 100% rename from src/redmine-net-api/Serialization/XmlSerializerCache.cs rename to src/redmine-net-api/Serialization/Xml/XmlSerializerCache.cs diff --git a/src/redmine-net-api/Internals/XmlTextReaderBuilder.cs b/src/redmine-net-api/Serialization/Xml/XmlTextReaderBuilder.cs similarity index 100% rename from src/redmine-net-api/Internals/XmlTextReaderBuilder.cs rename to src/redmine-net-api/Serialization/Xml/XmlTextReaderBuilder.cs From 3259d2a506d1ee421c29b3ff493d6c32324d2034 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 26 Nov 2023 18:55:17 +0200 Subject: [PATCH 265/549] [Rearrange] .NET 20 files & PagedResults --- .../{Extensions => Features/net20}/ExtensionAttribute.cs | 2 +- src/redmine-net-api/{Internals => Features/net20}/Func.cs | 0 src/redmine-net-api/{Serialization => Types}/PagedResults.cs | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename src/redmine-net-api/{Extensions => Features/net20}/ExtensionAttribute.cs (94%) rename src/redmine-net-api/{Internals => Features/net20}/Func.cs (100%) rename src/redmine-net-api/{Serialization => Types}/PagedResults.cs (100%) diff --git a/src/redmine-net-api/Extensions/ExtensionAttribute.cs b/src/redmine-net-api/Features/net20/ExtensionAttribute.cs similarity index 94% rename from src/redmine-net-api/Extensions/ExtensionAttribute.cs rename to src/redmine-net-api/Features/net20/ExtensionAttribute.cs index cdb7b66b..7aa12379 100755 --- a/src/redmine-net-api/Extensions/ExtensionAttribute.cs +++ b/src/redmine-net-api/Features/net20/ExtensionAttribute.cs @@ -23,7 +23,7 @@ namespace System.Runtime.CompilerServices /// /// [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method, AllowMultiple=false, Inherited=false)] - public class ExtensionAttribute: Attribute + public sealed class ExtensionAttribute: Attribute { } } diff --git a/src/redmine-net-api/Internals/Func.cs b/src/redmine-net-api/Features/net20/Func.cs similarity index 100% rename from src/redmine-net-api/Internals/Func.cs rename to src/redmine-net-api/Features/net20/Func.cs diff --git a/src/redmine-net-api/Serialization/PagedResults.cs b/src/redmine-net-api/Types/PagedResults.cs similarity index 100% rename from src/redmine-net-api/Serialization/PagedResults.cs rename to src/redmine-net-api/Types/PagedResults.cs From c577d3d6f60640467acdea05f3092d679a18030c Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 26 Nov 2023 19:28:56 +0200 Subject: [PATCH 266/549] [Rearrange] Grouped WebClient related files --- src/redmine-net-api/{ => Net}/HttpVerbs.cs | 0 src/redmine-net-api/{ => Net}/RedirectType.cs | 0 .../WebClient}/Extensions/NameValueCollectionExtensions.cs | 0 .../{ => Net/WebClient}/Extensions/WebExtensions.cs | 0 src/redmine-net-api/{ => Net/WebClient}/IRedmineWebClient.cs | 0 src/redmine-net-api/{ => Net/WebClient}/RedmineWebClient.cs | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename src/redmine-net-api/{ => Net}/HttpVerbs.cs (100%) rename src/redmine-net-api/{ => Net}/RedirectType.cs (100%) rename src/redmine-net-api/{ => Net/WebClient}/Extensions/NameValueCollectionExtensions.cs (100%) rename src/redmine-net-api/{ => Net/WebClient}/Extensions/WebExtensions.cs (100%) rename src/redmine-net-api/{ => Net/WebClient}/IRedmineWebClient.cs (100%) rename src/redmine-net-api/{ => Net/WebClient}/RedmineWebClient.cs (100%) diff --git a/src/redmine-net-api/HttpVerbs.cs b/src/redmine-net-api/Net/HttpVerbs.cs similarity index 100% rename from src/redmine-net-api/HttpVerbs.cs rename to src/redmine-net-api/Net/HttpVerbs.cs diff --git a/src/redmine-net-api/RedirectType.cs b/src/redmine-net-api/Net/RedirectType.cs similarity index 100% rename from src/redmine-net-api/RedirectType.cs rename to src/redmine-net-api/Net/RedirectType.cs diff --git a/src/redmine-net-api/Extensions/NameValueCollectionExtensions.cs b/src/redmine-net-api/Net/WebClient/Extensions/NameValueCollectionExtensions.cs similarity index 100% rename from src/redmine-net-api/Extensions/NameValueCollectionExtensions.cs rename to src/redmine-net-api/Net/WebClient/Extensions/NameValueCollectionExtensions.cs diff --git a/src/redmine-net-api/Extensions/WebExtensions.cs b/src/redmine-net-api/Net/WebClient/Extensions/WebExtensions.cs similarity index 100% rename from src/redmine-net-api/Extensions/WebExtensions.cs rename to src/redmine-net-api/Net/WebClient/Extensions/WebExtensions.cs diff --git a/src/redmine-net-api/IRedmineWebClient.cs b/src/redmine-net-api/Net/WebClient/IRedmineWebClient.cs similarity index 100% rename from src/redmine-net-api/IRedmineWebClient.cs rename to src/redmine-net-api/Net/WebClient/IRedmineWebClient.cs diff --git a/src/redmine-net-api/RedmineWebClient.cs b/src/redmine-net-api/Net/WebClient/RedmineWebClient.cs similarity index 100% rename from src/redmine-net-api/RedmineWebClient.cs rename to src/redmine-net-api/Net/WebClient/RedmineWebClient.cs From bd231dc79069d1921054f6fd468f1e6c63f38977 Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 24 Oct 2023 19:37:25 +0300 Subject: [PATCH 267/549] [RedmineKeys] Add new keys --- src/redmine-net-api/RedmineKeys.cs | 34 +++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/redmine-net-api/RedmineKeys.cs b/src/redmine-net-api/RedmineKeys.cs index 081ba422..ba82b08f 100644 --- a/src/redmine-net-api/RedmineKeys.cs +++ b/src/redmine-net-api/RedmineKeys.cs @@ -172,8 +172,11 @@ public static class RedmineKeys /// /// /// + public const string DEFAULT_ASSIGNED_TO_ID = "default_assigned_to_id"; + /// + /// + /// public const string DEFAULT_STATUS = "default_status"; - /// /// /// @@ -185,6 +188,10 @@ public static class RedmineKeys /// /// /// + public const string DEFAULT_VERSION_ID = "default_version_id"; + /// + /// + /// public const string DELAY = "delay"; /// /// @@ -229,6 +236,14 @@ public static class RedmineKeys /// /// /// + public const string ENUMERATION_ISSUE_PRIORITIES = "enumerations/issue_priorities"; + /// + /// + /// + public const string ENUMERATION_TIME_ENTRY_ACTIVITIES = "enumerations/time_entry_activities"; + /// + /// + /// public const string ERROR = "error"; /// /// @@ -351,6 +366,10 @@ public static class RedmineKeys /// /// /// + public const string ISSUE_STATUSES = "issue_statuses"; + /// + /// + /// public const string ISSUE_TO_ID = "issue_to_id"; /// /// @@ -542,6 +561,10 @@ public static class RedmineKeys /// public const string QUERY = "query"; /// + /// + /// + public const string QUERIES = "queries"; + /// /// /// public const string REASSIGN_TO_ID = "reassign_to_id"; @@ -592,6 +615,10 @@ public static class RedmineKeys /// /// /// + public const string SEARCH = "search"; + /// + /// + /// public const string SHARING = "sharing"; /// /// @@ -640,6 +667,10 @@ public static class RedmineKeys /// /// /// + public const string TIME_ENTRIES = "time_entries"; + /// + /// + /// public const string TIME_ENTRY_ACTIVITIES = "time_entry_activities"; /// /// @@ -783,5 +814,6 @@ public static class RedmineKeys /// public const string WIKI_PAGES = "wiki_pages"; + } } \ No newline at end of file From 943cb24188c651657e51d66ded7fc1ad26178120 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 26 Nov 2023 19:17:26 +0200 Subject: [PATCH 268/549] [New] RedmineConstants --- src/redmine-net-api/RedmineConstants.cs | 30 +++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/redmine-net-api/RedmineConstants.cs diff --git a/src/redmine-net-api/RedmineConstants.cs b/src/redmine-net-api/RedmineConstants.cs new file mode 100644 index 00000000..6aa78d05 --- /dev/null +++ b/src/redmine-net-api/RedmineConstants.cs @@ -0,0 +1,30 @@ +namespace Redmine.Net.Api +{ + /// + /// + /// + public static class RedmineConstants + { + /// + /// + /// + public const string OBSOLETE_TEXT = "In next major release, it will no longer be available."; + /// + /// + /// + public const int DEFAULT_PAGE_SIZE_VALUE = 25; + + /// + /// + /// + public const string CONTENT_TYPE_APPLICATION_JSON = "application/json"; + /// + /// + /// + public const string CONTENT_TYPE_APPLICATION_XML = "application/xml"; + /// + /// + /// + public const string CONTENT_TYPE_APPLICATION_STREAM = "application/octet-stream"; + } +} \ No newline at end of file From 5a66e17b788cc0e51947078799e38dcaa9451092 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 26 Nov 2023 19:18:22 +0200 Subject: [PATCH 269/549] [New] SerializationType --- .../Serialization/SerializationType.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/redmine-net-api/Serialization/SerializationType.cs diff --git a/src/redmine-net-api/Serialization/SerializationType.cs b/src/redmine-net-api/Serialization/SerializationType.cs new file mode 100644 index 00000000..e57dd054 --- /dev/null +++ b/src/redmine-net-api/Serialization/SerializationType.cs @@ -0,0 +1,16 @@ +namespace Redmine.Net.Api.Serialization +{ + /// + /// + /// + public enum SerializationType + { + /// + /// + Xml, + /// + /// The json + /// + Json + } +} \ No newline at end of file From d24a7b8ed38ad1f5452e75557ab8ee951ed42dfd Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 24 Oct 2023 19:40:20 +0300 Subject: [PATCH 270/549] [New] RedmineApiException --- .../Exceptions/RedmineApiException.cs | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 src/redmine-net-api/Exceptions/RedmineApiException.cs diff --git a/src/redmine-net-api/Exceptions/RedmineApiException.cs b/src/redmine-net-api/Exceptions/RedmineApiException.cs new file mode 100644 index 00000000..ad3b114b --- /dev/null +++ b/src/redmine-net-api/Exceptions/RedmineApiException.cs @@ -0,0 +1,95 @@ +using System; +using System.Runtime.Serialization; + +namespace Redmine.Net.Api.Exceptions +{ + /// + /// + /// + [Serializable] + public sealed class RedmineApiException : RedmineException + { + /// + /// + /// + public RedmineApiException() + : this(errorCode: null, false) + { + } + + /// + /// + /// + /// + public RedmineApiException(string message) + : this(message, errorCode: null, false) + { + } + + /// + /// + /// + /// + /// + public RedmineApiException(string message, Exception innerException) + : this(message, innerException, errorCode: null, false) + { + } + + /// + /// + /// + /// + /// + public RedmineApiException(string errorCode, bool isTransient) + : this(string.Empty, errorCode, isTransient) + { + } + + /// + /// + /// + /// + /// + /// + public RedmineApiException(string message, string errorCode, bool isTransient) + : this(message, null, errorCode, isTransient) + { + } + + /// + /// + /// + /// + /// + /// + /// + public RedmineApiException(string message, Exception inner, string errorCode, bool isTransient) + : base(message, inner) + { + this.ErrorCode = errorCode ?? "UNKNOWN"; + this.IsTransient = isTransient; + } + + /// + /// Gets the error code parameter. + /// + /// The error code associated with the exception. + public string ErrorCode { get; } + + /// + /// Gets a value indicating whether gets exception is Transient and operation can be retried. + /// + /// Value indicating whether the exception is transient or not. + public bool IsTransient { get; } + + /// + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + + info.AddValue(nameof(this.ErrorCode), this.ErrorCode); + info.AddValue(nameof(this.IsTransient), this.IsTransient); + } + } +} \ No newline at end of file From e54ea32a867f1c226dfd0c94730522ec9067c55d Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 26 Nov 2023 20:33:39 +0200 Subject: [PATCH 271/549] [Extensions] Improvements --- .../Extensions/CollectionExtensions.cs | 57 +++++++++--------- .../Extensions/StringExtensions.cs | 29 +++++----- .../NameValueCollectionExtensions.cs | 9 +-- .../Json/Extensions/JsonReaderExtensions.cs | 9 ++- .../Json/Extensions/JsonWriterExtensions.cs | 58 ++++++++++++++----- .../Xml/Extensions/XmlReaderExtensions.cs | 5 +- 6 files changed, 93 insertions(+), 74 deletions(-) diff --git a/src/redmine-net-api/Extensions/CollectionExtensions.cs b/src/redmine-net-api/Extensions/CollectionExtensions.cs index e01f1e57..5a14bfe3 100755 --- a/src/redmine-net-api/Extensions/CollectionExtensions.cs +++ b/src/redmine-net-api/Extensions/CollectionExtensions.cs @@ -23,8 +23,6 @@ namespace Redmine.Net.Api.Extensions /// /// /// - - public static class CollectionExtensions { /// @@ -35,18 +33,24 @@ public static class CollectionExtensions /// public static IList Clone(this IList listToClone) where T : ICloneable { - if (listToClone == null) return null; - IList clonedList = new List(); - foreach (var item in listToClone) + if (listToClone == null) + { + return null; + } + + var clonedList = new List(); + + for (var index = 0; index < listToClone.Count; index++) { + var item = listToClone[index]; clonedList.Add((T) item.Clone()); } + return clonedList; } - - + /// - /// Equalses the specified list to compare. + /// /// /// /// The list. @@ -54,26 +58,23 @@ public static IList Clone(this IList listToClone) where T : ICloneable /// public static bool Equals(this IList list, IList listToCompare) where T : class { - if (list ==null || listToCompare == null) return false; - -#if NET20 + if (list == null || listToCompare == null) + { + return false; + } + if (list.Count != listToCompare.Count) { return false; } + var index = 0; - while (index < list.Count && (list[index] as T).Equals(listToCompare[index] as T)) + while (index < list.Count && list[index].Equals(listToCompare[index])) { index++; } return index == list.Count; -#else - var set = new HashSet(list); - var setToCompare = new HashSet(listToCompare); - - return set.SetEquals(setToCompare); -#endif } /// @@ -87,21 +88,23 @@ public static string Dump(this IEnumerable collection) where TIn : cla return null; } - var sb = new StringBuilder(); + var sb = new StringBuilder("{"); + foreach (var item in collection) { - sb.Append(",").Append(item); + sb.Append(item).Append(','); } - sb[0] = '{'; - sb.Append("}"); + if (sb.Length > 1) + { + sb.Length -= 1; + } + + sb.Append('}'); var str = sb.ToString(); -#if NET20 - sb = null; -#else - sb.Clear(); -#endif + sb.Length = 0; + return str; } } diff --git a/src/redmine-net-api/Extensions/StringExtensions.cs b/src/redmine-net-api/Extensions/StringExtensions.cs index d0856246..00b73e2f 100644 --- a/src/redmine-net-api/Extensions/StringExtensions.cs +++ b/src/redmine-net-api/Extensions/StringExtensions.cs @@ -23,7 +23,7 @@ namespace Redmine.Net.Api.Extensions /// /// /// - public static class StringExtensions + public static partial class StringExtensions { /// /// @@ -59,15 +59,12 @@ public static bool IsNullOrWhiteSpace(this string value) /// public static string Truncate(this string text, int maximumLength) { - if (!text.IsNullOrWhiteSpace()) + if (text.IsNullOrWhiteSpace()) { - if (text.Length > maximumLength) - { - text = text.Substring(0, maximumLength); - } + return text; } - - return text; + + return text.Length > maximumLength ? text.Substring(0, maximumLength) : text; } /// @@ -97,23 +94,23 @@ internal static SecureString ToSecureString(this string value) return null; } - using (var rv = new SecureString()) + var rv = new SecureString(); + foreach (var c in value) { - foreach (var c in value) - { - rv.AppendChar(c); - } - - return rv; + rv.AppendChar(c); } + + return rv; } internal static string RemoveTrailingSlash(this string s) { if (string.IsNullOrEmpty(s)) + { return s; + } - if (s.EndsWith("/", StringComparison.OrdinalIgnoreCase) || s.EndsWith("\"", StringComparison.OrdinalIgnoreCase)) + if (s.EndsWith("/", StringComparison.OrdinalIgnoreCase) || s.EndsWith(@"\", StringComparison.OrdinalIgnoreCase)) { return s.Substring(0, s.Length - 1); } diff --git a/src/redmine-net-api/Net/WebClient/Extensions/NameValueCollectionExtensions.cs b/src/redmine-net-api/Net/WebClient/Extensions/NameValueCollectionExtensions.cs index 2fd504b8..1ce0a13d 100644 --- a/src/redmine-net-api/Net/WebClient/Extensions/NameValueCollectionExtensions.cs +++ b/src/redmine-net-api/Net/WebClient/Extensions/NameValueCollectionExtensions.cs @@ -79,19 +79,16 @@ public static string ToQueryString(this NameValueCollection requestParameters) { stringBuilder .Append(requestParameters.AllKeys[index].ToString(CultureInfo.InvariantCulture)) - .Append("=") + .Append('=') .Append(requestParameters[index].ToString(CultureInfo.InvariantCulture)) - .Append("&"); + .Append('&'); } stringBuilder.Length -= 1; var queryString = stringBuilder.ToString(); - #if !(NET20) - stringBuilder.Clear(); - #endif - stringBuilder = null; + stringBuilder.Length = 0; return queryString; } diff --git a/src/redmine-net-api/Serialization/Json/Extensions/JsonReaderExtensions.cs b/src/redmine-net-api/Serialization/Json/Extensions/JsonReaderExtensions.cs index ad54ba31..94776ceb 100644 --- a/src/redmine-net-api/Serialization/Json/Extensions/JsonReaderExtensions.cs +++ b/src/redmine-net-api/Serialization/Json/Extensions/JsonReaderExtensions.cs @@ -63,7 +63,7 @@ public static List ReadAsCollection(this JsonReader reader, bool readInner throw new RedmineException($"Entity of type '{typeof(T)}' should implement IJsonSerializable."); } - var col = new List(); + List collection = null; while (reader.Read()) { @@ -89,12 +89,11 @@ public static List ReadAsCollection(this JsonReader reader, bool readInner ((IJsonSerializable)entity).ReadJson(reader); - var des = entity; - - col.Add(des); + collection ??= new List(); + collection.Add(entity); } - return col; + return collection; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Serialization/Json/Extensions/JsonWriterExtensions.cs b/src/redmine-net-api/Serialization/Json/Extensions/JsonWriterExtensions.cs index 2bbca7dd..0d5bd08e 100644 --- a/src/redmine-net-api/Serialization/Json/Extensions/JsonWriterExtensions.cs +++ b/src/redmine-net-api/Serialization/Json/Extensions/JsonWriterExtensions.cs @@ -61,14 +61,7 @@ public static void WriteIfNotDefaultOrNull(this JsonWriter writer, string ele return; } - if (value is bool) - { - writer.WriteProperty(elementName, value.ToString().ToLowerInv()); - } - else - { - writer.WriteProperty(elementName, value.ToString()); - } + writer.WriteProperty(elementName, typeof(T) == typeof(bool) ? value.ToString().ToLowerInv() : value.ToString()); } /// @@ -155,6 +148,30 @@ public static void WriteProperty(this JsonWriter jsonWriter, string tag, object jsonWriter.WritePropertyName(tag); jsonWriter.WriteValue(value); } + + /// + /// + /// + /// + /// + /// + public static void WriteProperty(this JsonWriter jsonWriter, string tag, int value) + { + jsonWriter.WritePropertyName(tag); + jsonWriter.WriteValue(value); + } + + /// + /// + /// + /// + /// + /// + public static void WriteProperty(this JsonWriter jsonWriter, string tag, bool value) + { + jsonWriter.WritePropertyName(tag); + jsonWriter.WriteValue(value); + } /// /// @@ -171,8 +188,7 @@ public static void WriteRepeatableElement(this JsonWriter jsonWriter, string tag foreach (var value in collection) { - jsonWriter.WritePropertyName(tag); - jsonWriter.WriteValue(value.Value); + jsonWriter.WriteProperty(tag, value.Value); } } @@ -196,12 +212,17 @@ public static void WriteArrayIds(this JsonWriter jsonWriter, string tag, IEnumer foreach (var identifiableName in collection) { - sb.Append(identifiableName.Id.ToString(CultureInfo.InvariantCulture)).Append(","); + sb.Append(identifiableName.Id.ToString(CultureInfo.InvariantCulture)).Append(','); } - sb.Length -= 1; + if (sb.Length > 1) + { + sb.Length -= 1; + } + jsonWriter.WriteValue(sb.ToString()); - sb= null; + + sb.Length = 0; jsonWriter.WriteEndArray(); } @@ -226,12 +247,17 @@ public static void WriteArrayNames(this JsonWriter jsonWriter, string tag, IEnum foreach (var identifiableName in collection) { - sb.Append(identifiableName.Name).Append(","); + sb.Append(identifiableName.Name).Append(','); + } + + if (sb.Length > 1) + { + sb.Length -= 1; } - sb.Length -= 1; jsonWriter.WriteValue(sb.ToString()); - sb = null; + + sb.Length = 0; jsonWriter.WriteEndArray(); } diff --git a/src/redmine-net-api/Serialization/Xml/Extensions/XmlReaderExtensions.cs b/src/redmine-net-api/Serialization/Xml/Extensions/XmlReaderExtensions.cs index 87b2b528..f0312351 100644 --- a/src/redmine-net-api/Serialization/Xml/Extensions/XmlReaderExtensions.cs +++ b/src/redmine-net-api/Serialization/Xml/Extensions/XmlReaderExtensions.cs @@ -210,10 +210,7 @@ public static List ReadElementContentAsCollection(this XmlReader reader) w if (entity != null) { - if (result == null) - { - result = new List(); - } + result ??= new List(); result.Add(entity); } From 05838102cffa868df2039ef71b6619e734b784ed Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 27 Nov 2023 15:48:10 +0200 Subject: [PATCH 272/549] [Features] Enable for older versions (NET2.0..NET4.8.1) --- .../Features/IsExternalInit.cs | 20 +++++++++++++++++++ .../Features/net20/ExtensionAttribute.cs | 4 +++- src/redmine-net-api/Features/net20/Func.cs | 1 + 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 src/redmine-net-api/Features/IsExternalInit.cs diff --git a/src/redmine-net-api/Features/IsExternalInit.cs b/src/redmine-net-api/Features/IsExternalInit.cs new file mode 100644 index 00000000..6490f153 --- /dev/null +++ b/src/redmine-net-api/Features/IsExternalInit.cs @@ -0,0 +1,20 @@ +#if NET20_OR_GREATER + +// ReSharper disable once CheckNamespace +namespace System.Runtime.CompilerServices +{ + /// + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + internal static class IsExternalInit + { + } +} +#endif + + + + + diff --git a/src/redmine-net-api/Features/net20/ExtensionAttribute.cs b/src/redmine-net-api/Features/net20/ExtensionAttribute.cs index 7aa12379..41a3b55c 100755 --- a/src/redmine-net-api/Features/net20/ExtensionAttribute.cs +++ b/src/redmine-net-api/Features/net20/ExtensionAttribute.cs @@ -16,6 +16,7 @@ limitations under the License. #if NET20 +// ReSharper disable once CheckNamespace namespace System.Runtime.CompilerServices { /// @@ -28,4 +29,5 @@ public sealed class ExtensionAttribute: Attribute } } -#endif \ No newline at end of file +#endif + diff --git a/src/redmine-net-api/Features/net20/Func.cs b/src/redmine-net-api/Features/net20/Func.cs index 0b6a38f2..ce8db0e9 100644 --- a/src/redmine-net-api/Features/net20/Func.cs +++ b/src/redmine-net-api/Features/net20/Func.cs @@ -15,6 +15,7 @@ limitations under the License. */ #if NET20 +// ReSharper disable once CheckNamespace namespace System { /// From 3ab1bcb90607b075668b6c3f0cdc080c8efab478 Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 27 Nov 2023 15:48:46 +0200 Subject: [PATCH 273/549] [New] RedmineWebClientOptions --- .../Net/WebClient/RedmineWebClientOptions.cs | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 src/redmine-net-api/Net/WebClient/RedmineWebClientOptions.cs diff --git a/src/redmine-net-api/Net/WebClient/RedmineWebClientOptions.cs b/src/redmine-net-api/Net/WebClient/RedmineWebClientOptions.cs new file mode 100644 index 00000000..05b0c766 --- /dev/null +++ b/src/redmine-net-api/Net/WebClient/RedmineWebClientOptions.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Cache; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; + +namespace Redmine.Net.Api.Net.WebClient; +/// +/// +/// +public sealed class RedmineWebClientOptions: IRedmineApiClientOptions +{ + /// + /// + /// + public bool? AutoRedirect { get; set; } + + /// + /// + /// + public CookieContainer CookieContainer { get; set; } + + /// + /// + /// + public DecompressionMethods? DecompressionFormat { get; set; } + + /// + /// + /// + public ICredentials Credentials { get; set; } + + /// + /// + /// + public Dictionary DefaultHeaders { get; set; } + + /// + /// + /// + public IWebProxy Proxy { get; set; } + + /// + /// + /// + public bool? KeepAlive { get; set; } + + /// + /// + /// + public int? MaxAutomaticRedirections { get; set; } + + /// + /// + /// + public long? MaxRequestContentBufferSize { get; set; } + + /// + /// + /// + public long? MaxResponseContentBufferSize { get; set; } + + /// + /// + /// + public int? MaxConnectionsPerServer { get; set; } + + /// + /// + /// + public int? MaxResponseHeadersLength { get; set; } + + /// + /// + /// + public bool? PreAuthenticate { get; set; } + + /// + /// + /// + public RequestCachePolicy RequestCachePolicy { get; set; } + + /// + /// + /// + public string Scheme { get; set; } = "https"; + + /// + /// + /// + public RemoteCertificateValidationCallback ServerCertificateValidationCallback { get; set; } + + /// + /// + /// + public TimeSpan? Timeout { get; set; } + + /// + /// + /// + public bool? UnsafeAuthenticatedConnectionSharing { get; set; } + + /// + /// + /// + public string UserAgent { get; set; } = "RedmineDotNetAPIClient"; + + /// + /// + /// + public bool? UseCookies { get; set; } + + /// + /// + /// + public bool? UseDefaultCredentials { get; set; } + + /// + /// + /// + public bool? UseProxy { get; set; } + + /// + /// + /// + /// Only HTTP/1.0 and HTTP/1.1 version requests are currently supported. + public Version ProtocolVersion { get; set; } + + + #if NET40_OR_GREATER || NETCOREAPP + /// + /// + /// + public X509CertificateCollection ClientCertificates { get; set; } + #endif + + /// + /// + /// + public bool CheckCertificateRevocationList { get; set; } + + /// + /// + /// + public int? DefaultConnectionLimit { get; set; } + + /// + /// + /// + public int? DnsRefreshTimeout { get; set; } + + /// + /// + /// + public bool? EnableDnsRoundRobin { get; set; } + + /// + /// + /// + public int? MaxServicePoints { get; set; } + + /// + /// + /// + public int? MaxServicePointIdleTime { get; set; } + + #if(NET46_OR_GREATER || NETCOREAPP) + /// + /// + /// + public bool? ReusePort { get; set; } + #endif + + /// + /// + /// + public SecurityProtocolType? SecurityProtocolType { get; set; } +} \ No newline at end of file From 4766320ce3958ac06e96e73ebe97a51728a437b8 Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 27 Nov 2023 15:49:26 +0200 Subject: [PATCH 274/549] [New] RedmineManagerOptionsBuilder --- .../RedmineManagerOptionsBuilder.cs | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 src/redmine-net-api/RedmineManagerOptionsBuilder.cs diff --git a/src/redmine-net-api/RedmineManagerOptionsBuilder.cs b/src/redmine-net-api/RedmineManagerOptionsBuilder.cs new file mode 100644 index 00000000..e4e1357e --- /dev/null +++ b/src/redmine-net-api/RedmineManagerOptionsBuilder.cs @@ -0,0 +1,187 @@ +using System; +using System.Xml.Serialization; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Serialization; + +namespace Redmine.Net.Api +{ + /// + /// + /// + public sealed class RedmineManagerOptionsBuilder + { + + /// + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithPageSize(int pageSize) + { + this.PageSize = pageSize; + return this; + } + + /// + /// + /// + public int PageSize { get; private set; } + + /// + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithBaseAddress(string baseAddress) + { + return WithBaseAddress(new Uri(baseAddress)); + } + + /// + /// + /// + public Uri BaseAddress { get; private set; } + + /// + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithBaseAddress(Uri baseAddress) + { + this.BaseAddress = baseAddress; + return this; + } + + /// + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithSerializationType(SerializationType serializationType) + { + this.SerializationType = serializationType; + return this; + } + + /// + /// + /// + public SerializationType SerializationType { get; private set; } + + /// + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithAuthentication(IRedmineAuthentication authentication) + { + this.Authentication = authentication; + return this; + } + + /// + /// + /// + public IRedmineAuthentication Authentication { get; private set; } + + /// + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithClient(Func clientFunc) + { + this.ClientFunc = clientFunc; + return this; + } + + /// + /// + /// + public Func ClientFunc { get; private set; } + + /// + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithClientOptions(IRedmineApiClientOptions clientOptions) + { + this.ClientOptions = clientOptions; + return this; + } + + /// + /// + /// + public IRedmineApiClientOptions ClientOptions { get; private set; } + + /// + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithVersion(Version version) + { + this.Version = version; + return this; + } + + /// + /// + /// + public Version Version { get; set; } + + internal RedmineManagerOptionsBuilder WithVerifyServerCert(bool verifyServerCert) + { + this.VerifyServerCert = verifyServerCert; + return this; + } + + /// + /// + /// + public bool VerifyServerCert { get; private set; } + + /// + /// + /// + /// + internal RedmineManagerOptions Build() + { + if (Authentication == null) + { + throw new RedmineException("Authentication cannot be null"); + } + + var options = new RedmineManagerOptions() + { + PageSize = PageSize > 0 ? PageSize : RedmineManager.DEFAULT_PAGE_SIZE_VALUE, + VerifyServerCert = VerifyServerCert, + Serializer = SerializationType == SerializationType.Xml ? new XmlRedmineSerializer() : new JsonRedmineSerializer(), + Version = Version, + //Authentication = + ClientOptions = ClientOptions, + + }; + + + return options; + } + + + /// + /// + /// + /// + /// + /// + public static bool TryParse(string serviceName, out string parts) + { + parts = null; + return false; + } + + } +} \ No newline at end of file From d1c6adf4ffc41087f761259901c4411e196b737b Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 27 Nov 2023 15:49:45 +0200 Subject: [PATCH 275/549] [New] RedmineManagerOptions --- src/redmine-net-api/RedmineManagerOptions.cs | 53 ++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/redmine-net-api/RedmineManagerOptions.cs diff --git a/src/redmine-net-api/RedmineManagerOptions.cs b/src/redmine-net-api/RedmineManagerOptions.cs new file mode 100644 index 00000000..aa35b07f --- /dev/null +++ b/src/redmine-net-api/RedmineManagerOptions.cs @@ -0,0 +1,53 @@ +using System; +using Redmine.Net.Api.Serialization; + +namespace Redmine.Net.Api +{ + /// + /// + /// + internal sealed class RedmineManagerOptions + { + /// + /// + /// + public string BaseAddress { get; init; } + + /// + /// Gets or sets the page size for paginated Redmine API responses. + /// The default page size is 25, but you can customize it as needed. + /// + public int PageSize { get; init; } + + /// + /// Gets or sets the desired MIME format for Redmine API responses, which represents the way of serialization. + /// Supported formats include XML and JSON. The default format is XML. + /// + public IRedmineSerializer Serializer { get; init; } + + /// + /// Gets or sets the authentication method to be used when connecting to the Redmine server. + /// The available authentication types include API token-based authentication and basic authentication + /// (using a username and password). You can set an instance of the corresponding authentication class + /// to use the desired authentication method. + /// + public IRedmineAuthentication Authentication { get; init; } + + /// + /// Gets or sets a custom function that creates and returns a specialized instance of the WebClient class. + /// + public Func ClientFunc { get; init; } + + /// + /// Gets or sets the settings for configuring the Redmine web client. + /// + public IRedmineApiClientOptions ClientOptions { get; init; } + + /// + /// Gets or sets the version of the Redmine server to which this client will connect. + /// + public Version Version { get; init; } + + internal bool VerifyServerCert { get; set; } + } +} \ No newline at end of file From 7146a542f7ce8e919882594830d5673b81985241 Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 27 Nov 2023 15:51:21 +0200 Subject: [PATCH 276/549] [New] IRedmineApiClientOptions --- .../Net/IRedmineApiClientOptions.cs | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 src/redmine-net-api/Net/IRedmineApiClientOptions.cs diff --git a/src/redmine-net-api/Net/IRedmineApiClientOptions.cs b/src/redmine-net-api/Net/IRedmineApiClientOptions.cs new file mode 100644 index 00000000..23202f37 --- /dev/null +++ b/src/redmine-net-api/Net/IRedmineApiClientOptions.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Cache; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; + +namespace Redmine.Net.Api +{ + /// + /// + /// + public interface IRedmineApiClientOptions + { + /// + /// + /// + bool? AutoRedirect { get; set; } + + /// + /// + /// + CookieContainer CookieContainer { get; set; } + + /// + /// + /// + DecompressionMethods? DecompressionFormat { get; set; } + + /// + /// + /// + ICredentials Credentials { get; set; } + + /// + /// + /// + Dictionary DefaultHeaders { get; set; } + + /// + /// + /// + IWebProxy Proxy { get; set; } + + /// + /// + /// + bool? KeepAlive { get; set; } + + /// + /// + /// + int? MaxAutomaticRedirections { get; set; } + + /// + /// + /// + long? MaxRequestContentBufferSize { get; set; } + + /// + /// + /// + long? MaxResponseContentBufferSize { get; set; } + + /// + /// + /// + int? MaxConnectionsPerServer { get; set; } + + /// + /// + /// + int? MaxResponseHeadersLength { get; set; } + + /// + /// + /// + bool? PreAuthenticate { get; set; } + + /// + /// + /// + RequestCachePolicy RequestCachePolicy { get; set; } + + /// + /// + /// + string Scheme { get; set; } + + /// + /// + /// + RemoteCertificateValidationCallback ServerCertificateValidationCallback { get; set; } + + /// + /// + /// + TimeSpan? Timeout { get; set; } + + /// + /// + /// + bool? UnsafeAuthenticatedConnectionSharing { get; set; } + + /// + /// + /// + string UserAgent { get; set; } + + /// + /// + /// + bool? UseCookies { get; set; } + + /// + /// + /// + bool? UseDefaultCredentials { get; set; } + + /// + /// + /// + bool? UseProxy { get; set; } + + /// + /// + /// + /// Only HTTP/1.0 and HTTP/1.1 version requests are currently supported. + Version ProtocolVersion { get; set; } + + /// + /// + /// + bool CheckCertificateRevocationList { get; set; } + + /// + /// + /// + int? DefaultConnectionLimit { get; set; } + + /// + /// + /// + int? DnsRefreshTimeout { get; set; } + + /// + /// + /// + bool? EnableDnsRoundRobin { get; set; } + + /// + /// + /// + int? MaxServicePoints { get; set; } + + /// + /// + /// + int? MaxServicePointIdleTime { get; set; } + + /// + /// + /// + SecurityProtocolType? SecurityProtocolType { get; set; } + } +} \ No newline at end of file From 8b8f0d2cfd5752dd9a9ed8061d2eefc87c4654d9 Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 27 Nov 2023 15:52:12 +0200 Subject: [PATCH 277/549] [New] IRedmineAuthentication --- .../Authentication/IRedmineAuthentication.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/redmine-net-api/Authentication/IRedmineAuthentication.cs diff --git a/src/redmine-net-api/Authentication/IRedmineAuthentication.cs b/src/redmine-net-api/Authentication/IRedmineAuthentication.cs new file mode 100644 index 00000000..9604c83f --- /dev/null +++ b/src/redmine-net-api/Authentication/IRedmineAuthentication.cs @@ -0,0 +1,24 @@ +using System.Net; + +namespace Redmine.Net.Api; + +/// +/// +/// +public interface IRedmineAuthentication +{ + /// + /// + /// + string AuthenticationType { get; } + + /// + /// + /// + string Token { get; } + + /// + /// + /// + ICredentials Credentials { get; } +} \ No newline at end of file From c12da02c946a6de572e066ba4a67ca6cd30e3229 Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 27 Nov 2023 15:52:29 +0200 Subject: [PATCH 278/549] [New] RedmineApiKeyAuthentication --- .../RedmineApiKeyAuthentication.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/redmine-net-api/Authentication/RedmineApiKeyAuthentication.cs diff --git a/src/redmine-net-api/Authentication/RedmineApiKeyAuthentication.cs b/src/redmine-net-api/Authentication/RedmineApiKeyAuthentication.cs new file mode 100644 index 00000000..084752d3 --- /dev/null +++ b/src/redmine-net-api/Authentication/RedmineApiKeyAuthentication.cs @@ -0,0 +1,27 @@ +using System.Net; + +namespace Redmine.Net.Api.Authentication; + +/// +/// +/// +public sealed class RedmineApiKeyAuthentication: IRedmineAuthentication +{ + /// + public string AuthenticationType { get; } = "X-Redmine-API-Key"; + + /// + public string Token { get; init; } + + /// + public ICredentials Credentials { get; init; } + + /// + /// + /// + /// + public RedmineApiKeyAuthentication(string apiKey) + { + Token = apiKey; + } +} \ No newline at end of file From 72f23cdfb5d9967df66a3ed789bc38bf92d4eaa7 Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 27 Nov 2023 15:53:01 +0200 Subject: [PATCH 279/549] [New] RedmineBasicAuthentication --- .../RedmineBasicAuthentication.cs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/redmine-net-api/Authentication/RedmineBasicAuthentication.cs diff --git a/src/redmine-net-api/Authentication/RedmineBasicAuthentication.cs b/src/redmine-net-api/Authentication/RedmineBasicAuthentication.cs new file mode 100644 index 00000000..58c740ba --- /dev/null +++ b/src/redmine-net-api/Authentication/RedmineBasicAuthentication.cs @@ -0,0 +1,35 @@ +using System; +using System.Net; +using System.Text; +using Redmine.Net.Api.Exceptions; + +namespace Redmine.Net.Api.Authentication +{ + /// + /// + /// + public sealed class RedmineBasicAuthentication: IRedmineAuthentication + { + /// + public string AuthenticationType { get; } = "Basic"; + + /// + public string Token { get; init; } + + /// + public ICredentials Credentials { get; init; } + + /// + /// + /// + /// + /// + public RedmineBasicAuthentication(string username, string password) + { + if (username == null) throw new RedmineException(nameof(username)); + if (password == null) throw new RedmineException(nameof(password)); + + Token = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}")); + } + } +} \ No newline at end of file From 068b8276e2e68f7bcac6424a33ddfeea55aff7a4 Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 27 Nov 2023 15:54:17 +0200 Subject: [PATCH 280/549] [Serialization] Small improvements --- .../Serialization/Json/JsonObject.cs | 10 ++++++---- .../Json/JsonRedmineSerializer.cs | 19 +++++++------------ .../Serialization/Xml/XmlSerializerCache.cs | 2 +- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/redmine-net-api/Serialization/Json/JsonObject.cs b/src/redmine-net-api/Serialization/Json/JsonObject.cs index 7c8624e4..452df4a4 100644 --- a/src/redmine-net-api/Serialization/Json/JsonObject.cs +++ b/src/redmine-net-api/Serialization/Json/JsonObject.cs @@ -37,12 +37,14 @@ public JsonObject(JsonWriter writer, string root = null) Writer = writer; Writer.WriteStartObject(); - if (!root.IsNullOrWhiteSpace()) + if (root.IsNullOrWhiteSpace()) { - hasRoot = true; - Writer.WritePropertyName(root); - Writer.WriteStartObject(); + return; } + + hasRoot = true; + Writer.WritePropertyName(root); + Writer.WriteStartObject(); } private JsonWriter Writer { get; } diff --git a/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs b/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs index 2e09c76b..43cf4e8a 100644 --- a/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs +++ b/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs @@ -42,7 +42,7 @@ internal sealed class JsonRedmineSerializer : IRedmineSerializer using (var stringReader = new StringReader(jsonResponse)) { - using (JsonReader jsonReader = new JsonTextReader(stringReader)) + using (var jsonReader = new JsonTextReader(stringReader)) { var obj = Activator.CreateInstance(); @@ -68,7 +68,7 @@ internal sealed class JsonRedmineSerializer : IRedmineSerializer using (var sr = new StringReader(jsonResponse)) { - using (JsonReader reader = new JsonTextReader(sr)) + using (var reader = new JsonTextReader(sr)) { var total = 0; var offset = 0; @@ -111,7 +111,7 @@ internal sealed class JsonRedmineSerializer : IRedmineSerializer using (var sr = new StringReader(jsonResponse)) { - using (JsonReader reader = new JsonTextReader(sr)) + using (var reader = new JsonTextReader(sr)) { var total = 0; @@ -144,9 +144,7 @@ public string Serialize(T entity) where T : class throw new ArgumentNullException(nameof(entity), $"Could not serialize null of type {typeof(T).Name}"); } - var jsonSerializable = entity as IJsonSerializable; - - if (jsonSerializable == null) + if (entity is not IJsonSerializable jsonSerializable) { throw new RedmineException($"Entity of type '{typeof(T)}' should implement IJsonSerializable."); } @@ -155,7 +153,7 @@ public string Serialize(T entity) where T : class using (var sw = new StringWriter(stringBuilder)) { - using (JsonWriter writer = new JsonTextWriter(sw)) + using (var writer = new JsonTextWriter(sw)) { writer.Formatting = Newtonsoft.Json.Formatting.Indented; writer.DateFormatHandling = DateFormatHandling.IsoDateFormat; @@ -163,12 +161,9 @@ public string Serialize(T entity) where T : class jsonSerializable.WriteJson(writer); var json = stringBuilder.ToString(); + + stringBuilder.Length = 0; -#if NET20 - stringBuilder = null; -#else - stringBuilder.Clear(); -#endif return json; } } diff --git a/src/redmine-net-api/Serialization/Xml/XmlSerializerCache.cs b/src/redmine-net-api/Serialization/Xml/XmlSerializerCache.cs index bb90a370..cdc73fe8 100644 --- a/src/redmine-net-api/Serialization/Xml/XmlSerializerCache.cs +++ b/src/redmine-net-api/Serialization/Xml/XmlSerializerCache.cs @@ -24,7 +24,7 @@ namespace Redmine.Net.Api.Serialization /// /// /// - internal class XmlSerializerCache : IXmlSerializerCache + internal sealed class XmlSerializerCache : IXmlSerializerCache { #if !(NET20 || NET40 || NET45 || NET451 || NET452) private static readonly Type[] EmptyTypes = Array.Empty(); From 926b2d6e6c73c2cc6d4d33843f4f4ea9e96563ff Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 27 Nov 2023 21:50:05 +0200 Subject: [PATCH 281/549] [Authentication] RedmineNoAuthentication --- .../Authentication/RedmineNoAuthentication.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/redmine-net-api/Authentication/RedmineNoAuthentication.cs diff --git a/src/redmine-net-api/Authentication/RedmineNoAuthentication.cs b/src/redmine-net-api/Authentication/RedmineNoAuthentication.cs new file mode 100644 index 00000000..f568f1bc --- /dev/null +++ b/src/redmine-net-api/Authentication/RedmineNoAuthentication.cs @@ -0,0 +1,18 @@ +using System.Net; + +namespace Redmine.Net.Api.Authentication; + +/// +/// +/// +public sealed class RedmineNoAuthentication: IRedmineAuthentication +{ + /// + public string AuthenticationType { get; } = "NoAuth"; + + /// + public string Token { get; init; } + + /// + public ICredentials Credentials { get; init; } +} \ No newline at end of file From 08e7f81a48f3edd29920a043c8b1bd2dd3c84bec Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 27 Nov 2023 21:50:50 +0200 Subject: [PATCH 282/549] [StringExtensions] Add ValueOrFallback --- src/redmine-net-api/Extensions/StringExtensions.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/redmine-net-api/Extensions/StringExtensions.cs b/src/redmine-net-api/Extensions/StringExtensions.cs index 00b73e2f..2aa91799 100644 --- a/src/redmine-net-api/Extensions/StringExtensions.cs +++ b/src/redmine-net-api/Extensions/StringExtensions.cs @@ -117,5 +117,10 @@ internal static string RemoveTrailingSlash(this string s) return s; } + + internal static string ValueOrFallback(this string value, string fallback) + { + return !value.IsNullOrWhiteSpace() ? value : fallback; + } } } \ No newline at end of file From eef4868c5cedcfc3d9560c06f15693858079363b Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 27 Nov 2023 21:52:14 +0200 Subject: [PATCH 283/549] [New] [Features] CallerArgumentExpressionAttribute & NotNullAttribute --- .../CallerArgumentExpressionAttribute.cs | 31 +++++++++++++++++++ .../Features/NotNullAttribute.cs | 25 +++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 src/redmine-net-api/Features/CallerArgumentExpressionAttribute.cs create mode 100644 src/redmine-net-api/Features/NotNullAttribute.cs diff --git a/src/redmine-net-api/Features/CallerArgumentExpressionAttribute.cs b/src/redmine-net-api/Features/CallerArgumentExpressionAttribute.cs new file mode 100644 index 00000000..2c57f348 --- /dev/null +++ b/src/redmine-net-api/Features/CallerArgumentExpressionAttribute.cs @@ -0,0 +1,31 @@ +#if NET20_OR_GREATER +#pragma warning disable +#nullable enable annotations + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Runtime.CompilerServices +{ + /// + /// An attribute that allows parameters to receive the expression of other parameters. + /// + [global::System.AttributeUsage(global::System.AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] + internal sealed class CallerArgumentExpressionAttribute : global::System.Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// The condition parameter value. + public CallerArgumentExpressionAttribute(string parameterName) + { + ParameterName = parameterName; + } + + /// + /// Gets the parameter name the expression is retrieved from. + /// + public string ParameterName { get; } + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net-api/Features/NotNullAttribute.cs b/src/redmine-net-api/Features/NotNullAttribute.cs new file mode 100644 index 00000000..0a3e9093 --- /dev/null +++ b/src/redmine-net-api/Features/NotNullAttribute.cs @@ -0,0 +1,25 @@ +#if NET20_OR_GREATER +// +#pragma warning disable +#nullable enable annotations + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Diagnostics.CodeAnalysis +{ + /// + /// Specifies that an output will not be null even if the corresponding type allows it. + /// Specifies that an input argument was not null when the call returns. + /// + [global::System.AttributeUsage( + global::System.AttributeTargets.Field | + global::System.AttributeTargets.Parameter | + global::System.AttributeTargets.Property | + global::System.AttributeTargets.ReturnValue, + Inherited = false)] + internal sealed class NotNullAttribute : global::System.Attribute + { + } +} +#endif \ No newline at end of file From 3ab6676c538bd61776e71620d7c2c4ec323f752f Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 27 Nov 2023 21:54:00 +0200 Subject: [PATCH 284/549] [New] ApiRequestMessage & ApiRequestMessageContent --- src/redmine-net-api/Net/ApiRequestMessage.cs | 14 ++++++++++++++ .../Net/ApiRequestMessageContent.cs | 8 ++++++++ 2 files changed, 22 insertions(+) create mode 100644 src/redmine-net-api/Net/ApiRequestMessage.cs create mode 100644 src/redmine-net-api/Net/ApiRequestMessageContent.cs diff --git a/src/redmine-net-api/Net/ApiRequestMessage.cs b/src/redmine-net-api/Net/ApiRequestMessage.cs new file mode 100644 index 00000000..b4a2e11e --- /dev/null +++ b/src/redmine-net-api/Net/ApiRequestMessage.cs @@ -0,0 +1,14 @@ +using System.Collections.Specialized; + +namespace Redmine.Net.Api.Net; + +internal sealed class ApiRequestMessage +{ + public ApiRequestMessageContent Content { get; set; } + public string Method { get; set; } = HttpVerbs.GET; + public string RequestUri { get; set; } + public NameValueCollection QueryString { get; set; } + public string ImpersonateUser { get; set; } + + public string ContentType { get; set; } +} \ No newline at end of file diff --git a/src/redmine-net-api/Net/ApiRequestMessageContent.cs b/src/redmine-net-api/Net/ApiRequestMessageContent.cs new file mode 100644 index 00000000..f3e6fb48 --- /dev/null +++ b/src/redmine-net-api/Net/ApiRequestMessageContent.cs @@ -0,0 +1,8 @@ +namespace Redmine.Net.Api.Net; + +internal abstract class ApiRequestMessageContent +{ + public string ContentType { get; internal set; } + + public byte[] Body { get; internal set; } +} \ No newline at end of file From 941d63559a3d62c1c9a5a33ec3c14f5362c260dc Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 27 Nov 2023 21:56:01 +0200 Subject: [PATCH 285/549] [New] [WebClient] Byte|String|Stream message content --- .../ByteArrayApiRequestMessageContent.cs | 9 +++++++ .../StreamApiRequestMessageContent.cs | 9 +++++++ .../StringApiRequestMessageContent.cs | 24 +++++++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 src/redmine-net-api/Net/WebClient/ByteArrayApiRequestMessageContent.cs create mode 100644 src/redmine-net-api/Net/WebClient/StreamApiRequestMessageContent.cs create mode 100644 src/redmine-net-api/Net/WebClient/StringApiRequestMessageContent.cs diff --git a/src/redmine-net-api/Net/WebClient/ByteArrayApiRequestMessageContent.cs b/src/redmine-net-api/Net/WebClient/ByteArrayApiRequestMessageContent.cs new file mode 100644 index 00000000..66650e04 --- /dev/null +++ b/src/redmine-net-api/Net/WebClient/ByteArrayApiRequestMessageContent.cs @@ -0,0 +1,9 @@ +namespace Redmine.Net.Api.Net.WebClient; + +internal class ByteArrayApiRequestMessageContent : ApiRequestMessageContent +{ + public ByteArrayApiRequestMessageContent(byte[] content) + { + Body = content; + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Net/WebClient/StreamApiRequestMessageContent.cs b/src/redmine-net-api/Net/WebClient/StreamApiRequestMessageContent.cs new file mode 100644 index 00000000..c04820df --- /dev/null +++ b/src/redmine-net-api/Net/WebClient/StreamApiRequestMessageContent.cs @@ -0,0 +1,9 @@ +namespace Redmine.Net.Api.Net.WebClient; + +internal sealed class StreamApiRequestMessageContent : ByteArrayApiRequestMessageContent +{ + public StreamApiRequestMessageContent(byte[] content) : base(content) + { + ContentType = RedmineConstants.CONTENT_TYPE_APPLICATION_STREAM; + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Net/WebClient/StringApiRequestMessageContent.cs b/src/redmine-net-api/Net/WebClient/StringApiRequestMessageContent.cs new file mode 100644 index 00000000..80b77fe5 --- /dev/null +++ b/src/redmine-net-api/Net/WebClient/StringApiRequestMessageContent.cs @@ -0,0 +1,24 @@ +using System; +using System.Text; + +namespace Redmine.Net.Api.Net.WebClient; + +internal sealed class StringApiRequestMessageContent : ByteArrayApiRequestMessageContent +{ + private static readonly Encoding DefaultStringEncoding = Encoding.UTF8; + + public StringApiRequestMessageContent(string content, string mediaType) : this(content, mediaType, DefaultStringEncoding) + { + } + + public StringApiRequestMessageContent(string content, string mediaType, Encoding encoding) : base(GetContentByteArray(content, encoding)) + { + ContentType = mediaType; + } + + private static byte[] GetContentByteArray(string content, Encoding encoding) + { + if (content == null) throw new ArgumentNullException(nameof(content)); + return (encoding ?? DefaultStringEncoding).GetBytes(content); + } +} \ No newline at end of file From 0da74dc0b2cac68dd2156c21083d4167cf096774 Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 27 Nov 2023 21:57:21 +0200 Subject: [PATCH 286/549] [New] ApiResponseMessage --- src/redmine-net-api/Net/ApiResponseMessage.cs | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/redmine-net-api/Net/ApiResponseMessage.cs diff --git a/src/redmine-net-api/Net/ApiResponseMessage.cs b/src/redmine-net-api/Net/ApiResponseMessage.cs new file mode 100644 index 00000000..fc644ee5 --- /dev/null +++ b/src/redmine-net-api/Net/ApiResponseMessage.cs @@ -0,0 +1,9 @@ +using System.Collections.Specialized; + +namespace Redmine.Net.Api.Net; + +internal sealed class ApiResponseMessage +{ + public NameValueCollection Headers { get; init; } + public byte[] Content { get; set; } +} \ No newline at end of file From 49c0727ba4ea8d545ddea14aa5bf6bfc82a121d0 Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 27 Nov 2023 21:58:59 +0200 Subject: [PATCH 287/549] [HttpVerbs] Add internal DOWNLOAD & UPLOAD verbs --- src/redmine-net-api/Net/HttpVerbs.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/redmine-net-api/Net/HttpVerbs.cs b/src/redmine-net-api/Net/HttpVerbs.cs index 83c65de5..bcd88271 100644 --- a/src/redmine-net-api/Net/HttpVerbs.cs +++ b/src/redmine-net-api/Net/HttpVerbs.cs @@ -42,5 +42,10 @@ public static class HttpVerbs /// Represents an HTTP DELETE protocol method that is used to delete an existing entity identified by a URI. /// public const string DELETE = "DELETE"; + + + internal const string DOWNLOAD = "DOWNLOAD"; + + internal const string UPLOAD = "UPLOAD"; } } \ No newline at end of file From 9bbca14caa6d5c62d1f2ad5f614bca0456465e16 Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 27 Nov 2023 21:59:20 +0200 Subject: [PATCH 288/549] [New] RequestOptions --- src/redmine-net-api/Net/RequestOptions.cs | 30 +++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/redmine-net-api/Net/RequestOptions.cs diff --git a/src/redmine-net-api/Net/RequestOptions.cs b/src/redmine-net-api/Net/RequestOptions.cs new file mode 100644 index 00000000..eaaef3b8 --- /dev/null +++ b/src/redmine-net-api/Net/RequestOptions.cs @@ -0,0 +1,30 @@ +using System.Collections.Specialized; + +namespace Redmine.Net.Api.Net; + +/// +/// +/// +public sealed class RequestOptions +{ + /// + /// + /// + public NameValueCollection QueryString { get; set; } + /// + /// + /// + public string ImpersonateUser { get; set; } + /// + /// + /// + public string ContentType { get; set; } + /// + /// + /// + public string Accept { get; set; } + /// + /// + /// + public string UserAgent { get; set; } +} \ No newline at end of file From c22df68ff483a2c9693d769c78411b4d9f735745 Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 27 Nov 2023 22:00:21 +0200 Subject: [PATCH 289/549] [RedmineConstants] Add IMPERSONATE_HEADER_KEY --- src/redmine-net-api/RedmineConstants.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/redmine-net-api/RedmineConstants.cs b/src/redmine-net-api/RedmineConstants.cs index 6aa78d05..dea7584a 100644 --- a/src/redmine-net-api/RedmineConstants.cs +++ b/src/redmine-net-api/RedmineConstants.cs @@ -8,7 +8,7 @@ public static class RedmineConstants /// /// /// - public const string OBSOLETE_TEXT = "In next major release, it will no longer be available."; + internal const string OBSOLETE_TEXT = "In next major release, it will no longer be available."; /// /// /// @@ -26,5 +26,10 @@ public static class RedmineConstants /// /// public const string CONTENT_TYPE_APPLICATION_STREAM = "application/octet-stream"; + + /// + /// + /// + public const string IMPERSONATE_HEADER_KEY = "X-Redmine-Switch-User"; } } \ No newline at end of file From 4ea6b5f971947a3c1487f04e84dcbfe8c49744a6 Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 27 Nov 2023 22:01:17 +0200 Subject: [PATCH 290/549] [IRedmineApiClientOptions] Add ClientCertificates & ReusePort --- .../Net/IRedmineApiClientOptions.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/redmine-net-api/Net/IRedmineApiClientOptions.cs b/src/redmine-net-api/Net/IRedmineApiClientOptions.cs index 23202f37..56013ec2 100644 --- a/src/redmine-net-api/Net/IRedmineApiClientOptions.cs +++ b/src/redmine-net-api/Net/IRedmineApiClientOptions.cs @@ -5,7 +5,7 @@ using System.Net.Security; using System.Security.Cryptography.X509Certificates; -namespace Redmine.Net.Api +namespace Redmine.Net.Api.Net { /// /// @@ -162,5 +162,19 @@ public interface IRedmineApiClientOptions /// /// SecurityProtocolType? SecurityProtocolType { get; set; } + + #if NET40_OR_GREATER || NETCOREAPP + /// + /// + /// + public X509CertificateCollection ClientCertificates { get; set; } + #endif + + #if(NET46_OR_GREATER || NETCOREAPP) + /// + /// + /// + public bool? ReusePort { get; set; } + #endif } } \ No newline at end of file From 597af841bac220644821d024eb224296eb551b39 Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 27 Nov 2023 22:03:14 +0200 Subject: [PATCH 291/549] [RedmineManagerOptions] Change BaseAddress type from string to Uri --- src/redmine-net-api/RedmineManagerOptions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/redmine-net-api/RedmineManagerOptions.cs b/src/redmine-net-api/RedmineManagerOptions.cs index aa35b07f..95acc4f6 100644 --- a/src/redmine-net-api/RedmineManagerOptions.cs +++ b/src/redmine-net-api/RedmineManagerOptions.cs @@ -1,4 +1,5 @@ using System; +using Redmine.Net.Api.Net; using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api @@ -11,7 +12,7 @@ internal sealed class RedmineManagerOptions /// /// /// - public string BaseAddress { get; init; } + public Uri BaseAddress { get; init; } /// /// Gets or sets the page size for paginated Redmine API responses. From 252b39b72a36e076eed175c3232535cc39ee4685 Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 27 Nov 2023 22:09:28 +0200 Subject: [PATCH 292/549] [Improvements] Small improvements & clean up --- .../Features/IsExternalInit.cs | 1 + src/redmine-net-api/Internals/UrlHelper.cs | 28 +++++++++---------- src/redmine-net-api/Net/RedirectType.cs | 2 +- .../NameValueCollectionExtensions.cs | 11 ++++---- .../Net/WebClient/Extensions/WebExtensions.cs | 27 ++++++++++++------ src/redmine-net-api/SearchFilterBuilder.cs | 2 +- 6 files changed, 41 insertions(+), 30 deletions(-) diff --git a/src/redmine-net-api/Features/IsExternalInit.cs b/src/redmine-net-api/Features/IsExternalInit.cs index 6490f153..0de2af11 100644 --- a/src/redmine-net-api/Features/IsExternalInit.cs +++ b/src/redmine-net-api/Features/IsExternalInit.cs @@ -12,6 +12,7 @@ internal static class IsExternalInit { } } + #endif diff --git a/src/redmine-net-api/Internals/UrlHelper.cs b/src/redmine-net-api/Internals/UrlHelper.cs index a1867e3b..0b18b0ef 100644 --- a/src/redmine-net-api/Internals/UrlHelper.cs +++ b/src/redmine-net-api/Internals/UrlHelper.cs @@ -82,9 +82,9 @@ public static string GetUploadUrl(RedmineManager redmineManager, string id) { var type = typeof(T); - if (!RedmineManager.Suffixes.ContainsKey(type)) throw new KeyNotFoundException(type.Name); + if (!RedmineManager.Suffixes.TryGetValue(type, out string value)) throw new KeyNotFoundException(type.Name); - return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, RedmineManager.Suffixes[type], id, + return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, value, id, redmineManager.Format); } @@ -105,19 +105,19 @@ public static string GetUploadUrl(RedmineManager redmineManager, string id) { var type = typeof(T); - if (!RedmineManager.Suffixes.ContainsKey(type)) throw new KeyNotFoundException(type.Name); + if (!RedmineManager.Suffixes.TryGetValue(type, out string value)) throw new KeyNotFoundException(type.Name); if (type == typeof(Version) || type == typeof(IssueCategory) || type == typeof(ProjectMembership)) { if (string.IsNullOrEmpty(ownerId)) throw new RedmineException("The owner id(project id) is mandatory!"); return string.Format(CultureInfo.InvariantCulture,ENTITY_WITH_PARENT_FORMAT, redmineManager.Host, RedmineKeys.PROJECTS, - ownerId, RedmineManager.Suffixes[type], redmineManager.Format); + ownerId, value, redmineManager.Format); } if (type == typeof(IssueRelation)) { if (string.IsNullOrEmpty(ownerId)) throw new RedmineException("The owner id(issue id) is mandatory!"); return string.Format(CultureInfo.InvariantCulture,ENTITY_WITH_PARENT_FORMAT, redmineManager.Host, RedmineKeys.ISSUES, - ownerId, RedmineManager.Suffixes[type], redmineManager.Format); + ownerId, value, redmineManager.Format); } if (type == typeof(File)) @@ -129,7 +129,7 @@ public static string GetUploadUrl(RedmineManager redmineManager, string id) return string.Format(CultureInfo.InvariantCulture,FILE_URL_FORMAT, redmineManager.Host, ownerId, redmineManager.Format); } - return string.Format(CultureInfo.InvariantCulture,FORMAT, redmineManager.Host, RedmineManager.Suffixes[type], + return string.Format(CultureInfo.InvariantCulture,FORMAT, redmineManager.Host, value, redmineManager.Format); } @@ -146,9 +146,9 @@ public static string GetUploadUrl(RedmineManager redmineManager, string id) { var type = typeof(T); - if (!RedmineManager.Suffixes.ContainsKey(type)) throw new KeyNotFoundException(type.Name); + if (!RedmineManager.Suffixes.TryGetValue(type, out string value)) throw new KeyNotFoundException(type.Name); - return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, RedmineManager.Suffixes[type], id, + return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, value, id, redmineManager.Format); } @@ -164,9 +164,9 @@ public static string GetUploadUrl(RedmineManager redmineManager, string id) { var type = typeof(T); - if (!RedmineManager.Suffixes.ContainsKey(type)) throw new KeyNotFoundException(type.Name); + if (!RedmineManager.Suffixes.TryGetValue(type, out string value)) throw new KeyNotFoundException(type.Name); - return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, RedmineManager.Suffixes[type], id, + return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, value, id, redmineManager.Format); } @@ -188,7 +188,7 @@ public static string GetListUrl(RedmineManager redmineManager, NameValueColle { var type = typeof(T); - if (!RedmineManager.Suffixes.ContainsKey(type)) throw new KeyNotFoundException(type.Name); + if (!RedmineManager.Suffixes.TryGetValue(type, out string value)) throw new KeyNotFoundException(type.Name); if (type == typeof(Version) || type == typeof(IssueCategory) || type == typeof(ProjectMembership)) { @@ -197,7 +197,7 @@ public static string GetListUrl(RedmineManager redmineManager, NameValueColle throw new RedmineException("The project id is mandatory! \nCheck if you have included the parameter project_id to parameters."); return string.Format(CultureInfo.InvariantCulture,ENTITY_WITH_PARENT_FORMAT, redmineManager.Host, RedmineKeys.PROJECTS, - projectId, RedmineManager.Suffixes[type], redmineManager.Format); + projectId, value, redmineManager.Format); } if (type == typeof(IssueRelation)) { @@ -206,7 +206,7 @@ public static string GetListUrl(RedmineManager redmineManager, NameValueColle throw new RedmineException("The issue id is mandatory! \nCheck if you have included the parameter issue_id to parameters"); return string.Format(CultureInfo.InvariantCulture,ENTITY_WITH_PARENT_FORMAT, redmineManager.Host, RedmineKeys.ISSUES, - issueId, RedmineManager.Suffixes[type], redmineManager.Format); + issueId, value, redmineManager.Format); } if (type == typeof(File)) @@ -219,7 +219,7 @@ public static string GetListUrl(RedmineManager redmineManager, NameValueColle return string.Format(CultureInfo.InvariantCulture,FILE_URL_FORMAT, redmineManager.Host, projectId, redmineManager.Format); } - return string.Format(CultureInfo.InvariantCulture,FORMAT, redmineManager.Host, RedmineManager.Suffixes[type], + return string.Format(CultureInfo.InvariantCulture,FORMAT, redmineManager.Host, value, redmineManager.Format); } diff --git a/src/redmine-net-api/Net/RedirectType.cs b/src/redmine-net-api/Net/RedirectType.cs index ead5ed4a..7793e23c 100644 --- a/src/redmine-net-api/Net/RedirectType.cs +++ b/src/redmine-net-api/Net/RedirectType.cs @@ -33,5 +33,5 @@ public enum RedirectType /// /// All - }; + } } \ No newline at end of file diff --git a/src/redmine-net-api/Net/WebClient/Extensions/NameValueCollectionExtensions.cs b/src/redmine-net-api/Net/WebClient/Extensions/NameValueCollectionExtensions.cs index 1ce0a13d..48391fa3 100644 --- a/src/redmine-net-api/Net/WebClient/Extensions/NameValueCollectionExtensions.cs +++ b/src/redmine-net-api/Net/WebClient/Extensions/NameValueCollectionExtensions.cs @@ -73,19 +73,20 @@ public static string ToQueryString(this NameValueCollection requestParameters) return null; } + var delimiter = string.Empty; + var stringBuilder = new StringBuilder(); for (var index = 0; index < requestParameters.Count; ++index) { stringBuilder + .Append(delimiter) .Append(requestParameters.AllKeys[index].ToString(CultureInfo.InvariantCulture)) .Append('=') - .Append(requestParameters[index].ToString(CultureInfo.InvariantCulture)) - .Append('&'); + .Append(requestParameters[index].ToString(CultureInfo.InvariantCulture)); + delimiter = "&"; } - - stringBuilder.Length -= 1; - + var queryString = stringBuilder.ToString(); stringBuilder.Length = 0; diff --git a/src/redmine-net-api/Net/WebClient/Extensions/WebExtensions.cs b/src/redmine-net-api/Net/WebClient/Extensions/WebExtensions.cs index 493a07c9..9d454562 100644 --- a/src/redmine-net-api/Net/WebClient/Extensions/WebExtensions.cs +++ b/src/redmine-net-api/Net/WebClient/Extensions/WebExtensions.cs @@ -18,6 +18,7 @@ limitations under the License. using System.Collections.Generic; using System.IO; using System.Net; +using System.Text; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Types; @@ -78,18 +79,26 @@ public static void HandleWebException(this WebException exception, IRedmineSeria throw new ConflictException("The page that you are trying to update is staled!", innerException); case 422: + RedmineException redmineException; var errors = GetRedmineExceptions(exception.Response, serializer); - var message = string.Empty; - - if (errors == null) - throw new RedmineException($"Invalid or missing attribute parameters: {message}", innerException); - - foreach (var error in errors) + + if (errors != null) + { + var sb = new StringBuilder(); + foreach (var error in errors) + { + sb.Append(error.Info).Append(Environment.NewLine); + } + + redmineException = new RedmineException($"Invalid or missing attribute parameters: {sb}", innerException, "Unprocessable Content"); + sb.Length = 0; + } + else { - message = message + error.Info + Environment.NewLine; + redmineException = new RedmineException("Invalid or missing attribute parameters", innerException); } - - throw new RedmineException("Invalid or missing attribute parameters: " + message, innerException); + + throw redmineException; case (int)HttpStatusCode.NotAcceptable: throw new NotAcceptableException(response.StatusDescription, innerException); diff --git a/src/redmine-net-api/SearchFilterBuilder.cs b/src/redmine-net-api/SearchFilterBuilder.cs index d4c6e9fd..fe2bc806 100644 --- a/src/redmine-net-api/SearchFilterBuilder.cs +++ b/src/redmine-net-api/SearchFilterBuilder.cs @@ -50,7 +50,7 @@ public SearchScope? Scope _internalScope = "subprojects"; break; default: - throw new ArgumentOutOfRangeException(); + throw new ArgumentOutOfRangeException(nameof(value)); } } } From 4d44ba7fa9b7dac77605e09f85ec00808819a105 Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 28 Nov 2023 22:29:50 +0200 Subject: [PATCH 293/549] [RedmineKeys] Add keys used by SearchFilterBuilder --- src/redmine-net-api/RedmineKeys.cs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/redmine-net-api/RedmineKeys.cs b/src/redmine-net-api/RedmineKeys.cs index ba82b08f..f5863407 100644 --- a/src/redmine-net-api/RedmineKeys.cs +++ b/src/redmine-net-api/RedmineKeys.cs @@ -43,6 +43,10 @@ public static class RedmineKeys /// /// /// + public const string ALL_WORDS = "all_words"; + /// + /// + /// public const string ALLOWED_STATUSES = "allowed_statuses"; /// /// @@ -216,6 +220,10 @@ public static class RedmineKeys /// /// /// + public const string DOCUMENTS = "documents"; + /// + /// + /// public const string DOWNLOADS = "downloads"; /// /// @@ -455,6 +463,10 @@ public static class RedmineKeys /// /// /// + public const string MESSAGES = "messages"; + /// + /// + /// public const string MIN_LENGTH = "min_length"; /// /// @@ -491,6 +503,10 @@ public static class RedmineKeys /// /// /// + public const string OPEN_ISSUES = "open_issues"; + /// + /// + /// public const string PARENT = "parent"; /// /// @@ -611,6 +627,10 @@ public static class RedmineKeys /// /// /// + public const string SCOPE = "scope"; + /// + /// + /// public const string SEARCHABLE = "searchable"; /// /// @@ -687,6 +707,10 @@ public static class RedmineKeys /// /// /// + public const string TITLES_ONLY = "titles_only"; + /// + /// + /// public const string THUMBNAIL_URL = "thumbnail_url"; /// /// From 9f21631fb597d4f3482dbc3a30f8a273be5fcfaf Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 28 Nov 2023 22:31:06 +0200 Subject: [PATCH 294/549] [New] RedmineSerializerFactory --- .../Serialization/RedmineSerializerFactory.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/redmine-net-api/Serialization/RedmineSerializerFactory.cs diff --git a/src/redmine-net-api/Serialization/RedmineSerializerFactory.cs b/src/redmine-net-api/Serialization/RedmineSerializerFactory.cs new file mode 100644 index 00000000..d371f59b --- /dev/null +++ b/src/redmine-net-api/Serialization/RedmineSerializerFactory.cs @@ -0,0 +1,19 @@ +using System; + +namespace Redmine.Net.Api.Serialization; + +/// +/// Factory for creating RedmineSerializer instances +/// +internal static class RedmineSerializerFactory +{ + public static IRedmineSerializer CreateSerializer(SerializationType type) + { + return type switch + { + SerializationType.Xml => new XmlRedmineSerializer(), + SerializationType.Json => new JsonRedmineSerializer(), + _ => throw new NotImplementedException($"No serializer has been implemented for the serialization type: {type}") + }; + } +} \ No newline at end of file From ce37abed412db333149265d02f734e2ee7f5a193 Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 28 Nov 2023 22:31:41 +0200 Subject: [PATCH 295/549] [New] RedmineApiUrls --- src/redmine-net-api/Net/RedmineApiUrls.cs | 192 ++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 src/redmine-net-api/Net/RedmineApiUrls.cs diff --git a/src/redmine-net-api/Net/RedmineApiUrls.cs b/src/redmine-net-api/Net/RedmineApiUrls.cs new file mode 100644 index 00000000..4b3e5470 --- /dev/null +++ b/src/redmine-net-api/Net/RedmineApiUrls.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; +using Version = Redmine.Net.Api.Types.Version; + +namespace Redmine.Net.Api.Net +{ + internal sealed class RedmineApiUrls + { + public string Format { get; init; } + + private static readonly Dictionary TypeUrlFragments = new Dictionary() + { + {typeof(Attachment), RedmineKeys.ATTACHMENTS}, + {typeof(CustomField), RedmineKeys.CUSTOM_FIELDS}, + {typeof(Group), RedmineKeys.GROUPS}, + {typeof(Issue), RedmineKeys.ISSUES}, + {typeof(IssueCategory), RedmineKeys.ISSUE_CATEGORIES}, + {typeof(IssueCustomField), RedmineKeys.CUSTOM_FIELDS}, + {typeof(IssuePriority), RedmineKeys.ENUMERATION_ISSUE_PRIORITIES}, + {typeof(IssueRelation), RedmineKeys.RELATIONS}, + {typeof(IssueStatus), RedmineKeys.ISSUE_STATUSES}, + {typeof(Journal), RedmineKeys.JOURNALS}, + {typeof(News), RedmineKeys.NEWS}, + {typeof(Project), RedmineKeys.PROJECTS}, + {typeof(ProjectMembership), RedmineKeys.MEMBERSHIPS}, + {typeof(Query), RedmineKeys.QUERIES}, + {typeof(Role), RedmineKeys.ROLES}, + {typeof(Search), RedmineKeys.SEARCH}, + {typeof(TimeEntry), RedmineKeys.TIME_ENTRIES}, + {typeof(TimeEntryActivity), RedmineKeys.ENUMERATION_TIME_ENTRY_ACTIVITIES}, + {typeof(Tracker), RedmineKeys.TRACKERS}, + {typeof(User), RedmineKeys.USERS}, + {typeof(Version), RedmineKeys.VERSIONS}, + {typeof(Watcher), RedmineKeys.WATCHERS}, + }; + + public RedmineApiUrls(string format) + { + Format = format; + } + + public string ProjectFilesFragment(string projectId) + { + if (string.IsNullOrEmpty(projectId)) + { + throw new RedmineException("The owner id(project id) is mandatory!"); + } + + return $"{RedmineKeys.PROJECTS}/{projectId}/{RedmineKeys.FILES}.{Format}"; + } + + public string IssueAttachmentFragment(string issueId) + { + if (issueId.IsNullOrWhiteSpace()) + { + throw new RedmineException("The issue id is mandatory!"); + } + + return $"/{RedmineKeys.ATTACHMENTS}/{RedmineKeys.ISSUES}/{issueId}.{Format}"; + } + + public string ProjectParentFragment(string projectId, string mapTypeFragment) + { + if (string.IsNullOrEmpty(projectId)) + { + throw new RedmineException("The owner project id is mandatory!"); + } + + return $"{RedmineKeys.PROJECTS}/{projectId}/{mapTypeFragment}.{Format}"; + } + + public string IssueParentFragment(string issueId, string mapTypeFragment) + { + if (string.IsNullOrEmpty(issueId)) + { + throw new RedmineException("The owner issue id is mandatory!"); + } + + return $"{RedmineKeys.ISSUES}/{issueId}/{mapTypeFragment}.{Format}"; + } + + public static string TypeFragment(Dictionary mapTypeUrlFragments, Type type) + { + if (!mapTypeUrlFragments.TryGetValue(type, out var fragment)) + { + throw new RedmineException($"There is no uri fragment defined for type {type.Name}"); + } + + return fragment; + } + + public string CreateEntityFragment(string ownerId = null) + { + var type = typeof(T); + + if (type == typeof(Version) || type == typeof(IssueCategory) || type == typeof(ProjectMembership)) + { + return ProjectParentFragment(ownerId, TypeUrlFragments[type]); + } + + if (type == typeof(IssueRelation)) + { + return IssueParentFragment(ownerId, TypeUrlFragments[type]); + } + + if (type == typeof(File)) + { + return ProjectFilesFragment(ownerId); + } + + if (type == typeof(Upload)) + { + return RedmineKeys.UPLOADS; + } + + if (type == typeof(Attachment) || type == typeof(Attachments)) + { + return IssueAttachmentFragment(ownerId); + } + + return $"{TypeFragment(TypeUrlFragments, type)}.{Format}"; + } + + public string GetFragment(string id) where T : class, new() + { + var type = typeof(T); + + return $"{TypeFragment(TypeUrlFragments, type)}/{id}.{Format}"; + } + + public string PatchFragment(string ownerId) + { + var type = typeof(T); + + if (type == typeof(Attachment) || type == typeof(Attachments)) + { + return IssueAttachmentFragment(ownerId); + } + + throw new RedmineException($"No endpoint defined for type {type} for PATCH operation."); + } + + public string DeleteFragment(string id) + { + var type = typeof(T); + + return $"{TypeFragment(TypeUrlFragments, type)}/{id}.{Format}"; + } + + public string UpdateFragment(string id) + { + var type = typeof(T); + + return $"{TypeFragment(TypeUrlFragments, type)}/{id}.{Format}"; + } + + public string GetListFragment(string ownerId = null) where T : class, new() + { + var type = typeof(T); + + return GetListFragment(type, ownerId); + } + + public string GetListFragment(Type type, string ownerId = null) + { + if (type == typeof(Version) || type == typeof(IssueCategory) || type == typeof(ProjectMembership)) + { + return ProjectParentFragment(ownerId, TypeUrlFragments[type]); + } + + if (type == typeof(IssueRelation)) + { + return IssueParentFragment(ownerId, TypeUrlFragments[type]); + } + + if (type == typeof(File)) + { + return ProjectFilesFragment(ownerId); + } + + return $"{TypeFragment(TypeUrlFragments, type)}.{Format}"; + } + + public string UploadFragment() + { + return $"{RedmineKeys.UPLOADS}.{Format}"; + } + } +} \ No newline at end of file From e4d6e60d4c1116cf05fb62ede6c109111970a341 Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 28 Nov 2023 22:31:55 +0200 Subject: [PATCH 296/549] [New] RedmineApiUrlsExtensions --- .../Net/RedmineApiUrlsExtensions.cs | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs diff --git a/src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs b/src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs new file mode 100644 index 00000000..4ccd918b --- /dev/null +++ b/src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs @@ -0,0 +1,137 @@ +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; + +namespace Redmine.Net.Api.Net; + +internal static class RedmineApiUrlsExtensions +{ + public static string MyAccount(this RedmineApiUrls redmineApiUrls) + { + return $"my/account.{redmineApiUrls.Format}"; + } + + public static string CurrentUser(this RedmineApiUrls redmineApiUrls) + { + return $"{RedmineKeys.CURRENT}.{redmineApiUrls.Format}"; + } + + public static string ProjectNews(this RedmineApiUrls redmineApiUrls, string projectIdentifier) + { + if (projectIdentifier.IsNullOrWhiteSpace()) + { + throw new RedmineException($"Argument '{nameof(projectIdentifier)}' is null or whitespace"); + } + + return $"{RedmineKeys.PROJECT}/{projectIdentifier}/{RedmineKeys.NEWS}.{redmineApiUrls.Format}"; + } + + public static string ProjectMemberships(this RedmineApiUrls redmineApiUrls, string projectIdentifier) + { + if (projectIdentifier.IsNullOrWhiteSpace()) + { + throw new RedmineException($"Argument '{nameof(projectIdentifier)}' is null or whitespace"); + } + + return $"{RedmineKeys.PROJECT}/{projectIdentifier}/{RedmineKeys.MEMBERSHIPS}.{redmineApiUrls.Format}"; + } + + public static string ProjectWikiIndex(this RedmineApiUrls redmineApiUrls, string projectId) + { + return $"{RedmineKeys.PROJECTS}/{projectId}/{RedmineKeys.WIKI}/index.{redmineApiUrls.Format}"; + } + + public static string ProjectWikiPage(this RedmineApiUrls redmineApiUrls, string projectId, string wikiPageName) + { + return $"{RedmineKeys.PROJECTS}/{projectId}/{RedmineKeys.WIKI}/{wikiPageName}.{redmineApiUrls.Format}"; + } + + public static string ProjectWikiPageVersion(this RedmineApiUrls redmineApiUrls, string projectId, string wikiPageName, string version) + { + return $"{RedmineKeys.PROJECTS}/{projectId}/{RedmineKeys.WIKI}/{wikiPageName}/{version}.{redmineApiUrls.Format}"; + } + + public static string ProjectWikiPageCreate(this RedmineApiUrls redmineApiUrls, string projectId, string wikiPageName) + { + return $"{RedmineKeys.PROJECTS}/{projectId}/{RedmineKeys.WIKI}/{wikiPageName}.{redmineApiUrls.Format}"; + } + + public static string ProjectWikiPageUpdate(this RedmineApiUrls redmineApiUrls, string projectId, string wikiPageName) + { + return $"{RedmineKeys.PROJECTS}/{projectId}/{RedmineKeys.WIKI}/{wikiPageName}.{redmineApiUrls.Format}"; + } + + public static string ProjectWikiPageDelete(this RedmineApiUrls redmineApiUrls, string projectId, string wikiPageName) + { + return $"{RedmineKeys.PROJECTS}/{projectId}/{RedmineKeys.WIKI}/{wikiPageName}.{redmineApiUrls.Format}"; + } + + public static string ProjectWikis(this RedmineApiUrls redmineApiUrls, string projectId) + { + return ProjectWikiIndex(redmineApiUrls, projectId); + } + + public static string IssueWatcherAdd(this RedmineApiUrls redmineApiUrls, string issueIdentifier) + { + if (issueIdentifier.IsNullOrWhiteSpace()) + { + throw new RedmineException($"Argument '{nameof(issueIdentifier)}' is null or whitespace"); + } + + return $"{RedmineKeys.ISSUES}/{issueIdentifier}/{RedmineKeys.WATCHERS}.{redmineApiUrls.Format}"; + } + + public static string IssueWatcherRemove(this RedmineApiUrls redmineApiUrls, string issueIdentifier, string userId) + { + if (issueIdentifier.IsNullOrWhiteSpace()) + { + throw new RedmineException($"Argument '{nameof(issueIdentifier)}' is null or whitespace"); + } + + if (userId.IsNullOrWhiteSpace()) + { + throw new RedmineException($"Argument '{nameof(userId)}' is null or whitespace"); + } + + return $"{RedmineKeys.ISSUES}/{issueIdentifier}/{RedmineKeys.WATCHERS}/{userId}.{redmineApiUrls.Format}"; + } + + public static string GroupUserAdd(this RedmineApiUrls redmineApiUrls, string groupIdentifier) + { + if (groupIdentifier.IsNullOrWhiteSpace()) + { + throw new RedmineException($"Argument '{nameof(groupIdentifier)}' is null or whitespace"); + } + + return $"{RedmineKeys.GROUPS}/{groupIdentifier}/{RedmineKeys.USERS}.{redmineApiUrls.Format}"; + } + + public static string GroupUserRemove(this RedmineApiUrls redmineApiUrls, string groupIdentifier, string userId) + { + if (groupIdentifier.IsNullOrWhiteSpace()) + { + throw new RedmineException($"Argument '{nameof(groupIdentifier)}' is null or whitespace"); + } + + if (userId.IsNullOrWhiteSpace()) + { + throw new RedmineException($"Argument '{nameof(userId)}' is null or whitespace"); + } + + return $"{RedmineKeys.GROUPS}/{groupIdentifier}/{RedmineKeys.USERS}/{userId}.{redmineApiUrls.Format}"; + } + + public static string AttachmentUpdate(this RedmineApiUrls redmineApiUrls, string issueIdentifier) + { + if (issueIdentifier.IsNullOrWhiteSpace()) + { + throw new RedmineException($"Argument '{nameof(issueIdentifier)}' is null or whitespace"); + } + + return $"{RedmineKeys.ATTACHMENTS}/{RedmineKeys.ISSUES}/{issueIdentifier}.{redmineApiUrls.Format}"; + } + + public static string Uploads(this RedmineApiUrls redmineApiUrls) + { + return $"{RedmineKeys.UPLOADS}.{redmineApiUrls.Format}"; + } +} \ No newline at end of file From 49ef45d8f8e0defeba80db3057761197b8256e4c Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 28 Nov 2023 22:33:38 +0200 Subject: [PATCH 297/549] [NameValueCollectionExtensions] More extensions added --- .../NameValueCollectionExtensions.cs | 64 +++++++++++++++---- 1 file changed, 53 insertions(+), 11 deletions(-) diff --git a/src/redmine-net-api/Net/WebClient/Extensions/NameValueCollectionExtensions.cs b/src/redmine-net-api/Net/WebClient/Extensions/NameValueCollectionExtensions.cs index 48391fa3..7e3420b0 100644 --- a/src/redmine-net-api/Net/WebClient/Extensions/NameValueCollectionExtensions.cs +++ b/src/redmine-net-api/Net/WebClient/Extensions/NameValueCollectionExtensions.cs @@ -33,30 +33,23 @@ public static class NameValueCollectionExtensions /// public static string GetParameterValue(this NameValueCollection parameters, string parameterName) { - if (parameters == null) - { - return null; - } - - var value = parameters.Get(parameterName); - - return value.IsNullOrWhiteSpace() ? null : value; + return GetValue(parameters, parameterName); } /// /// Gets the parameter value. /// /// The parameters. - /// Name of the parameter. + /// Name of the parameter. /// - public static string GetValue(this NameValueCollection parameters, string parameterName) + public static string GetValue(this NameValueCollection parameters, string key) { if (parameters == null) { return null; } - var value = parameters.Get(parameterName); + var value = parameters.Get(key); return value.IsNullOrWhiteSpace() ? null : value; } @@ -93,6 +86,55 @@ public static string ToQueryString(this NameValueCollection requestParameters) return queryString; } + + internal static NameValueCollection AddPagingParameters(this NameValueCollection parameters, int pageSize, int offset) + { + parameters ??= new NameValueCollection(); + + if(pageSize <= 0) + { + pageSize = RedmineConstants.DEFAULT_PAGE_SIZE_VALUE; + } + + if(offset < 0) + { + offset = 0; + } + + parameters.Set(RedmineKeys.LIMIT, pageSize.ToString(CultureInfo.InvariantCulture)); + parameters.Set(RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)); + + return parameters; + } + + internal static NameValueCollection AddParamsIfExist(this NameValueCollection parameters, string[] include) + { + if (include is not {Length: > 0}) + { + return parameters; + } + + parameters ??= new NameValueCollection(); + parameters.Add(RedmineKeys.INCLUDE, string.Join(",", include)); + + return parameters; + } + + internal static void AddIfNotNull(this NameValueCollection nameValueCollection, string key, string value) + { + if (!value.IsNullOrWhiteSpace()) + { + nameValueCollection.Add(key, value); + } + } + + internal static void AddIfNotNull(this NameValueCollection nameValueCollection, string key, bool? value) + { + if (value.HasValue) + { + nameValueCollection.Add(key, value.Value.ToString(CultureInfo.InvariantCulture)); + } + } } } \ No newline at end of file From 95c50b781c48f9d83626f2516f0c10522d7d5a4c Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 28 Nov 2023 22:35:07 +0200 Subject: [PATCH 298/549] [SearchFilterBuilder] Rearrange code & remove AddIfNotNull helpers --- src/redmine-net-api/SearchFilterBuilder.cs | 83 +++++++--------------- 1 file changed, 25 insertions(+), 58 deletions(-) diff --git a/src/redmine-net-api/SearchFilterBuilder.cs b/src/redmine-net-api/SearchFilterBuilder.cs index fe2bc806..65e8a7df 100644 --- a/src/redmine-net-api/SearchFilterBuilder.cs +++ b/src/redmine-net-api/SearchFilterBuilder.cs @@ -16,7 +16,6 @@ limitations under the License. using System; using System.Collections.Specialized; -using System.Globalization; using Redmine.Net.Api.Extensions; namespace Redmine.Net.Api @@ -24,6 +23,7 @@ namespace Redmine.Net.Api /// /// /// + // ReSharper disable once ClassNeverInstantiated.Global public sealed class SearchFilterBuilder { /// @@ -38,20 +38,13 @@ public SearchScope? Scope _scope = value; if (_scope != null) { - switch (_scope) + _internalScope = _scope switch { - case SearchScope.All: - _internalScope = "all"; - break; - case SearchScope.MyProject: - _internalScope = "my_project"; - break; - case SearchScope.SubProjects: - _internalScope = "subprojects"; - break; - default: - throw new ArgumentOutOfRangeException(nameof(value)); - } + SearchScope.All => "all", + SearchScope.MyProject => "my_project", + SearchScope.SubProjects => "subprojects", + _ => throw new ArgumentOutOfRangeException(nameof(value)) + }; } } } @@ -106,7 +99,6 @@ public SearchScope? Scope /// public bool? OpenIssues{ get; set; } - /// /// public SearchAttachment? Attachments @@ -117,21 +109,13 @@ public SearchAttachment? Attachments _attachments = value; if (_attachments != null) { - switch (_attachments) + _internalAttachments = _attachments switch { - case SearchAttachment.OnlyInAttachment: - _internalAttachments = "only"; - break; - - case SearchAttachment.InDescription: - _internalAttachments = "0"; - break; - case SearchAttachment.InDescriptionAndAttachment: - _internalAttachments = "1"; - break; - default: - throw new ArgumentOutOfRangeException(); - } + SearchAttachment.OnlyInAttachment => "only", + SearchAttachment.InDescription => "0", + SearchAttachment.InDescriptionAndAttachment => "1", + _ => throw new ArgumentOutOfRangeException(nameof(Attachments)) + }; } } } @@ -146,38 +130,21 @@ public SearchAttachment? Attachments /// public NameValueCollection Build(NameValueCollection sb) { - AddIfNotNull(sb,"scope",_internalScope); - AddIfNotNull(sb,"projects",IncludeProjects); - AddIfNotNull(sb,"open_issues",OpenIssues); - AddIfNotNull(sb,"messages",IncludeMessages); - AddIfNotNull(sb,"wiki_pages",IncludeWikiPages); - AddIfNotNull(sb,"changesets",IncludeChangeSets); - AddIfNotNull(sb,"documents",IncludeDocuments); - AddIfNotNull(sb,"news",IncludeNews); - AddIfNotNull(sb,"issues",IncludeIssues); - AddIfNotNull(sb,"titles_only",TitlesOnly); - AddIfNotNull(sb,"all_words", AllWords); - AddIfNotNull(sb,"attachments", _internalAttachments); + sb.AddIfNotNull(RedmineKeys.SCOPE,_internalScope); + sb.AddIfNotNull(RedmineKeys.PROJECTS, IncludeProjects); + sb.AddIfNotNull(RedmineKeys.OPEN_ISSUES, OpenIssues); + sb.AddIfNotNull(RedmineKeys.MESSAGES, IncludeMessages); + sb.AddIfNotNull(RedmineKeys.WIKI_PAGES, IncludeWikiPages); + sb.AddIfNotNull(RedmineKeys.CHANGE_SETS, IncludeChangeSets); + sb.AddIfNotNull(RedmineKeys.DOCUMENTS, IncludeDocuments); + sb.AddIfNotNull(RedmineKeys.NEWS, IncludeNews); + sb.AddIfNotNull(RedmineKeys.ISSUES, IncludeIssues); + sb.AddIfNotNull(RedmineKeys.TITLES_ONLY, TitlesOnly); + sb.AddIfNotNull(RedmineKeys.ALL_WORDS, AllWords); + sb.AddIfNotNull(RedmineKeys.ATTACHMENTS, _internalAttachments); return sb; } - - private static void AddIfNotNull(NameValueCollection nameValueCollection, string key, bool? value) - { - if (value.HasValue) - { - nameValueCollection.Add(key, value.Value.ToString(CultureInfo.InvariantCulture)); - } - } - - private static void AddIfNotNull(NameValueCollection nameValueCollection, string key, string value) - { - if (!value.IsNullOrWhiteSpace()) - { - nameValueCollection.Add(key, value); - } - } - } /// From 4b5a0b407985d44c19e38cb8b64b39f2dd451b9d Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 28 Nov 2023 22:36:46 +0200 Subject: [PATCH 299/549] [NET20] Group all related files --- .../{Features/net20 => _net20}/ExtensionAttribute.cs | 6 ++---- src/redmine-net-api/{Features/net20 => _net20}/Func.cs | 0 .../{Async => _net20}/RedmineManagerAsync.cs | 1 - 3 files changed, 2 insertions(+), 5 deletions(-) rename src/redmine-net-api/{Features/net20 => _net20}/ExtensionAttribute.cs (94%) rename src/redmine-net-api/{Features/net20 => _net20}/Func.cs (100%) rename src/redmine-net-api/{Async => _net20}/RedmineManagerAsync.cs (99%) diff --git a/src/redmine-net-api/Features/net20/ExtensionAttribute.cs b/src/redmine-net-api/_net20/ExtensionAttribute.cs similarity index 94% rename from src/redmine-net-api/Features/net20/ExtensionAttribute.cs rename to src/redmine-net-api/_net20/ExtensionAttribute.cs index 41a3b55c..c6857d81 100755 --- a/src/redmine-net-api/Features/net20/ExtensionAttribute.cs +++ b/src/redmine-net-api/_net20/ExtensionAttribute.cs @@ -1,4 +1,5 @@ -/* +#if NET20 +/* Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,9 +15,6 @@ You may obtain a copy of the License at limitations under the License. */ -#if NET20 - -// ReSharper disable once CheckNamespace namespace System.Runtime.CompilerServices { /// diff --git a/src/redmine-net-api/Features/net20/Func.cs b/src/redmine-net-api/_net20/Func.cs similarity index 100% rename from src/redmine-net-api/Features/net20/Func.cs rename to src/redmine-net-api/_net20/Func.cs diff --git a/src/redmine-net-api/Async/RedmineManagerAsync.cs b/src/redmine-net-api/_net20/RedmineManagerAsync.cs similarity index 99% rename from src/redmine-net-api/Async/RedmineManagerAsync.cs rename to src/redmine-net-api/_net20/RedmineManagerAsync.cs index 124c2b45..acceb9b5 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync.cs +++ b/src/redmine-net-api/_net20/RedmineManagerAsync.cs @@ -1,4 +1,3 @@ - #if NET20 /* From 5b1b5ccf07607a341361e46008bca6dbe65a1bb2 Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 28 Nov 2023 22:38:14 +0200 Subject: [PATCH 300/549] [ReSharper] Disable CheckNamespace --- .../Features/CallerArgumentExpressionAttribute.cs | 2 +- src/redmine-net-api/Features/NotNullAttribute.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/redmine-net-api/Features/CallerArgumentExpressionAttribute.cs b/src/redmine-net-api/Features/CallerArgumentExpressionAttribute.cs index 2c57f348..62165823 100644 --- a/src/redmine-net-api/Features/CallerArgumentExpressionAttribute.cs +++ b/src/redmine-net-api/Features/CallerArgumentExpressionAttribute.cs @@ -4,7 +4,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. - +// ReSharper disable once CheckNamespace namespace System.Runtime.CompilerServices { /// diff --git a/src/redmine-net-api/Features/NotNullAttribute.cs b/src/redmine-net-api/Features/NotNullAttribute.cs index 0a3e9093..8aa30659 100644 --- a/src/redmine-net-api/Features/NotNullAttribute.cs +++ b/src/redmine-net-api/Features/NotNullAttribute.cs @@ -5,7 +5,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. - +// ReSharper disable once CheckNamespace namespace System.Diagnostics.CodeAnalysis { /// From 234e86a49e3f61ee2738708fd24289c548b403d5 Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 28 Nov 2023 22:39:50 +0200 Subject: [PATCH 301/549] [Delete] *.csproj.DotSettings --- src/redmine-net-api/redmine-net-api.csproj.DotSettings | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 src/redmine-net-api/redmine-net-api.csproj.DotSettings diff --git a/src/redmine-net-api/redmine-net-api.csproj.DotSettings b/src/redmine-net-api/redmine-net-api.csproj.DotSettings deleted file mode 100644 index b9fd6ee4..00000000 --- a/src/redmine-net-api/redmine-net-api.csproj.DotSettings +++ /dev/null @@ -1,2 +0,0 @@ - - CSharp80 \ No newline at end of file From fd8350d42c32e8dcb5196e1417bef3dfc262357f Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 28 Nov 2023 22:44:26 +0200 Subject: [PATCH 302/549] [Refactor] Use C#11 new features --- src/redmine-net-api/Net/ApiResponseMessage.cs | 2 +- .../Net/WebClient/StringApiRequestMessageContent.cs | 9 ++++++++- src/redmine-net-api/RedmineManagerOptions.cs | 4 ++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/redmine-net-api/Net/ApiResponseMessage.cs b/src/redmine-net-api/Net/ApiResponseMessage.cs index fc644ee5..c6f6431c 100644 --- a/src/redmine-net-api/Net/ApiResponseMessage.cs +++ b/src/redmine-net-api/Net/ApiResponseMessage.cs @@ -5,5 +5,5 @@ namespace Redmine.Net.Api.Net; internal sealed class ApiResponseMessage { public NameValueCollection Headers { get; init; } - public byte[] Content { get; set; } + public byte[] Content { get; init; } } \ No newline at end of file diff --git a/src/redmine-net-api/Net/WebClient/StringApiRequestMessageContent.cs b/src/redmine-net-api/Net/WebClient/StringApiRequestMessageContent.cs index 80b77fe5..8806a562 100644 --- a/src/redmine-net-api/Net/WebClient/StringApiRequestMessageContent.cs +++ b/src/redmine-net-api/Net/WebClient/StringApiRequestMessageContent.cs @@ -18,7 +18,14 @@ public StringApiRequestMessageContent(string content, string mediaType, Encoding private static byte[] GetContentByteArray(string content, Encoding encoding) { - if (content == null) throw new ArgumentNullException(nameof(content)); + #if NET5_0_OR_GREATER + ArgumentNullException.ThrowIfNull(content); + #else + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + #endif return (encoding ?? DefaultStringEncoding).GetBytes(content); } } \ No newline at end of file diff --git a/src/redmine-net-api/RedmineManagerOptions.cs b/src/redmine-net-api/RedmineManagerOptions.cs index 95acc4f6..41f8c45f 100644 --- a/src/redmine-net-api/RedmineManagerOptions.cs +++ b/src/redmine-net-api/RedmineManagerOptions.cs @@ -47,8 +47,8 @@ internal sealed class RedmineManagerOptions /// /// Gets or sets the version of the Redmine server to which this client will connect. /// - public Version Version { get; init; } + public Version RedmineVersion { get; init; } - internal bool VerifyServerCert { get; set; } + internal bool VerifyServerCert { get; init; } } } \ No newline at end of file From 915df2c312be6306fb96d42f52468b51deb882cf Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 28 Nov 2023 22:45:55 +0200 Subject: [PATCH 303/549] [RedmineManagerOptions] Change return type --- src/redmine-net-api/RedmineManagerOptions.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/redmine-net-api/RedmineManagerOptions.cs b/src/redmine-net-api/RedmineManagerOptions.cs index 41f8c45f..828f1422 100644 --- a/src/redmine-net-api/RedmineManagerOptions.cs +++ b/src/redmine-net-api/RedmineManagerOptions.cs @@ -1,4 +1,5 @@ using System; +using System.Net; using Redmine.Net.Api.Net; using Redmine.Net.Api.Serialization; @@ -37,7 +38,7 @@ internal sealed class RedmineManagerOptions /// /// Gets or sets a custom function that creates and returns a specialized instance of the WebClient class. /// - public Func ClientFunc { get; init; } + public Func ClientFunc { get; init; } /// /// Gets or sets the settings for configuring the Redmine web client. From 769cb00d60a3167dfcd72fcf846b5cc3d8ccc60d Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 2 Dec 2023 00:13:06 +0200 Subject: [PATCH 304/549] [StringExtensions] Small improvements --- .../Extensions/StringExtensions.cs | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/redmine-net-api/Extensions/StringExtensions.cs b/src/redmine-net-api/Extensions/StringExtensions.cs index 2aa91799..1fd04cdf 100644 --- a/src/redmine-net-api/Extensions/StringExtensions.cs +++ b/src/redmine-net-api/Extensions/StringExtensions.cs @@ -23,7 +23,7 @@ namespace Redmine.Net.Api.Extensions /// /// /// - public static partial class StringExtensions + public static class StringExtensions { /// /// @@ -32,7 +32,6 @@ public static partial class StringExtensions /// public static bool IsNullOrWhiteSpace(this string value) { -#if NET20 if (value == null) { return true; @@ -45,10 +44,8 @@ public static bool IsNullOrWhiteSpace(this string value) return false; } } + return true; -#else - return string.IsNullOrWhiteSpace(value); -#endif } /// @@ -59,12 +56,16 @@ public static bool IsNullOrWhiteSpace(this string value) /// public static string Truncate(this string text, int maximumLength) { - if (text.IsNullOrWhiteSpace()) + if (text.IsNullOrWhiteSpace() || maximumLength < 1 || text.Length <= maximumLength) { return text; } - return text.Length > maximumLength ? text.Substring(0, maximumLength) : text; + #if (NET5_0_OR_GREATER) + return text.AsSpan()[..maximumLength].ToString(); + #else + return text.Substring(0, maximumLength); + #endif } /// @@ -95,11 +96,12 @@ internal static SecureString ToSecureString(this string value) } var rv = new SecureString(); - foreach (var c in value) + + for (var index = 0; index < value.Length; ++index) { - rv.AppendChar(c); + rv.AppendChar(value[index]); } - + return rv; } @@ -110,11 +112,18 @@ internal static string RemoveTrailingSlash(this string s) return s; } + #if (NET5_0_OR_GREATER) + if (s.EndsWith('/') || s.EndsWith('\\')) + { + return s.AsSpan()[..(s.Length - 1)].ToString(); + } + #else if (s.EndsWith("/", StringComparison.OrdinalIgnoreCase) || s.EndsWith(@"\", StringComparison.OrdinalIgnoreCase)) { return s.Substring(0, s.Length - 1); } - + #endif + return s; } From ee8e807b314451b882c7aec68e66be15e6b4be2a Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 2 Dec 2023 00:14:12 +0200 Subject: [PATCH 305/549] [Delete] RedmineManagerAsync40 --- .../Async/RedmineManagerAsync40.cs | 328 ------------------ 1 file changed, 328 deletions(-) delete mode 100644 src/redmine-net-api/Async/RedmineManagerAsync40.cs diff --git a/src/redmine-net-api/Async/RedmineManagerAsync40.cs b/src/redmine-net-api/Async/RedmineManagerAsync40.cs deleted file mode 100644 index 93845ccc..00000000 --- a/src/redmine-net-api/Async/RedmineManagerAsync40.cs +++ /dev/null @@ -1,328 +0,0 @@ -/* - Copyright 2011 - 2023 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - - -#if NET40 -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Globalization; -using System.Threading; -using System.Threading.Tasks; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; -using Redmine.Net.Api.Serialization; - -namespace Redmine.Net.Api.Async -{ - /// - /// - /// - public static class RedmineManagerAsync - { - /// - /// Gets the current user asynchronous. - /// - /// The redmine manager. - /// The parameters. - /// - public static Task GetCurrentUserAsync(this RedmineManager redmineManager, NameValueCollection parameters = null) - { - return Task.Factory.StartNew(() => redmineManager.GetCurrentUser(parameters), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Creates the or update wiki page asynchronous. - /// - /// The redmine manager. - /// The project identifier. - /// Name of the page. - /// The wiki page. - /// - public static Task CreateWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage) - { - return Task.Factory.StartNew(() => redmineManager.CreateWikiPage(projectId, pageName, wikiPage), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// - /// - /// - /// - /// - /// - /// - public static Task UpdateWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage) - { - return Task.Factory.StartNew(() => redmineManager.UpdateWikiPage(projectId, pageName, wikiPage), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Deletes the wiki page asynchronous. - /// - /// The redmine manager. - /// The project identifier. - /// Name of the page. - /// - public static Task DeleteWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName) - { - return Task.Factory.StartNew(() => redmineManager.DeleteWikiPage(projectId, pageName), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Gets the wiki page asynchronous. - /// - /// The redmine manager. - /// The project identifier. - /// The parameters. - /// Name of the page. - /// The version. - /// - public static Task GetWikiPageAsync(this RedmineManager redmineManager, string projectId, NameValueCollection parameters, string pageName, uint version = 0) - { - return Task.Factory.StartNew(() => redmineManager.GetWikiPage(projectId, parameters, pageName, version), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Gets all wiki pages asynchronous. - /// - /// The redmine manager. - /// The project identifier. - /// - public static Task> GetAllWikiPagesAsync(this RedmineManager redmineManager, string projectId) - { - return Task.Factory.StartNew(() => redmineManager.GetAllWikiPages(projectId), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Adds the user to group asynchronous. - /// - /// The redmine manager. - /// The group identifier. - /// The user identifier. - /// - public static Task AddUserToGroupAsync(this RedmineManager redmineManager, int groupId, int userId) - { - return Task.Factory.StartNew(() => redmineManager.AddUserToGroup(groupId, userId), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Removes the user from group asynchronous. - /// - /// The redmine manager. - /// The group identifier. - /// The user identifier. - /// - public static Task RemoveUserFromGroupAsync(this RedmineManager redmineManager, int groupId, int userId) - { - return Task.Factory.StartNew(() => redmineManager.RemoveUserFromGroup(groupId, userId), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Adds the watcher to issue asynchronous. - /// - /// The redmine manager. - /// The issue identifier. - /// The user identifier. - /// - public static Task AddWatcherToIssueAsync(this RedmineManager redmineManager, int issueId, int userId) - { - return Task.Factory.StartNew(() => redmineManager.AddWatcherToIssue(issueId, userId), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Removes the watcher from issue asynchronous. - /// - /// The redmine manager. - /// The issue identifier. - /// The user identifier. - /// - public static Task RemoveWatcherFromIssueAsync(this RedmineManager redmineManager, int issueId, int userId) - { - return Task.Factory.StartNew(() => redmineManager.RemoveWatcherFromIssue(issueId, userId), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Gets the object asynchronous. - /// - /// - /// The redmine manager. - /// The identifier. - /// The parameters. - /// - public static Task GetObjectAsync(this RedmineManager redmineManager, string id, NameValueCollection parameters) where T : class, new() - { - return Task.Factory.StartNew(() => redmineManager.GetObject(id, parameters), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Creates the object asynchronous. - /// - /// - /// The redmine manager. - /// The object. - /// - public static Task CreateObjectAsync(this RedmineManager redmineManager, T entity) where T : class, new() - { - return CreateObjectAsync(redmineManager, entity, null); - } - - - /// - /// - /// - /// - /// - /// - /// - public static Task CountAsync(this RedmineManager redmineManager, params string[] include) where T : class, new() - { - return Task.Factory.StartNew(()=> redmineManager.Count(include), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// - /// - /// - /// - /// - /// - public static Task CountAsync(this RedmineManager redmineManager, NameValueCollection parameters) where T : class, new() - { - return Task.Factory.StartNew(() => redmineManager.Count(parameters), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Creates the object asynchronous. - /// - /// - /// The redmine manager. - /// The object. - /// The owner identifier. - /// - public static Task CreateObjectAsync(this RedmineManager redmineManager, T entity, string ownerId) where T : class, new() - { - return Task.Factory.StartNew(() => redmineManager.CreateObject(entity, ownerId), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Gets the paginated objects asynchronous. - /// - /// - /// The redmine manager. - /// The parameters. - /// - public static Task> GetPaginatedObjectsAsync(this RedmineManager redmineManager, NameValueCollection parameters) where T : class, new() - { - return Task.Factory.StartNew(() => redmineManager.GetPaginatedObjects(parameters), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Gets the objects asynchronous. - /// - /// - /// The redmine manager. - /// The parameters. - /// - public static Task> GetObjectsAsync(this RedmineManager redmineManager, NameValueCollection parameters) where T : class, new() - { - return Task.Factory.StartNew(() => redmineManager.GetObjects(parameters), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Updates the object asynchronous. - /// - /// - /// The redmine manager. - /// The identifier. - /// The object. - /// The project identifier. - /// - public static Task UpdateObjectAsync(this RedmineManager redmineManager, string id, T entity, string projectId = null) where T : class, new() - { - return Task.Factory.StartNew(() => redmineManager.UpdateObject(id, entity, projectId), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Deletes the object asynchronous. - /// - /// - /// The redmine manager. - /// The identifier. - /// - public static Task DeleteObjectAsync(this RedmineManager redmineManager, string id) where T : class, new() - { - return Task.Factory.StartNew(() => redmineManager.DeleteObject(id), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Uploads the file asynchronous. - /// - /// The redmine manager. - /// The data. - /// - public static Task UploadFileAsync(this RedmineManager redmineManager, byte[] data) - { - return Task.Factory.StartNew(() => redmineManager.UploadFile(data), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// Downloads the file asynchronous. - /// - /// The redmine manager. - /// The address. - /// - public static Task DownloadFileAsync(this RedmineManager redmineManager, string address) - { - return Task.Factory.StartNew(() => redmineManager.DownloadFile(address), CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); - } - - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public static Task> SearchAsync(this RedmineManager redmineManager, string q, int limit = RedmineManager.DEFAULT_PAGE_SIZE_VALUE, int offset = 0, SearchFilterBuilder searchFilter = null) - { - if (q.IsNullOrWhiteSpace()) - { - throw new ArgumentNullException(nameof(q)); - } - - var parameters = new NameValueCollection - { - {RedmineKeys.Q, q}, - {RedmineKeys.LIMIT, limit.ToString(CultureInfo.InvariantCulture)}, - {RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)}, - }; - - if (searchFilter != null) - { - parameters = searchFilter.Build(parameters); - } - - var result = redmineManager.GetPaginatedObjectsAsync(parameters); - - return result; - } - } -} -#endif \ No newline at end of file From 1dbb3ce111ef1ffe70750b16ad7fed3c482ba1b7 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 2 Dec 2023 00:15:47 +0200 Subject: [PATCH 306/549] [New] ApiResponseMessageExtensions --- .../Net/ApiResponseMessageExtensions.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 src/redmine-net-api/Net/ApiResponseMessageExtensions.cs diff --git a/src/redmine-net-api/Net/ApiResponseMessageExtensions.cs b/src/redmine-net-api/Net/ApiResponseMessageExtensions.cs new file mode 100644 index 00000000..7efdfd95 --- /dev/null +++ b/src/redmine-net-api/Net/ApiResponseMessageExtensions.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.Text; +using Redmine.Net.Api.Serialization; + +namespace Redmine.Net.Api.Net; + +internal static class ApiResponseMessageExtensions +{ + internal static T DeserializeTo(this ApiResponseMessage responseMessage, IRedmineSerializer redmineSerializer) where T : new() + { + if (responseMessage?.Content == null) + { + return default; + } + + var responseAsString = Encoding.UTF8.GetString(responseMessage.Content); + + return redmineSerializer.Deserialize(responseAsString); + } + + internal static PagedResults DeserializeToPagedResults(this ApiResponseMessage responseMessage, IRedmineSerializer redmineSerializer) where T : class, new() + { + if (responseMessage?.Content == null) + { + return default; + } + + var responseAsString = Encoding.UTF8.GetString(responseMessage.Content); + + return redmineSerializer.DeserializeToPagedResults(responseAsString); + } + + internal static List DeserializeToList(this ApiResponseMessage responseMessage, IRedmineSerializer redmineSerializer) where T : class, new() + { + if (responseMessage?.Content == null) + { + return default; + } + + var responseAsString = Encoding.UTF8.GetString(responseMessage.Content); + + return redmineSerializer.Deserialize>(responseAsString); + } +} \ No newline at end of file From a1744135460960a9998fb0e27673cee1177a4e0b Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 4 Dec 2023 13:13:13 +0200 Subject: [PATCH 307/549] [Csproj] Cleanup test project file --- .../redmine-net-api.Tests.csproj | 37 +++++-------------- 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj index 3de7e771..6f78956b 100644 --- a/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj +++ b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj @@ -6,7 +6,7 @@ $(AssemblyName) false net481 - net451;net452;net46;net461;net462;net47;net471;net472;net48;net481; + net40;net451;net452;net46;net461;net462;net47;net471;net472;net48;net481; false f8b9e946-b547-42f1-861c-f719dca00a84 Release;Debug;DebugJson @@ -14,41 +14,18 @@ |net45|net451|net452|net46|net461| + |net40|net45|net451|net452|net46|net461| - AnyCPU - true - full - false - bin\Debug\ DEBUG;TRACE;DEBUG_XML - prompt - 4 - AnyCPU - true - full - false - bin\Debug\ DEBUG;TRACE;DEBUG_JSON - prompt - 4 - - AnyCPU - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - + @@ -73,8 +50,14 @@ + + + + + - + + runtime; build; native; contentfiles; analyzers; buildtransitive From d484c617613f20cc403efcfbc3b765b30aeb38d1 Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 4 Dec 2023 13:14:20 +0200 Subject: [PATCH 308/549] [Delete] Old test files --- .../Tests/Async/AttachmentAsyncTests.cs | 105 ------ .../Tests/Async/IssueAsyncTests.cs | 46 --- .../Tests/Async/UserAsyncTests.cs | 216 ------------ .../Tests/Async/WikiPageAsyncTests.cs | 101 ------ .../Tests/RedmineTest.cs | 59 ---- .../Tests/Sync/AttachmentTests.cs | 122 ------- .../Tests/Sync/CustomFieldTests.cs | 48 --- .../Tests/Sync/GroupTests.cs | 130 ------- .../Tests/Sync/IssueCategoryTests.cs | 132 ------- .../Tests/Sync/IssuePriorityTests.cs | 46 --- .../Tests/Sync/IssueRelationTests.cs | 105 ------ .../Tests/Sync/IssueStatusTests.cs | 46 --- .../Tests/Sync/IssueTests.cs | 332 ------------------ .../Tests/Sync/NewsTests.cs | 61 ---- .../Tests/Sync/ProjectMembershipTests.cs | 125 ------- .../Tests/Sync/ProjectTests.cs | 263 -------------- .../Tests/Sync/QueryTests.cs | 47 --- .../Tests/Sync/RoleTests.cs | 63 ---- .../Tests/Sync/TimeEntryActivtiyTests.cs | 48 --- .../Tests/Sync/TimeEntryTests.cs | 145 -------- .../Tests/Sync/TrackerTests.cs | 46 --- .../Tests/Sync/UserTests.cs | 219 ------------ .../Tests/Sync/VersionTests.cs | 144 -------- .../Tests/Sync/WikiPageTests.cs | 140 -------- 24 files changed, 2789 deletions(-) delete mode 100644 tests/redmine-net-api.Tests/Tests/Async/AttachmentAsyncTests.cs delete mode 100644 tests/redmine-net-api.Tests/Tests/Async/IssueAsyncTests.cs delete mode 100644 tests/redmine-net-api.Tests/Tests/Async/UserAsyncTests.cs delete mode 100644 tests/redmine-net-api.Tests/Tests/Async/WikiPageAsyncTests.cs delete mode 100644 tests/redmine-net-api.Tests/Tests/RedmineTest.cs delete mode 100644 tests/redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs delete mode 100644 tests/redmine-net-api.Tests/Tests/Sync/CustomFieldTests.cs delete mode 100644 tests/redmine-net-api.Tests/Tests/Sync/GroupTests.cs delete mode 100644 tests/redmine-net-api.Tests/Tests/Sync/IssueCategoryTests.cs delete mode 100644 tests/redmine-net-api.Tests/Tests/Sync/IssuePriorityTests.cs delete mode 100644 tests/redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs delete mode 100644 tests/redmine-net-api.Tests/Tests/Sync/IssueStatusTests.cs delete mode 100644 tests/redmine-net-api.Tests/Tests/Sync/IssueTests.cs delete mode 100644 tests/redmine-net-api.Tests/Tests/Sync/NewsTests.cs delete mode 100644 tests/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs delete mode 100644 tests/redmine-net-api.Tests/Tests/Sync/ProjectTests.cs delete mode 100644 tests/redmine-net-api.Tests/Tests/Sync/QueryTests.cs delete mode 100644 tests/redmine-net-api.Tests/Tests/Sync/RoleTests.cs delete mode 100644 tests/redmine-net-api.Tests/Tests/Sync/TimeEntryActivtiyTests.cs delete mode 100644 tests/redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs delete mode 100644 tests/redmine-net-api.Tests/Tests/Sync/TrackerTests.cs delete mode 100644 tests/redmine-net-api.Tests/Tests/Sync/UserTests.cs delete mode 100644 tests/redmine-net-api.Tests/Tests/Sync/VersionTests.cs delete mode 100644 tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs diff --git a/tests/redmine-net-api.Tests/Tests/Async/AttachmentAsyncTests.cs b/tests/redmine-net-api.Tests/Tests/Async/AttachmentAsyncTests.cs deleted file mode 100644 index f454a497..00000000 --- a/tests/redmine-net-api.Tests/Tests/Async/AttachmentAsyncTests.cs +++ /dev/null @@ -1,105 +0,0 @@ -#if !(NET20 || NET40) - -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Threading.Tasks; -using Redmine.Net.Api.Async; -using Redmine.Net.Api.Types; -using Xunit; - -namespace Padi.RedmineApi.Tests.Tests.Async -{ - [Collection("RedmineCollection")] - public class AttachmentAsyncTests - { - private const string ATTACHMENT_ID = "10"; - - private readonly RedmineFixture fixture; - public AttachmentAsyncTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - [Fact] - public async Task Should_Get_Attachment_By_Id() - { - var attachment = await fixture.RedmineManager.GetObjectAsync(ATTACHMENT_ID, null); - - Assert.NotNull(attachment); - Assert.IsType(attachment); - } - - [Fact] - public async Task Should_Upload_Attachment() - { - //read document from specified path - var documentPath = AppDomain.CurrentDomain.BaseDirectory + "/uploadAttachment.pages"; - var documentData = System.IO.File.ReadAllBytes(documentPath); - - //upload attachment to redmine - var attachment = await fixture.RedmineManager.UploadFileAsync(documentData); - - //set attachment properties - attachment.FileName = "uploadAttachment.pages"; - attachment.Description = "File uploaded using REST API"; - attachment.ContentType = "text/plain"; - - //create list of attachments to be added to issue - IList attachments = new List(); - attachments.Add(attachment); - - - var icf = (IssueCustomField)IdentifiableName.Create(13); - icf.Values = new List { new CustomFieldValue { Info = "Issue custom field completed" } }; - - var issue = new Issue - { - Project = IdentifiableName.Create(9), - Tracker = IdentifiableName.Create(3), - Status = IdentifiableName.Create(6), - Priority = IdentifiableName.Create(9), - Subject = "Issue with attachments", - Description = "Issue description...", - Category = IdentifiableName.Create(18), - FixedVersion = IdentifiableName.Create(9), - AssignedTo = IdentifiableName.Create(8), - ParentIssue = IdentifiableName.Create(96), - CustomFields = new List {icf}, - IsPrivate = true, - EstimatedHours = 12, - StartDate = DateTime.Now, - DueDate = DateTime.Now.AddMonths(1), - Uploads = attachments, - Watchers = new List - { - (Watcher) IdentifiableName.Create(8), - (Watcher) IdentifiableName.Create(2) - } - }; - - //create issue and attach document - var issueWithAttachment = await fixture.RedmineManager.CreateObjectAsync(issue); - - issue = await fixture.RedmineManager.GetObjectAsync(issueWithAttachment.Id.ToString(), - new NameValueCollection { { "include", "attachments" } }); - - Assert.NotNull(issue); - Assert.IsType(issue); - - Assert.True(issue.Attachments.Count == 1, "Attachments count != 1"); - Assert.True(issue.Attachments[0].FileName == attachment.FileName); - } - - [Fact] - public async Task Sould_Download_Attachment() - { - var attachment = await fixture.RedmineManager.GetObjectAsync(ATTACHMENT_ID, null); - - var document = await fixture.RedmineManager.DownloadFileAsync(attachment.ContentUrl); - - Assert.NotNull(document); - } - } -} -#endif \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Tests/Async/IssueAsyncTests.cs b/tests/redmine-net-api.Tests/Tests/Async/IssueAsyncTests.cs deleted file mode 100644 index b800e3c4..00000000 --- a/tests/redmine-net-api.Tests/Tests/Async/IssueAsyncTests.cs +++ /dev/null @@ -1,46 +0,0 @@ -#if !(NET20 || NET40) -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Threading.Tasks; -using Redmine.Net.Api.Async; -using Redmine.Net.Api.Types; -using Xunit; - -namespace Padi.RedmineApi.Tests.Tests.Async -{ - [Collection("RedmineCollection")] - public class IssueAsyncTests - { - private const int WATCHER_ISSUE_ID = 91; - private const int WATCHER_USER_ID = 2; - - private readonly RedmineFixture fixture; - public IssueAsyncTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - [Fact] - public async Task Should_Add_Watcher_To_Issue() - { - await fixture.RedmineManager.AddWatcherToIssueAsync(WATCHER_ISSUE_ID, WATCHER_USER_ID); - - var issue = await fixture.RedmineManager.GetObjectAsync(WATCHER_ISSUE_ID.ToString(), new NameValueCollection { { "include", "watchers" } }); - - Assert.NotNull(issue); - Assert.True(issue.Watchers.Count == 1, "Number of watchers != 1"); - Assert.True(((List)issue.Watchers).Find(w => w.Id == WATCHER_USER_ID) != null, "Watcher not added to issue."); - } - - [Fact] - public async Task Should_Remove_Watcher_From_Issue() - { - await fixture.RedmineManager.RemoveWatcherFromIssueAsync(WATCHER_ISSUE_ID, WATCHER_USER_ID); - - var issue = await fixture.RedmineManager.GetObjectAsync(WATCHER_ISSUE_ID.ToString(), new NameValueCollection { { "include", "watchers" } }); - - Assert.True(issue.Watchers == null || ((List)issue.Watchers).Find(w => w.Id == WATCHER_USER_ID) == null); - } - } -} -#endif \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Tests/Async/UserAsyncTests.cs b/tests/redmine-net-api.Tests/Tests/Async/UserAsyncTests.cs deleted file mode 100644 index f3690f44..00000000 --- a/tests/redmine-net-api.Tests/Tests/Async/UserAsyncTests.cs +++ /dev/null @@ -1,216 +0,0 @@ -#if !(NET20 || NET40) -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Globalization; -using System.Linq; -using System.Threading.Tasks; -using Redmine.Net.Api; -using Redmine.Net.Api.Async; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Types; -using Xunit; - -namespace Padi.RedmineApi.Tests.Tests.Async -{ - [Collection("RedmineCollection")] - public class UserAsyncTests - { - private const string USER_ID = "8"; - private const string LIMIT = "2"; - private const string OFFSET = "1"; - private const int GROUP_ID = 9; - - private readonly RedmineFixture fixture; - public UserAsyncTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - [Fact] - public async Task Should_Get_CurrentUser() - { - var currentUser = await fixture.RedmineManager.GetCurrentUserAsync(); - Assert.NotNull(currentUser); - } - - [Fact] - public async Task Should_Get_User_By_Id() - { - var user = await fixture.RedmineManager.GetObjectAsync(USER_ID, null); - Assert.NotNull(user); - } - - [Fact] - public async Task Should_Get_User_By_Id_Including_Groups_And_Memberships() - { - var user = await fixture.RedmineManager.GetObjectAsync(USER_ID, new NameValueCollection() { { RedmineKeys.INCLUDE, "groups,memberships" } }); - - Assert.NotNull(user); - - Assert.NotNull(user.Groups); - Assert.True(user.Groups.Count == 1, "Group count != 1"); - - Assert.NotNull(user.Memberships); - Assert.True(user.Memberships.Count == 3, "Membership count != 3"); - } - - [Fact] - public async Task Should_Get_X_Users_From_Offset_Y() - { - var result = await fixture.RedmineManager.GetPaginatedObjectsAsync(new NameValueCollection() { - { RedmineKeys.INCLUDE, "groups, memberships" }, - {RedmineKeys.LIMIT,LIMIT }, - {RedmineKeys.OFFSET,OFFSET } - }); - - Assert.NotNull(result); - Assert.All(result.Items, u => Assert.IsType(u)); - } - - [Fact] - public async Task Should_Get_All_Users_With_Groups_And_Memberships() - { - var users = await fixture.RedmineManager.GetObjectsAsync(new NameValueCollection { { RedmineKeys.INCLUDE, "groups, memberships" } }); - - Assert.NotNull(users); - Assert.All(users, u => Assert.IsType(u)); - } - - [Fact] - public async Task Should_Get_Active_Users() - { - var users = await fixture.RedmineManager.GetObjectsAsync(new NameValueCollection() - { - { RedmineKeys.STATUS, ((int)UserStatus.StatusActive).ToString(CultureInfo.InvariantCulture) } - }); - - Assert.NotNull(users); - Assert.True(users.Count == 6); - Assert.All(users, u => Assert.IsType(u)); - } - - [Fact] - public async Task Should_Get_Anonymous_Users() - { - var users = await fixture.RedmineManager.GetObjectsAsync(new NameValueCollection() - { - { RedmineKeys.STATUS, ((int)UserStatus.StatusAnonymous).ToString(CultureInfo.InvariantCulture) } - }); - - Assert.NotNull(users); - Assert.True(users.Count == 0); - Assert.All(users, u => Assert.IsType(u)); - } - - [Fact] - public async Task Should_Get_Locked_Users() - { - var users = await fixture.RedmineManager.GetObjectsAsync(new NameValueCollection() - { - { RedmineKeys.STATUS, ((int)UserStatus.StatusLocked).ToString(CultureInfo.InvariantCulture) } - }); - - Assert.NotNull(users); - Assert.True(users.Count == 1); - Assert.All(users, u => Assert.IsType(u)); - } - - [Fact] - public async Task Should_Get_Registered_Users() - { - var users = await fixture.RedmineManager.GetObjectsAsync(new NameValueCollection() - { - { RedmineKeys.STATUS, ((int)UserStatus.StatusRegistered).ToString(CultureInfo.InvariantCulture) } - }); - - Assert.NotNull(users); - Assert.True(users.Count == 1); - Assert.All(users, u => Assert.IsType(u)); - } - - [Fact] - public async Task Should_Get_Users_By_Group() - { - var users = await fixture.RedmineManager.GetObjectsAsync(new NameValueCollection() - { - {RedmineKeys.GROUP_ID, GROUP_ID.ToString(CultureInfo.InvariantCulture)} - }); - - Assert.NotNull(users); - Assert.True(users.Count == 3); - Assert.All(users, u => Assert.IsType(u)); - } - - [Fact] - public async Task Should_Add_User_To_Group() - { - await fixture.RedmineManager.AddUserToGroupAsync(GROUP_ID, int.Parse(USER_ID)); - - var user = fixture.RedmineManager.GetObject(USER_ID.ToString(CultureInfo.InvariantCulture), new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.GROUPS } }); - - Assert.NotNull(user.Groups); - Assert.True(user.Groups.FirstOrDefault(g => g.Id == GROUP_ID) != null); - } - - [Fact] - public async Task Should_Remove_User_From_Group() - { - await fixture.RedmineManager.RemoveUserFromGroupAsync(GROUP_ID, int.Parse(USER_ID)); - - var user = await fixture.RedmineManager.GetObjectAsync(USER_ID.ToString(CultureInfo.InvariantCulture), new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.GROUPS } }); - - Assert.True(user.Groups == null || user.Groups.FirstOrDefault(g => g.Id == GROUP_ID) == null); - } - - [Fact] - public async Task Should_Create_User() - { - var user = new User - { - Login = "userTestLogin4", - FirstName = "userTestFirstName", - LastName = "userTestLastName", - Email = "testTest4@redmineapi.com", - Password = "123456", - AuthenticationModeId = 1, - MustChangePassword = false - }; - - - var icf = (IssueCustomField)IdentifiableName.Create(4); - icf.Values = new List { new CustomFieldValue { Info = "userTestCustomField:" + DateTime.UtcNow } }; - - user.CustomFields = new List(); - user.CustomFields.Add(icf); - - var createdUser = await fixture.RedmineManager.CreateObjectAsync(user); - - Assert.Equal(user.Login, createdUser.Login); - Assert.Equal(user.Email, createdUser.Email); - } - - [Fact] - public async Task Should_Update_User() - { - var userId = 59.ToString(); - var user = fixture.RedmineManager.GetObject(userId, null); - user.FirstName = "modified first name"; - await fixture.RedmineManager.UpdateObjectAsync(userId, user); - - var updatedUser = await fixture.RedmineManager.GetObjectAsync(userId, null); - - Assert.Equal(user.FirstName, updatedUser.FirstName); - } - - [Fact] - public async Task Should_Delete_User() - { - var userId = 62.ToString(); - await fixture.RedmineManager.DeleteObjectAsync(userId); - await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetObjectAsync(userId, null)); - - } - } -} -#endif \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Tests/Async/WikiPageAsyncTests.cs b/tests/redmine-net-api.Tests/Tests/Async/WikiPageAsyncTests.cs deleted file mode 100644 index 52d1cccb..00000000 --- a/tests/redmine-net-api.Tests/Tests/Async/WikiPageAsyncTests.cs +++ /dev/null @@ -1,101 +0,0 @@ -#if !(NET20 || NET40) -using System; -using System.Collections.Specialized; -using System.Threading.Tasks; -using Redmine.Net.Api.Async; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Types; -using Xunit; - -namespace Padi.RedmineApi.Tests.Tests.Async -{ - [Collection("RedmineCollection")] - public class WikiPageAsyncTests - { - private const string PROJECT_ID = "redmine-net-testq"; - private const string WIKI_PAGE_NAME = "Wiki"; - private const int NO_OF_WIKI_PAGES = 1; - private const int WIKI_PAGE_VERSION = 1; - - private const string WIKI_PAGE_UPDATED_TEXT = "Updated again and again wiki page"; - private const string WIKI_PAGE_COMMENT = "Comment added through code"; - - private readonly RedmineFixture fixture; - public WikiPageAsyncTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - [Fact] - public async Task Should_Add_Wiki_Page() - { - var page = await fixture.RedmineManager.CreateWikiPageAsync(PROJECT_ID, WIKI_PAGE_NAME, new WikiPage { Text = WIKI_PAGE_UPDATED_TEXT, Comments = WIKI_PAGE_COMMENT }); - - Assert.NotNull(page); - Assert.True(page.Title == WIKI_PAGE_NAME, "Wiki page " + WIKI_PAGE_NAME + " does not exist."); - } - - [Fact] - public async Task Should_Update_Wiki_Page() - { - await fixture.RedmineManager.UpdateWikiPageAsync(PROJECT_ID, WIKI_PAGE_NAME, new WikiPage { Text = WIKI_PAGE_UPDATED_TEXT, Comments = WIKI_PAGE_COMMENT }); - } - - [Fact] - public async Task Should_Get_All_Pages() - { - var pages = await fixture.RedmineManager.GetAllWikiPagesAsync(null, PROJECT_ID); - - Assert.NotNull(pages); - - Assert.True(pages.Count == NO_OF_WIKI_PAGES, "Number of pages != "+NO_OF_WIKI_PAGES); - Assert.True(pages.Exists(p => p.Title == WIKI_PAGE_NAME), "Wiki page "+WIKI_PAGE_NAME+" does not exist." ); - } - - [Fact] - public async Task Should_Get_Page_By_Name() - { - var page = await fixture.RedmineManager.GetWikiPageAsync(PROJECT_ID, new NameValueCollection { { "include", "attachments" } }, WIKI_PAGE_NAME); - - Assert.NotNull(page); - Assert.True(page.Title == WIKI_PAGE_NAME, "Wiki page " + WIKI_PAGE_NAME + " does not exist."); - } - - [Fact] - public async Task Should_Get_Wiki_Page_Old_Version() - { - var oldPage = await fixture.RedmineManager.GetWikiPageAsync(PROJECT_ID, new NameValueCollection { { "include", "attachments" } }, WIKI_PAGE_NAME, WIKI_PAGE_VERSION); - - Assert.True(oldPage.Title == WIKI_PAGE_NAME, "Wiki page " + WIKI_PAGE_NAME + " does not exist."); - Assert.True(oldPage.Version == WIKI_PAGE_VERSION, "Wiki page version " + WIKI_PAGE_VERSION + " does not exist."); - } - - [Fact] - public async Task Should_Delete_WikiPage() - { - await fixture.RedmineManager.DeleteWikiPageAsync(PROJECT_ID, WIKI_PAGE_NAME); - await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetWikiPageAsync(PROJECT_ID, null, WIKI_PAGE_NAME)); - } - - [Fact] - public async Task Should_Get_Wiki_Page_With_Special_Chars() - { - var wikiPageName = "some-page-with-umlauts-and-other-special-chars-äöüÄÖÜß"; - - var wikiPage = await fixture.RedmineManager.CreateWikiPageAsync(PROJECT_ID, wikiPageName, - new WikiPage { Text = "WIKI_PAGE_TEXT", Comments = "WIKI_PAGE_COMMENT" }); - - WikiPage page = await fixture.RedmineManager.GetWikiPageAsync - ( - PROJECT_ID, - null, - wikiPageName - ); - - Assert.NotNull(page); - Assert.True(string.Equals(page.Title,wikiPageName, StringComparison.OrdinalIgnoreCase),$"Wiki page {wikiPageName} does not exist."); - } - - } -} -#endif \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Tests/RedmineTest.cs b/tests/redmine-net-api.Tests/Tests/RedmineTest.cs deleted file mode 100644 index 83157397..00000000 --- a/tests/redmine-net-api.Tests/Tests/RedmineTest.cs +++ /dev/null @@ -1,59 +0,0 @@ -using Padi.RedmineApi.Tests.Infrastructure; -using Redmine.Net.Api; -using Redmine.Net.Api.Exceptions; -using Xunit; - -namespace Padi.RedmineApi.Tests.Tests -{ - [Trait("Redmine-api", "Credentials")] -#if !(NET20 || NET40) - [Collection("RedmineCollection")] -#endif - [Order(1)] - public sealed class RedmineTest - { - private static readonly RedmineCredentials Credentials; - - - static RedmineTest() - { - Credentials = TestHelper.GetApplicationConfiguration(); - } - - [Fact] - public void Should_Throw_Redmine_Exception_When_Host_Is_Null() - { - Assert.Throws(() => new RedmineManager(null, Credentials.Username, Credentials.Password)); - } - - [Fact] - public void Should_Throw_Redmine_Exception_When_Host_Is_Empty() - { - Assert.Throws(() => new RedmineManager(string.Empty, Credentials.Username, Credentials.Password)); - } - - [Fact] - public void Should_Throw_Redmine_Exception_When_Host_Is_Invalid() - { - Assert.Throws(() => new RedmineManager("invalid<>", Credentials.Username, Credentials.Password)); - } - - [Fact] - public void Should_Connect_With_Username_And_Password() - { - var a = new RedmineManager(Credentials.Uri, Credentials.Username, Credentials.Password); - var currentUser = a.GetCurrentUser(); - Assert.NotNull(currentUser); - Assert.True(currentUser.Login.Equals(Credentials.Username), "usernames not equals."); - } - - [Fact] - public void Should_Connect_With_Api_Key() - { - var a = new RedmineManager(Credentials.Uri, Credentials.ApiKey); - var currentUser = a.GetCurrentUser(); - Assert.NotNull(currentUser); - Assert.True(currentUser.ApiKey.Equals(Credentials.ApiKey),"api keys not equals."); - } - } -} diff --git a/tests/redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs deleted file mode 100644 index 0a5d2f3f..00000000 --- a/tests/redmine-net-api.Tests/Tests/Sync/AttachmentTests.cs +++ /dev/null @@ -1,122 +0,0 @@ -/* - Copyright 2011 - 2023 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using Padi.RedmineApi.Tests.Infrastructure; -using Redmine.Net.Api; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Types; -using Xunit; - -namespace Padi.RedmineApi.Tests.Tests.Sync -{ - [Trait("Redmine-Net-Api", "Attachments")] -#if !(NET20 || NET40) - [Collection("RedmineCollection")] -#endif - public class AttachmentTests - { - public AttachmentTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private readonly RedmineFixture fixture; - - private const string ATTACHMENT_ID = "48"; - private const string ATTACHMENT_FILE_NAME = "uploadAttachment.pages"; - - [Fact, Order(1)] - public void Should_Download_Attachment() - { - var url = fixture.Credentials.Uri + "/attachments/download/" + ATTACHMENT_ID + "/" + ATTACHMENT_FILE_NAME; - - var document = fixture.RedmineManager.DownloadFile(url); - - Assert.NotNull(document); - } - - [Fact, Order(2)] - public void Should_Get_Attachment_By_Id() - { - var attachment = fixture.RedmineManager.GetObject(ATTACHMENT_ID, null); - - Assert.NotNull(attachment); - Assert.IsType(attachment); - Assert.True(attachment.FileName == ATTACHMENT_FILE_NAME, "Attachment file name ( " + attachment.FileName + " ) " + - "is not the expected one ( " + ATTACHMENT_FILE_NAME + " )."); - } - - [Fact, Order(3)] - public void Should_Upload_Attachment() - { - const string ATTACHMENT_LOCAL_PATH = "uploadAttachment.pages"; - const string ATTACHMENT_NAME = "AttachmentUploaded.txt"; - const string ATTACHMENT_DESCRIPTION = "File uploaded using REST API"; - const string ATTACHMENT_CONTENT_TYPE = "text/plain"; - const int PROJECT_ID = 9; - const string ISSUE_SUBJECT = "Issue with attachments"; - - //read document from specified path - var documentData = System.IO.File.ReadAllBytes(AppDomain.CurrentDomain.BaseDirectory + ATTACHMENT_LOCAL_PATH); - - //upload attachment to redmine - var attachment = fixture.RedmineManager.UploadFile(documentData); - - //set attachment properties - attachment.FileName = ATTACHMENT_NAME; - attachment.Description = ATTACHMENT_DESCRIPTION; - attachment.ContentType = ATTACHMENT_CONTENT_TYPE; - - //create list of attachments to be added to issue - IList attachments = new List(); - attachments.Add(attachment); - - var issue = new Issue - { - Project = IdentifiableName.Create(PROJECT_ID ), - Subject = ISSUE_SUBJECT, - Uploads = attachments - }; - - //create issue and attach document - var issueWithAttachment = fixture.RedmineManager.CreateObject(issue); - - issue = fixture.RedmineManager.GetObject(issueWithAttachment.Id.ToString(), - new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.ATTACHMENTS } }); - - Assert.NotNull(issue); - Assert.NotNull(issue.Attachments); - Assert.True(issue.Attachments.Count == 1, "Number of attachments ( " + issue.Attachments.Count + " ) != 1"); - - var firstAttachment = issue.Attachments[0]; - Assert.True(firstAttachment.FileName == ATTACHMENT_NAME, "Attachment name is invalid."); - Assert.True(firstAttachment.Description == ATTACHMENT_DESCRIPTION,"Attachment description is invalid."); - Assert.True(firstAttachment.ContentType == ATTACHMENT_CONTENT_TYPE,"Attachment content type is invalid."); - } - - - [Fact, Order(4)] - public void Should_Delete_Attachment() - { - var exception = (RedmineException)Record.Exception(() => fixture.RedmineManager.DeleteObject(ATTACHMENT_ID)); - Assert.Null(exception); - Assert.Throws(() => fixture.RedmineManager.GetObject(ATTACHMENT_ID, null)); - } - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Tests/Sync/CustomFieldTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/CustomFieldTests.cs deleted file mode 100644 index de6db017..00000000 --- a/tests/redmine-net-api.Tests/Tests/Sync/CustomFieldTests.cs +++ /dev/null @@ -1,48 +0,0 @@ -/* - Copyright 2011 - 2023 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using Padi.RedmineApi.Tests.Infrastructure; -using Redmine.Net.Api.Types; -using Xunit; - -namespace Padi.RedmineApi.Tests.Tests.Sync -{ - [Trait("Redmine-Net-Api", "CustomFields")] -#if !(NET20 || NET40) - [Collection("RedmineCollection")] -#endif - public class CustomFieldTests - { - public CustomFieldTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private readonly RedmineFixture fixture; - - [Fact, Order(1)] - public void Should_Get_All_CustomFields() - { - const int NUMBER_OF_CUSTOM_FIELDS = 10; - - var customFields = fixture.RedmineManager.GetObjects(); - - Assert.NotNull(customFields); - Assert.True(customFields.Count == NUMBER_OF_CUSTOM_FIELDS, - "Custom fields count(" + customFields.Count + ") != " + NUMBER_OF_CUSTOM_FIELDS); - } - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Tests/Sync/GroupTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/GroupTests.cs deleted file mode 100644 index eae41a6f..00000000 --- a/tests/redmine-net-api.Tests/Tests/Sync/GroupTests.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System.Collections.Generic; -using System.Collections.Specialized; -using Padi.RedmineApi.Tests.Infrastructure; -using Redmine.Net.Api; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Types; -using Xunit; - -namespace Padi.RedmineApi.Tests.Tests.Sync -{ - [Trait("Redmine-Net-Api", "Groups")] -#if !(NET20 || NET40) - [Collection("RedmineCollection")] -#endif - public class GroupTests - { - public GroupTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private readonly RedmineFixture fixture; - - private const string GROUP_ID = "57"; - private const int NUMBER_OF_MEMBERSHIPS = 1; - private const int NUMBER_OF_USERS = 2; - - [Fact, Order(1)] - public void Should_Add_Group() - { - const string NEW_GROUP_NAME = "Developers1"; - const int NEW_GROUP_USER_ID = 8; - - var group = new Group(); - group.Name = NEW_GROUP_NAME; - group.Users = new List { (GroupUser)IdentifiableName.Create(NEW_GROUP_USER_ID )}; - - Group savedGroup = null; - var exception = - (RedmineException)Record.Exception(() => savedGroup = fixture.RedmineManager.CreateObject(group)); - - Assert.Null(exception); - Assert.NotNull(savedGroup); - Assert.True(group.Name.Equals(savedGroup.Name), "Group name is not valid."); - } - - [Fact, Order(2)] - public void Should_Update_Group() - { - const string UPDATED_GROUP_ID = "58"; - const string UPDATED_GROUP_NAME = "Best Developers"; - const int UPDATED_GROUP_USER_ID = 2; - - var group = fixture.RedmineManager.GetObject(UPDATED_GROUP_ID, - new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.USERS } }); - group.Name = UPDATED_GROUP_NAME; - group.Users.Add((GroupUser)IdentifiableName.Create(UPDATED_GROUP_USER_ID)); - - fixture.RedmineManager.UpdateObject(UPDATED_GROUP_ID, group); - - var updatedGroup = fixture.RedmineManager.GetObject(UPDATED_GROUP_ID, - new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.USERS } }); - - Assert.NotNull(updatedGroup); - Assert.True(updatedGroup.Name.Equals(UPDATED_GROUP_NAME), "Group name was not updated."); - Assert.NotNull(updatedGroup.Users); - // Assert.True(updatedGroup.Users.Find(u => u.Id == UPDATED_GROUP_USER_ID) != null, - //"User was not added to group."); - } - - [Fact, Order(3)] - public void Should_Get_All_Groups() - { - const int NUMBER_OF_GROUPS = 3; - - var groups = fixture.RedmineManager.GetObjects(); - - Assert.NotNull(groups); - Assert.True(groups.Count == NUMBER_OF_GROUPS, "Number of groups ( " + groups.Count + " ) != " + NUMBER_OF_GROUPS); - } - - [Fact, Order(4)] - public void Should_Get_Group_With_All_Associated_Data() - { - var group = fixture.RedmineManager.GetObject(GROUP_ID, - new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.MEMBERSHIPS + "," + RedmineKeys.USERS } }); - - Assert.NotNull(group); - - Assert.True(group.Memberships.Count == NUMBER_OF_MEMBERSHIPS, - "Number of memberships != " + NUMBER_OF_MEMBERSHIPS); - - Assert.True(group.Users.Count == NUMBER_OF_USERS, "Number of users ( " + group.Users.Count + " ) != " + NUMBER_OF_USERS); - Assert.True(group.Name.Equals("Test"), "Group name is not valid."); - } - - [Fact, Order(5)] - public void Should_Get_Group_With_Memberships() - { - var group = fixture.RedmineManager.GetObject(GROUP_ID, - new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.MEMBERSHIPS } }); - - Assert.NotNull(group); - Assert.True(group.Memberships.Count == NUMBER_OF_MEMBERSHIPS, - "Number of memberships ( " + group.Memberships.Count + " ) != " + NUMBER_OF_MEMBERSHIPS); - } - - [Fact, Order(6)] - public void Should_Get_Group_With_Users() - { - var group = fixture.RedmineManager.GetObject(GROUP_ID, - new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.USERS } }); - - Assert.NotNull(group); - Assert.True(group.Users.Count == NUMBER_OF_USERS, "Number of users ( " + group.Users.Count + " ) != " + NUMBER_OF_USERS); - } - - [Fact, Order(99)] - public void Should_Delete_Group() - { - const string DELETED_GROUP_ID = "63"; - - var exception = - (RedmineException) - Record.Exception(() => fixture.RedmineManager.DeleteObject(DELETED_GROUP_ID)); - Assert.Null(exception); - Assert.Throws(() => fixture.RedmineManager.GetObject(DELETED_GROUP_ID, null)); - } - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Tests/Sync/IssueCategoryTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/IssueCategoryTests.cs deleted file mode 100644 index fa47d282..00000000 --- a/tests/redmine-net-api.Tests/Tests/Sync/IssueCategoryTests.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System.Collections.Specialized; -using Padi.RedmineApi.Tests.Infrastructure; -using Redmine.Net.Api; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Types; -using Xunit; - -namespace Padi.RedmineApi.Tests.Tests.Sync -{ - [Trait("Redmine-Net-Api", "IssueCategories")] -#if !(NET20 || NET40) - [Collection("RedmineCollection")] -#endif - public class IssueCategoryTests - { - private readonly RedmineFixture fixture; - - private const string PROJECT_ID = "redmine-net-testq"; - private const string NEW_ISSUE_CATEGORY_NAME = "Test category"; - private const int NEW_ISSUE_CATEGORY_ASIGNEE_ID = 1; - private static string createdIssueCategoryId; - - public IssueCategoryTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - [Fact, Order(1)] - public void Should_Create_IssueCategory() - { - var issueCategory = new IssueCategory - { - Name = NEW_ISSUE_CATEGORY_NAME, - AssignTo = IdentifiableName.Create(NEW_ISSUE_CATEGORY_ASIGNEE_ID) - }; - - var savedIssueCategory = fixture.RedmineManager.CreateObject(issueCategory, PROJECT_ID); - - createdIssueCategoryId = savedIssueCategory.Id.ToString(); - - Assert.NotNull(savedIssueCategory); - Assert.True(savedIssueCategory.Name.Equals(NEW_ISSUE_CATEGORY_NAME), "Saved issue category name is invalid."); - } - - [Fact, Order(99)] - public void Should_Delete_IssueCategory() - { - var exception = - (RedmineException) - Record.Exception( - () => fixture.RedmineManager.DeleteObject(createdIssueCategoryId)); - Assert.Null(exception); - Assert.Throws( - () => fixture.RedmineManager.GetObject(createdIssueCategoryId, null)); - } - - [Fact, Order(2)] - public void Should_Get_All_IssueCategories_By_ProjectId() - { - const int NUMBER_OF_ISSUE_CATEGORIES = 3; - var issueCategories = - fixture.RedmineManager.GetObjects(new NameValueCollection - { - {RedmineKeys.PROJECT_ID, PROJECT_ID} - }); - - Assert.NotNull(issueCategories); - Assert.True(issueCategories.Count == NUMBER_OF_ISSUE_CATEGORIES, - "Number of issue categories ( "+issueCategories.Count+" ) != " + NUMBER_OF_ISSUE_CATEGORIES); - } - - [Fact, Order(3)] - public void Should_Get_IssueCategory_By_Id() - { - const string ISSUE_CATEGORY_PROJECT_NAME_TO_GET = "Redmine tests"; - const string ISSUE_CATEGORY_ASIGNEE_NAME_TO_GET = "Redmine"; - - var issueCategory = fixture.RedmineManager.GetObject(createdIssueCategoryId, null); - - Assert.NotNull(issueCategory); - Assert.True(issueCategory.Name.Equals(NEW_ISSUE_CATEGORY_NAME), "Issue category name is invalid."); - Assert.NotNull(issueCategory.AssignTo); - Assert.True(issueCategory.AssignTo.Name.Contains(ISSUE_CATEGORY_ASIGNEE_NAME_TO_GET), - "Asignee name is invalid."); - Assert.NotNull(issueCategory.Project); - Assert.True(issueCategory.Project.Name.Equals(ISSUE_CATEGORY_PROJECT_NAME_TO_GET), - "Project name is invalid."); - } - - [Fact, Order(4)] - public void Should_Update_IssueCategory() - { - const string ISSUE_CATEGORY_NAME_TO_UPDATE = "Category updated"; - const int ISSUE_CATEGORY_ASIGNEE_ID_TO_UPDATE = 2; - - var issueCategory = fixture.RedmineManager.GetObject(createdIssueCategoryId, null); - issueCategory.Name = ISSUE_CATEGORY_NAME_TO_UPDATE; - issueCategory.AssignTo = IdentifiableName.Create(ISSUE_CATEGORY_ASIGNEE_ID_TO_UPDATE); - - fixture.RedmineManager.UpdateObject(createdIssueCategoryId, issueCategory); - - var updatedIssueCategory = fixture.RedmineManager.GetObject(createdIssueCategoryId, null); - - Assert.NotNull(updatedIssueCategory); - Assert.True(updatedIssueCategory.Name.Equals(ISSUE_CATEGORY_NAME_TO_UPDATE), - "Issue category name was not updated."); - Assert.NotNull(updatedIssueCategory.AssignTo); - Assert.True(updatedIssueCategory.AssignTo.Id == ISSUE_CATEGORY_ASIGNEE_ID_TO_UPDATE, - "Issue category asignee was not updated."); - } - - [Fact, Order(5)] - public void Should_Reassign_Issue_After_Issue_Category_Is_Deleted() - { - const string ISSUE_CATEGORY_ID_TO_DELETE = "8"; - const string ISSUE_CATEGORY_ID_TO_REASSIGN = "10"; - const string ISSUE_ID_REASSIGNED = "9"; - - var exception = - (RedmineException) - Record.Exception( - () => fixture.RedmineManager.DeleteObject(ISSUE_CATEGORY_ID_TO_DELETE, new NameValueCollection{{RedmineKeys.REASSIGN_TO_ID, ISSUE_CATEGORY_ID_TO_REASSIGN}})); - Assert.Null(exception); - Assert.Throws( - () => fixture.RedmineManager.GetObject(ISSUE_CATEGORY_ID_TO_DELETE, null)); - - var issue = fixture.RedmineManager.GetObject(ISSUE_ID_REASSIGNED, null); - Assert.NotNull(issue.Category); - Assert.True(ISSUE_CATEGORY_ID_TO_REASSIGN.Equals(issue.Category.Id.ToString()), string.Format("Issue was not reassigned. Issue category id {0} != {1}", issue.Category.Id, ISSUE_CATEGORY_ID_TO_REASSIGN)); - } - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Tests/Sync/IssuePriorityTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/IssuePriorityTests.cs deleted file mode 100644 index 6d82c763..00000000 --- a/tests/redmine-net-api.Tests/Tests/Sync/IssuePriorityTests.cs +++ /dev/null @@ -1,46 +0,0 @@ -/* - Copyright 2011 - 2023 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using Redmine.Net.Api.Types; -using Xunit; - -namespace Padi.RedmineApi.Tests.Tests.Sync -{ - [Trait("Redmine-Net-Api", "IssuePriorities")] -#if !(NET20 || NET40) - [Collection("RedmineCollection")] -#endif - public class IssuePriorityTests - { - public IssuePriorityTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private readonly RedmineFixture fixture; - - [Fact] - public void Should_Get_All_Issue_Priority() - { - const int NUMBER_OF_ISSUE_PRIORITIES = 4; - var issuePriorities = fixture.RedmineManager.GetObjects(); - - Assert.NotNull(issuePriorities); - Assert.True(issuePriorities.Count == NUMBER_OF_ISSUE_PRIORITIES, - "Issue priorities count(" + issuePriorities.Count + ") != " + NUMBER_OF_ISSUE_PRIORITIES); - } - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs deleted file mode 100644 index e8e74148..00000000 --- a/tests/redmine-net-api.Tests/Tests/Sync/IssueRelationTests.cs +++ /dev/null @@ -1,105 +0,0 @@ -/* - Copyright 2011 - 2023 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System.Collections.Specialized; -using Padi.RedmineApi.Tests.Infrastructure; -using Redmine.Net.Api; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Types; -using Xunit; - -namespace Padi.RedmineApi.Tests.Tests.Sync -{ - [Trait("Redmine-Net-Api", "IssueRelations")] -#if !(NET20 || NET40) - [Collection("RedmineCollection")] -#endif - public class IssueRelationTests - { - public IssueRelationTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private readonly RedmineFixture fixture; - - private const string ISSUE_ID = "96"; - private const int RELATED_ISSUE_ID = 94; - private const int RELATION_DELAY = 2; - - private const IssueRelationType OPPOSED_RELATION_TYPE = IssueRelationType.Precedes; - - [Fact, Order(1)] - public void Should_Add_Issue_Relation() - { - const IssueRelationType RELATION_TYPE = IssueRelationType.Follows; - var relation = new IssueRelation - { - IssueToId = RELATED_ISSUE_ID, - Type = RELATION_TYPE, - Delay = RELATION_DELAY - }; - - var savedRelation = fixture.RedmineManager.CreateObject(relation, ISSUE_ID); - - Assert.NotNull(savedRelation); - Assert.True(savedRelation.IssueId == RELATED_ISSUE_ID, "Related issue id is not valid."); - Assert.True(savedRelation.IssueToId.ToString().Equals(ISSUE_ID), "Issue id is not valid."); - Assert.True(savedRelation.Delay == RELATION_DELAY, "Delay is not valid."); - Assert.True(savedRelation.Type == OPPOSED_RELATION_TYPE, "Relation type is not valid."); - } - - [Fact, Order(4)] - public void Should_Delete_Issue_Relation() - { - const string RELATION_ID_TO_DELETE = "23"; - var exception = - (RedmineException) - Record.Exception( - () => fixture.RedmineManager.DeleteObject(RELATION_ID_TO_DELETE)); - Assert.Null(exception); - Assert.Throws( - () => fixture.RedmineManager.GetObject(RELATION_ID_TO_DELETE, null)); - } - - [Fact, Order(2)] - public void Should_Get_IssueRelation_By_Id() - { - const string RELATION_ID_TO_GET = "27"; - var relation = fixture.RedmineManager.GetObject(RELATION_ID_TO_GET, null); - - Assert.NotNull(relation); - Assert.True(relation.IssueId == RELATED_ISSUE_ID, "Related issue id is not valid."); - Assert.True(relation.IssueToId.ToString().Equals(ISSUE_ID), "Issue id is not valid."); - Assert.True(relation.Delay == RELATION_DELAY, "Delay is not valid."); - Assert.True(relation.Type == OPPOSED_RELATION_TYPE, "Relation type is not valid."); - } - - [Fact, Order(3)] - public void Should_Get_IssueRelations_By_Issue_Id() - { - const int NUMBER_OF_RELATIONS = 1; - var relations = - fixture.RedmineManager.GetObjects(new NameValueCollection - { - {RedmineKeys.ISSUE_ID, ISSUE_ID} - }); - - Assert.NotNull(relations); - Assert.True(relations.Count == NUMBER_OF_RELATIONS, "Number of issue relations ( "+relations.Count+" ) != " + NUMBER_OF_RELATIONS); - } - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Tests/Sync/IssueStatusTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/IssueStatusTests.cs deleted file mode 100644 index 1ae6b7c1..00000000 --- a/tests/redmine-net-api.Tests/Tests/Sync/IssueStatusTests.cs +++ /dev/null @@ -1,46 +0,0 @@ -/* - Copyright 2011 - 2023 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using Redmine.Net.Api.Types; -using Xunit; - -namespace Padi.RedmineApi.Tests.Tests.Sync -{ - [Trait("Redmine-Net-Api", "IssueStatuses")] -#if !(NET20 || NET40) - [Collection("RedmineCollection")] -#endif - public class IssueStatusTests - { - public IssueStatusTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private readonly RedmineFixture fixture; - - [Fact] - public void Should_Get_All_Issue_Statuses() - { - const int NUMBER_OF_ISSUE_STATUSES = 7; - var issueStatuses = fixture.RedmineManager.GetObjects(); - - Assert.NotNull(issueStatuses); - Assert.True(issueStatuses.Count == NUMBER_OF_ISSUE_STATUSES, - "Issue statuses count(" + issueStatuses.Count + ") != " + NUMBER_OF_ISSUE_STATUSES); - } - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Tests/Sync/IssueTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/IssueTests.cs deleted file mode 100644 index ab3b58bd..00000000 --- a/tests/redmine-net-api.Tests/Tests/Sync/IssueTests.cs +++ /dev/null @@ -1,332 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using Padi.RedmineApi.Tests.Infrastructure; -using Redmine.Net.Api; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Types; -using Xunit; - -namespace Padi.RedmineApi.Tests.Tests.Sync -{ - [Trait("Redmine-Net-Api", "Issues")] -#if !(NET20 || NET40) - [Collection("RedmineCollection")] -#endif - public class IssueTests - { - public IssueTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - private readonly RedmineFixture fixture; - - //filters - private const string PROJECT_ID = "redmine-net-testq"; - - //watcher - private const int WATCHER_ISSUE_ID = 96; - private const int WATCHER_USER_ID = 8; - - - [Fact, Order(1)] - public void Should_Get_All_Issues() - { - var issues = fixture.RedmineManager.GetObjects(); - - Assert.NotNull(issues); - } - - [Fact, Order(2)] - public void Should_Get_Paginated_Issues() - { - const int NUMBER_OF_PAGINATED_ISSUES = 3; - const int OFFSET = 1; - - var issues = fixture.RedmineManager.GetPaginatedObjects(new NameValueCollection - { - { RedmineKeys.OFFSET, OFFSET.ToString() }, { RedmineKeys.LIMIT, NUMBER_OF_PAGINATED_ISSUES.ToString() }, { "sort", "id:desc" } - }); - - Assert.NotNull(issues.Items); - //Assert.True(issues.Items.Count <= NUMBER_OF_PAGINATED_ISSUES, "number of issues ( "+ issues.Items.Count +" ) != " + NUMBER_OF_PAGINATED_ISSUES.ToString()); - } - - [Fact, Order(3)] - public void Should_Get_Issues_By_Project_Id() - { - var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { RedmineKeys.PROJECT_ID, PROJECT_ID } }); - - Assert.NotNull(issues); - } - - [Fact, Order(4)] - public void Should_Get_Issues_By_SubProject_Id() - { - const string SUB_PROJECT_ID_VALUE = "redmine-net-testr"; - - var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { RedmineKeys.SUB_PROJECT_ID, SUB_PROJECT_ID_VALUE } }); - - Assert.NotNull(issues); - } - - [Fact, Order(5)] - public void Should_Get_Issues_By_Project_Without_SubProject() - { - const string ALL_SUB_PROJECTS = "!*"; - - var issues = fixture.RedmineManager.GetObjects(new NameValueCollection - { - { RedmineKeys.PROJECT_ID, PROJECT_ID }, { RedmineKeys.SUB_PROJECT_ID, ALL_SUB_PROJECTS } - }); - - Assert.NotNull(issues); - } - - [Fact, Order(6)] - public void Should_Get_Issues_By_Tracker() - { - const string TRACKER_ID = "3"; - var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { RedmineKeys.TRACKER_ID, TRACKER_ID } }); - - Assert.NotNull(issues); - } - - [Fact, Order(7)] - public void Should_Get_Issues_By_Status() - { - const string STATUS_ID = "*"; - var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { RedmineKeys.STATUS_ID, STATUS_ID } }); - Assert.NotNull(issues); - } - - [Fact, Order(8)] - public void Should_Get_Issues_By_Assignee() - { - const string ASSIGNED_TO_ID = "me"; - var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { RedmineKeys.ASSIGNED_TO_ID, ASSIGNED_TO_ID } }); - - Assert.NotNull(issues); - } - - [Fact, Order(9)] - public void Should_Get_Issues_By_Custom_Field() - { - const string CUSTOM_FIELD_NAME = "cf_13"; - const string CUSTOM_FIELD_VALUE = "Testx"; - - var issues = fixture.RedmineManager.GetObjects(new NameValueCollection { { CUSTOM_FIELD_NAME, CUSTOM_FIELD_VALUE } }); - - Assert.NotNull(issues); - } - - [Fact, Order(10)] - public void Should_Get_Issue_By_Id() - { - const string ISSUE_ID = "96"; - - var issue = fixture.RedmineManager.GetObject(ISSUE_ID, new NameValueCollection - { - { RedmineKeys.INCLUDE, $"{RedmineKeys.CHILDREN},{RedmineKeys.ATTACHMENTS},{RedmineKeys.RELATIONS},{RedmineKeys.CHANGE_SETS},{RedmineKeys.JOURNALS},{RedmineKeys.WATCHERS}" } - }); - - Assert.NotNull(issue); - } - - [Fact, Order(11)] - public void Should_Add_Issue() - { - const bool NEW_ISSUE_IS_PRIVATE = true; - - const int NEW_ISSUE_PROJECT_ID = 1; - const int NEW_ISSUE_TRACKER_ID = 1; - const int NEW_ISSUE_STATUS_ID = 1; - const int NEW_ISSUE_PRIORITY_ID = 9; - const int NEW_ISSUE_CATEGORY_ID = 18; - const int NEW_ISSUE_FIXED_VERSION_ID = 9; - const int NEW_ISSUE_ASSIGNED_TO_ID = 8; - const int NEW_ISSUE_PARENT_ISSUE_ID = 96; - const int NEW_ISSUE_CUSTOM_FIELD_ID = 13; - const int NEW_ISSUE_ESTIMATED_HOURS = 12; - const int NEW_ISSUE_FIRST_WATCHER_ID = 2; - const int NEW_ISSUE_SECOND_WATCHER_ID = 8; - - const string NEW_ISSUE_CUSTOM_FIELD_VALUE = "Issue custom field completed"; - const string NEW_ISSUE_SUBJECT = "Issue created using Rest API"; - const string NEW_ISSUE_DESCRIPTION = "Issue description..."; - - var newIssueStartDate = DateTime.Now; - var newIssueDueDate = DateTime.Now.AddDays(10); - - var icf = IdentifiableName.Create(NEW_ISSUE_CUSTOM_FIELD_ID); - if (icf != null) - { - icf.Values = new List {new CustomFieldValue {Info = NEW_ISSUE_CUSTOM_FIELD_VALUE}}; - } - - var issue = new Issue - { - Project = IdentifiableName.Create(NEW_ISSUE_PROJECT_ID), - Tracker = IdentifiableName.Create(NEW_ISSUE_TRACKER_ID), - Status = IdentifiableName.Create(NEW_ISSUE_STATUS_ID), - Priority = IdentifiableName.Create(NEW_ISSUE_PRIORITY_ID), - Subject = NEW_ISSUE_SUBJECT, - Description = NEW_ISSUE_DESCRIPTION, - Category = IdentifiableName.Create(NEW_ISSUE_CATEGORY_ID), - FixedVersion = IdentifiableName.Create(NEW_ISSUE_FIXED_VERSION_ID), - AssignedTo = IdentifiableName.Create(NEW_ISSUE_ASSIGNED_TO_ID), - ParentIssue = IdentifiableName.Create(NEW_ISSUE_PARENT_ISSUE_ID), - - CustomFields = new List {icf}, - IsPrivate = NEW_ISSUE_IS_PRIVATE, - EstimatedHours = NEW_ISSUE_ESTIMATED_HOURS, - StartDate = newIssueStartDate, - DueDate = newIssueDueDate, - Watchers = new List - { - IdentifiableName.Create(NEW_ISSUE_FIRST_WATCHER_ID), - IdentifiableName.Create(NEW_ISSUE_SECOND_WATCHER_ID) - } - }; - - var savedIssue = fixture.RedmineManager.CreateObject(issue); - - Assert.NotNull(savedIssue); - Assert.True(issue.Subject.Equals(savedIssue.Subject), "Issue subject is invalid."); - Assert.NotEqual(issue, savedIssue); - - } - - [Fact, Order(12)] - public void Should_Update_Issue() - { - const string UPDATED_ISSUE_ID = "98"; - const string UPDATED_ISSUE_SUBJECT = "Issue updated subject"; - const string UPDATED_ISSUE_DESCRIPTION = null; - const int UPDATED_ISSUE_PROJECT_ID = 9; - const int UPDATED_ISSUE_TRACKER_ID = 3; - const int UPDATED_ISSUE_PRIORITY_ID = 8; - const int UPDATED_ISSUE_CATEGORY_ID = 18; - const int UPDATED_ISSUE_ASSIGNED_TO_ID = 2; - const int UPDATED_ISSUE_PARENT_ISSUE_ID = 91; - const int UPDATED_ISSUE_CUSTOM_FIELD_ID = 13; - const string UPDATED_ISSUE_CUSTOM_FIELD_VALUE = "Another custom field completed"; - const int UPDATED_ISSUE_ESTIMATED_HOURS = 23; - const string UPDATED_ISSUE_NOTES = "A lot is changed"; - const bool UPDATED_ISSUE_PRIVATE_NOTES = true; - - DateTime? updatedIssueStartDate = default(DateTime?); - - var updatedIssueDueDate = DateTime.Now.AddMonths(1); - - var issue = fixture.RedmineManager.GetObject(UPDATED_ISSUE_ID, new NameValueCollection - { - { RedmineKeys.INCLUDE, $"{RedmineKeys.CHILDREN},{RedmineKeys.ATTACHMENTS},{RedmineKeys.RELATIONS},{RedmineKeys.CHANGE_SETS},{RedmineKeys.JOURNALS},{RedmineKeys.WATCHERS}" } - }); - - issue.Subject = UPDATED_ISSUE_SUBJECT; - issue.Description = UPDATED_ISSUE_DESCRIPTION; - issue.StartDate = updatedIssueStartDate; - issue.DueDate = updatedIssueDueDate; - issue.Project = IdentifiableName.Create(UPDATED_ISSUE_PROJECT_ID); - issue.Tracker = IdentifiableName.Create(UPDATED_ISSUE_TRACKER_ID); - issue.Priority = IdentifiableName.Create(UPDATED_ISSUE_PRIORITY_ID); - issue.Category = IdentifiableName.Create(UPDATED_ISSUE_CATEGORY_ID); - issue.AssignedTo = IdentifiableName.Create(UPDATED_ISSUE_ASSIGNED_TO_ID); - issue.ParentIssue = IdentifiableName.Create(UPDATED_ISSUE_PARENT_ISSUE_ID); - - var icf = (IssueCustomField)IdentifiableName.Create(UPDATED_ISSUE_CUSTOM_FIELD_ID); - icf.Values = new List { new CustomFieldValue { Info = UPDATED_ISSUE_CUSTOM_FIELD_VALUE } }; - - issue.CustomFields?.Add(icf); - issue.EstimatedHours = UPDATED_ISSUE_ESTIMATED_HOURS; - issue.Notes = UPDATED_ISSUE_NOTES; - issue.PrivateNotes = UPDATED_ISSUE_PRIVATE_NOTES; - - fixture.RedmineManager.UpdateObject(UPDATED_ISSUE_ID, issue); - - var updatedIssue = fixture.RedmineManager.GetObject(UPDATED_ISSUE_ID, new NameValueCollection - { - { RedmineKeys.INCLUDE, $"{RedmineKeys.CHILDREN},{RedmineKeys.ATTACHMENTS},{RedmineKeys.RELATIONS},{RedmineKeys.CHANGE_SETS},{RedmineKeys.JOURNALS},{RedmineKeys.WATCHERS}" } - }); - - Assert.NotNull(updatedIssue); - Assert.True(issue.Subject.Equals(updatedIssue.Subject), "Issue subject is invalid."); - - } - - [Fact, Order(99)] - public void Should_Delete_Issue() - { - const string DELETED_ISSUE_ID = "90"; - - var exception = (RedmineException)Record.Exception(() => fixture.RedmineManager.DeleteObject(DELETED_ISSUE_ID)); - Assert.Null(exception); - Assert.Throws(() => fixture.RedmineManager.GetObject(DELETED_ISSUE_ID, null)); - } - - [Fact, Order(13)] - public void Should_Add_Watcher_To_Issue() - { - fixture.RedmineManager.AddWatcherToIssue(WATCHER_ISSUE_ID, WATCHER_USER_ID); - - var issue = fixture.RedmineManager.GetObject(WATCHER_ISSUE_ID.ToString(), new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.WATCHERS } }); - - Assert.NotNull(issue); - Assert.NotNull(issue.Watchers); - Assert.True(((List)issue.Watchers).Find(w => w.Id == WATCHER_USER_ID) != null, "Watcher was not added."); - } - - [Fact, Order(14)] - public void Should_Remove_Watcher_From_Issue() - { - fixture.RedmineManager.RemoveWatcherFromIssue(WATCHER_ISSUE_ID, WATCHER_USER_ID); - - var issue = fixture.RedmineManager.GetObject(WATCHER_ISSUE_ID.ToString(), new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.WATCHERS } }); - - Assert.NotNull(issue); - Assert.True(issue.Watchers == null || ((List)issue.Watchers).Find(w => w.Id == WATCHER_USER_ID) == null, "Watcher was not removed."); - } - - [Fact, Order(15)] - public void Should_Clone_Issue() - { - const string ISSUE_TO_CLONE_SUBJECT = "Issue to clone"; - const int ISSUE_TO_CLONE_CUSTOM_FIELD_ID = 13; - const string ISSUE_TO_CLONE_CUSTOM_FIELD_VALUE = "Issue to clone custom field value"; - const int CLONED_ISSUE_CUSTOM_FIELD_ID = 13; - const string CLONED_ISSUE_CUSTOM_FIELD_VALUE = "Cloned issue custom field value"; - - var icfc = (IssueCustomField)IdentifiableName.Create(ISSUE_TO_CLONE_CUSTOM_FIELD_ID); - icfc.Values = new List { new CustomFieldValue { Info = ISSUE_TO_CLONE_CUSTOM_FIELD_VALUE } }; - - var issueToClone = new Issue - { - Subject = ISSUE_TO_CLONE_SUBJECT, - CustomFields = new List() { icfc } - }; - - var clonedIssue = (Issue)issueToClone.Clone(); - - var icf = (IssueCustomField)IdentifiableName.Create(CLONED_ISSUE_CUSTOM_FIELD_ID); - icf.Values = new List { new CustomFieldValue { Info = CLONED_ISSUE_CUSTOM_FIELD_VALUE } }; - - clonedIssue.CustomFields.Add(icf); - - Assert.True(issueToClone.CustomFields.Count != clonedIssue.CustomFields.Count); - } - - [Fact] - public void Should_Get_Issue_With_Hours() - { - const string ISSUE_ID = "1"; - - var issue = fixture.RedmineManager.GetObject(ISSUE_ID, null); - - Assert.Equal(8.0f, issue.EstimatedHours); - Assert.Equal(8.0f, issue.TotalEstimatedHours); - Assert.Equal(5.0f, issue.TotalSpentHours); - Assert.Equal(5.0f, issue.SpentHours); - } - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Tests/Sync/NewsTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/NewsTests.cs deleted file mode 100644 index e7a4482c..00000000 --- a/tests/redmine-net-api.Tests/Tests/Sync/NewsTests.cs +++ /dev/null @@ -1,61 +0,0 @@ -/* - Copyright 2011 - 2023 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System.Collections.Specialized; -using Padi.RedmineApi.Tests.Infrastructure; -using Redmine.Net.Api; -using Redmine.Net.Api.Types; -using Xunit; - -namespace Padi.RedmineApi.Tests.Tests.Sync -{ - [Trait("Redmine-Net-Api", "News")] -#if !(NET20 || NET40) - [Collection("RedmineCollection")] -#endif - public class NewsTests - { - public NewsTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private readonly RedmineFixture fixture; - - [Fact, Order(1)] - public void Should_Get_All_News() - { - const int NUMBER_OF_NEWS = 2; - var news = fixture.RedmineManager.GetObjects(); - - Assert.NotNull(news); - Assert.True(news.Count == NUMBER_OF_NEWS, "News count(" + news.Count + ") != " + NUMBER_OF_NEWS); - } - - [Fact, Order(2)] - public void Should_Get_News_By_Project_Id() - { - const string PROJECT_ID = "redmine-net-testq"; - const int NUMBER_OF_NEWS_BY_PROJECT_ID = 1; - var news = - fixture.RedmineManager.GetObjects(new NameValueCollection {{RedmineKeys.PROJECT_ID, PROJECT_ID}}); - - Assert.NotNull(news); - Assert.True(news.Count == NUMBER_OF_NEWS_BY_PROJECT_ID, - "News count(" + news.Count + ") != " + NUMBER_OF_NEWS_BY_PROJECT_ID); - } - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs deleted file mode 100644 index 0b9e7ae8..00000000 --- a/tests/redmine-net-api.Tests/Tests/Sync/ProjectMembershipTests.cs +++ /dev/null @@ -1,125 +0,0 @@ -/* - Copyright 2011 - 2023 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System.Collections.Generic; -using System.Collections.Specialized; -using Padi.RedmineApi.Tests.Infrastructure; -using Redmine.Net.Api; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Types; -using Xunit; - -namespace Padi.RedmineApi.Tests.Tests.Sync -{ - [Trait("Redmine-Net-Api", "ProjectMemberships")] -#if !(NET20 || NET40) - [Collection("RedmineCollection")] -#endif - public class ProjectMembershipTests - { - public ProjectMembershipTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private readonly RedmineFixture fixture; - - private const string PROJECT_IDENTIFIER = "redmine-net-testq"; - - - [Fact, Order(1)] - public void Should_Add_Project_Membership() - { - const int NEW_PROJECT_MEMBERSHIP_USER_ID = 2; - const int NEW_PROJECT_MEMBERSHIP_ROLE_ID = 5; - - var pm = new ProjectMembership - { - User = IdentifiableName.Create(NEW_PROJECT_MEMBERSHIP_USER_ID), - Roles = new List { (MembershipRole)IdentifiableName.Create(NEW_PROJECT_MEMBERSHIP_ROLE_ID)} - }; - - var createdPm = fixture.RedmineManager.CreateObject(pm, PROJECT_IDENTIFIER); - - Assert.NotNull(createdPm); - Assert.True(createdPm.User.Id == NEW_PROJECT_MEMBERSHIP_USER_ID, "User is invalid."); - Assert.NotNull(createdPm.Roles); - //Assert.True(createdPm.Roles.Exists(r => r.Id == NEW_PROJECT_MEMBERSHIP_ROLE_ID), - // string.Format("Role id {0} does not exist.", NEW_PROJECT_MEMBERSHIP_ROLE_ID)); - } - - [Fact,Order(99)] - public void Should_Delete_Project_Membership() - { - const string DELETED_PROJECT_MEMBERSHIP_ID = "142"; - var exception = - (RedmineException) - Record.Exception( - () => - fixture.RedmineManager.DeleteObject(DELETED_PROJECT_MEMBERSHIP_ID)); - Assert.Null(exception); - Assert.Throws( - () => fixture.RedmineManager.GetObject(DELETED_PROJECT_MEMBERSHIP_ID, null)); - } - - [Fact, Order(2)] - public void Should_Get_Memberships_By_Project_Identifier() - { - const int NUMBER_OF_PROJECT_MEMBERSHIPS = 3; - var projectMemberships = - fixture.RedmineManager.GetObjects(new NameValueCollection - { - {RedmineKeys.PROJECT_ID, PROJECT_IDENTIFIER} - }); - - Assert.NotNull(projectMemberships); - Assert.True(projectMemberships.Count == NUMBER_OF_PROJECT_MEMBERSHIPS, - "Project memberships count ( "+ projectMemberships.Count +" ) != " + NUMBER_OF_PROJECT_MEMBERSHIPS); - } - - [Fact, Order(3)] - public void Should_Get_Project_Membership_By_Id() - { - const string PROJECT_MEMBERSHIP_ID = "143"; - var projectMembership = fixture.RedmineManager.GetObject(PROJECT_MEMBERSHIP_ID, null); - - Assert.NotNull(projectMembership); - Assert.NotNull(projectMembership.Project); - Assert.True(projectMembership.User != null || projectMembership.Group != null, - "User and group are both null."); - Assert.NotNull(projectMembership.Roles); - } - - [Fact, Order(4)] - public void Should_Update_Project_Membership() - { - const string UPDATED_PROJECT_MEMBERSHIP_ID = "143"; - const int UPDATED_PROJECT_MEMBERSHIP_ROLE_ID = 4; - - var pm = fixture.RedmineManager.GetObject(UPDATED_PROJECT_MEMBERSHIP_ID, null); - pm.Roles.Add((MembershipRole)IdentifiableName.Create(UPDATED_PROJECT_MEMBERSHIP_ROLE_ID)); - - fixture.RedmineManager.UpdateObject(UPDATED_PROJECT_MEMBERSHIP_ID, pm); - - var updatedPm = fixture.RedmineManager.GetObject(UPDATED_PROJECT_MEMBERSHIP_ID, null); - - Assert.NotNull(updatedPm); - Assert.NotNull(updatedPm.Roles); - //Assert.True(updatedPm.Roles.Find(r => r.Id == UPDATED_PROJECT_MEMBERSHIP_ROLE_ID) != null, - // string.Format("Role with id {0} was not found in roles list.", UPDATED_PROJECT_MEMBERSHIP_ROLE_ID)); - } - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Tests/Sync/ProjectTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/ProjectTests.cs deleted file mode 100644 index b07e0065..00000000 --- a/tests/redmine-net-api.Tests/Tests/Sync/ProjectTests.cs +++ /dev/null @@ -1,263 +0,0 @@ -/* - Copyright 2011 - 2023 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System.Collections.Generic; -using System.Collections.Specialized; -using Padi.RedmineApi.Tests.Infrastructure; -using Redmine.Net.Api; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Types; -using Xunit; - -namespace Padi.RedmineApi.Tests.Tests.Sync -{ - [Trait("Redmine-Net-Api", "Projects")] -#if !(NET20 || NET40) - [Collection("RedmineCollection")] -#endif - [Order(1)] - public class ProjectTests - { - public ProjectTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private const string PROJECT_IDENTIFIER = "redmine-net-api-project-test"; - private const string PROJECT_NAME = "Redmine Net Api Project Test"; - - private readonly RedmineFixture fixture; - - private static Project CreateTestProjectWithRequiredPropertiesSet() - { - var project = new Project - { - Name = PROJECT_NAME, - Identifier = PROJECT_IDENTIFIER - }; - - return project; - } - - private static Project CreateTestProjectWithAllPropertiesSet() - { - var project = new Project - { - Name = "Redmine Net Api Project Test All Properties", - Description = "This is a test project.", - Identifier = "rnaptap", - HomePage = "www.redminetest.com", - IsPublic = true, - InheritMembers = true, - EnabledModules = new List - { - new ProjectEnabledModule {Name = "issue_tracking"}, - new ProjectEnabledModule {Name = "time_tracking"} - }, - Trackers = new List - { - (ProjectTracker) IdentifiableName.Create( 1), - (ProjectTracker) IdentifiableName.Create(2) - } - }; - - return project; - } - - private static Project CreateTestProjectWithInvalidTrackersId() - { - var project = new Project - { - Name = "Redmine Net Api Project Test Invalid Trackers", - Identifier = "rnaptit", - Trackers = new List - { - (ProjectTracker) IdentifiableName.Create(999999), - (ProjectTracker) IdentifiableName.Create(999998) - } - }; - - return project; - } - - private static Project CreateTestProjectWithParentSet(int parentId) - { - var project = new Project - { - Name = "Redmine Net Api Project With Parent Set", - Identifier = "rnapwps", - Parent = IdentifiableName.Create(parentId) - }; - - return project; - } - - [Fact, Order(0)] - public void Should_Create_Project_With_Required_Properties() - { - var savedProject = fixture.RedmineManager.CreateObject(CreateTestProjectWithRequiredPropertiesSet()); - - Assert.NotNull(savedProject); - Assert.NotEqual(0, savedProject.Id); - Assert.True(savedProject.Name.Equals(PROJECT_NAME), "Project name is invalid."); - Assert.True(savedProject.Identifier.Equals(PROJECT_IDENTIFIER), "Project identifier is invalid."); - } - - [Fact, Order(1)] - public void Should_Create_Project_With_All_Properties_Set() - { - var savedProject = fixture.RedmineManager.CreateObject(CreateTestProjectWithAllPropertiesSet()); - - Assert.NotNull(savedProject); - Assert.NotEqual(0, savedProject.Id); - Assert.True(savedProject.Identifier.Equals("rnaptap"), "Project identifier is invalid."); - Assert.True(savedProject.Name.Equals("Redmine Net Api Project Test All Properties"), - "Project name is invalid."); - } - - [Fact, Order(2)] - public void Should_Create_Project_With_Parent() - { - var parentProject = - fixture.RedmineManager.CreateObject(new Project { Identifier = "parent-project", Name = "Parent project" }); - - var savedProject = fixture.RedmineManager.CreateObject(CreateTestProjectWithParentSet(parentProject.Id)); - - Assert.NotNull(savedProject); - Assert.True(savedProject.Parent.Id == parentProject.Id, "Parent project is invalid."); - } - - [Fact, Order(3)] - public void Should_Get_Redmine_Net_Api_Project_Test_Project() - { - var project = fixture.RedmineManager.GetObject(PROJECT_IDENTIFIER, null); - - Assert.NotNull(project); - Assert.IsType(project); - Assert.Equal(project.Identifier, PROJECT_IDENTIFIER); - Assert.Equal(project.Name, PROJECT_NAME); - } - - [Fact, Order(4)] - public void Should_Get_Test_Project_With_All_Properties_Set() - { - var project = fixture.RedmineManager.GetObject("rnaptap", new NameValueCollection - { - {RedmineKeys.INCLUDE, string.Join(",", RedmineKeys.TRACKERS, RedmineKeys.ENABLED_MODULES)} - }); - - Assert.NotNull(project); - Assert.IsType(project); - Assert.True(project.Name.Equals("Redmine Net Api Project Test All Properties"), "Project name not equal."); - Assert.True(project.Identifier.Equals("rnaptap"), "Project identifier not equal."); - Assert.True(project.Description.Equals("This is a test project."), "Project description not equal."); - Assert.True(project.HomePage.Equals("www.redminetest.com"), "Project homepage not equal."); - Assert.True(project.IsPublic.Equals(true), - "Project is_public not equal. (This property is available starting with 2.6.0)"); - - Assert.NotNull(project.Trackers); - Assert.True(project.Trackers.Count == 2, "Trackers count != " + 2); - - Assert.NotNull(project.EnabledModules); - Assert.True(project.EnabledModules.Count == 2, - "Enabled modules count (" + project.EnabledModules.Count + ") != " + 2); - } - - [Fact, Order(5)] - public void Should_Update_Redmine_Net_Api_Project_Test_Project() - { - const string UPDATED_PROJECT_NAME = "Project created using API updated"; - const string UPDATED_PROJECT_DESCRIPTION = "Test project description updated"; - const string UPDATED_PROJECT_HOMEPAGE = "http://redminetestsupdated.com/"; - const bool UPDATED_PROJECT_ISPUBLIC = true; - const bool UPDATED_PROJECT_INHERIT_MEMBERS = false; - - var project = fixture.RedmineManager.GetObject(PROJECT_IDENTIFIER, null); - project.Name = UPDATED_PROJECT_NAME; - project.Description = UPDATED_PROJECT_DESCRIPTION; - project.HomePage = UPDATED_PROJECT_HOMEPAGE; - project.IsPublic = UPDATED_PROJECT_ISPUBLIC; - project.InheritMembers = UPDATED_PROJECT_INHERIT_MEMBERS; - - var exception = - (RedmineException) - Record.Exception(() => fixture.RedmineManager.UpdateObject(PROJECT_IDENTIFIER, project)); - Assert.Null(exception); - - var updatedProject = fixture.RedmineManager.GetObject(PROJECT_IDENTIFIER, null); - - Assert.True(updatedProject.Name.Equals(UPDATED_PROJECT_NAME), "Project name was not updated."); - Assert.True(updatedProject.Description.Equals(UPDATED_PROJECT_DESCRIPTION), - "Project description was not updated."); - Assert.True(updatedProject.HomePage.Equals(UPDATED_PROJECT_HOMEPAGE), "Project homepage was not updated."); - Assert.True(updatedProject.IsPublic.Equals(UPDATED_PROJECT_ISPUBLIC), - "Project is_public was not updated. (This property is available starting with 2.6.0)"); - } - - [Fact, Order(7)] - public void Should_Throw_Exception_When_Create_Empty_Project() - { - Assert.Throws(() => fixture.RedmineManager.CreateObject(new Project())); - } - - [Fact, Order(8)] - public void Should_Throw_Exception_When_Project_Identifier_Is_Invalid() - { - Assert.Throws(() => fixture.RedmineManager.GetObject("99999999", null)); - } - - [Fact, Order(9)] - public void Should_Delete_Project_And_Parent_Project() - { - var exception = - (RedmineException)Record.Exception(() => fixture.RedmineManager.DeleteObject("rnapwps")); - Assert.Null(exception); - Assert.Throws(() => fixture.RedmineManager.GetObject("rnapwps", null)); - - exception = - (RedmineException) - Record.Exception(() => fixture.RedmineManager.DeleteObject("parent-project")); - Assert.Null(exception); - Assert.Throws(() => fixture.RedmineManager.GetObject("parent-project", null)); - } - - [Fact, Order(10)] - public void Should_Delete_Project_With_All_Properties_Set() - { - var exception = - (RedmineException)Record.Exception(() => fixture.RedmineManager.DeleteObject("rnaptap")); - Assert.Null(exception); - Assert.Throws(() => fixture.RedmineManager.GetObject("rnaptap", null)); - } - - [Fact, Order(11)] - public void Should_Delete_Redmine_Net_Api_Project_Test_Project() - { - var exception = - (RedmineException) - Record.Exception(() => fixture.RedmineManager.DeleteObject(PROJECT_IDENTIFIER)); - Assert.Null(exception); - Assert.Throws(() => fixture.RedmineManager.GetObject(PROJECT_IDENTIFIER, null)); - } - - [Fact, Order(12)] - public void Should_Throw_Exception_Create_Project_Invalid_Trackers() - { - Assert.Throws( - () => fixture.RedmineManager.CreateObject(CreateTestProjectWithInvalidTrackersId())); - } - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Tests/Sync/QueryTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/QueryTests.cs deleted file mode 100644 index 87f5c4b0..00000000 --- a/tests/redmine-net-api.Tests/Tests/Sync/QueryTests.cs +++ /dev/null @@ -1,47 +0,0 @@ -/* - Copyright 2011 - 2023 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using Padi.RedmineApi.Tests.Infrastructure; -using Redmine.Net.Api.Types; -using Xunit; - -namespace Padi.RedmineApi.Tests.Tests.Sync -{ - [Trait("Redmine-Net-Api", "Queries")] -#if !(NET20 || NET40) - [Collection("RedmineCollection")] -#endif - public class QueryTests - { - public QueryTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private readonly RedmineFixture fixture; - - [Fact, Order(1)] - public void Should_Get_All_Queries() - { - const int NUMBER_OF_QUERIES = 2; - var queries = fixture.RedmineManager.GetObjects(); - - Assert.NotNull(queries); - Assert.True(queries.Count == NUMBER_OF_QUERIES, - "Queries count(" + queries.Count + ") != " + NUMBER_OF_QUERIES); - } - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Tests/Sync/RoleTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/RoleTests.cs deleted file mode 100644 index 6c44ea55..00000000 --- a/tests/redmine-net-api.Tests/Tests/Sync/RoleTests.cs +++ /dev/null @@ -1,63 +0,0 @@ -/* - Copyright 2011 - 2023 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using Padi.RedmineApi.Tests.Infrastructure; -using Redmine.Net.Api.Types; -using Xunit; - -namespace Padi.RedmineApi.Tests.Tests.Sync -{ - [Trait("Redmine-Net-Api", "Roles")] -#if !(NET20 || NET40) - [Collection("RedmineCollection")] -#endif - public class RoleTests - { - public RoleTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private readonly RedmineFixture fixture; - - [Fact, Order(1)] - public void Should_Get_All_Roles() - { - const int NUMBER_OF_ROLES = 3; - var roles = fixture.RedmineManager.GetObjects(); - - Assert.NotNull(roles); - Assert.True(roles.Count == NUMBER_OF_ROLES, "Roles count(" + roles.Count + ") != " + NUMBER_OF_ROLES); - } - - [Fact, Order(2)] - public void Should_Get_Role_By_Id() - { - const string ROLE_ID = "5"; - const int NUMBER_OF_ROLE_PERMISSIONS = 1; - const string ROLE_NAME = "CustomRole"; - - var role = fixture.RedmineManager.GetObject(ROLE_ID, null); - - Assert.NotNull(role); - Assert.True(role.Name.Equals(ROLE_NAME), "Role name is invalid."); - - Assert.NotNull(role.Permissions); - Assert.True(role.Permissions.Count == NUMBER_OF_ROLE_PERMISSIONS, - "Permissions count(" + role.Permissions.Count + ") != " + NUMBER_OF_ROLE_PERMISSIONS); - } - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryActivtiyTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryActivtiyTests.cs deleted file mode 100644 index 77d7150b..00000000 --- a/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryActivtiyTests.cs +++ /dev/null @@ -1,48 +0,0 @@ -/* - Copyright 2011 - 2023 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using Padi.RedmineApi.Tests.Infrastructure; -using Redmine.Net.Api.Types; -using Xunit; - -namespace Padi.RedmineApi.Tests.Tests.Sync -{ - [Trait("Redmine-Net-Api", "TimeEntryActivities")] -#if !(NET20 || NET40) - [Collection("RedmineCollection")] -#endif - public class TimeEntryActivityTests - { - public TimeEntryActivityTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private readonly RedmineFixture fixture; - - [Fact, Order(1)] - public void Should_Get_All_TimeEntryActivities() - { - const int NUMBER_OF_TIME_ENTRY_ACTIVITIES = 3; - - var timeEntryActivities = fixture.RedmineManager.GetObjects(); - - Assert.NotNull(timeEntryActivities); - Assert.True(timeEntryActivities.Count == NUMBER_OF_TIME_ENTRY_ACTIVITIES, - "Time entry activities count ( "+ timeEntryActivities.Count +" ) != " + NUMBER_OF_TIME_ENTRY_ACTIVITIES); - } - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs deleted file mode 100644 index f52808ec..00000000 --- a/tests/redmine-net-api.Tests/Tests/Sync/TimeEntryTests.cs +++ /dev/null @@ -1,145 +0,0 @@ -/* - Copyright 2011 - 2023 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using Padi.RedmineApi.Tests.Infrastructure; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Types; -using Xunit; - -namespace Padi.RedmineApi.Tests.Tests.Sync -{ - [Trait("Redmine-Net-Api", "TimeEntries")] -#if !(NET20 || NET40) - [Collection("RedmineCollection")] -#endif - public class TimeEntryTests - { - public TimeEntryTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private readonly RedmineFixture fixture; - - [Fact, Order(1)] - public void Should_Create_Time_Entry() - { - const int NEW_TIME_ENTRY_ISSUE_ID = 18; - const int NEW_TIME_ENTRY_PROJECT_ID = 9; - var newTimeEntryDate = DateTime.Now; - const int NEW_TIME_ENTRY_HOURS = 1; - const int NEW_TIME_ENTRY_ACTIVITY_ID = 16; - const string NEW_TIME_ENTRY_COMMENTS = "Added time entry on project"; - - var timeEntry = new TimeEntry - { - Issue = IdentifiableName.Create(NEW_TIME_ENTRY_ISSUE_ID), - Project = IdentifiableName.Create(NEW_TIME_ENTRY_PROJECT_ID), - SpentOn = newTimeEntryDate, - Hours = NEW_TIME_ENTRY_HOURS, - Activity = IdentifiableName.Create(NEW_TIME_ENTRY_ACTIVITY_ID), - Comments = NEW_TIME_ENTRY_COMMENTS - }; - - var savedTimeEntry = fixture.RedmineManager.CreateObject(timeEntry); - - Assert.NotNull(savedTimeEntry); - Assert.NotNull(savedTimeEntry.Issue); - Assert.True(savedTimeEntry.Issue.Id == NEW_TIME_ENTRY_ISSUE_ID, "Issue id is invalid."); - Assert.NotNull(savedTimeEntry.Project); - Assert.True(savedTimeEntry.Project.Id == NEW_TIME_ENTRY_PROJECT_ID, "Project id is invalid."); - Assert.NotNull(savedTimeEntry.SpentOn); - Assert.True(DateTime.Compare(savedTimeEntry.SpentOn.Value.Date, newTimeEntryDate.Date) == 0, - "Date is invalid."); - Assert.True(savedTimeEntry.Hours == NEW_TIME_ENTRY_HOURS, "Hours value is not valid."); - Assert.NotNull(savedTimeEntry.Activity); - Assert.True(savedTimeEntry.Activity.Id == NEW_TIME_ENTRY_ACTIVITY_ID, "Activity id is invalid."); - Assert.NotNull(savedTimeEntry.Comments); - Assert.True(savedTimeEntry.Comments.Equals(NEW_TIME_ENTRY_COMMENTS), "Coments value is invalid."); - } - - [Fact, Order(99)] - public void Should_Delete_Time_Entry() - { - const string DELETED_TIME_ENTRY_ID = "43"; - var exception = - (RedmineException) - Record.Exception(() => fixture.RedmineManager.DeleteObject(DELETED_TIME_ENTRY_ID)); - Assert.Null(exception); - Assert.Throws( - () => fixture.RedmineManager.GetObject(DELETED_TIME_ENTRY_ID, null)); - } - - [Fact, Order(2)] - public void Should_Get_All_Time_Entries() - { - var timeEntries = fixture.RedmineManager.GetObjects(); - - Assert.NotNull(timeEntries); - Assert.NotEmpty(timeEntries); - } - - [Fact, Order(3)] - public void Should_Get_Time_Entry_By_Id() - { - const string TIME_ENTRY_ID = "30"; - - var timeEntry = fixture.RedmineManager.GetObject(TIME_ENTRY_ID, null); - - Assert.NotNull(timeEntry); - Assert.IsType(timeEntry); - Assert.NotNull(timeEntry.Project); - Assert.NotNull(timeEntry.SpentOn); - Assert.NotNull(timeEntry.Activity); - } - - [Fact, Order(4)] - public void Should_Update_Time_Entry() - { - const string UPDATED_TIME_ENTRY_ID = "31"; - const int UPDATED_TIME_ENTRY_ISSUE_ID = 18; - const int UPDATED_TIME_ENTRY_PROJECT_ID = 9; - const int UPDATED_TIME_ENTRY_HOURS = 3; - const int UPDATED_TIME_ENTRY_ACTIVITY_ID = 17; - const string UPDATED_TIME_ENTRY_COMMENTS = "Time entry updated"; - var updatedTimeEntryDate = DateTime.Now.AddDays(-2); - - var timeEntry = fixture.RedmineManager.GetObject(UPDATED_TIME_ENTRY_ID, null); - timeEntry.Project = IdentifiableName.Create(UPDATED_TIME_ENTRY_PROJECT_ID); - timeEntry.Issue = IdentifiableName.Create(UPDATED_TIME_ENTRY_ISSUE_ID); - timeEntry.SpentOn = updatedTimeEntryDate; - timeEntry.Hours = UPDATED_TIME_ENTRY_HOURS; - timeEntry.Comments = UPDATED_TIME_ENTRY_COMMENTS; - - if (timeEntry.Activity == null) timeEntry.Activity = IdentifiableName.Create(UPDATED_TIME_ENTRY_ACTIVITY_ID); - - fixture.RedmineManager.UpdateObject(UPDATED_TIME_ENTRY_ID, timeEntry); - - var updatedTimeEntry = fixture.RedmineManager.GetObject(UPDATED_TIME_ENTRY_ID, null); - - Assert.NotNull(updatedTimeEntry); - Assert.True(updatedTimeEntry.Project.Id == timeEntry.Project.Id, "Time entry project was not updated."); - Assert.True(updatedTimeEntry.Issue.Id == timeEntry.Issue.Id, "Time entry issue was not updated."); - Assert.True( - updatedTimeEntry.SpentOn != null && timeEntry.SpentOn != null && - DateTime.Compare(updatedTimeEntry.SpentOn.Value.Date, timeEntry.SpentOn.Value.Date) == 0, - "Time entry spent on field was not updated."); - Assert.True(updatedTimeEntry.Hours == timeEntry.Hours, "Time entry hours was not updated."); - Assert.True(updatedTimeEntry.Comments.Equals(timeEntry.Comments), "Time entry comments was not updated."); - } - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Tests/Sync/TrackerTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/TrackerTests.cs deleted file mode 100644 index b5061e55..00000000 --- a/tests/redmine-net-api.Tests/Tests/Sync/TrackerTests.cs +++ /dev/null @@ -1,46 +0,0 @@ -/* - Copyright 2011 - 2023 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using Redmine.Net.Api.Types; -using Xunit; - -namespace Padi.RedmineApi.Tests.Tests.Sync -{ - [Trait("Redmine-Net-Api", "Trackers")] -#if !(NET20 || NET40) - [Collection("RedmineCollection")] -#endif - public class TrackerTests - { - public TrackerTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private readonly RedmineFixture fixture; - - [Fact] - public void RedmineTrackers_ShouldGetAllTrackers() - { - const int NUMBER_OF_TRACKERS = 2; - - var trackers = fixture.RedmineManager.GetObjects(); - - Assert.NotNull(trackers); - Assert.True(trackers.Count == NUMBER_OF_TRACKERS, "Trackers count(" + trackers.Count + ") != " + NUMBER_OF_TRACKERS); - } - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Tests/Sync/UserTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/UserTests.cs deleted file mode 100644 index bf771e3e..00000000 --- a/tests/redmine-net-api.Tests/Tests/Sync/UserTests.cs +++ /dev/null @@ -1,219 +0,0 @@ -/* - Copyright 2011 - 2023 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System.Collections.Specialized; -using System.Globalization; -using Padi.RedmineApi.Tests.Infrastructure; -using Redmine.Net.Api; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Types; -using Xunit; - -namespace Padi.RedmineApi.Tests.Tests.Sync -{ - [Trait("Redmine-Net-Api", "Users")] -#if !(NET20 || NET40) - [Collection("RedmineCollection")] -#endif - [Order(2)] - public class UserTests - { - private readonly RedmineFixture fixture; - - public UserTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private const string USER_LOGIN = "testUser"; - private const string USER_FIRST_NAME = "User"; - private const string USER_LAST_NAME = "One"; - private const string USER_EMAIL = "testUser@mail.com"; - - private static string createdUserId; - private static string createdUserWithAllPropId; - - private static User CreateTestUserWithRequiredPropertiesSet() - { - var user = new User() - { - Login = USER_LOGIN, - FirstName = USER_FIRST_NAME, - LastName = USER_LAST_NAME, - Email = USER_EMAIL, - }; - - return user; - } - - [Fact, Order(1)] - public void Should_Create_User_With_Required_Properties() - { - var savedUser = fixture.RedmineManager.CreateObject(CreateTestUserWithRequiredPropertiesSet()); - - Assert.NotNull(savedUser); - Assert.NotEqual(0, savedUser.Id); - - createdUserId = savedUser.Id.ToString(); - - Assert.True(savedUser.Login.Equals(USER_LOGIN), "User login is invalid."); - Assert.True(savedUser.FirstName.Equals(USER_FIRST_NAME), "User first name is invalid."); - Assert.True(savedUser.LastName.Equals(USER_LAST_NAME), "User last name is invalid."); - Assert.True(savedUser.Email.Equals(USER_EMAIL), "User email is invalid."); - } - - [Fact, Order(2)] - public void Should_Throw_Exception_When_Create_Empty_User() - { - Assert.Throws(() => fixture.RedmineManager.CreateObject(new User())); - } - - [Fact, Order(3)] - public void Should_Create_User_With_All_Properties_Set() - { - var login = "testUserAllProp"; - var firstName = "firstName"; - var lastName = "lastName"; - var email = "email@a.com"; - var password = "pass123456"; - var mailNotification = "only_assigned"; - - var savedUser = fixture.RedmineManager.CreateObject(new User() - { - Login = login, - FirstName = firstName, - LastName = lastName, - Email = email, - Password = password, - MustChangePassword = true, - MailNotification = mailNotification - }); - - Assert.NotNull(savedUser); - Assert.NotEqual(0, savedUser.Id); - - createdUserWithAllPropId = savedUser.Id.ToString(); - - Assert.True(savedUser.Login.Equals(login), "User login is invalid."); - Assert.True(savedUser.FirstName.Equals(firstName), "User first name is invalid."); - Assert.True(savedUser.LastName.Equals(lastName), "User last name is invalid."); - Assert.True(savedUser.Email.Equals(email), "User email is invalid."); - } - - [Fact, Order(4)] - public void Should_Get_Created_User_With_Required_Fields() - { - var user = fixture.RedmineManager.GetObject(createdUserId, null); - - Assert.NotNull(user); - Assert.IsType(user); - Assert.True(user.Login.Equals(USER_LOGIN), "User login is invalid."); - Assert.True(user.FirstName.Equals(USER_FIRST_NAME), "User first name is invalid."); - Assert.True(user.LastName.Equals(USER_LAST_NAME), "User last name is invalid."); - Assert.True(user.Email.Equals(USER_EMAIL), "User email is invalid."); - } - - [Fact, Order(5)] - public void Should_Update_User() - { - const string UPDATED_USER_FIRST_NAME = "UpdatedFirstName"; - const string UPDATED_USER_LAST_NAME = "UpdatedLastName"; - const string UPDATED_USER_EMAIL = "updatedEmail@mail.com"; - - var user = fixture.RedmineManager.GetObject("8", null); - user.FirstName = UPDATED_USER_FIRST_NAME; - user.LastName = UPDATED_USER_LAST_NAME; - user.Email = UPDATED_USER_EMAIL; - - var exception = - (RedmineException) - Record.Exception(() => fixture.RedmineManager.UpdateObject("8", user)); - Assert.Null(exception); - - var updatedUser = fixture.RedmineManager.GetObject("8", null); - - Assert.True(updatedUser.FirstName.Equals(UPDATED_USER_FIRST_NAME), "User first name was not updated."); - Assert.True(updatedUser.LastName.Equals(UPDATED_USER_LAST_NAME), "User last name was not updated."); - Assert.True(updatedUser.Email.Equals(UPDATED_USER_EMAIL), "User email was not updated."); - - // curl -v --user zapadi:1qaz2wsx -H 'Content-Type: application/json' -X PUT -d '{"user":{"login":"testuser","firstname":"UpdatedFirstName","lastname":"UpdatedLastName","mail":"updatedEmail@mail.com","must_change_passwd":"false","status":"1"}}' http://192.168.1.53:8089/users/8.json - } - - [Fact, Order(6)] - public void Should_Not_Update_User_With_Invalid_Properties() - { - var user = fixture.RedmineManager.GetObject(createdUserId, null); - user.FirstName = ""; - - Assert.Throws(() => fixture.RedmineManager.UpdateObject(createdUserId, user)); - } - - [Fact, Order(7)] - public void Should_Delete_User() - { - var exception = - (RedmineException) - Record.Exception(() => fixture.RedmineManager.DeleteObject(createdUserId)); - Assert.Null(exception); - Assert.Throws(() => fixture.RedmineManager.GetObject(createdUserId, null)); - - } - - [Fact, Order(8)] - public void Should_Delete_User_Created_With_All_Properties_Set() - { - var exception = - (RedmineException) - Record.Exception(() => fixture.RedmineManager.DeleteObject(createdUserWithAllPropId)); - Assert.Null(exception); - Assert.Throws(() => fixture.RedmineManager.GetObject(createdUserWithAllPropId, null)); - - } - - [Fact, Order(9)] - public void Should_Get_Current_User() - { - var currentUser = fixture.RedmineManager.GetCurrentUser(); - - Assert.NotNull(currentUser); - Assert.Equal(currentUser.ApiKey, fixture.Credentials.ApiKey); - } - - [Fact, Order(10)] - public void Should_Get_X_Users_From_Offset_Y() - { - var result = fixture.RedmineManager.GetPaginatedObjects(new NameValueCollection() - { - {RedmineKeys.INCLUDE, RedmineKeys.GROUPS + "," + RedmineKeys.MEMBERSHIPS}, - {RedmineKeys.LIMIT, "2"}, - {RedmineKeys.OFFSET, "1"} - }); - - Assert.NotNull(result); - } - - [Fact, Order(11)] - public void Should_Get_Users_By_State() - { - var users = fixture.RedmineManager.GetObjects(new NameValueCollection() - { - {RedmineKeys.STATUS, ((int) UserStatus.StatusActive).ToString(CultureInfo.InvariantCulture)} - }); - - Assert.NotNull(users); - } - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Tests/Sync/VersionTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/VersionTests.cs deleted file mode 100644 index f629df86..00000000 --- a/tests/redmine-net-api.Tests/Tests/Sync/VersionTests.cs +++ /dev/null @@ -1,144 +0,0 @@ -/* - Copyright 2011 - 2023 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Specialized; -using Padi.RedmineApi.Tests.Infrastructure; -using Redmine.Net.Api; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Types; -using Xunit; -using Version = Redmine.Net.Api.Types.Version; - -namespace Padi.RedmineApi.Tests.Tests.Sync -{ - [Trait("Redmine-Net-Api", "Versions")] -#if !(NET20 || NET40) - [Collection("RedmineCollection")] -#endif - public class VersionTests - { - public VersionTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private readonly RedmineFixture fixture; - - private const string PROJECT_ID = "redmine-net-api"; - - [Fact] - [Order(1)] - public void Should_Create_Version() - { - const string NEW_VERSION_NAME = "VersionTesting"; - const VersionStatus NEW_VERSION_STATUS = VersionStatus.Locked; - const VersionSharing NEW_VERSION_SHARING = VersionSharing.Hierarchy; - DateTime newVersionDueDate = DateTime.Now.AddDays(7); - const string NEW_VERSION_DESCRIPTION = "Version description"; - - var version = new Version - { - Name = NEW_VERSION_NAME, - Status = NEW_VERSION_STATUS, - Sharing = NEW_VERSION_SHARING, - DueDate = newVersionDueDate, - Description = NEW_VERSION_DESCRIPTION - }; - - var savedVersion = fixture.RedmineManager.CreateObject(version, PROJECT_ID); - - Assert.NotNull(savedVersion); - Assert.NotNull(savedVersion.Project); - Assert.True(savedVersion.Name.Equals(NEW_VERSION_NAME), "Version name is invalid."); - Assert.True(savedVersion.Status.Equals(NEW_VERSION_STATUS), "Version status is invalid."); - Assert.True(savedVersion.Sharing.Equals(NEW_VERSION_SHARING), "Version sharing is invalid."); - Assert.NotNull(savedVersion.DueDate); - Assert.True(savedVersion.DueDate.Value.Date.Equals(newVersionDueDate.Date), "Version due date is invalid."); - Assert.True(savedVersion.Description.Equals(NEW_VERSION_DESCRIPTION), "Version description is invalid."); - } - - [Fact] - [Order(99)] - public void Should_Delete_Version() - { - const string DELETED_VERSION_ID = "22"; - var exception = - (RedmineException) - Record.Exception(() => fixture.RedmineManager.DeleteObject(DELETED_VERSION_ID)); - Assert.Null(exception); - Assert.Throws(() => fixture.RedmineManager.GetObject(DELETED_VERSION_ID, null)); - } - - [Fact] - [Order(3)] - public void Should_Get_Version_By_Id() - { - const string VERSION_ID = "6"; - - var version = fixture.RedmineManager.GetObject(VERSION_ID, null); - - Assert.NotNull(version); - } - - [Fact] - [Order(2)] - public void Should_Get_Versions_By_Project_Id() - { - const int NUMBER_OF_VERSIONS = 5; - var versions = - fixture.RedmineManager.GetObjects(new NameValueCollection - { - {RedmineKeys.PROJECT_ID, PROJECT_ID} - }); - - Assert.NotNull(versions); - Assert.True(versions.Count == NUMBER_OF_VERSIONS, "Versions count ( "+versions.Count+" ) != " + NUMBER_OF_VERSIONS); - } - - [Fact] - [Order(4)] - public void Should_Update_Version() - { - const string UPDATED_VERSION_ID = "15"; - const string UPDATED_VERSION_NAME = "Updated version"; - const VersionStatus UPDATED_VERSION_STATUS = VersionStatus.Closed; - const VersionSharing UPDATED_VERSION_SHARING = VersionSharing.System; - const string UPDATED_VERSION_DESCRIPTION = "Updated description"; - - DateTime updatedVersionDueDate = DateTime.Now.AddMonths(1); - - var version = fixture.RedmineManager.GetObject(UPDATED_VERSION_ID, null); - version.Name = UPDATED_VERSION_NAME; - version.Status = UPDATED_VERSION_STATUS; - version.Sharing = UPDATED_VERSION_SHARING; - version.DueDate = updatedVersionDueDate; - version.Description = UPDATED_VERSION_DESCRIPTION; - - fixture.RedmineManager.UpdateObject(UPDATED_VERSION_ID, version); - - var updatedVersion = fixture.RedmineManager.GetObject(UPDATED_VERSION_ID, null); - - Assert.NotNull(version); - Assert.True(updatedVersion.Name.Equals(version.Name), "Version name not updated."); - Assert.True(updatedVersion.Status.Equals(version.Status), "Status not updated"); - Assert.True(updatedVersion.Sharing.Equals(version.Sharing), "Sharing not updated"); - Assert.True(updatedVersion.DueDate != null && DateTime.Compare(updatedVersion.DueDate.Value.Date, version.DueDate.Value.Date) == 0, - "DueDate not updated"); - Assert.True(updatedVersion.Description.Equals(version.Description), "Description not updated"); - } - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs b/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs deleted file mode 100644 index c5788f0e..00000000 --- a/tests/redmine-net-api.Tests/Tests/Sync/WikiPageTests.cs +++ /dev/null @@ -1,140 +0,0 @@ -/* - Copyright 2011 - 2023 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Specialized; -using System.Linq; -using Padi.RedmineApi.Tests.Infrastructure; -using Redmine.Net.Api; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Types; -using Xunit; - -namespace Padi.RedmineApi.Tests.Tests.Sync -{ - [Trait("Redmine-Net-Api", "WikiPages")] -#if !(NET20 || NET40) - [Collection("RedmineCollection")] -#endif - public class WikiPageTests - { - public WikiPageTests(RedmineFixture fixture) - { - this.fixture = fixture; - } - - private readonly RedmineFixture fixture; - - private const string PROJECT_ID = "redmine-net-api-project-test"; - private const string WIKI_PAGE_NAME = "Wiki"; - - [Fact, Order(1)] - public void Should_Add_WikiPage() - { - const string WIKI_PAGE_TEXT = "Create wiki page"; - const string WIKI_PAGE_COMMENT = "I did it through code"; - - var page = fixture.RedmineManager.CreateWikiPage(PROJECT_ID, "Wiki test page name", - new WikiPage { Text = WIKI_PAGE_TEXT, Comments = WIKI_PAGE_COMMENT }); - - Assert.NotNull(page); - Assert.True(page.Title.Equals("Wiki test page name"), "Wiki page name is invalid."); - Assert.True(page.Text.Equals(WIKI_PAGE_TEXT), "Wiki page text is invalid."); - Assert.True(page.Comments.Equals(WIKI_PAGE_COMMENT), "Wiki page comments are invalid."); - } - - [Fact, Order(2)] - public void Should_Update_WikiPage() - { - const string WIKI_PAGE_UPDATED_TEXT = "Updated again and again wiki page and again"; - const string WIKI_PAGE_COMMENT = "I did it through code"; - - fixture.RedmineManager.UpdateWikiPage(PROJECT_ID, "Wiki test page name", - new WikiPage { Text = WIKI_PAGE_UPDATED_TEXT, Comments = WIKI_PAGE_COMMENT }); - } - - [Fact, Order(99)] - public void Should_Delete_Wiki_Page() - { - fixture.RedmineManager.DeleteWikiPage(PROJECT_ID, WIKI_PAGE_NAME); - Assert.Throws(() => fixture.RedmineManager.GetWikiPage(PROJECT_ID, null, WIKI_PAGE_NAME)); - } - - [Fact, Order(2)] - public void Should_Get_All_Wiki_Pages_By_Project_Id() - { - const int NUMBER_OF_WIKI_PAGES = 2; - - var pages = fixture.RedmineManager.GetAllWikiPages(PROJECT_ID); - - Assert.NotNull(pages); - Assert.True(pages.Count == NUMBER_OF_WIKI_PAGES, "Wiki pages count != " + NUMBER_OF_WIKI_PAGES); - Assert.True(pages.Exists(p => p.Title == WIKI_PAGE_NAME), $"Wiki page {WIKI_PAGE_NAME} does not exist"); - } - - [Fact, Order(3)] - public void Should_Get_Wiki_Page_By_Title() - { - const string WIKI_PAGE_TITLE = "Wiki2"; - - var page = fixture.RedmineManager.GetWikiPage(PROJECT_ID, null, WIKI_PAGE_TITLE); - - Assert.NotNull(page); - Assert.True(page.Title.Equals(WIKI_PAGE_TITLE), "Wiki page title is invalid."); - } - - [Fact, Order(4)] - public void Should_Get_Wiki_Page_By_Title_With_Attachments() - { - var page = fixture.RedmineManager.GetWikiPage(PROJECT_ID, - new NameValueCollection { { RedmineKeys.INCLUDE, RedmineKeys.ATTACHMENTS } }, WIKI_PAGE_NAME); - - Assert.NotNull(page); - Assert.Equal(page.Title, WIKI_PAGE_NAME); - Assert.NotNull(page.Attachments.ToList()); - } - - [Fact, Order(5)] - public void Should_Get_Wiki_Page_By_Version() - { - const int WIKI_PAGE_VERSION = 1; - var oldPage = fixture.RedmineManager.GetWikiPage(PROJECT_ID, null, WIKI_PAGE_NAME, WIKI_PAGE_VERSION); - - Assert.NotNull(oldPage); - Assert.Equal(oldPage.Title, WIKI_PAGE_NAME); - Assert.True(oldPage.Version == WIKI_PAGE_VERSION, "Wiki page version is invalid."); - } - - [Fact, Order(6)] - public void Should_Get_Wiki_Page_With_Special_Chars() - { - var wikiPageName = "some-page-with-umlauts-and-other-special-chars-äöüÄÖÜß"; - - var wikiPage = fixture.RedmineManager.CreateWikiPage(PROJECT_ID, wikiPageName, - new WikiPage { Text = "WIKI_PAGE_TEXT", Comments = "WIKI_PAGE_COMMENT" }); - - WikiPage page = fixture.RedmineManager.GetWikiPage - ( - PROJECT_ID, - null, - wikiPageName - ); - - Assert.NotNull(page); - Assert.True(string.Equals(page.Title,wikiPageName, StringComparison.OrdinalIgnoreCase),$"Wiki page {wikiPageName} does not exist."); - } - } -} \ No newline at end of file From 1940df6550f9894e91eafcd03d8ad1d8ec67dc3b Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 4 Dec 2023 13:15:54 +0200 Subject: [PATCH 309/549] [Tests] Make all classes sealed & fix namespaces --- .../Infrastructure/{ => Order}/CaseOrder.cs | 13 ++++--- .../{ => Order}/CollectionOrderer.cs | 24 +++++++----- .../{ => Order}/OrderAttribute.cs | 4 +- .../Infrastructure/RedmineCollection.cs | 7 +--- .../RedmineCredentials.cs | 2 +- .../Infrastructure/RedmineFixture.cs | 39 +++++++++++++++++++ tests/redmine-net-api.Tests/RedmineFixture.cs | 30 -------------- tests/redmine-net-api.Tests/TestHelper.cs | 5 ++- 8 files changed, 69 insertions(+), 55 deletions(-) rename tests/redmine-net-api.Tests/Infrastructure/{ => Order}/CaseOrder.cs (68%) rename tests/redmine-net-api.Tests/Infrastructure/{ => Order}/CollectionOrderer.cs (65%) rename tests/redmine-net-api.Tests/Infrastructure/{ => Order}/OrderAttribute.cs (61%) rename tests/redmine-net-api.Tests/{ => Infrastructure}/RedmineCredentials.cs (80%) create mode 100644 tests/redmine-net-api.Tests/Infrastructure/RedmineFixture.cs delete mode 100644 tests/redmine-net-api.Tests/RedmineFixture.cs diff --git a/tests/redmine-net-api.Tests/Infrastructure/CaseOrder.cs b/tests/redmine-net-api.Tests/Infrastructure/Order/CaseOrder.cs similarity index 68% rename from tests/redmine-net-api.Tests/Infrastructure/CaseOrder.cs rename to tests/redmine-net-api.Tests/Infrastructure/Order/CaseOrder.cs index 22096ad4..97c849af 100644 --- a/tests/redmine-net-api.Tests/Infrastructure/CaseOrder.cs +++ b/tests/redmine-net-api.Tests/Infrastructure/Order/CaseOrder.cs @@ -6,17 +6,17 @@ using Xunit.Abstractions; using Xunit.Sdk; -namespace Padi.RedmineApi.Tests.Infrastructure +namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure { /// /// Custom xUnit test case orderer that uses the OrderAttribute /// - public class CaseOrderer : ITestCaseOrderer + public sealed class CaseOrderer : ITestCaseOrderer { - public const string TYPE_NAME = "redmine.net.api.Tests.Infrastructure.CaseOrderer"; - public const string ASSEMBY_NAME = "redmine-net-api.Tests"; + // public const string TYPE_NAME = "redmine.net.api.Tests.Infrastructure.CaseOrderer"; + // public const string ASSEMBLY_NAME = "redmine-net-api.Tests"; - public static readonly ConcurrentDictionary> QueuedTests = new ConcurrentDictionary>(); + private static readonly ConcurrentDictionary> QueuedTests = new ConcurrentDictionary>(); public IEnumerable OrderTestCases(IEnumerable testCases) where TTestCase : ITestCase @@ -36,7 +36,8 @@ private static int GetOrder(TTestCase testCase) var attr = testCase.TestMethod.Method .ToRuntimeMethod() .GetCustomAttribute(); - return attr != null ? attr.Index : 0; + + return attr?.Index ?? 0; } } } diff --git a/tests/redmine-net-api.Tests/Infrastructure/CollectionOrderer.cs b/tests/redmine-net-api.Tests/Infrastructure/Order/CollectionOrderer.cs similarity index 65% rename from tests/redmine-net-api.Tests/Infrastructure/CollectionOrderer.cs rename to tests/redmine-net-api.Tests/Infrastructure/Order/CollectionOrderer.cs index fdbe5d03..abe9cd91 100644 --- a/tests/redmine-net-api.Tests/Infrastructure/CollectionOrderer.cs +++ b/tests/redmine-net-api.Tests/Infrastructure/Order/CollectionOrderer.cs @@ -7,15 +7,15 @@ using Xunit; using Xunit.Abstractions; -namespace Padi.RedmineApi.Tests.Infrastructure +namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure { /// /// Custom xUnit test collection orderer that uses the OrderAttribute /// - public class CollectionOrderer : ITestCollectionOrderer + public sealed class CollectionOrderer : ITestCollectionOrderer { - public const string TYPE_NAME = "redmine.net.api.Tests.Infrastructure.CollectionOrderer"; - public const string ASSEMBY_NAME = "redmine-net-api.Tests"; + // public const string TYPE_NAME = "redmine.net.api.Tests.Infrastructure.CollectionOrderer"; + // public const string ASSEMBLY_NAME = "redmine-net-api.Tests"; public IEnumerable OrderTestCollections(IEnumerable testCollections) { @@ -30,15 +30,21 @@ public IEnumerable OrderTestCollections(IEnumerable private static int GetOrder(ITestCollection testCollection) { - var i = testCollection.DisplayName.LastIndexOf(' '); - if (i <= -1) return 0; + var index = testCollection.DisplayName.LastIndexOf(' '); + if (index <= -1) + { + return 0; + } - var className = testCollection.DisplayName.Substring(i + 1); + var className = testCollection.DisplayName.Substring(index + 1); var type = Type.GetType(className); - if (type == null) return 0; + if (type == null) + { + return 0; + } var attr = type.GetCustomAttribute(); - return attr != null ? attr.Index : 0; + return attr?.Index ?? 0; } } } diff --git a/tests/redmine-net-api.Tests/Infrastructure/OrderAttribute.cs b/tests/redmine-net-api.Tests/Infrastructure/Order/OrderAttribute.cs similarity index 61% rename from tests/redmine-net-api.Tests/Infrastructure/OrderAttribute.cs rename to tests/redmine-net-api.Tests/Infrastructure/Order/OrderAttribute.cs index b13b5af8..bc837971 100644 --- a/tests/redmine-net-api.Tests/Infrastructure/OrderAttribute.cs +++ b/tests/redmine-net-api.Tests/Infrastructure/Order/OrderAttribute.cs @@ -1,8 +1,8 @@ using System; -namespace Padi.RedmineApi.Tests.Infrastructure +namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure { - public class OrderAttribute : Attribute + public sealed class OrderAttribute : Attribute { public OrderAttribute(int index) { diff --git a/tests/redmine-net-api.Tests/Infrastructure/RedmineCollection.cs b/tests/redmine-net-api.Tests/Infrastructure/RedmineCollection.cs index 831b2245..fa2bcbd4 100644 --- a/tests/redmine-net-api.Tests/Infrastructure/RedmineCollection.cs +++ b/tests/redmine-net-api.Tests/Infrastructure/RedmineCollection.cs @@ -1,12 +1,9 @@ #if !(NET20 || NET40) using Xunit; -namespace Padi.RedmineApi.Tests.Infrastructure +namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure { [CollectionDefinition("RedmineCollection")] - public class RedmineCollection : ICollectionFixture - { - - } + public sealed class RedmineCollection : ICollectionFixture { } } #endif \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/RedmineCredentials.cs b/tests/redmine-net-api.Tests/Infrastructure/RedmineCredentials.cs similarity index 80% rename from tests/redmine-net-api.Tests/RedmineCredentials.cs rename to tests/redmine-net-api.Tests/Infrastructure/RedmineCredentials.cs index 380def2b..e3c489be 100644 --- a/tests/redmine-net-api.Tests/RedmineCredentials.cs +++ b/tests/redmine-net-api.Tests/Infrastructure/RedmineCredentials.cs @@ -1,4 +1,4 @@ -namespace Padi.RedmineApi.Tests +namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure { public sealed class RedmineCredentials { diff --git a/tests/redmine-net-api.Tests/Infrastructure/RedmineFixture.cs b/tests/redmine-net-api.Tests/Infrastructure/RedmineFixture.cs new file mode 100644 index 00000000..f4e7e2c1 --- /dev/null +++ b/tests/redmine-net-api.Tests/Infrastructure/RedmineFixture.cs @@ -0,0 +1,39 @@ +using System.Diagnostics; +using Redmine.Net.Api; +using Redmine.Net.Api.Authentication; +using Redmine.Net.Api.Serialization; + +namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure +{ + public sealed class RedmineFixture + { + public RedmineCredentials Credentials { get; } + public RedmineManager RedmineManager { get; private set; } + + private readonly RedmineManagerOptionsBuilder _redmineManagerOptionsBuilder; + + public RedmineFixture () + { + Credentials = TestHelper.GetApplicationConfiguration(); + + _redmineManagerOptionsBuilder = new RedmineManagerOptionsBuilder() + .WithHost(Credentials.Uri) + .WithAuthentication(new RedmineApiKeyAuthentication(Credentials.ApiKey)); + + SetMimeTypeXml(); + SetMimeTypeJson(); + } + + [Conditional("DEBUG_JSON")] + private void SetMimeTypeJson() + { + RedmineManager = new RedmineManager(_redmineManagerOptionsBuilder.WithSerializationType(SerializationType.Json)); + } + + [Conditional("DEBUG_XML")] + private void SetMimeTypeXml() + { + RedmineManager = new RedmineManager(_redmineManagerOptionsBuilder.WithSerializationType(SerializationType.Xml)); + } + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/RedmineFixture.cs b/tests/redmine-net-api.Tests/RedmineFixture.cs deleted file mode 100644 index 89988e18..00000000 --- a/tests/redmine-net-api.Tests/RedmineFixture.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Diagnostics; -using Redmine.Net.Api; - -namespace Padi.RedmineApi.Tests -{ - public class RedmineFixture - { - public RedmineCredentials Credentials { get; private set; } - public RedmineManager RedmineManager { get; set; } - - public RedmineFixture () - { - Credentials = TestHelper.GetApplicationConfiguration(); - SetMimeTypeXml(); - SetMimeTypeJson(); - } - - [Conditional("DEBUG_JSON")] - private void SetMimeTypeJson() - { - RedmineManager = new RedmineManager(Credentials.Uri, Credentials.ApiKey, MimeFormat.Json); - } - - [Conditional("DEBUG_XML")] - private void SetMimeTypeXml() - { - RedmineManager = new RedmineManager(Credentials.Uri, Credentials.ApiKey); - } - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/TestHelper.cs b/tests/redmine-net-api.Tests/TestHelper.cs index 785a5607..fbc1995f 100644 --- a/tests/redmine-net-api.Tests/TestHelper.cs +++ b/tests/redmine-net-api.Tests/TestHelper.cs @@ -1,12 +1,13 @@ using System; using System.IO; using Microsoft.Extensions.Configuration; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; -namespace Padi.RedmineApi.Tests +namespace Padi.DotNet.RedmineAPI.Tests { internal static class TestHelper { - public static IConfigurationRoot GetIConfigurationRoot(string outputPath) + private static IConfigurationRoot GetIConfigurationRoot(string outputPath) { var environment = Environment.GetEnvironmentVariable("Environment"); From 4da710b50ea233ce420e27d87953def559e6f309 Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 3 Jan 2024 17:39:05 +0200 Subject: [PATCH 310/549] [GitActions] Delete dotnetcore.yml --- .github/workflows/dotnetcore.yml | 129 ------------------------------- 1 file changed, 129 deletions(-) delete mode 100644 .github/workflows/dotnetcore.yml diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml deleted file mode 100644 index 3214bad2..00000000 --- a/.github/workflows/dotnetcore.yml +++ /dev/null @@ -1,129 +0,0 @@ -name: Redmine .NET Api - -on: - push: - paths-ignore: - - '**/*.md' - - '**/*.gif' - - '**/*.png' - - '**/*.gitignore' - - '**/*.gitattributes' - - LICENSE - - tests/* - tags: - - v[1-9].[0-9]+.[0-9]+ - pull_request: - workflow_dispatch: - branches: - - master - path-ignore: - - '**/*.md' - - '**/*.gif' - - '**/*.png' - - '**/*.gitignore' - - '**/*.gitattributes' - - LICENSE - - tests/* - -env: - DOTNET_CLI_TELEMETRY_OPTOUT: 1 - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 - DOTNET_NOLOGO: true - DOTNET_GENERATE_ASPNET_CERTIFICATE: false - DOTNET_ADD_GLOBAL_TOOLS_TO_PATH: false - DOTNET_MULTILEVEL_LOOKUP: 0 - -jobs: - build: - - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macOS-latest] - dotnet: [ '3.1.x', '5.x.x', '6.x.x'] - name: OS ${{ matrix.os }} - dotnet ${{ matrix.dotnet }} - - steps: - - uses: actions/checkout@v2 - - - name: Setup .NET Core SDK - uses: actions/setup-dotnet@v1 - with: - dotnet-version: ${{ matrix.dotnet }} - - # Fetches all tags for the repo - - name: Fetch tags - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* - - - name: Install dependencies - run: dotnet restore redmine-net-api.sln - - - name: Get the version - #id: get_version - #run: echo ::set-output name=VERSION::${GITHUB_REF/refs\/tags\//} - #${{ steps.get_version.outputs.VERSION }} - run: echo "VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - - - name: Test - run: | - echo $VERSION - echo ${{ env.VERSION }} - echo $github.run_number - -# - name: Build -# run: dotnet build redmine-net-api.sln --configuration Release --no-restore --version-suffix=${{ env.VERSION }} - -# - name: Build Signed -# run: dotnet build redmine-net-api.sln --configuration Release --no-restore --version-suffix=${{ env.VERSION }} -p:Sign=true - -# #- name: Test -# # run: dotnet test redmine-net-api.sln --no-restore --verbosity normal - -# - name: Pack -# run: dotnet pack redmine-net-api.sln --configuration Release -o .\artifacts --include-symbols -p:SymbolPackageFormat=snupkg --no-build ${{ env.VERSION }} -# if: runner.os != 'Windows' - -# - name: Pack Signed -# run: dotnet pack redmine-net-api.sln --configuration Release -o .\artifacts --include-symbols -p:SymbolPackageFormat=snupkg --no-build ${{ env.VERSION }} -p:Sign=true -# if: runner.os != 'Windows' - -# - name: Publish NuGet Packages -# uses: actions/upload-artifact@master -# with: -# name: nupkg -# path: .\artifacts\**\*.nupkg - -# - name: Publish Symbol Packages -# uses: actions/upload-artifact@master -# with: -# name: snupkg -# path: .\artifacts\**\*.snupkg - -# deploy: -# runs-on: macOS-latest -# needs: build -# name: Deploy Packages -# steps: -# - name: Download Package artifact -# uses: actions/download-artifact@master -# with: -# name: nupkg -# - name: Download Package artifact -# uses: actions/download-artifact@master -# with: -# name: snupkg - -# - name: Setup NuGet -# uses: NuGet/setup-nuget@v1.0.2 -# with: -# nuget-api-key: ${{ secrets.NUGET_API_KEY }} -# nuget-version: latest - -# - name: Setup .NET Core SDK -# uses: actions/setup-dotnet@v1 -# with: -# dotnet-version: '3.1.x' - -# - name: Push to NuGet -# run: dotnet nuget push nupkg\*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://nuget.org - From 1e73667ccb71fbd737646c180e8de230c4d7ea91 Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 3 Jan 2024 17:41:44 +0200 Subject: [PATCH 311/549] [Internals] Delete UrlHelper --- src/redmine-net-api/Internals/UrlHelper.cs | 379 --------------------- 1 file changed, 379 deletions(-) delete mode 100644 src/redmine-net-api/Internals/UrlHelper.cs diff --git a/src/redmine-net-api/Internals/UrlHelper.cs b/src/redmine-net-api/Internals/UrlHelper.cs deleted file mode 100644 index 0b18b0ef..00000000 --- a/src/redmine-net-api/Internals/UrlHelper.cs +++ /dev/null @@ -1,379 +0,0 @@ -/* - Copyright 2011 - 2023 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Globalization; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; -using File = Redmine.Net.Api.Types.File; -using Version = Redmine.Net.Api.Types.Version; - -namespace Redmine.Net.Api.Internals -{ - /// - /// - internal static class UrlHelper - { - /// - /// - private const string REQUEST_FORMAT = "{0}/{1}/{2}.{3}"; - - /// - /// - private const string FORMAT = "{0}/{1}.{2}"; - - /// - /// - private const string WIKI_INDEX_FORMAT = "{0}/projects/{1}/wiki/index.{2}"; - - /// - /// - private const string WIKI_PAGE_FORMAT = "{0}/projects/{1}/wiki/{2}.{3}"; - - /// - /// - private const string WIKI_VERSION_FORMAT = "{0}/projects/{1}/wiki/{2}/{3}.{4}"; - - /// - /// - private const string ENTITY_WITH_PARENT_FORMAT = "{0}/{1}/{2}/{3}.{4}"; - - /// - /// - private const string ATTACHMENT_UPDATE_FORMAT = "{0}/attachments/issues/{1}.{2}"; - - /// - /// - /// - private const string FILE_URL_FORMAT = "{0}/projects/{1}/files.{2}"; - - private const string MY_ACCOUNT_FORMAT = "{0}/my/account.{1}"; - - - /// - /// - private const string CURRENT_USER_URI = "current"; - /// - /// Gets the upload URL. - /// - /// - /// The redmine manager. - /// The identifier. - /// - /// - public static string GetUploadUrl(RedmineManager redmineManager, string id) - where T : class, new() - { - var type = typeof(T); - - if (!RedmineManager.Suffixes.TryGetValue(type, out string value)) throw new KeyNotFoundException(type.Name); - - return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, value, id, - redmineManager.Format); - } - - /// - /// Gets the create URL. - /// - /// - /// The redmine manager. - /// The owner identifier. - /// - /// - /// - /// The owner id(project id) is mandatory! - /// or - /// The owner id(issue id) is mandatory! - /// - public static string GetCreateUrl(RedmineManager redmineManager, string ownerId) where T : class, new() - { - var type = typeof(T); - - if (!RedmineManager.Suffixes.TryGetValue(type, out string value)) throw new KeyNotFoundException(type.Name); - - if (type == typeof(Version) || type == typeof(IssueCategory) || type == typeof(ProjectMembership)) - { - if (string.IsNullOrEmpty(ownerId)) throw new RedmineException("The owner id(project id) is mandatory!"); - return string.Format(CultureInfo.InvariantCulture,ENTITY_WITH_PARENT_FORMAT, redmineManager.Host, RedmineKeys.PROJECTS, - ownerId, value, redmineManager.Format); - } - if (type == typeof(IssueRelation)) - { - if (string.IsNullOrEmpty(ownerId)) throw new RedmineException("The owner id(issue id) is mandatory!"); - return string.Format(CultureInfo.InvariantCulture,ENTITY_WITH_PARENT_FORMAT, redmineManager.Host, RedmineKeys.ISSUES, - ownerId, value, redmineManager.Format); - } - - if (type == typeof(File)) - { - if (string.IsNullOrEmpty(ownerId)) - { - throw new RedmineException("The owner id(project id) is mandatory!"); - } - return string.Format(CultureInfo.InvariantCulture,FILE_URL_FORMAT, redmineManager.Host, ownerId, redmineManager.Format); - } - - return string.Format(CultureInfo.InvariantCulture,FORMAT, redmineManager.Host, value, - redmineManager.Format); - } - - /// - /// Gets the delete URL. - /// - /// - /// The redmine manager. - /// The identifier. - /// - /// - /// - public static string GetDeleteUrl(RedmineManager redmineManager, string id) where T : class, new() - { - var type = typeof(T); - - if (!RedmineManager.Suffixes.TryGetValue(type, out string value)) throw new KeyNotFoundException(type.Name); - - return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, value, id, - redmineManager.Format); - } - - /// - /// Gets the get URL. - /// - /// - /// The redmine manager. - /// The identifier. - /// - /// - public static string GetGetUrl(RedmineManager redmineManager, string id) where T : class, new() - { - var type = typeof(T); - - if (!RedmineManager.Suffixes.TryGetValue(type, out string value)) throw new KeyNotFoundException(type.Name); - - return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, value, id, - redmineManager.Format); - } - - /// - /// Gets the list URL. - /// - /// - /// The redmine manager. - /// The parameters. - /// - /// - /// - /// The project id is mandatory! \nCheck if you have included the parameter project_id to parameters. - /// or - /// The issue id is mandatory! \nCheck if you have included the parameter issue_id to parameters - /// - public static string GetListUrl(RedmineManager redmineManager, NameValueCollection parameters) - where T : class, new() - { - var type = typeof(T); - - if (!RedmineManager.Suffixes.TryGetValue(type, out string value)) throw new KeyNotFoundException(type.Name); - - if (type == typeof(Version) || type == typeof(IssueCategory) || type == typeof(ProjectMembership)) - { - var projectId = parameters.GetParameterValue(RedmineKeys.PROJECT_ID); - if (string.IsNullOrEmpty(projectId)) - throw new RedmineException("The project id is mandatory! \nCheck if you have included the parameter project_id to parameters."); - - return string.Format(CultureInfo.InvariantCulture,ENTITY_WITH_PARENT_FORMAT, redmineManager.Host, RedmineKeys.PROJECTS, - projectId, value, redmineManager.Format); - } - if (type == typeof(IssueRelation)) - { - var issueId = parameters.GetParameterValue(RedmineKeys.ISSUE_ID); - if (string.IsNullOrEmpty(issueId)) - throw new RedmineException("The issue id is mandatory! \nCheck if you have included the parameter issue_id to parameters"); - - return string.Format(CultureInfo.InvariantCulture,ENTITY_WITH_PARENT_FORMAT, redmineManager.Host, RedmineKeys.ISSUES, - issueId, value, redmineManager.Format); - } - - if (type == typeof(File)) - { - var projectId = parameters.GetParameterValue(RedmineKeys.PROJECT_ID); - if (string.IsNullOrEmpty(projectId)) - { - throw new RedmineException("The project id is mandatory! \nCheck if you have included the parameter project_id to parameters."); - } - return string.Format(CultureInfo.InvariantCulture,FILE_URL_FORMAT, redmineManager.Host, projectId, redmineManager.Format); - } - - return string.Format(CultureInfo.InvariantCulture,FORMAT, redmineManager.Host, value, - redmineManager.Format); - } - - /// - /// Gets the wikis URL. - /// - /// The redmine manager. - /// The project identifier. - /// - public static string GetWikisUrl(RedmineManager redmineManager, string projectId) - { - return string.Format(CultureInfo.InvariantCulture,WIKI_INDEX_FORMAT, redmineManager.Host, projectId, - redmineManager.Format); - } - - /// - /// Gets the wiki page URL. - /// - /// The redmine manager. - /// The project identifier. - /// Name of the page. - /// The version. - /// - public static string GetWikiPageUrl(RedmineManager redmineManager, string projectId, string pageName, uint version = 0) - { - var uri = version == 0 - ? string.Format(CultureInfo.InvariantCulture,WIKI_PAGE_FORMAT, redmineManager.Host, projectId, pageName, - redmineManager.Format) - : string.Format(CultureInfo.InvariantCulture,WIKI_VERSION_FORMAT, redmineManager.Host, projectId, pageName, version.ToString(CultureInfo.InvariantCulture), - redmineManager.Format); - return uri; - } - - /// - /// Gets the add user to group URL. - /// - /// The redmine manager. - /// The group identifier. - /// - public static string GetAddUserToGroupUrl(RedmineManager redmineManager, int groupId) - { - return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, - RedmineManager.Suffixes[typeof(Group)], - $"{groupId.ToString(CultureInfo.InvariantCulture)}/users", redmineManager.Format); - } - - /// - /// Gets the remove user from group URL. - /// - /// The redmine manager. - /// The group identifier. - /// The user identifier. - /// - public static string GetRemoveUserFromGroupUrl(RedmineManager redmineManager, int groupId, int userId) - { - return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, - RedmineManager.Suffixes[typeof(Group)], - $"{groupId.ToString(CultureInfo.InvariantCulture)}/users/{userId.ToString(CultureInfo.InvariantCulture)}", redmineManager.Format); - } - - /// - /// Gets the upload file URL. - /// - /// The redmine manager. - /// - public static string GetUploadFileUrl(RedmineManager redmineManager) - { - return string.Format(CultureInfo.InvariantCulture,FORMAT, redmineManager.Host, RedmineKeys.UPLOADS, - redmineManager.Format); - } - - /// - /// Gets the current user URL. - /// - /// The redmine manager. - /// - public static string GetCurrentUserUrl(RedmineManager redmineManager) - { - return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, - RedmineManager.Suffixes[typeof(User)], CURRENT_USER_URI, - redmineManager.Format); - } - - public static string GetMyAccountUrl(RedmineManager redmineManager) - { - return string.Format(CultureInfo.InvariantCulture,MY_ACCOUNT_FORMAT, redmineManager.Host, redmineManager.Format); - } - - /// - /// Gets the wiki create or updater URL. - /// - /// The redmine manager. - /// The project identifier. - /// Name of the page. - /// - public static string GetWikiCreateOrUpdaterUrl(RedmineManager redmineManager, string projectId, string pageName) - { - return string.Format(CultureInfo.InvariantCulture,WIKI_PAGE_FORMAT, redmineManager.Host, projectId, pageName, - redmineManager.Format); - } - - /// - /// Gets the delete wiki URL. - /// - /// The redmine manager. - /// The project identifier. - /// Name of the page. - /// - public static string GetDeleteWikiUrl(RedmineManager redmineManager, string projectId, string pageName) - { - return string.Format(CultureInfo.InvariantCulture,WIKI_PAGE_FORMAT, redmineManager.Host, projectId, pageName, - redmineManager.Format); - } - - /// - /// Gets the add watcher URL. - /// - /// The redmine manager. - /// The issue identifier. - /// - public static string GetAddWatcherUrl(RedmineManager redmineManager, int issueId) - { - return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, - RedmineManager.Suffixes[typeof(Issue)], $"{issueId.ToString(CultureInfo.InvariantCulture)}/watchers", - redmineManager.Format); - } - - /// - /// Gets the remove watcher URL. - /// - /// The redmine manager. - /// The issue identifier. - /// The user identifier. - /// - public static string GetRemoveWatcherUrl(RedmineManager redmineManager, int issueId, int userId) - { - return string.Format(CultureInfo.InvariantCulture,REQUEST_FORMAT, redmineManager.Host, - RedmineManager.Suffixes[typeof(Issue)], $"{issueId.ToString(CultureInfo.InvariantCulture)}/watchers/{userId.ToString(CultureInfo.InvariantCulture)}", - redmineManager.Format); - } - - /// - /// Gets the attachment update URL. - /// - /// The redmine manager. - /// The issue identifier. - /// - public static string GetAttachmentUpdateUrl(RedmineManager redmineManager, int issueId) - { - return string.Format(CultureInfo.InvariantCulture, - ATTACHMENT_UPDATE_FORMAT, - redmineManager.Host, - issueId.ToString(CultureInfo.InvariantCulture), - redmineManager.Format); - } - } -} \ No newline at end of file From 1c25a03c030e3d8134b52672b6d0c00070a27da0 Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 3 Jan 2024 17:42:13 +0200 Subject: [PATCH 312/549] [Internals] Delete WebApiHelper --- src/redmine-net-api/Internals/WebApiHelper.cs | 199 ------------------ 1 file changed, 199 deletions(-) delete mode 100644 src/redmine-net-api/Internals/WebApiHelper.cs diff --git a/src/redmine-net-api/Internals/WebApiHelper.cs b/src/redmine-net-api/Internals/WebApiHelper.cs deleted file mode 100644 index 27406f2c..00000000 --- a/src/redmine-net-api/Internals/WebApiHelper.cs +++ /dev/null @@ -1,199 +0,0 @@ -/* - Copyright 2011 - 2023 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Specialized; -using System.ComponentModel; -using System.Net; -using System.Text; -using System.Threading; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.Internals -{ - /// - /// - /// - internal static class WebApiHelper - { - /// - /// Executes the upload. - /// - /// The redmine manager. - /// The address. - /// Type of the action. - /// The data. - /// The parameters - public static void ExecuteUpload(RedmineManager redmineManager, string address, string actionType, string data, - NameValueCollection parameters = null) - { - using (var wc = redmineManager.CreateWebClient(parameters)) - { - if (actionType == HttpVerbs.POST || actionType == HttpVerbs.DELETE || actionType == HttpVerbs.PUT || - actionType == HttpVerbs.PATCH) - { - wc.UploadString(address, actionType, data); - } - } - } - - /// - /// Executes the upload. - /// - /// - /// The redmine manager. - /// The address. - /// Type of the action. - /// The data. - /// - public static T ExecuteUpload(RedmineManager redmineManager, string address, string actionType, string data) - where T : class, new() - { - using (var wc = redmineManager.CreateWebClient(null)) - { - switch (actionType) - { - case HttpVerbs.POST: - case HttpVerbs.DELETE: - case HttpVerbs.PUT: - case HttpVerbs.PATCH: - { - var response = wc.UploadString(address, actionType, data); - return redmineManager.Serializer.Deserialize(response); - } - - default: - return default; - } - } - } - - /// - /// Executes the download. - /// - /// - /// The redmine manager. - /// The address. - /// The parameters. - /// - public static T ExecuteDownload(RedmineManager redmineManager, string address, - NameValueCollection parameters = null) - where T : class, new() - { - using (var wc = redmineManager.CreateWebClient(parameters)) - { - var response = wc.DownloadString(address); - if (!string.IsNullOrEmpty(response)) - { - return redmineManager.Serializer.Deserialize(response); - } - - return default; - } - } - - /// - /// Executes the download list. - /// - /// - /// The redmine manager. - /// The address. - /// The parameters. - /// - public static PagedResults ExecuteDownloadList(RedmineManager redmineManager, string address, - NameValueCollection parameters = null) where T : class, new() - { - using (var wc = redmineManager.CreateWebClient(parameters)) - { - var response = wc.DownloadString(address); - return redmineManager.Serializer.DeserializeToPagedResults(response); - } - } - - /// - /// Executes the download file. - /// - /// The redmine manager. - /// The address. - /// The name of the file to be placed on the local computer. - /// - public static void ExecuteDownloadFile(RedmineManager redmineManager, string address, string filename) - { - using (var wc = redmineManager.CreateWebClient(null, true)) - { - wc.DownloadProgressChanged += HandleDownloadProgress; - wc.DownloadFileCompleted += HandleDownloadComplete; - - var syncObject = new object(); - lock (syncObject) - { - wc.DownloadFileAsync(new Uri(address), filename, syncObject); - //This would block the thread until download completes - Monitor.Wait(syncObject); - } - - wc.DownloadProgressChanged -= HandleDownloadProgress; - wc.DownloadFileCompleted -= HandleDownloadComplete; - } - } - - /// - /// Executes the download file. - /// - /// The redmine manager. - /// The address. - /// - public static byte[] ExecuteDownloadFile(RedmineManager redmineManager, string address) - { - using (var wc = redmineManager.CreateWebClient(null, true)) - { - return wc.DownloadData(address); - } - } - - private static void HandleDownloadComplete(object sender, AsyncCompletedEventArgs e) - { - lock (e.UserState) - { - //releases blocked thread - Monitor.Pulse(e.UserState); - } - } - - private static void HandleDownloadProgress(object sender, DownloadProgressChangedEventArgs e) - { - } - - /// - /// Executes the upload file. - /// - /// The redmine manager. - /// The address. - /// The data. - /// - public static Upload ExecuteUploadFile(RedmineManager redmineManager, string address, byte[] data) - { - using (var wc = redmineManager.CreateWebClient(null, true)) - { - var response = wc.UploadData(address, data); - var responseString = Encoding.ASCII.GetString(response); - return redmineManager.Serializer.Deserialize(responseString); - } - } - } -} \ No newline at end of file From d35adbea8831fbb85636b7f6efa3d078c0d04fb3 Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 3 Jan 2024 17:42:37 +0200 Subject: [PATCH 313/549] [Internals] Delete WebApiAsyncHelper --- .../Internals/WebApiAsyncHelper.cs | 148 ------------------ 1 file changed, 148 deletions(-) delete mode 100644 src/redmine-net-api/Internals/WebApiAsyncHelper.cs diff --git a/src/redmine-net-api/Internals/WebApiAsyncHelper.cs b/src/redmine-net-api/Internals/WebApiAsyncHelper.cs deleted file mode 100644 index 6f0b3f16..00000000 --- a/src/redmine-net-api/Internals/WebApiAsyncHelper.cs +++ /dev/null @@ -1,148 +0,0 @@ -/* - Copyright 2011 - 2023 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ -#if !(NET20 || NET40) -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Net; -using System.Text; -using System.Threading.Tasks; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.Internals -{ - /// - /// - /// - internal static class WebApiAsyncHelper - { - /// - /// Executes the upload. - /// - /// The redmine manager. - /// The address. - /// Type of the action. - /// The data. - /// - public static async Task ExecuteUpload(RedmineManager redmineManager, string address, string actionType, string data) - { - using (var wc = redmineManager.CreateWebClient(null)) - { - if (actionType == HttpVerbs.POST || actionType == HttpVerbs.DELETE || actionType == HttpVerbs.PUT || - actionType == HttpVerbs.PATCH) - { - return await wc.UploadStringTaskAsync(address, actionType, data).ConfigureAwait(false); - } - } - - return null; - } - - /// - /// Executes the download. - /// - /// - /// The redmine manager. - /// The address. - /// The parameters. - /// - public static async Task ExecuteDownload(RedmineManager redmineManager, string address, - NameValueCollection parameters = null) - where T : class, new() - { - using (var wc = redmineManager.CreateWebClient(parameters)) - { - var response = await wc.DownloadStringTaskAsync(address).ConfigureAwait(false); - return redmineManager.Serializer.Deserialize(response); - } - } - - /// - /// Executes the download list. - /// - /// - /// The redmine manager. - /// The address. - /// The parameters. - /// - public static async Task> ExecuteDownloadList(RedmineManager redmineManager, string address, - NameValueCollection parameters = null) where T : class, new() - { - using (var wc = redmineManager.CreateWebClient(parameters)) - { - var response = await wc.DownloadStringTaskAsync(address).ConfigureAwait(false); - var result = redmineManager.Serializer.DeserializeToPagedResults(response); - if (result != null) - { - return new List(result.Items); - } - return null; - } - } - - - /// - /// Executes the download paginated list. - /// - /// - /// The redmine manager. - /// The address. - /// The parameters. - /// - public static async Task> ExecuteDownloadPaginatedList(RedmineManager redmineManager, string address, - NameValueCollection parameters = null) where T : class, new() - { - using (var wc = redmineManager.CreateWebClient(parameters)) - { - var response = await wc.DownloadStringTaskAsync(address).ConfigureAwait(false); - return redmineManager.Serializer.DeserializeToPagedResults(response); - } - } - - /// - /// Executes the download file. - /// - /// The redmine manager. - /// The address. - /// - public static async Task ExecuteDownloadFile(RedmineManager redmineManager, string address) - { - using (var wc = redmineManager.CreateWebClient(null, true)) - { - return await wc.DownloadDataTaskAsync(address).ConfigureAwait(false); - } - } - - /// - /// Executes the upload file. - /// - /// The redmine manager. - /// The address. - /// The data. - /// - public static async Task ExecuteUploadFile(RedmineManager redmineManager, string address, byte[] data) - { - using (var wc = redmineManager.CreateWebClient(null, true)) - { - var response = await wc.UploadDataTaskAsync(address, data).ConfigureAwait(false); - var responseString = Encoding.ASCII.GetString(response); - return redmineManager.Serializer.Deserialize(responseString); - } - } - } -} -#endif \ No newline at end of file From 0692e9ed37922cce5486d5653f1eae63212a85b5 Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 3 Jan 2024 17:45:04 +0200 Subject: [PATCH 314/549] [Extensions] Mark as obsolete RedmineManagerAsyncExtensions --- .../RedmineManagerAsyncExtensions.cs} | 264 +++++------------- 1 file changed, 76 insertions(+), 188 deletions(-) rename src/redmine-net-api/{Async/RedmineManagerAsync45.cs => Extensions/RedmineManagerAsyncExtensions.cs} (56%) diff --git a/src/redmine-net-api/Async/RedmineManagerAsync45.cs b/src/redmine-net-api/Extensions/RedmineManagerAsyncExtensions.cs similarity index 56% rename from src/redmine-net-api/Async/RedmineManagerAsync45.cs rename to src/redmine-net-api/Extensions/RedmineManagerAsyncExtensions.cs index 2020dee6..fad99217 100644 --- a/src/redmine-net-api/Async/RedmineManagerAsync45.cs +++ b/src/redmine-net-api/Extensions/RedmineManagerAsyncExtensions.cs @@ -14,17 +14,14 @@ You may obtain a copy of the License at limitations under the License. */ -#if !(NET20 || NET40) +#if !(NET20) using System; using System.Collections.Generic; using System.Collections.Specialized; -using System.Globalization; -using System.Net; -using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Internals; using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Types; @@ -32,18 +29,22 @@ namespace Redmine.Net.Api.Async { /// /// - public static class RedmineManagerAsync + [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use RedmineManger async methods instead")] + public static class RedmineManagerAsyncExtensions { /// /// Gets the current user asynchronous. /// /// The redmine manager. /// The parameters. + /// + /// /// - public static async Task GetCurrentUserAsync(this RedmineManager redmineManager, NameValueCollection parameters = null) + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + public static async Task GetCurrentUserAsync(this RedmineManager redmineManager, NameValueCollection parameters = null, string impersonateUserName = null, CancellationToken cancellationToken = default) { - var uri = UrlHelper.GetCurrentUserUrl(redmineManager); - return await WebApiAsyncHelper.ExecuteDownload(redmineManager, uri, parameters).ConfigureAwait(false); + var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); + return await redmineManager.GetCurrentUserAsync(requestOptions, cancellationToken).ConfigureAwait(false); } /// @@ -54,20 +55,15 @@ public static async Task GetCurrentUserAsync(this RedmineManager redmineMa /// Name of the page. /// The wiki page. /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] public static async Task CreateWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage) { - var data = redmineManager.Serializer.Serialize(wikiPage); - if (string.IsNullOrEmpty(data)) return null; + var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); - var url = UrlHelper.GetWikiCreateOrUpdaterUrl(redmineManager, projectId, pageName); - - url = Uri.EscapeUriString(url); - - var response = await WebApiAsyncHelper.ExecuteUpload(redmineManager, url, HttpVerbs.PUT, data).ConfigureAwait(false); - return redmineManager.Serializer.Deserialize(response); + return await redmineManager.CreateWikiPageAsync(projectId, pageName, wikiPage, requestOptions).ConfigureAwait(false); } - /// + /// /// Creates the or update wiki page asynchronous. /// /// The redmine manager. @@ -75,19 +71,11 @@ public static async Task CreateWikiPageAsync(this RedmineManager redmi /// Name of the page. /// The wiki page. /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] public static async Task UpdateWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage) { - var data = redmineManager.Serializer.Serialize(wikiPage); - if (string.IsNullOrEmpty(data)) - { - return ; - } - - var url = UrlHelper.GetWikiCreateOrUpdaterUrl(redmineManager, projectId, pageName); - - url = Uri.EscapeUriString(url); - - var response = await WebApiAsyncHelper.ExecuteUpload(redmineManager, url, HttpVerbs.PUT, data).ConfigureAwait(false); + var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); + await redmineManager.UpdateWikiPageAsync(projectId, pageName, wikiPage, requestOptions).ConfigureAwait(false); } /// @@ -97,12 +85,11 @@ public static async Task UpdateWikiPageAsync(this RedmineManager redmineManager, /// The project identifier. /// Name of the page. /// - public static async Task DeleteWikiPageAsync(this RedmineManager redmineManager, string projectId, - string pageName) + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + public static async Task DeleteWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName) { - var uri = UrlHelper.GetDeleteWikiUrl(redmineManager, projectId, pageName); - uri = Uri.EscapeUriString(uri); - await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.DELETE, string.Empty).ConfigureAwait(false); + var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); + await redmineManager.DeleteWikiPageAsync(projectId, pageName, requestOptions).ConfigureAwait(false); } /// @@ -114,10 +101,11 @@ public static async Task DeleteWikiPageAsync(this RedmineManager redmineManager, /// /// . /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] public static async Task UploadFileAsync(this RedmineManager redmineManager, byte[] data) { - var uri = UrlHelper.GetUploadFileUrl(redmineManager); - return await WebApiAsyncHelper.ExecuteUploadFile(redmineManager, uri, data).ConfigureAwait(false); + var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); + return await redmineManager.UploadFileAsync(data, requestOptions).ConfigureAwait(false); } /// @@ -126,9 +114,12 @@ public static async Task UploadFileAsync(this RedmineManager redmineMana /// The redmine manager. /// The address. /// + [Obsolete("Use DownloadFileAsync instead")] public static async Task DownloadFileAsync(this RedmineManager redmineManager, string address) { - return await WebApiAsyncHelper.ExecuteDownloadFile(redmineManager, address).ConfigureAwait(false); + var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); + + return await redmineManager.DownloadFileAsync(address, requestOptions).ConfigureAwait(false); } /// @@ -140,12 +131,11 @@ public static async Task DownloadFileAsync(this RedmineManager redmineMa /// Name of the page. /// The version. /// - public static async Task GetWikiPageAsync(this RedmineManager redmineManager, string projectId, - NameValueCollection parameters, string pageName, uint version = 0) + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + public static async Task GetWikiPageAsync(this RedmineManager redmineManager, string projectId, NameValueCollection parameters, string pageName, uint version = 0) { - var uri = UrlHelper.GetWikiPageUrl(redmineManager, projectId, pageName, version); - uri = Uri.EscapeUriString(uri); - return await WebApiAsyncHelper.ExecuteDownload(redmineManager, uri, parameters).ConfigureAwait(false); + var requestOptions = RedmineManagerExtensions.CreateRequestOptions(parameters); + return await redmineManager.GetWikiPageAsync(projectId, pageName, requestOptions, version).ConfigureAwait(false); } /// @@ -155,10 +145,11 @@ public static async Task GetWikiPageAsync(this RedmineManager redmineM /// The parameters. /// The project identifier. /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] public static async Task> GetAllWikiPagesAsync(this RedmineManager redmineManager, NameValueCollection parameters, string projectId) { - var uri = UrlHelper.GetWikisUrl(redmineManager, projectId); - return await WebApiAsyncHelper.ExecuteDownloadList(redmineManager, uri, parameters).ConfigureAwait(false); + var requestOptions = RedmineManagerExtensions.CreateRequestOptions(parameters); + return await redmineManager.GetAllWikiPagesAsync(projectId, requestOptions).ConfigureAwait(false); } /// @@ -170,12 +161,11 @@ public static async Task> GetAllWikiPagesAsync(this RedmineManage /// /// Returns the Guid associated with the async request. /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] public static async Task AddUserToGroupAsync(this RedmineManager redmineManager, int groupId, int userId) { - var data = SerializationHelper.SerializeUserId(userId, redmineManager.MimeFormat); - var uri = UrlHelper.GetAddUserToGroupUrl(redmineManager, groupId); - - await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.POST, data).ConfigureAwait(false); + var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); + await redmineManager.AddUserToGroupAsync(groupId, userId, requestOptions).ConfigureAwait(false); } /// @@ -185,10 +175,11 @@ public static async Task AddUserToGroupAsync(this RedmineManager redmineManager, /// The group id. /// The user id. /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] public static async Task RemoveUserFromGroupAsync(this RedmineManager redmineManager, int groupId, int userId) { - var uri = UrlHelper.GetRemoveUserFromGroupUrl(redmineManager, groupId, userId); - await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.DELETE, string.Empty).ConfigureAwait(false); + var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); + await redmineManager.RemoveUserFromGroupAsync(groupId, userId, requestOptions).ConfigureAwait(false); } /// @@ -198,12 +189,11 @@ public static async Task RemoveUserFromGroupAsync(this RedmineManager redmineMan /// The issue identifier. /// The user identifier. /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] public static async Task AddWatcherToIssueAsync(this RedmineManager redmineManager, int issueId, int userId) { - var data = SerializationHelper.SerializeUserId(userId, redmineManager.MimeFormat); - var uri = UrlHelper.GetAddWatcherUrl(redmineManager, issueId); - - await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.POST, data).ConfigureAwait(false); + var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); + await redmineManager.AddWatcherToIssueAsync(issueId, userId, requestOptions).ConfigureAwait(false); } /// @@ -213,10 +203,11 @@ public static async Task AddWatcherToIssueAsync(this RedmineManager redmineManag /// The issue identifier. /// The user identifier. /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] public static async Task RemoveWatcherFromIssueAsync(this RedmineManager redmineManager, int issueId, int userId) { - var uri = UrlHelper.GetRemoveWatcherUrl(redmineManager, issueId, userId); - await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.DELETE, string.Empty).ConfigureAwait(false); + var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); + await redmineManager.RemoveWatcherFromIssueAsync(issueId, userId, requestOptions).ConfigureAwait(false); } /// @@ -226,16 +217,10 @@ public static async Task RemoveWatcherFromIssueAsync(this RedmineManager redmine /// /// /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] public static async Task CountAsync(this RedmineManager redmineManager, params string[] include) where T : class, new() { - var parameters = new NameValueCollection(); - - if (include != null) - { - parameters.Add(RedmineKeys.INCLUDE, string.Join(",", include)); - } - - return await CountAsync(redmineManager,parameters).ConfigureAwait(false); + return await RedmineManagerExtensions.CountAsync(redmineManager, include).ConfigureAwait(false); } /// @@ -245,32 +230,11 @@ public static async Task RemoveWatcherFromIssueAsync(this RedmineManager redmine /// /// /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] public static async Task CountAsync(this RedmineManager redmineManager, NameValueCollection parameters) where T : class, new() { - int totalCount = 0, pageSize = 1, offset = 0; - - if (parameters == null) - { - parameters = new NameValueCollection(); - } - - parameters.Set(RedmineKeys.LIMIT, pageSize.ToString(CultureInfo.InvariantCulture)); - parameters.Set(RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)); - - try - { - var tempResult = await GetPaginatedObjectsAsync(redmineManager,parameters).ConfigureAwait(false); - if (tempResult != null) - { - totalCount = tempResult.TotalItems; - } - } - catch (WebException wex) - { - wex.HandleWebException(redmineManager.Serializer); - } - - return totalCount; + var requestOptions = RedmineManagerExtensions.CreateRequestOptions(parameters); + return await redmineManager.CountAsync(requestOptions).ConfigureAwait(false); } @@ -281,12 +245,12 @@ public static async Task RemoveWatcherFromIssueAsync(this RedmineManager redmine /// The redmine manager. /// The parameters. /// - public static async Task> GetPaginatedObjectsAsync(this RedmineManager redmineManager, - NameValueCollection parameters) + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + public static async Task> GetPaginatedObjectsAsync(this RedmineManager redmineManager, NameValueCollection parameters) where T : class, new() { - var uri = UrlHelper.GetListUrl(redmineManager, parameters); - return await WebApiAsyncHelper.ExecuteDownloadPaginatedList(redmineManager, uri, parameters).ConfigureAwait(false); + var requestOptions = RedmineManagerExtensions.CreateRequestOptions(parameters); + return await redmineManager.GetPagedAsync(requestOptions).ConfigureAwait(false); } /// @@ -296,70 +260,12 @@ public static async Task> GetPaginatedObjectsAsync(this Redmi /// The redmine manager. /// The parameters. /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] public static async Task> GetObjectsAsync(this RedmineManager redmineManager, NameValueCollection parameters) where T : class, new() { - int pageSize = 0, offset = 0; - var isLimitSet = false; - List resultList = null; - - if (parameters == null) - { - parameters = new NameValueCollection(); - } - else - { - isLimitSet = int.TryParse(parameters[RedmineKeys.LIMIT], out pageSize); - int.TryParse(parameters[RedmineKeys.OFFSET], out offset); - } - - if (pageSize == default(int)) - { - pageSize = redmineManager.PageSize > 0 - ? redmineManager.PageSize - : RedmineManager.DEFAULT_PAGE_SIZE_VALUE; - parameters.Set(RedmineKeys.LIMIT, pageSize.ToString(CultureInfo.InvariantCulture)); - } - try - { - var hasOffset = RedmineManager.TypesWithOffset.ContainsKey(typeof(T)); - if (hasOffset) - { - var totalCount = 0; - do - { - parameters.Set(RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)); - var tempResult = await redmineManager.GetPaginatedObjectsAsync(parameters).ConfigureAwait(false); - totalCount = isLimitSet ? pageSize : tempResult.TotalItems; - - if (tempResult?.Items != null) - { - if (resultList == null) - { - resultList = new List(tempResult.Items); - } - else - { - resultList.AddRange(tempResult.Items); - } - } - offset += pageSize; - } while (offset < totalCount); - } - else - { - var result = await redmineManager.GetPaginatedObjectsAsync(parameters).ConfigureAwait(false); - if (result?.Items != null) - { - return new List(result.Items); - } - } - } - catch (WebException wex) - { - wex.HandleWebException(redmineManager.Serializer); - } - return resultList; + var requestOptions = RedmineManagerExtensions.CreateRequestOptions(parameters); + return await redmineManager.GetObjectsAsync(requestOptions).ConfigureAwait(false); } /// @@ -370,11 +276,12 @@ public static async Task> GetObjectsAsync(this RedmineManager redmine /// The id of the object. /// Optional filters and/or optional fetched data. /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] public static async Task GetObjectAsync(this RedmineManager redmineManager, string id, NameValueCollection parameters) where T : class, new() { - var uri = UrlHelper.GetGetUrl(redmineManager, id); - return await WebApiAsyncHelper.ExecuteDownload(redmineManager, uri, parameters).ConfigureAwait(false); + var requestOptions = RedmineManagerExtensions.CreateRequestOptions(parameters); + return await redmineManager.GetObjectAsync(id, requestOptions).ConfigureAwait(false); } /// @@ -384,10 +291,12 @@ public static async Task GetObjectAsync(this RedmineManager redmineManager /// The redmine manager. /// The object to create. /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] public static async Task CreateObjectAsync(this RedmineManager redmineManager, T entity) where T : class, new() { - return await CreateObjectAsync(redmineManager, entity, null).ConfigureAwait(false); + var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); + return await redmineManager.CreateObjectAsync(entity, null, requestOptions).ConfigureAwait(false); } /// @@ -398,14 +307,12 @@ public static async Task CreateObjectAsync(this RedmineManager redmineMana /// The object to create. /// The owner identifier. /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] public static async Task CreateObjectAsync(this RedmineManager redmineManager, T entity, string ownerId) where T : class, new() { - var uri = UrlHelper.GetCreateUrl(redmineManager, ownerId); - var data = redmineManager.Serializer.Serialize(entity); - - var response = await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.POST, data).ConfigureAwait(false); - return redmineManager.Serializer.Deserialize(response); + var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); + return await redmineManager.CreateObjectAsync(entity, ownerId, requestOptions, CancellationToken.None).ConfigureAwait(false); } /// @@ -416,14 +323,12 @@ public static async Task CreateObjectAsync(this RedmineManager redmineMana /// The identifier. /// The object. /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] public static async Task UpdateObjectAsync(this RedmineManager redmineManager, string id, T entity) where T : class, new() { - var uri = UrlHelper.GetUploadUrl(redmineManager, id); - var data = redmineManager.Serializer.Serialize(entity); - data = Regex.Replace(data, @"\r\n|\r|\n", "\r\n"); - - await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.PUT, data).ConfigureAwait(false); + var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); + await redmineManager.UpdateObjectAsync(id, entity, requestOptions).ConfigureAwait(false); } /// @@ -433,11 +338,12 @@ public static async Task UpdateObjectAsync(this RedmineManager redmineManager /// The redmine manager. /// The id of the object to delete /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] public static async Task DeleteObjectAsync(this RedmineManager redmineManager, string id) where T : class, new() { - var uri = UrlHelper.GetDeleteUrl(redmineManager, id); - await WebApiAsyncHelper.ExecuteUpload(redmineManager, uri, HttpVerbs.DELETE, string.Empty).ConfigureAwait(false); + var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); + await redmineManager.DeleteObjectAsync(id, requestOptions).ConfigureAwait(false); } /// @@ -450,28 +356,10 @@ public static async Task DeleteObjectAsync(this RedmineManager redmineManager /// /// /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] public static async Task> SearchAsync(this RedmineManager redmineManager, string q, int limit = RedmineManager.DEFAULT_PAGE_SIZE_VALUE, int offset = 0, SearchFilterBuilder searchFilter = null) { - if (q.IsNullOrWhiteSpace()) - { - throw new ArgumentNullException(nameof(q)); - } - - var parameters = new NameValueCollection - { - {RedmineKeys.Q, q}, - {RedmineKeys.LIMIT, limit.ToString(CultureInfo.InvariantCulture)}, - {RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)}, - }; - - if (searchFilter != null) - { - parameters = searchFilter.Build(parameters); - } - - var result = await redmineManager.GetPaginatedObjectsAsync(parameters).ConfigureAwait(false); - - return result; + return await RedmineManagerExtensions.SearchAsync(redmineManager, q, limit, offset, searchFilter).ConfigureAwait(false); } } } From f0eb860bbe358fc01c45763019120a0b51c7a9ca Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 3 Jan 2024 17:46:01 +0200 Subject: [PATCH 315/549] [New] IRedmineApiClient --- src/redmine-net-api/Net/IRedmineApiClient.cs | 32 ++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/redmine-net-api/Net/IRedmineApiClient.cs diff --git a/src/redmine-net-api/Net/IRedmineApiClient.cs b/src/redmine-net-api/Net/IRedmineApiClient.cs new file mode 100644 index 00000000..00a28bd7 --- /dev/null +++ b/src/redmine-net-api/Net/IRedmineApiClient.cs @@ -0,0 +1,32 @@ +using System.Threading; +#if!(NET20) +using System.Threading.Tasks; +#endif + +namespace Redmine.Net.Api.Net; + +/// +/// +/// +internal interface IRedmineApiClient +{ + ApiResponseMessage Get(string address, RequestOptions requestOptions = null); + ApiResponseMessage GetPaged(string address, RequestOptions requestOptions = null); + ApiResponseMessage Create(string address, string payload, RequestOptions requestOptions = null); + ApiResponseMessage Update(string address, string payload, RequestOptions requestOptions = null); + ApiResponseMessage Patch(string address, string payload, RequestOptions requestOptions = null); + ApiResponseMessage Delete(string address, RequestOptions requestOptions = null); + ApiResponseMessage Upload(string address, byte[] data, RequestOptions requestOptions = null); + ApiResponseMessage Download(string address, RequestOptions requestOptions = null); + + #if !(NET20) + Task GetAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + Task GetPagedAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + Task CreateAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + Task UpdateAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + Task PatchAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + Task DeleteAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + Task UploadFileAsync(string address, byte[] data, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + Task DownloadAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + #endif +} \ No newline at end of file From 2f74e346ef789fca52666e58564c92a1a2e606c5 Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 3 Jan 2024 17:47:22 +0200 Subject: [PATCH 316/549] [New][Net][WebClient]InternalRedmineWebClient --- .../Net/WebClient/InternalRedmineWebClient.cs | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 src/redmine-net-api/Net/WebClient/InternalRedmineWebClient.cs diff --git a/src/redmine-net-api/Net/WebClient/InternalRedmineWebClient.cs b/src/redmine-net-api/Net/WebClient/InternalRedmineWebClient.cs new file mode 100644 index 00000000..c2f35660 --- /dev/null +++ b/src/redmine-net-api/Net/WebClient/InternalRedmineWebClient.cs @@ -0,0 +1,95 @@ +using System; +using System.Net; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; + +namespace Redmine.Net.Api.Net.WebClient; + +internal sealed class InternalRedmineWebClient : System.Net.WebClient +{ + private readonly IRedmineApiClientOptions _webClientSettings; + + public InternalRedmineWebClient(RedmineManagerOptions redmineManagerOptions) + { + _webClientSettings = redmineManagerOptions.ClientOptions; + BaseAddress = redmineManagerOptions.BaseAddress.ToString(); + } + + protected override WebRequest GetWebRequest(Uri address) + { + try + { + var webRequest = base.GetWebRequest(address); + + if (webRequest is not HttpWebRequest httpWebRequest) + { + return base.GetWebRequest(address); + } + + httpWebRequest.UserAgent = _webClientSettings.UserAgent.ValueOrFallback("RedmineDotNetAPIClient"); + + httpWebRequest.AutomaticDecompression = _webClientSettings.DecompressionFormat ?? DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.None; + + AssignIfHasValue(_webClientSettings.AutoRedirect, value => httpWebRequest.AllowAutoRedirect = value); + + AssignIfHasValue(_webClientSettings.MaxAutomaticRedirections, value => httpWebRequest.MaximumAutomaticRedirections = value); + + AssignIfHasValue(_webClientSettings.KeepAlive, value => httpWebRequest.KeepAlive = value); + + AssignIfHasValue(_webClientSettings.Timeout, value => httpWebRequest.Timeout = (int) value.TotalMilliseconds); + + AssignIfHasValue(_webClientSettings.PreAuthenticate, value => httpWebRequest.PreAuthenticate = value); + + AssignIfHasValue(_webClientSettings.UseCookies, value => httpWebRequest.CookieContainer = _webClientSettings.CookieContainer); + + AssignIfHasValue(_webClientSettings.UnsafeAuthenticatedConnectionSharing, value => httpWebRequest.UnsafeAuthenticatedConnectionSharing = value); + + AssignIfHasValue(_webClientSettings.MaxResponseContentBufferSize, value => { }); + + if (_webClientSettings.DefaultHeaders != null) + { + httpWebRequest.Headers = new WebHeaderCollection(); + foreach (var defaultHeader in _webClientSettings.DefaultHeaders) + { + httpWebRequest.Headers.Add(defaultHeader.Key, defaultHeader.Value); + } + } + + httpWebRequest.CachePolicy = _webClientSettings.RequestCachePolicy; + + httpWebRequest.Proxy = _webClientSettings.Proxy; + + httpWebRequest.Credentials = _webClientSettings.Credentials; + + #if !(NET20) + if (_webClientSettings.ClientCertificates != null) + { + httpWebRequest.ClientCertificates = _webClientSettings.ClientCertificates; + } + #endif + + #if (NET45_OR_GREATER || NETCOREAPP) + httpWebRequest.ServerCertificateValidationCallback = _webClientSettings.ServerCertificateValidationCallback; + #endif + + if (_webClientSettings.ProtocolVersion != default) + { + httpWebRequest.ProtocolVersion = _webClientSettings.ProtocolVersion; + } + + return httpWebRequest; + } + catch (Exception webException) + { + throw new RedmineException(webException.GetBaseException().Message, webException); + } + } + + private static void AssignIfHasValue(T? nullableValue, Action assignAction) where T : struct + { + if (nullableValue.HasValue) + { + assignAction(nullableValue.Value); + } + } +} \ No newline at end of file From 12d668ed7f91cca3c3482213a5a15a4427f86f70 Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 3 Jan 2024 17:47:55 +0200 Subject: [PATCH 317/549] [New][Net][WebClient] RedmineApiClient --- .../Net/WebClient/RedmineApiClient.cs | 327 ++++++++++++++++++ 1 file changed, 327 insertions(+) create mode 100644 src/redmine-net-api/Net/WebClient/RedmineApiClient.cs diff --git a/src/redmine-net-api/Net/WebClient/RedmineApiClient.cs b/src/redmine-net-api/Net/WebClient/RedmineApiClient.cs new file mode 100644 index 00000000..2bb19404 --- /dev/null +++ b/src/redmine-net-api/Net/WebClient/RedmineApiClient.cs @@ -0,0 +1,327 @@ +using System; +using System.Collections.Specialized; +using System.Net; +using System.Text; +using System.Threading; +#if!(NET20) +using System.Threading.Tasks; +#endif +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Serialization; + +namespace Redmine.Net.Api.Net.WebClient +{ + /// + /// + /// + internal sealed class RedmineApiClient : IRedmineApiClient + { + private readonly Func _webClientFunc; + private readonly IRedmineAuthentication _credentials; + private readonly IRedmineSerializer _serializer; + + public RedmineApiClient(RedmineManagerOptions redmineManagerOptions) + : this(() => new InternalRedmineWebClient(redmineManagerOptions), redmineManagerOptions.Authentication, redmineManagerOptions.Serializer) + { + ConfigureServicePointManager(redmineManagerOptions.ClientOptions); + } + + public RedmineApiClient(Func webClientFunc, IRedmineAuthentication authentication, IRedmineSerializer serializer) + { + _webClientFunc = webClientFunc; + _credentials = authentication; + _serializer = serializer; + } + + private static void ConfigureServicePointManager(IRedmineApiClientOptions webClientSettings) + { + if (webClientSettings.MaxServicePoints.HasValue) + { + ServicePointManager.MaxServicePoints = webClientSettings.MaxServicePoints.Value; + } + + if (webClientSettings.MaxServicePointIdleTime.HasValue) + { + ServicePointManager.MaxServicePointIdleTime = webClientSettings.MaxServicePointIdleTime.Value; + } + + ServicePointManager.SecurityProtocol = webClientSettings.SecurityProtocolType ?? ServicePointManager.SecurityProtocol; + + if (webClientSettings.DefaultConnectionLimit.HasValue) + { + ServicePointManager.DefaultConnectionLimit = webClientSettings.DefaultConnectionLimit.Value; + } + + if (webClientSettings.DnsRefreshTimeout.HasValue) + { + ServicePointManager.DnsRefreshTimeout = webClientSettings.DnsRefreshTimeout.Value; + } + + ServicePointManager.CheckCertificateRevocationList = webClientSettings.CheckCertificateRevocationList; + + if (webClientSettings.EnableDnsRoundRobin.HasValue) + { + ServicePointManager.EnableDnsRoundRobin = webClientSettings.EnableDnsRoundRobin.Value; + } + + #if(NET46_OR_GREATER || NETCOREAPP) + if (webClientSettings.ReusePort.HasValue) + { + ServicePointManager.ReusePort = webClientSettings.ReusePort.Value; + } + #endif + } + + public ApiResponseMessage Get(string address, RequestOptions requestOptions = null) + { + return HandleRequest(address, HttpVerbs.GET, requestOptions); + } + + public ApiResponseMessage GetPaged(string address, RequestOptions requestOptions = null) + { + return Get(address, requestOptions); + } + + public ApiResponseMessage Create(string address, string payload, RequestOptions requestOptions = null) + { + var content = new StringApiRequestMessageContent(payload, GetContentType(_serializer)); + return HandleRequest(address, HttpVerbs.POST, requestOptions, content); + } + + public ApiResponseMessage Update(string address, string payload, RequestOptions requestOptions = null) + { + var content = new StringApiRequestMessageContent(payload, GetContentType(_serializer)); + return HandleRequest(address, HttpVerbs.PUT, requestOptions, content); + } + + public ApiResponseMessage Patch(string address, string payload, RequestOptions requestOptions = null) + { + var content = new StringApiRequestMessageContent(payload, GetContentType(_serializer)); + return HandleRequest(address, HttpVerbs.PATCH, requestOptions, content); + } + + public ApiResponseMessage Delete(string address, RequestOptions requestOptions = null) + { + return HandleRequest(address, HttpVerbs.DELETE, requestOptions); + } + + public ApiResponseMessage Download(string address, RequestOptions requestOptions = null) + { + return HandleRequest(address, HttpVerbs.DOWNLOAD, requestOptions); + } + + public ApiResponseMessage Upload(string address, byte[] data, RequestOptions requestOptions = null) + { + var content = new ByteArrayApiRequestMessageContent(data); + return HandleRequest(address, HttpVerbs.UPLOAD, requestOptions, content); + } + + #if !(NET20) + public async Task GetAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + return await HandleRequestAsync(address, HttpVerbs.GET, requestOptions, cancellationToken:cancellationToken).ConfigureAwait(false); + } + + public Task GetPagedAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + return GetAsync(address, requestOptions, cancellationToken); + } + + public async Task CreateAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var content = new StringApiRequestMessageContent(payload, GetContentType(_serializer)); + return await HandleRequestAsync(address, HttpVerbs.POST, requestOptions, content, cancellationToken:cancellationToken).ConfigureAwait(false); + } + + public async Task UpdateAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var content = new StringApiRequestMessageContent(payload, GetContentType(_serializer)); + return await HandleRequestAsync(address, HttpVerbs.PUT, requestOptions, content, cancellationToken:cancellationToken).ConfigureAwait(false); + } + + public async Task UploadFileAsync(string address, byte[] data, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var content = new ByteArrayApiRequestMessageContent(data); + return await HandleRequestAsync(address, HttpVerbs.UPLOAD, requestOptions, content, cancellationToken:cancellationToken).ConfigureAwait(false); + } + + public async Task PatchAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var content = new StringApiRequestMessageContent(payload, GetContentType(_serializer)); + return await HandleRequestAsync(address, HttpVerbs.PATCH, requestOptions, content, cancellationToken:cancellationToken).ConfigureAwait(false); + } + + public async Task DeleteAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + return await HandleRequestAsync(address, HttpVerbs.DELETE, requestOptions, cancellationToken:cancellationToken).ConfigureAwait(false); + } + + public async Task DownloadAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + return await HandleRequestAsync(address, HttpVerbs.DOWNLOAD, requestOptions, cancellationToken:cancellationToken).ConfigureAwait(false); + } + + private Task HandleRequestAsync(string address, string verb, RequestOptions requestOptions = null, ApiRequestMessageContent content = null, CancellationToken cancellationToken = default) + { + return SendAsync(CreateRequestMessage(address, verb, requestOptions, content), cancellationToken); + } + + private async Task SendAsync(ApiRequestMessage requestMessage, CancellationToken cancellationToken) + { + System.Net.WebClient webClient = null; + byte[] response = null; + NameValueCollection responseHeaders = null; + try + { + webClient = _webClientFunc(); + + cancellationToken.Register(webClient.CancelAsync); + + SetWebClientHeaders(webClient, requestMessage); + + if (requestMessage.Method is HttpVerbs.GET or HttpVerbs.DOWNLOAD) + { + response = await webClient.DownloadDataTaskAsync(requestMessage.RequestUri).ConfigureAwait(false); + } + else + { + byte[] payload; + if (requestMessage.Content != null) + { + webClient.Headers.Add(HttpRequestHeader.ContentType, requestMessage.Content.ContentType); + payload = requestMessage.Content.Body; + } + else + { + payload = Encoding.UTF8.GetBytes(string.Empty); + } + + response = await webClient.UploadDataTaskAsync(requestMessage.RequestUri, requestMessage.Method, payload).ConfigureAwait(false); + } + + responseHeaders = webClient.ResponseHeaders; + } + catch (WebException ex) when (ex.Status == WebExceptionStatus.RequestCanceled) + { + //TODO: Handle cancellation... + } + catch (WebException webException) + { + webException.HandleWebException(_serializer); + } + finally + { + webClient?.Dispose(); + } + + return new ApiResponseMessage() + { + Headers = responseHeaders, + Content = response + }; + } + #endif + + + private static ApiRequestMessage CreateRequestMessage(string address, string verb, RequestOptions requestOptions = null, ApiRequestMessageContent content = null) + { + var req = new ApiRequestMessage() + { + RequestUri = address, + Method = verb, + }; + + if (requestOptions != null) + { + req.QueryString = requestOptions.QueryString; + req.ImpersonateUser = requestOptions.ImpersonateUser; + } + + if (content != null) + { + req.Content = content; + } + + return req; + } + + private ApiResponseMessage HandleRequest(string address, string verb, RequestOptions requestOptions = null, ApiRequestMessageContent content = null) + { + return Send(CreateRequestMessage(address, verb, requestOptions, content)); + } + + private ApiResponseMessage Send(ApiRequestMessage requestMessage) + { + System.Net.WebClient webClient = null; + byte[] response = null; + NameValueCollection responseHeaders = null; + + try + { + webClient = _webClientFunc(); + SetWebClientHeaders(webClient, requestMessage); + + if (IsGetOrDownload(requestMessage.Method)) + { + response = webClient.DownloadData(requestMessage.RequestUri); + } + else + { + byte[] payload; + if (requestMessage.Content != null) + { + webClient.Headers.Add(HttpRequestHeader.ContentType, requestMessage.Content.ContentType); + payload = requestMessage.Content.Body; + } + else + { + payload = Encoding.UTF8.GetBytes(string.Empty); + } + + response = webClient.UploadData(requestMessage.RequestUri, requestMessage.Method, payload); + } + + responseHeaders = webClient.ResponseHeaders; + } + catch (WebException webException) + { + webException.HandleWebException(_serializer); + } + finally + { + webClient?.Dispose(); + } + + return new ApiResponseMessage() + { + Headers = responseHeaders, + Content = response + }; + } + + private void SetWebClientHeaders(System.Net.WebClient webClient, ApiRequestMessage requestMessage) + { + if (requestMessage.QueryString != null) + { + webClient.QueryString = requestMessage.QueryString; + } + + webClient.Headers.Add(_credentials.AuthenticationType, _credentials.Token); + + if (!requestMessage.ImpersonateUser.IsNullOrWhiteSpace()) + { + webClient.Headers.Add(RedmineConstants.IMPERSONATE_HEADER_KEY, requestMessage.ImpersonateUser); + } + } + + private static bool IsGetOrDownload(string method) + { + return method is HttpVerbs.GET or HttpVerbs.DOWNLOAD; + } + + private static string GetContentType(IRedmineSerializer serializer) + { + return serializer.Format == "xml" ? RedmineConstants.CONTENT_TYPE_APPLICATION_XML : RedmineConstants.CONTENT_TYPE_APPLICATION_JSON; + } + } +} \ No newline at end of file From 39dd6e28e6b7cda4ff10f1475b2e8a65d1d8dd62 Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 3 Jan 2024 17:50:27 +0200 Subject: [PATCH 318/549] [New] RedmineManagerObsolete --- src/redmine-net-api/RedmineManagerObsolete.cs | 391 ++++++++++++++++++ 1 file changed, 391 insertions(+) create mode 100644 src/redmine-net-api/RedmineManagerObsolete.cs diff --git a/src/redmine-net-api/RedmineManagerObsolete.cs b/src/redmine-net-api/RedmineManagerObsolete.cs new file mode 100644 index 00000000..34764d20 --- /dev/null +++ b/src/redmine-net-api/RedmineManagerObsolete.cs @@ -0,0 +1,391 @@ +/* + Copyright 2011 - 2023 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Net; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using Redmine.Net.Api.Authentication; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Net.WebClient; +using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api +{ + /// + /// The main class to access Redmine API. + /// + public partial class RedmineManager + { + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use RedmineConstants.DEFAULT_PAGE_SIZE")]public const int DEFAULT_PAGE_SIZE_VALUE = 25; + + /// + /// Initializes a new instance of the class. + /// + /// The host. + /// The MIME format. + /// if set to true [verify server cert]. + /// The proxy. + /// Use this parameter to specify a SecurityProtocolType. + /// Note: it is recommended to leave this parameter at its default value as this setting also affects the calling application process. + /// http or https. Default is https. + /// The webclient timeout. Default is 100 seconds. + [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use RedmineManager(RedmineManagerOptionsBuilder")] + public RedmineManager(string host, MimeFormat mimeFormat = MimeFormat.Xml, bool verifyServerCert = true, + IWebProxy proxy = null, SecurityProtocolType securityProtocolType = default, string scheme = "https", TimeSpan? timeout = null) + :this(new RedmineManagerOptionsBuilder() + .WithHost(host) + .WithAuthentication(new RedmineNoAuthentication()) + .WithSerializationType(mimeFormat) + .WithVerifyServerCert(verifyServerCert) + .WithClientOptions(new RedmineWebClientOptions() + { + Proxy = proxy, + Scheme = scheme, + Timeout = timeout, + SecurityProtocolType = securityProtocolType + }) + ) { } + + /// + /// Initializes a new instance of the class using your API key for authentication. + /// + /// + /// To enable the API-style authentication, you have to check Enable REST API in Administration -&gt; Settings -&gt; Authentication. + /// You can find your API key on your account page ( /my/account ) when logged in, on the right-hand pane of the default layout. + /// + /// The host. + /// The API key. + /// The MIME format. + /// if set to true [verify server cert]. + /// The proxy. + /// Use this parameter to specify a SecurityProtocolType. + /// Note: it is recommended to leave this parameter at its default value as this setting also affects the calling application process. + /// + /// The webclient timeout. Default is 100 seconds. + [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use RedmineManager(RedmineManagerOptionsBuilder")] + public RedmineManager(string host, string apiKey, MimeFormat mimeFormat = MimeFormat.Xml, + bool verifyServerCert = true, IWebProxy proxy = null, + SecurityProtocolType securityProtocolType = default, string scheme = "https", TimeSpan? timeout = null) + : this(new RedmineManagerOptionsBuilder() + .WithHost(host) + .WithAuthentication(new RedmineApiKeyAuthentication(apiKey)) + .WithSerializationType(mimeFormat) + .WithVerifyServerCert(verifyServerCert) + .WithClientOptions(new RedmineWebClientOptions() + { + Proxy = proxy, + Scheme = scheme, + Timeout = timeout, + SecurityProtocolType = securityProtocolType + })){} + + /// + /// Initializes a new instance of the class using your login and password for authentication. + /// + /// The host. + /// The login. + /// The password. + /// The MIME format. + /// if set to true [verify server cert]. + /// The proxy. + /// Use this parameter to specify a SecurityProtocolType. Note: it is recommended to leave this parameter at its default value as this setting also affects the calling application process. + /// + /// The webclient timeout. Default is 100 seconds. + [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use RedmineManager(RedmineManagerOptionsBuilder")] + public RedmineManager(string host, string login, string password, MimeFormat mimeFormat = MimeFormat.Xml, + bool verifyServerCert = true, IWebProxy proxy = null, + SecurityProtocolType securityProtocolType = default, string scheme = "https", TimeSpan? timeout = null) + : this(new RedmineManagerOptionsBuilder() + .WithHost(host) + .WithAuthentication(new RedmineBasicAuthentication(login, password)) + .WithSerializationType(mimeFormat) + .WithVerifyServerCert(verifyServerCert) + .WithClientOptions(new RedmineWebClientOptions() + { + Proxy = proxy, + Scheme = scheme, + Timeout = timeout, + SecurityProtocolType = securityProtocolType + })) {} + + #region Obsolete + /// + /// Gets the suffixes. + /// + /// + /// The suffixes. + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)]public static Dictionary Suffixes => null; + + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)]public string Format { get; } + + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)]public string Scheme { get; } + + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)]public TimeSpan? Timeout { get; } + + /// + /// Gets the host. + /// + /// + /// The host. + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)]public string Host { get; } + + /// + /// The ApiKey used to authenticate. + /// + /// + /// The API key. + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)]public string ApiKey { get; } + + /// + /// Gets the MIME format. + /// + /// + /// The MIME format. + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)]public MimeFormat MimeFormat { get; } + + /// + /// Gets the proxy. + /// + /// + /// The proxy. + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)]public IWebProxy Proxy { get; } + + /// + /// Gets the type of the security protocol. + /// + /// + /// The type of the security protocol. + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)]public SecurityProtocolType SecurityProtocolType { get; } + #endregion + + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Returns null")] + public static readonly Dictionary TypesWithOffset = null; + + /// + /// Returns the user whose credentials are used to access the API. + /// + /// The accepted parameters are: memberships and groups (added in 2.1). + /// + /// + /// An error occurred during deserialization. The original exception is available + /// using the System.Exception.InnerException property. + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use GetCurrentUser extension instead")] + public User GetCurrentUser(NameValueCollection parameters = null) + { + return this.GetCurrentUser(RedmineManagerExtensions.CreateRequestOptions(parameters)); + } + + /// + /// + /// + /// Returns the my account details. + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use GetMyAccount extension instead")] + public MyAccount GetMyAccount() + { + return RedmineManagerExtensions.GetMyAccount(this); + } + + /// + /// Adds the watcher to issue. + /// + /// The issue identifier. + /// The user identifier. + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use AddWatcherToIssue extension instead")] + public void AddWatcherToIssue(int issueId, int userId) + { + RedmineManagerExtensions.AddWatcherToIssue(this, issueId, userId); + } + + /// + /// Removes the watcher from issue. + /// + /// The issue identifier. + /// The user identifier. + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use RemoveWatcherFromIssue extension instead")] + public void RemoveWatcherFromIssue(int issueId, int userId) + { + RedmineManagerExtensions.RemoveWatcherFromIssue(this, issueId, userId); + } + + /// + /// Adds an existing user to a group. + /// + /// The group id. + /// The user id. + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use AddUserToGroup extension instead")] + public void AddUserToGroup(int groupId, int userId) + { + RedmineManagerExtensions.AddUserToGroup(this, groupId, userId); + } + + /// + /// Removes an user from a group. + /// + /// The group id. + /// The user id. + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use RemoveUserFromGroup extension instead")] + public void RemoveUserFromGroup(int groupId, int userId) + { + RedmineManagerExtensions.RemoveUserFromGroup(this, groupId, userId); + } + + /// + /// Creates or updates a wiki page. + /// + /// The project id or identifier. + /// The wiki page name. + /// The wiki page to create or update. + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use UpdateWikiPage extension instead")] + public void UpdateWikiPage(string projectId, string pageName, WikiPage wikiPage) + { + RedmineManagerExtensions.UpdateWikiPage(this, projectId, pageName, wikiPage); + } + + /// + /// + /// + /// + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use CreateWikiPage extension instead")] + public WikiPage CreateWikiPage(string projectId, string pageName, WikiPage wikiPage) + { + return RedmineManagerExtensions.CreateWikiPage(this, projectId, pageName, wikiPage); + } + + /// + /// Gets the wiki page. + /// + /// The project identifier. + /// The parameters. + /// Name of the page. + /// The version. + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use GetWikiPage extension instead")] + public WikiPage GetWikiPage(string projectId, NameValueCollection parameters, string pageName, uint version = 0) + { + return this.GetWikiPage(projectId, pageName, RedmineManagerExtensions.CreateRequestOptions(parameters), version); + } + + /// + /// Returns the list of all pages in a project wiki. + /// + /// The project id or identifier. + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use GetAllWikiPages extension instead")] + public List GetAllWikiPages(string projectId) + { + return RedmineManagerExtensions.GetAllWikiPages(this, projectId); + } + + /// + /// Deletes a wiki page, its attachments and its history. If the deleted page is a parent page, its child pages are not + /// deleted but changed as root pages. + /// + /// The project id or identifier. + /// The wiki page name. + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use DeleteWikiPage extension instead")] + public void DeleteWikiPage(string projectId, string pageName) + { + RedmineManagerExtensions.DeleteWikiPage(this, projectId, pageName); + } + + /// + /// Updates the attachment. + /// + /// The issue identifier. + /// The attachment. + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use UpdateAttachment extension instead")] + public void UpdateAttachment(int issueId, Attachment attachment) + { + this.UpdateIssueAttachment(issueId, attachment); + } + + /// + /// + /// + /// query strings. enable to specify multiple values separated by a space " ". + /// number of results in response. + /// skip this number of results in response + /// Optional filters. + /// + /// Returns the search results by the specified condition parameters. + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use Search extension instead")] + public PagedResults Search(string q, int limit = DEFAULT_PAGE_SIZE_VALUE, int offset = 0, SearchFilterBuilder searchFilter = null) + { + return RedmineManagerExtensions.Search(this, q, limit, offset, searchFilter); + } + + /// + /// Creates the Redmine web client. + /// + /// The parameters. + /// if set to true [upload file]. + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "If a custom webClient is needed, use Func from RedmineManagerSettings instead")] + public virtual RedmineWebClient CreateWebClient(NameValueCollection parameters, bool uploadFile = false) + { + throw new NotImplementedException(); + } + + /// + /// This is to take care of SSL certification validation which are not issued by Trusted Root CA. + /// + /// The sender. + /// The cert. + /// The chain. + /// The error. + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use WebClientSettings.ServerCertificateValidationCallback instead")] + public virtual bool RemoteCertValidate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors sslPolicyErrors) + { + const SslPolicyErrors IGNORED_ERRORS = SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNameMismatch; + + return (sslPolicyErrors & ~IGNORED_ERRORS) == SslPolicyErrors.None; + } + } +} \ No newline at end of file From 9c38c4be8922e4aec8f137fc2b00caa0901c9fcc Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 3 Jan 2024 17:51:34 +0200 Subject: [PATCH 319/549] Add Obsolete attribute --- src/redmine-net-api/IRedmineManager.cs | 32 +++++++++++--------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/src/redmine-net-api/IRedmineManager.cs b/src/redmine-net-api/IRedmineManager.cs index 57f8a423..c3519e8b 100644 --- a/src/redmine-net-api/IRedmineManager.cs +++ b/src/redmine-net-api/IRedmineManager.cs @@ -14,6 +14,7 @@ You may obtain a copy of the License at limitations under the License. */ +using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Net; @@ -31,31 +32,31 @@ public interface IRedmineManager /// /// /// - string Host { get; } + [Obsolete(RedmineConstants.OBSOLETE_TEXT)]string Host { get; } /// /// /// - string ApiKey { get; } + [Obsolete(RedmineConstants.OBSOLETE_TEXT)]string ApiKey { get; } /// /// /// - int PageSize { get; set; } + [Obsolete(RedmineConstants.OBSOLETE_TEXT)]int PageSize { get; set; } /// /// /// - string ImpersonateUser { get; set; } + [Obsolete(RedmineConstants.OBSOLETE_TEXT)]string ImpersonateUser { get; set; } /// /// /// - MimeFormat MimeFormat { get; } + [Obsolete(RedmineConstants.OBSOLETE_TEXT)]MimeFormat MimeFormat { get; } /// /// /// - IWebProxy Proxy { get; } + [Obsolete(RedmineConstants.OBSOLETE_TEXT)]IWebProxy Proxy { get; } /// /// /// - SecurityProtocolType SecurityProtocolType { get; } + [Obsolete(RedmineConstants.OBSOLETE_TEXT)]SecurityProtocolType SecurityProtocolType { get; } /// /// @@ -184,7 +185,7 @@ PagedResults Search(string q, int limit , int offset = 0, /// /// /// - int Count(NameValueCollection parameters) where T : class, new(); + int Count(NameValueCollection parameters = null) where T : class, new(); /// /// @@ -235,13 +236,6 @@ PagedResults Search(string q, int limit , int offset = 0, /// T CreateObject(T entity, string ownerId) where T : class, new(); - /// - /// - /// - /// - /// - /// - void UpdateObject(string id, T entity) where T : class, new(); /// /// /// @@ -249,7 +243,7 @@ PagedResults Search(string q, int limit , int offset = 0, /// /// /// - void UpdateObject(string id, T entity, string projectId) where T : class, new(); + void UpdateObject(string id, T entity, string projectId = null) where T : class, new(); /// /// @@ -257,7 +251,7 @@ PagedResults Search(string q, int limit , int offset = 0, /// /// /// - void DeleteObject(string id, NameValueCollection parameters) where T : class, new(); + void DeleteObject(string id, NameValueCollection parameters = null) where T : class, new(); /// /// @@ -265,7 +259,7 @@ PagedResults Search(string q, int limit , int offset = 0, /// /// /// - RedmineWebClient CreateWebClient(NameValueCollection parameters, bool uploadFile = false); + [Obsolete(RedmineConstants.OBSOLETE_TEXT)]RedmineWebClient CreateWebClient(NameValueCollection parameters, bool uploadFile = false); /// /// /// @@ -274,6 +268,6 @@ PagedResults Search(string q, int limit , int offset = 0, /// /// /// - bool RemoteCertValidate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors sslPolicyErrors); + [Obsolete(RedmineConstants.OBSOLETE_TEXT)]bool RemoteCertValidate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors sslPolicyErrors); } } \ No newline at end of file From 3798353af90c71c93dcff2393d14e38113a29ff9 Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 3 Jan 2024 17:52:57 +0200 Subject: [PATCH 320/549] [RedmineManager] Remove old code --- src/redmine-net-api/RedmineManager.cs | 895 ++++++-------------------- 1 file changed, 180 insertions(+), 715 deletions(-) diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index 4b732745..cac2c338 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -23,9 +23,12 @@ limitations under the License. using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.RegularExpressions; +using Redmine.Net.Api.Authentication; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Net; +using Redmine.Net.Api.Net.WebClient; using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Types; using Group = Redmine.Net.Api.Types.Group; @@ -36,242 +39,58 @@ namespace Redmine.Net.Api /// /// The main class to access Redmine API. /// - public class RedmineManager : IRedmineManager + public partial class RedmineManager : IRedmineManager { - /// - /// - public const int DEFAULT_PAGE_SIZE_VALUE = 25; - - private static readonly Dictionary Routes = new Dictionary - { - {typeof(Issue), "issues"}, - {typeof(Project), "projects"}, - {typeof(User), "users"}, - {typeof(News), "news"}, - {typeof(Query), "queries"}, - {typeof(Version), "versions"}, - {typeof(Attachment), "attachments"}, - {typeof(IssueRelation), "relations"}, - {typeof(TimeEntry), "time_entries"}, - {typeof(IssueStatus), "issue_statuses"}, - {typeof(Tracker), "trackers"}, - {typeof(IssueCategory), "issue_categories"}, - {typeof(Role), "roles"}, - {typeof(ProjectMembership), "memberships"}, - {typeof(Group), "groups"}, - {typeof(TimeEntryActivity), "enumerations/time_entry_activities"}, - {typeof(IssuePriority), "enumerations/issue_priorities"}, - {typeof(Watcher), "watchers"}, - {typeof(IssueCustomField), "custom_fields"}, - {typeof(CustomField), "custom_fields"}, - {typeof(Search), "search"}, - {typeof(Journal), "journals"} - }; - - /// - /// - /// - public static readonly Dictionary TypesWithOffset = new Dictionary{ - {typeof(Issue), true}, - {typeof(Project), true}, - {typeof(User), true}, - {typeof(News), true}, - {typeof(Query), true}, - {typeof(TimeEntry), true}, - {typeof(ProjectMembership), true}, - {typeof(Search), true} - }; - - private readonly string basicAuthorization; - private readonly CredentialCache cache; - private string host; + private readonly RedmineManagerOptions _redmineManagerOptions; internal IRedmineSerializer Serializer { get; } - + internal RedmineApiUrls RedmineApiUrls { get; } + internal IRedmineApiClient ApiClient { get; } + /// - /// Initializes a new instance of the class. + /// /// - /// The host. - /// The MIME format. - /// if set to true [verify server cert]. - /// The proxy. - /// Use this parameter to specify a SecurityProtcolType. Note: it is recommended to leave this parameter at its default value as this setting also affects the calling application process. - /// http or https. Default is https. - /// The webclient timeout. Default is 100 seconds. - /// - /// Host is not defined! - /// or - /// The host is not valid! - /// - public RedmineManager(string host, MimeFormat mimeFormat = MimeFormat.Xml, bool verifyServerCert = true, - IWebProxy proxy = null, SecurityProtocolType securityProtocolType = default, string scheme = "https", TimeSpan? timeout = null) + /// + /// + public RedmineManager(RedmineManagerOptionsBuilder optionsBuilder) { - if (host.IsNullOrWhiteSpace()) + #if NET5_0_OR_GREATER + ArgumentNullException.ThrowIfNull(optionsBuilder); + #else + if (optionsBuilder == null) { - throw new RedmineException("Host is not defined!"); + throw new ArgumentNullException(nameof(optionsBuilder)); } - - PageSize = 25; - Scheme = scheme; - Host = host; - MimeFormat = mimeFormat; - Timeout = timeout; - Proxy = proxy; - - if (mimeFormat == MimeFormat.Xml) - { - Format = "xml"; - Serializer = new XmlRedmineSerializer(); - } - else - { - Format = "json"; - Serializer = new JsonRedmineSerializer(); - } - - if (securityProtocolType == default) + #endif + _redmineManagerOptions = optionsBuilder.Build(); + if (_redmineManagerOptions.VerifyServerCert) { - securityProtocolType = ServicePointManager.SecurityProtocol; + _redmineManagerOptions.ClientOptions.ServerCertificateValidationCallback = RemoteCertValidate; } - SecurityProtocolType = securityProtocolType; - - ServicePointManager.SecurityProtocol = securityProtocolType; - - if (!verifyServerCert) + Serializer = _redmineManagerOptions.Serializer; + + Host = _redmineManagerOptions.BaseAddress.ToString(); + PageSize = _redmineManagerOptions.PageSize; + Format = Serializer.Format; + Scheme = _redmineManagerOptions.BaseAddress.Scheme; + Proxy = _redmineManagerOptions.ClientOptions.Proxy; + Timeout = _redmineManagerOptions.ClientOptions.Timeout; + MimeFormat = "xml".Equals(Serializer.Format, StringComparison.OrdinalIgnoreCase) ? MimeFormat.Xml : MimeFormat.Json; + + _redmineManagerOptions.ClientOptions.SecurityProtocolType ??= ServicePointManager.SecurityProtocol; + + SecurityProtocolType = _redmineManagerOptions.ClientOptions.SecurityProtocolType.Value; + + if (_redmineManagerOptions.Authentication is RedmineApiKeyAuthentication) { - ServicePointManager.ServerCertificateValidationCallback += RemoteCertValidate; + ApiKey = _redmineManagerOptions.Authentication.Token; } + + RedmineApiUrls = new RedmineApiUrls(Serializer.Format); + ApiClient = new RedmineApiClient(_redmineManagerOptions); } - - /// - /// Initializes a new instance of the class. - /// Most of the time, the API requires authentication. To enable the API-style authentication, you have to check Enable - /// REST API in Administration -> Settings -> Authentication. Then, authentication can be done in 2 different - /// ways: - /// using your regular login/password via HTTP Basic authentication. - /// using your API key which is a handy way to avoid putting a password in a script. The API key may be attached to - /// each request in one of the following way: - /// passed in as a "key" parameter - /// passed in as a username with a random password via HTTP Basic authentication - /// passed in as a "X-Redmine-API-Key" HTTP header (added in Redmine 1.1.0) - /// You can find your API key on your account page ( /my/account ) when logged in, on the right-hand pane of the - /// default layout. - /// - /// The host. - /// The API key. - /// The MIME format. - /// if set to true [verify server cert]. - /// The proxy. - /// Use this parameter to specify a SecurityProtcolType. Note: it is recommended to leave this parameter at its default value as this setting also affects the calling application process. - /// - /// The webclient timeout. Default is 100 seconds. - public RedmineManager(string host, string apiKey, MimeFormat mimeFormat = MimeFormat.Xml, - bool verifyServerCert = true, IWebProxy proxy = null, - SecurityProtocolType securityProtocolType = default, string scheme = "https", TimeSpan? timeout = null) - : this(host, mimeFormat, verifyServerCert, proxy, securityProtocolType, scheme, timeout: timeout) - { - ApiKey = apiKey; - } - - /// - /// Initializes a new instance of the class. - /// Most of the time, the API requires authentication. To enable the API-style authentication, you have to check Enable - /// REST API in Administration -> Settings -> Authentication. Then, authentication can be done in 2 different - /// ways: - /// using your regular login/password via HTTP Basic authentication. - /// using your API key which is a handy way to avoid putting a password in a script. The API key may be attached to - /// each request in one of the following way: - /// passed in as a "key" parameter - /// passed in as a username with a random password via HTTP Basic authentication - /// passed in as a "X-Redmine-API-Key" HTTP header (added in Redmine 1.1.0) - /// You can find your API key on your account page ( /my/account ) when logged in, on the right-hand pane of the - /// default layout. - /// - /// The host. - /// The login. - /// The password. - /// The MIME format. - /// if set to true [verify server cert]. - /// The proxy. - /// Use this parameter to specify a SecurityProtcolType. Note: it is recommended to leave this parameter at its default value as this setting also affects the calling application process. - /// - /// The webclient timeout. Default is 100 seconds. - public RedmineManager(string host, string login, string password, MimeFormat mimeFormat = MimeFormat.Xml, - bool verifyServerCert = true, IWebProxy proxy = null, - SecurityProtocolType securityProtocolType = default, string scheme = "https", TimeSpan? timeout = null) - : this(host, mimeFormat, verifyServerCert, proxy, securityProtocolType, scheme, timeout: timeout) - - { - cache = new CredentialCache { { new Uri(host), "Basic", new NetworkCredential(login, password) } }; - - var token = Convert.ToBase64String(Encoding.UTF8.GetBytes(string.Format(CultureInfo.InvariantCulture, "{0}:{1}", login, password))); - basicAuthorization = string.Format(CultureInfo.InvariantCulture, "Basic {0}", token); - } - - /// - /// Gets the suffixes. - /// - /// - /// The suffixes. - /// - public static Dictionary Suffixes => Routes; - - /// - /// - /// - public string Format { get; } - - /// - /// - /// - public string Scheme { get; private set; } - /// - /// - /// - public TimeSpan? Timeout { get; private set; } - - /// - /// Gets the host. - /// - /// - /// The host. - /// - public string Host - { - get => host; - private set - { - host = value; - - if (Uri.TryCreate(host, UriKind.Absolute, out Uri uriResult) && - (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps)) - { - if (Scheme.IsNullOrWhiteSpace()) - { - Scheme = uriResult.Scheme; - } - - return; - } - - host = $"{Scheme ?? "https"}://{host}"; - - if (!Uri.TryCreate(host, UriKind.Absolute, out uriResult)) throw new RedmineException("The host is not valid!"); - - Scheme = uriResult.Scheme; - } - } - - /// - /// The ApiKey used to authenticate. - /// - /// - /// The API key. - /// - public string ApiKey { get; } - /// /// Maximum page-size when retrieving complete object lists /// @@ -284,7 +103,7 @@ private set /// The size of the page. /// public int PageSize { get; set; } - + /// /// As of Redmine 2.2.0 you can impersonate user setting user login (eg. jsmith). This only works when using the API /// with an administrator account, this header will be ignored when using the API with a regular user account. @@ -294,191 +113,19 @@ private set /// public string ImpersonateUser { get; set; } - /// - /// Gets the MIME format. - /// - /// - /// The MIME format. - /// - public MimeFormat MimeFormat { get; } - - /// - /// Gets the proxy. - /// - /// - /// The proxy. - /// - public IWebProxy Proxy { get; } - - /// - /// Gets the type of the security protocol. - /// - /// - /// The type of the security protocol. - /// - public SecurityProtocolType SecurityProtocolType { get; } - - /// - /// Returns the user whose credentials are used to access the API. - /// - /// The accepted parameters are: memberships and groups (added in 2.1). - /// - /// - /// An error occurred during deserialization. The original exception is available - /// using the System.Exception.InnerException property. - /// - public User GetCurrentUser(NameValueCollection parameters = null) - { - var url = UrlHelper.GetCurrentUserUrl(this); - return WebApiHelper.ExecuteDownload(this, url, parameters); - } - - /// - /// - /// - /// Returns the my account details. - public MyAccount GetMyAccount() - { - var url = UrlHelper.GetMyAccountUrl(this); - return WebApiHelper.ExecuteDownload(this, url); - } - - /// - /// Adds the watcher to issue. - /// - /// The issue identifier. - /// The user identifier. - public void AddWatcherToIssue(int issueId, int userId) - { - var url = UrlHelper.GetAddWatcherUrl(this, issueId); - WebApiHelper.ExecuteUpload(this, url, HttpVerbs.POST, SerializationHelper.SerializeUserId(userId, MimeFormat)); - } - - /// - /// Removes the watcher from issue. - /// - /// The issue identifier. - /// The user identifier. - public void RemoveWatcherFromIssue(int issueId, int userId) - { - var url = UrlHelper.GetRemoveWatcherUrl(this, issueId, userId); - WebApiHelper.ExecuteUpload(this, url, HttpVerbs.DELETE, string.Empty); - } - - /// - /// Adds an existing user to a group. - /// - /// The group id. - /// The user id. - public void AddUserToGroup(int groupId, int userId) - { - var url = UrlHelper.GetAddUserToGroupUrl(this, groupId); - WebApiHelper.ExecuteUpload(this, url, HttpVerbs.POST, SerializationHelper.SerializeUserId(userId, MimeFormat)); - } - - /// - /// Removes an user from a group. - /// - /// The group id. - /// The user id. - public void RemoveUserFromGroup(int groupId, int userId) - { - var url = UrlHelper.GetRemoveUserFromGroupUrl(this, groupId, userId); - WebApiHelper.ExecuteUpload(this, url, HttpVerbs.DELETE, string.Empty); - } - - /// - /// Creates or updates a wiki page. - /// - /// The project id or identifier. - /// The wiki page name. - /// The wiki page to create or update. - /// - public void UpdateWikiPage(string projectId, string pageName, WikiPage wikiPage) - { - var result = Serializer.Serialize(wikiPage); - - if (string.IsNullOrEmpty(result)) - { - return; - } - - var url = UrlHelper.GetWikiCreateOrUpdaterUrl(this, projectId, pageName); - - url = Uri.EscapeUriString(url); - - WebApiHelper.ExecuteUpload(this, url, HttpVerbs.PUT, result); - } - /// /// /// - /// - /// - /// - /// - public WikiPage CreateWikiPage(string projectId, string pageName, WikiPage wikiPage) - { - var result = Serializer.Serialize(wikiPage); - - if (string.IsNullOrEmpty(result)) - { - return null; - } - - var url = UrlHelper.GetWikiCreateOrUpdaterUrl(this, projectId, pageName); - - url = Uri.EscapeUriString(url); - - return WebApiHelper.ExecuteUpload(this, url, HttpVerbs.PUT, result); - } - - /// - /// Gets the wiki page. - /// - /// The project identifier. - /// The parameters. - /// Name of the page. - /// The version. - /// - public WikiPage GetWikiPage(string projectId, NameValueCollection parameters, string pageName, uint version = 0) - { - var url = UrlHelper.GetWikiPageUrl(this, projectId, pageName, version); - - url = Uri.EscapeUriString(url); - - return WebApiHelper.ExecuteDownload(this, url, parameters); - } - - /// - /// Returns the list of all pages in a project wiki. - /// - /// The project id or identifier. + /// + /// /// - public List GetAllWikiPages(string projectId) - { - var url = UrlHelper.GetWikisUrl(this, projectId); - - var result = WebApiHelper.ExecuteDownloadList(this, url); - - return result == null ? null : new List(result.Items); - } - - /// - /// Deletes a wiki page, its attachments and its history. If the deleted page is a parent page, its child pages are not - /// deleted but changed as root pages. - /// - /// The project id or identifier. - /// The wiki page name. - public void DeleteWikiPage(string projectId, string pageName) + public int Count(params string[] include) where T : class, new() { - var url = UrlHelper.GetDeleteWikiUrl(this, projectId, pageName); + var parameters = NameValueCollectionExtensions.AddParamsIfExist(null, include); - url = Uri.EscapeUriString(url); - - WebApiHelper.ExecuteUpload(this, url, HttpVerbs.DELETE, string.Empty); + return Count(parameters); } - + /// /// /// @@ -487,28 +134,17 @@ public void DeleteWikiPage(string projectId, string pageName) /// public int Count(NameValueCollection parameters) where T : class, new() { - int totalCount = 0, pageSize = 1, offset = 0; - - if (parameters == null) - { - parameters = new NameValueCollection(); - } - - parameters.Set(RedmineKeys.LIMIT, pageSize.ToString(CultureInfo.InvariantCulture)); - parameters.Set(RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)); + var totalCount = 0; + const int PAGE_SIZE = 1; + const int OFFSET = 0; - try - { - var tempResult = GetPaginatedObjects(parameters); + parameters.AddPagingParameters(PAGE_SIZE, OFFSET); + + var tempResult = GetPaginatedObjects(parameters); - if (tempResult != null) - { - totalCount = tempResult.TotalItems; - } - } - catch (WebException wex) + if (tempResult != null) { - wex.HandleWebException(Serializer); + totalCount = tempResult.TotalItems; } return totalCount; @@ -523,87 +159,48 @@ public void DeleteWikiPage(string projectId, string pageName) /// /// Returns the object of type T. /// - /// - /// An error occurred during deserialization. The original exception is available - /// using the System.Exception.InnerException property. - /// /// /// /// string issueId = "927"; /// NameValueCollection parameters = null; /// Issue issue = redmineManager.GetObject<Issue>(issueId, parameters); - /// + /// /// public T GetObject(string id, NameValueCollection parameters) where T : class, new() { - var url = UrlHelper.GetGetUrl(this, id); - return WebApiHelper.ExecuteDownload(this, url, parameters); - } - - /// - /// Gets the paginated objects. - /// - /// - /// The parameters. - /// - public PagedResults GetPaginatedObjects(NameValueCollection parameters) where T : class, new() - { - var url = UrlHelper.GetListUrl(this, parameters); - return WebApiHelper.ExecuteDownloadList(this, url, parameters); - } - - /// - /// - /// - /// - /// - /// - public int Count(params string[] include) where T : class, new() - { - var parameters = new NameValueCollection(); - - if (include != null && include.Length > 0) - { - parameters.Add(RedmineKeys.INCLUDE, string.Join(",", include)); - } + var url = RedmineApiUrls.GetFragment(id); - return Count(parameters); + var response = ApiClient.Get(url, parameters != null ? new RequestOptions { QueryString = parameters } : null); + + return response.DeserializeTo(Serializer); } /// /// Returns the complete list of objects. /// /// - /// The page size. - /// The offset. /// Optional fetched data. /// /// Optional fetched data: - /// Project: trackers, issue_categories, enabled_modules (since 2.6.0) - /// Issue: children, attachments, relations, changesets, journals, watchers - Since 2.3.0 - /// Users: memberships, groups (added in 2.1) + /// Project: trackers, issue_categories, enabled_modules (since Redmine 2.6.0) + /// Issue: children, attachments, relations, changesets, journals, watchers (since Redmine 2.3.0) + /// Users: memberships, groups (since Redmine 2.1) /// Groups: users, memberships /// /// Returns the complete list of objects. - public List GetObjects(int limit, int offset, params string[] include) where T : class, new() + public List GetObjects(params string[] include) where T : class, new() { - var parameters = new NameValueCollection(); - - parameters.Add(RedmineKeys.LIMIT, limit.ToString(CultureInfo.InvariantCulture)); - parameters.Add(RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)); - - if (include != null && include.Length > 0) - { - parameters.Add(RedmineKeys.INCLUDE, string.Join(",", include)); - } + var parameters = NameValueCollectionExtensions.AddParamsIfExist(null, include); return GetObjects(parameters); } - + /// /// Returns the complete list of objects. /// /// + /// The page size. + /// The offset. /// Optional fetched data. /// /// Optional fetched data: @@ -613,14 +210,11 @@ public void DeleteWikiPage(string projectId, string pageName) /// Groups: users, memberships /// /// Returns the complete list of objects. - public List GetObjects(params string[] include) where T : class, new() + public List GetObjects(int limit, int offset, params string[] include) where T : class, new() { - var parameters = new NameValueCollection(); - - if (include != null && include.Length > 0) - { - parameters.Add(RedmineKeys.INCLUDE, string.Join(",", include)); - } + var parameters = NameValueCollectionExtensions + .AddParamsIfExist(null, include) + .AddPagingParameters(limit, offset); return GetObjects(parameters); } @@ -633,100 +227,134 @@ public void DeleteWikiPage(string projectId, string pageName) /// /// Returns a complete list of objects. /// - public List GetObjects(NameValueCollection parameters) where T : class, new() + public List GetObjects(NameValueCollection parameters = null) where T : class, new() + { + var uri = RedmineApiUrls.GetListFragment(); + + return GetObjects(uri, parameters != null ? new RequestOptions { QueryString = parameters } : null); + } + + /// + /// + /// + /// + /// + /// + /// + internal List GetObjects(string uri, RequestOptions requestOptions = null) where T : class, new() { int pageSize = 0, offset = 0; var isLimitSet = false; List resultList = null; - if (parameters == null) + requestOptions ??= new RequestOptions(); + + if (requestOptions.QueryString == null) { - parameters = new NameValueCollection(); + requestOptions.QueryString = new NameValueCollection(); } else { - isLimitSet = int.TryParse(parameters[RedmineKeys.LIMIT], out pageSize); - int.TryParse(parameters[RedmineKeys.OFFSET], out offset); + isLimitSet = int.TryParse(requestOptions.QueryString[RedmineKeys.LIMIT], out pageSize); + int.TryParse(requestOptions.QueryString[RedmineKeys.OFFSET], out offset); } + if (pageSize == default) { - pageSize = PageSize > 0 ? PageSize : DEFAULT_PAGE_SIZE_VALUE; - parameters.Set(RedmineKeys.LIMIT, pageSize.ToString(CultureInfo.InvariantCulture)); + pageSize = _redmineManagerOptions.PageSize > 0 ? _redmineManagerOptions.PageSize : RedmineConstants.DEFAULT_PAGE_SIZE_VALUE; + requestOptions.QueryString.Set(RedmineKeys.LIMIT, pageSize.ToString(CultureInfo.InvariantCulture)); } - - try + + var hasOffset = TypesWithOffset.ContainsKey(typeof(T)); + if (hasOffset) { - var hasOffset = TypesWithOffset.ContainsKey(typeof(T)); - if (hasOffset) + int totalCount; + do { - var totalCount = 0; - do - { - parameters.Set(RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)); + requestOptions.QueryString.Set(RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)); - var tempResult = GetPaginatedObjects(parameters); + var tempResult = GetPaginatedObjects(uri, requestOptions); - totalCount = isLimitSet ? pageSize : tempResult.TotalItems; + totalCount = isLimitSet ? pageSize : tempResult.TotalItems; - if (tempResult?.Items != null) + if (tempResult?.Items != null) + { + if (resultList == null) { - if (resultList == null) - { - resultList = new List(tempResult.Items); - } - else - { - resultList.AddRange(tempResult.Items); - } + resultList = new List(tempResult.Items); + } + else + { + resultList.AddRange(tempResult.Items); } - - offset += pageSize; - } - while (offset < totalCount); - } - else - { - var result = GetPaginatedObjects(parameters); - if (result?.Items != null) - { - return new List(result.Items); } + + offset += pageSize; } + while (offset < totalCount); } - catch (WebException wex) + else { - wex.HandleWebException(Serializer); + var result = GetPaginatedObjects(uri, requestOptions); + if (result?.Items != null) + { + return new List(result.Items); + } } + return resultList; } + + /// + /// Gets the paginated objects. + /// + /// + /// The parameters. + /// + public PagedResults GetPaginatedObjects(NameValueCollection parameters) where T : class, new() + { + var url = RedmineApiUrls.GetListFragment(); + + return GetPaginatedObjects(url, parameters != null ? new RequestOptions { QueryString = parameters } : null); + } + + /// + /// + /// + /// + /// + /// + /// + internal PagedResults GetPaginatedObjects(string uri = null, RequestOptions requestOptions = null) where T : class, new() + { + uri = uri.IsNullOrWhiteSpace() ? RedmineApiUrls.GetListFragment() : uri; + + var response= ApiClient.Get(uri, requestOptions); + + return response.DeserializeToPagedResults(Serializer); + } /// /// Creates a new Redmine object. /// /// The type of object to create. - /// The object to create. + /// The object to create. /// /// - /// - /// - /// - /// - /// - /// /// /// When trying to create an object with invalid or missing attribute parameters, you will get a 422 Unprocessable /// Entity response. That means that the object could not be created. /// - public T CreateObject(T obj) where T : class, new() + public T CreateObject(T entity) where T : class, new() { - return CreateObject(obj, null); + return CreateObject(entity, null); } /// /// Creates a new Redmine object. /// /// The type of object to create. - /// The object to create. + /// The object to create. /// The owner identifier. /// /// @@ -743,13 +371,15 @@ public void DeleteWikiPage(string projectId, string pageName) /// redmineManager.CreateObject(project); /// /// - public T CreateObject(T obj, string ownerId) where T : class, new() + public T CreateObject(T entity, string ownerId) where T : class, new() { - var url = UrlHelper.GetCreateUrl(this, ownerId); + var url = RedmineApiUrls.CreateEntityFragment(ownerId); - var data = Serializer.Serialize(obj); + var payload = Serializer.Serialize(entity); + + var response = ApiClient.Create(url, payload); - return WebApiHelper.ExecuteUpload(this, url, HttpVerbs.POST, data); + return response.DeserializeTo(Serializer); } /// @@ -757,40 +387,22 @@ public void DeleteWikiPage(string projectId, string pageName) /// /// The type of object to be update. /// The id of the object to be update. - /// The object to be update. - /// - /// - /// When trying to update an object with invalid or missing attribute parameters, you will get a 422(RedmineException) - /// Unprocessable Entity response. That means that the object could not be updated. - /// - /// - public void UpdateObject(string id, T obj) where T : class, new() - { - UpdateObject(id, obj, null); - } - - /// - /// Updates a Redmine object. - /// - /// The type of object to be update. - /// The id of the object to be update. - /// The object to be update. + /// The object to be update. /// The project identifier. /// /// /// When trying to update an object with invalid or missing attribute parameters, you will get a /// 422(RedmineException) Unprocessable Entity response. That means that the object could not be updated. /// - /// - public void UpdateObject(string id, T obj, string projectId) where T : class, new() + /// + /// + public void UpdateObject(string id, T entity, string projectId = null) where T : class, new() { - var url = UrlHelper.GetUploadUrl(this, id); - - var data = Serializer.Serialize(obj); - - data = Regex.Replace(data, @"\r\n|\r|\n", "\r\n"); + var url = RedmineApiUrls.UpdateFragment(id); - WebApiHelper.ExecuteUpload(this, url, HttpVerbs.PUT, data); + var payload = Serializer.Serialize(entity); + + ApiClient.Update(url, payload); } /// @@ -803,8 +415,9 @@ public void DeleteWikiPage(string projectId, string pageName) /// public void DeleteObject(string id, NameValueCollection parameters = null) where T : class, new() { - var url = UrlHelper.GetDeleteUrl(this, id); - WebApiHelper.ExecuteUpload(this, url, HttpVerbs.DELETE, string.Empty, parameters); + var url = RedmineApiUrls.DeleteFragment(id); + + ApiClient.Delete(url, parameters != null ? new RequestOptions { QueryString = parameters } : null); } /// @@ -816,173 +429,25 @@ public void DeleteWikiPage(string projectId, string pageName) /// Returns the token for uploaded file. /// /// - /// - /// - /// - /// - /// - /// public Upload UploadFile(byte[] data) { - var url = UrlHelper.GetUploadFileUrl(this); - return WebApiHelper.ExecuteUploadFile(this, url, data); - } - - /// - /// Updates the attachment. - /// - /// The issue identifier. - /// The attachment. - public void UpdateAttachment(int issueId, Attachment attachment) - { - var address = UrlHelper.GetAttachmentUpdateUrl(this, issueId); - - var attachments = new Attachments { { attachment.Id, attachment } }; + var url = RedmineApiUrls.UploadFragment(); - var data = Serializer.Serialize(attachments); - - WebApiHelper.ExecuteUpload(this, address, HttpVerbs.PATCH, data); + var response = ApiClient.Upload(url, data); + + return response.DeserializeTo(Serializer); } /// - /// Downloads the file. + /// Downloads a file from the specified address. /// /// The address. - /// + /// The content of the downloaded file as a byte array. /// - /// - /// - /// - /// - /// - /// - /// public byte[] DownloadFile(string address) { - return WebApiHelper.ExecuteDownloadFile(this, address); - } - - - /// - /// - /// - /// query strings. enable to specify multiple values separated by a space " ". - /// number of results in response. - /// skip this number of results in response - /// Optional filters. - /// - /// Returns the search results by the specified condition parameters. - /// - /// - public PagedResults Search(string q, int limit = DEFAULT_PAGE_SIZE_VALUE, int offset = 0, SearchFilterBuilder searchFilter = null) - { - if (q.IsNullOrWhiteSpace()) - { - throw new ArgumentNullException(nameof(q)); - } - - var parameters = new NameValueCollection - { - {RedmineKeys.Q, q}, - {RedmineKeys.LIMIT, limit.ToString(CultureInfo.InvariantCulture)}, - {RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)}, - }; - - if (searchFilter != null) - { - parameters = searchFilter.Build(parameters); - } - - var result = GetPaginatedObjects(parameters); - - return result; - } - - private const string UA = "Mozilla/5.0 (Windows NT 6.1) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.835.163 Safari/535.1"; - - /// - /// Creates the Redmine web client. - /// - /// The parameters. - /// if set to true [upload file]. - /// - /// - public virtual RedmineWebClient CreateWebClient(NameValueCollection parameters, bool uploadFile = false) - { - var webClient = new RedmineWebClient { Scheme = Scheme, RedmineSerializer = Serializer}; - webClient.UserAgent = UA; - webClient.Timeout = Timeout; - if (!uploadFile) - { - webClient.Headers.Add(HttpRequestHeader.ContentType, - MimeFormat is MimeFormat.Xml ? "application/xml" : "application/json"); - webClient.Encoding = Encoding.UTF8; - } - else - { - webClient.Headers.Add(HttpRequestHeader.ContentType, "application/octet-stream"); - } - - if (parameters != null) - { - webClient.QueryString = parameters; - } - - if (!string.IsNullOrEmpty(ApiKey)) - { - webClient.QueryString[RedmineKeys.KEY] = ApiKey; - } - else - { - if (cache != null) - { - webClient.PreAuthenticate = true; - webClient.Credentials = cache; - webClient.Headers[HttpRequestHeader.Authorization] = basicAuthorization; - } - else - { - webClient.UseDefaultCredentials = true; - webClient.Credentials = CredentialCache.DefaultCredentials; - } - } - - if (Proxy != null) - { - Proxy.Credentials = cache; - webClient.Proxy = Proxy; - webClient.UseProxy = true; - } - - if (!string.IsNullOrEmpty(ImpersonateUser)) - { - webClient.Headers.Add("X-Redmine-Switch-User", ImpersonateUser); - } - - return webClient; - } - - /// - /// This is to take care of SSL certification validation which are not issued by Trusted Root CA. - /// - /// The sender. - /// The cert. - /// The chain. - /// The error. - /// - /// - public virtual bool RemoteCertValidate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors sslPolicyErrors) - { - const SslPolicyErrors ignoredErrors = - SslPolicyErrors.RemoteCertificateChainErrors | - SslPolicyErrors.RemoteCertificateNameMismatch; - - if ((sslPolicyErrors & ~ignoredErrors) == SslPolicyErrors.None) - { - return true; - } - - return false; + var response = ApiClient.Download(address); + return response.Content; } } } \ No newline at end of file From 0789fb2dba954d67fe85c3f918ccd497f43c45ae Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 3 Jan 2024 17:53:16 +0200 Subject: [PATCH 321/549] [New] Directory.Build.props --- Directory.Build.props | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index d0f44311..2f24981b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,29 +2,6 @@ - - Adrian Popescu - Redmine Api is a .NET rest client for Redmine. - p.adi - Adrian Popescu, 2011 - $([System.DateTime]::Now.Year.ToString()) - en-US - - redmine-api - redmine-api-signed - https://raw.githubusercontent.com/zapadi/redmine-net-api/master/logo.png - logo.png - LICENSE - Apache-2.0 - https://github.com/zapadi/redmine-net-api - true - Redmine; REST; API; Client; .NET; Adrian Popescu; - Redmine .NET API Client - git - https://github.com/zapadi/redmine-net-api - ... - Redmine .NET API Client - - 11 strict From f164975de99c73d52859b73125306a4a251f57e5 Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 3 Jan 2024 17:54:10 +0200 Subject: [PATCH 322/549] [Csproj] Update --- src/redmine-net-api/redmine-net-api.csproj | 35 ++++++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index d6eff592..7bd68c94 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -6,7 +6,7 @@ redmine-net-api net20;net40;net45;net451;net452;net46;net461;net462;net47;net471;net472;net48;net481;net60;net70; false - False + True true TRACE Debug;Release;DebugJson @@ -38,6 +38,28 @@ + + Adrian Popescu + Redmine Api is a .NET rest client for Redmine. + p.adi + Adrian Popescu, 2011 - $([System.DateTime]::Now.Year.ToString()) + en-US + redmine-api + redmine-api-signed + https://raw.githubusercontent.com/zapadi/redmine-net-api/master/logo.png + logo.png + LICENSE + Apache-2.0 + https://github.com/zapadi/redmine-net-api + README.md + true + Redmine; REST; API; Client; .NET; Adrian Popescu; + Redmine .NET API Client + git + https://github.com/zapadi/redmine-net-api + Redmine .NET API Client + + @@ -59,10 +81,17 @@ + - - + + + <_Parameter1>Padi.DotNet.RedmineAPI.Tests + + + + + \ No newline at end of file From 2bfe7ed796de7342556330fdf2cf5e401ec7b714 Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 3 Jan 2024 17:55:05 +0200 Subject: [PATCH 323/549] [RedmineManagerOptionsBuilder] Add Clienttype --- .../RedmineManagerOptionsBuilder.cs | 224 +++++++++++++++--- 1 file changed, 196 insertions(+), 28 deletions(-) diff --git a/src/redmine-net-api/RedmineManagerOptionsBuilder.cs b/src/redmine-net-api/RedmineManagerOptionsBuilder.cs index e4e1357e..56a2d195 100644 --- a/src/redmine-net-api/RedmineManagerOptionsBuilder.cs +++ b/src/redmine-net-api/RedmineManagerOptionsBuilder.cs @@ -1,6 +1,10 @@ using System; +using System.Net; using System.Xml.Serialization; using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Net; +using Redmine.Net.Api.Net.WebClient; using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api @@ -10,6 +14,12 @@ namespace Redmine.Net.Api /// public sealed class RedmineManagerOptionsBuilder { + private enum ClientType + { + None, + WebClient, + } + private ClientType _clientType = ClientType.None; /// /// @@ -32,24 +42,26 @@ public RedmineManagerOptionsBuilder WithPageSize(int pageSize) /// /// /// - public RedmineManagerOptionsBuilder WithBaseAddress(string baseAddress) + public RedmineManagerOptionsBuilder WithHost(string baseAddress) { - return WithBaseAddress(new Uri(baseAddress)); + this.Host = baseAddress; + return this; } /// /// /// - public Uri BaseAddress { get; private set; } + public string Host { get; private set; } + /// /// /// - /// + /// /// - public RedmineManagerOptionsBuilder WithBaseAddress(Uri baseAddress) + internal RedmineManagerOptionsBuilder WithSerializationType(MimeFormat mimeFormat) { - this.BaseAddress = baseAddress; + this.SerializationType = mimeFormat == MimeFormat.Xml ? SerializationType.Xml : SerializationType.Json; return this; } @@ -90,8 +102,17 @@ public RedmineManagerOptionsBuilder WithAuthentication(IRedmineAuthentication au /// /// /// - public RedmineManagerOptionsBuilder WithClient(Func clientFunc) + public RedmineManagerOptionsBuilder WithWebClient(Func clientFunc) { + if (clientFunc != null) + { + _clientType = ClientType.WebClient; + } + + if (clientFunc == null && _clientType == ClientType.WebClient) + { + _clientType = ClientType.None; + } this.ClientFunc = clientFunc; return this; } @@ -99,7 +120,7 @@ public RedmineManagerOptionsBuilder WithClient(Func clientFun /// /// /// - public Func ClientFunc { get; private set; } + public Func ClientFunc { get; private set; } /// /// @@ -150,38 +171,185 @@ internal RedmineManagerOptionsBuilder WithVerifyServerCert(bool verifyServerCert /// internal RedmineManagerOptions Build() { - if (Authentication == null) - { - throw new RedmineException("Authentication cannot be null"); - } + ClientOptions ??= new RedmineWebClientOptions(); + + var baseAddress = CreateRedmineUri(Host, ClientOptions.Scheme); var options = new RedmineManagerOptions() { - PageSize = PageSize > 0 ? PageSize : RedmineManager.DEFAULT_PAGE_SIZE_VALUE, + BaseAddress = baseAddress, + PageSize = PageSize > 0 ? PageSize : RedmineConstants.DEFAULT_PAGE_SIZE_VALUE, VerifyServerCert = VerifyServerCert, - Serializer = SerializationType == SerializationType.Xml ? new XmlRedmineSerializer() : new JsonRedmineSerializer(), - Version = Version, - //Authentication = - ClientOptions = ClientOptions, - + Serializer = RedmineSerializerFactory.CreateSerializer(SerializationType), + RedmineVersion = Version, + Authentication = Authentication, + ClientOptions = ClientOptions }; - return options; } - /// - /// - /// - /// - /// - /// - public static bool TryParse(string serviceName, out string parts) + internal static void EnsureDomainNameIsValid(string domainName) { - parts = null; - return false; + if (domainName.IsNullOrWhiteSpace()) + { + throw new RedmineException("Domain name cannot be null or empty."); + } + + if (domainName.Length > 255) + { + throw new RedmineException("Domain name cannot be longer than 255 characters."); + } + + var labels = domainName.Split('.'); + if (labels.Length == 1) + { + throw new RedmineException("Domain name is not valid."); + } + foreach (var label in labels) + { + if (label.IsNullOrWhiteSpace() || label.Length > 63) + { + throw new RedmineException("Domain name must be between 1 and 63 characters."); + } + + if (!char.IsLetterOrDigit(label[0]) || !char.IsLetterOrDigit(label[label.Length - 1])) + { + throw new RedmineException("Domain name starts or ends with a hyphen."); + } + + for (var i = 0; i < label.Length; i++) + { + var c = label[i]; + + if (!char.IsLetterOrDigit(c) && c != '-') + { + throw new RedmineException("Domain name contains an invalid character."); + } + + if (c != '-') + { + continue; + } + + if (i + 1 < label.Length && (c ^ label[i+1]) == 0) + { + throw new RedmineException("Domain name contains consecutive hyphens."); + } + } + } } + internal static Uri CreateRedmineUri(string host, string scheme = null) + { + if (host.IsNullOrWhiteSpace() || host.Equals("string.Empty", StringComparison.OrdinalIgnoreCase)) + { + throw new RedmineException("The host is null or empty."); + } + + if (!Uri.TryCreate(host, UriKind.Absolute, out var uri)) + { + host = host.TrimEnd('/', '\\'); + EnsureDomainNameIsValid(host); + + if (!host.StartsWith(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) || !host.StartsWith(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) + { + host = $"{scheme ?? Uri.UriSchemeHttps}://{host}"; + + if (!Uri.TryCreate(host, UriKind.Absolute, out uri)) + { + throw new RedmineException("The host is not valid."); + } + } + } + + if (!uri.IsWellFormedOriginalString()) + { + throw new RedmineException("The host is not well-formed."); + } + + scheme ??= Uri.UriSchemeHttps; + var hasScheme = false; + if (!uri.Scheme.IsNullOrWhiteSpace()) + { + if (uri.Host.IsNullOrWhiteSpace() && uri.IsAbsoluteUri && !uri.IsFile) + { + if (uri.Scheme.Equals("localhost", StringComparison.OrdinalIgnoreCase)) + { + int port = 0; + var portAsString = uri.AbsolutePath.RemoveTrailingSlash(); + if (!portAsString.IsNullOrWhiteSpace()) + { + int.TryParse(portAsString, out port); + } + + var ub = new UriBuilder(scheme, "localhost", port); + return ub.Uri; + } + } + else + { + if (!IsSchemaHttpOrHttps(uri.Scheme)) + { + throw new RedmineException("Invalid host scheme. Only HTTP and HTTPS are supported."); + } + + hasScheme = true; + } + } + else + { + + if (!IsSchemaHttpOrHttps(scheme)) + { + throw new RedmineException("Invalid host scheme. Only HTTP and HTTPS are supported."); + } + } + + var uriBuilder = new UriBuilder(); + + if (uri.HostNameType == UriHostNameType.IPv6) + { + uriBuilder.Scheme = (hasScheme ? uri.Scheme : scheme ?? Uri.UriSchemeHttps); + uriBuilder.Host = uri.Host; + } + else + { + if (uri.Authority.IsNullOrWhiteSpace()) + { + if (uri.Port == -1) + { + if (int.TryParse(uri.LocalPath, out var port)) + { + uriBuilder.Port = port; + } + } + + uriBuilder.Scheme = scheme ?? Uri.UriSchemeHttps; + uriBuilder.Host = uri.Scheme; + } + else + { + uriBuilder.Scheme = uri.Scheme; + uriBuilder.Port = int.TryParse(uri.LocalPath, out var port) ? port : uri.Port; + uriBuilder.Host = uri.Host; + } + } + + try + { + return uriBuilder.Uri; + } + catch (Exception ex) + { + throw new RedmineException(ex.Message); + } + } + + private static bool IsSchemaHttpOrHttps(string scheme) + { + return scheme == Uri.UriSchemeHttp || scheme == Uri.UriSchemeHttps; + } } } \ No newline at end of file From 27219557a2892da8b53f2ab7280c90f9e3c7547d Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 3 Jan 2024 17:57:24 +0200 Subject: [PATCH 324/549] [RemineManagerExtensions] Add extensions --- .../Extensions/RedmineManagerExtensions.cs | 850 +++++++++++++++++- 1 file changed, 814 insertions(+), 36 deletions(-) diff --git a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs index 987bc928..cdba6637 100644 --- a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs +++ b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs @@ -1,11 +1,16 @@ using System; +using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; +using System.Net; +#if !(NET20) +using System.Threading; +using System.Threading.Tasks; +#endif using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Net; using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Types; -using Version = Redmine.Net.Api.Types.Version; namespace Redmine.Net.Api.Extensions { @@ -14,38 +19,33 @@ namespace Redmine.Net.Api.Extensions /// public static class RedmineManagerExtensions { - /// + /// /// /// /// /// - /// + /// /// - public static PagedResults GetProjectNews(this RedmineManager redmineManager, string projectIdentifier, NameValueCollection nameValueCollection) + public static PagedResults GetProjectNews(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null) { - if (projectIdentifier.IsNullOrWhiteSpace()) - { - throw new RedmineException("Argument 'projectIdentifier' is null"); - } - - return WebApiHelper.ExecuteDownloadList(redmineManager, Uri.EscapeUriString($"{redmineManager.Host}/project/{projectIdentifier}/news.{redmineManager.Format}"), nameValueCollection); + var uri = Uri.EscapeDataString(redmineManager.RedmineApiUrls.ProjectNews(projectIdentifier)); + + var response = redmineManager.GetPaginatedObjects(uri, requestOptions); + + return response; } - + /// /// /// /// /// /// + /// /// /// - public static News AddProjectNews(this RedmineManager redmineManager, string projectIdentifier, News news) + public static News AddProjectNews(this RedmineManager redmineManager, string projectIdentifier, News news, RequestOptions requestOptions = null) { - if (projectIdentifier.IsNullOrWhiteSpace()) - { - throw new RedmineException("Argument 'projectIdentifier' is null"); - } - if (news == null) { throw new RedmineException("Argument news is null"); @@ -53,48 +53,826 @@ public static News AddProjectNews(this RedmineManager redmineManager, string pro if (news.Title.IsNullOrWhiteSpace()) { - throw new RedmineException("Title cannot be blank"); + throw new RedmineException("News title cannot be blank"); } - - var data = redmineManager.Serializer.Serialize(news); - - return WebApiHelper.ExecuteUpload(redmineManager, Uri.EscapeUriString($"{redmineManager.Host}/project/{projectIdentifier}/news.{redmineManager.Format}"), HttpVerbs.POST, data); + + var uri = Uri.EscapeDataString(redmineManager.RedmineApiUrls.ProjectNews(projectIdentifier)); + + var payload = redmineManager.Serializer.Serialize(news); + + var response = redmineManager.ApiClient.Create(uri, payload, requestOptions); + + return response.DeserializeTo(redmineManager.Serializer); } - + + /// + /// + /// + /// + /// + /// + /// + /// + public static PagedResults GetProjectMemberships(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null) + { + var uri = redmineManager.RedmineApiUrls.ProjectMemberships(projectIdentifier); + + var response = redmineManager.GetPaginatedObjects(uri, requestOptions); + + return response; + } + /// /// /// /// /// - /// + /// /// /// - public static PagedResults GetProjectMemberships(this RedmineManager redmineManager, string projectIdentifier, NameValueCollection nameValueCollection) + public static PagedResults GetProjectFiles(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null) { - if (projectIdentifier.IsNullOrWhiteSpace()) + var uri = redmineManager.RedmineApiUrls.ProjectFilesFragment(projectIdentifier); + + var response = redmineManager.GetPaginatedObjects(uri, requestOptions); + + return response; + } + + /// + /// Returns the user whose credentials are used to access the API. + /// + /// + /// + /// + public static User GetCurrentUser(this RedmineManager redmineManager, RequestOptions requestOptions = null) + { + var uri = redmineManager.RedmineApiUrls.CurrentUser(); + + var response = redmineManager.ApiClient.Get(uri, requestOptions); + + return response.DeserializeTo(redmineManager.Serializer); + } + + /// + /// + /// + /// Returns the my account details. + public static MyAccount GetMyAccount(this RedmineManager redmineManager, RequestOptions requestOptions = null) + { + var uri = redmineManager.RedmineApiUrls.MyAccount(); + + var response = redmineManager.ApiClient.Get(uri, requestOptions); + + return response.DeserializeTo(redmineManager.Serializer); + } + + /// + /// Adds the watcher to issue. + /// + /// + /// The issue identifier. + /// The user identifier. + /// + public static void AddWatcherToIssue(this RedmineManager redmineManager, int issueId, int userId, RequestOptions requestOptions = null) + { + var uri = redmineManager.RedmineApiUrls.IssueWatcherAdd(issueId.ToString(CultureInfo.InvariantCulture)); + + var payload = SerializationHelper.SerializeUserId(userId, redmineManager.MimeFormat); + + redmineManager.ApiClient.Create(uri, payload, requestOptions); + } + + /// + /// Removes the watcher from issue. + /// + /// + /// The issue identifier. + /// The user identifier. + /// + public static void RemoveWatcherFromIssue(this RedmineManager redmineManager, int issueId, int userId, RequestOptions requestOptions = null) + { + var uri = redmineManager.RedmineApiUrls.IssueWatcherRemove(issueId.ToString(CultureInfo.InvariantCulture), userId.ToString(CultureInfo.InvariantCulture)); + + redmineManager.ApiClient.Delete(uri, requestOptions); + } + + /// + /// Adds an existing user to a group. + /// + /// + /// The group id. + /// The user id. + /// + public static void AddUserToGroup(this RedmineManager redmineManager, int groupId, int userId, RequestOptions requestOptions = null) + { + var uri = redmineManager.RedmineApiUrls.GroupUserAdd(groupId.ToString(CultureInfo.InvariantCulture)); + + var payload = SerializationHelper.SerializeUserId(userId, redmineManager.MimeFormat); + + redmineManager.ApiClient.Create(uri, payload, requestOptions); + } + + /// + /// Removes an user from a group. + /// + /// + /// The group id. + /// The user id. + /// + public static void RemoveUserFromGroup(this RedmineManager redmineManager, int groupId, int userId, RequestOptions requestOptions = null) + { + var uri = redmineManager.RedmineApiUrls.GroupUserRemove(groupId.ToString(CultureInfo.InvariantCulture), userId.ToString(CultureInfo.InvariantCulture)); + + redmineManager.ApiClient.Delete(uri, requestOptions); + } + + /// + /// Creates or updates a wiki page. + /// + /// + /// The project id or identifier. + /// The wiki page name. + /// The wiki page to create or update. + /// + /// + public static void UpdateWikiPage(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage, RequestOptions requestOptions = null) + { + var payload = redmineManager.Serializer.Serialize(wikiPage); + + if (string.IsNullOrEmpty(payload)) { - throw new RedmineException($"Argument '{nameof(projectIdentifier)}' is null"); + return; } + + var uri = redmineManager.RedmineApiUrls.ProjectWikiPageUpdate(projectId, pageName); + + uri = Uri.EscapeDataString(uri); - return WebApiHelper.ExecuteDownloadList(redmineManager, Uri.EscapeUriString($"{redmineManager.Host}/project/{projectIdentifier}/memberships.{redmineManager.Format}"), nameValueCollection); + redmineManager.ApiClient.Patch(uri, payload, requestOptions); + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static WikiPage CreateWikiPage(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage, RequestOptions requestOptions = null) + { + var payload = redmineManager.Serializer.Serialize(wikiPage); + + if (string.IsNullOrEmpty(payload)) + { + throw new RedmineException("The payload is empty"); + } + + var uri = redmineManager.RedmineApiUrls.ProjectWikiPageUpdate(projectId, pageName); + + uri = Uri.EscapeDataString(uri); + + var response = redmineManager.ApiClient.Create(uri, payload, requestOptions); + + return response.DeserializeTo(redmineManager.Serializer); + } + + /// + /// Gets the wiki page. + /// + /// + /// The project identifier. + /// Name of the page. + /// + /// The version. + /// + public static WikiPage GetWikiPage(this RedmineManager redmineManager, string projectId, string pageName, RequestOptions requestOptions = null, uint version = 0) + { + var uri = version == 0 + ? redmineManager.RedmineApiUrls.ProjectWikiPage(projectId, pageName) + : redmineManager.RedmineApiUrls.ProjectWikiPageVersion(projectId, pageName, version.ToString(CultureInfo.InvariantCulture)); + + uri = Uri.EscapeDataString(uri); + + var response = redmineManager.ApiClient.Get(uri, requestOptions); + + return response.DeserializeTo(redmineManager.Serializer); + } + + /// + /// Returns the list of all pages in a project wiki. + /// + /// + /// The project id or identifier. + /// + /// + public static List GetAllWikiPages(this RedmineManager redmineManager, string projectId, RequestOptions requestOptions = null) + { + var uri = redmineManager.RedmineApiUrls.ProjectWikiIndex(projectId); + + var response = redmineManager.GetObjects(uri, requestOptions); + + return response; + } + + /// + /// Deletes a wiki page, its attachments and its history. If the deleted page is a parent page, its child pages are not + /// deleted but changed as root pages. + /// + /// + /// The project id or identifier. + /// The wiki page name. + /// + public static void DeleteWikiPage(this RedmineManager redmineManager, string projectId, string pageName, RequestOptions requestOptions = null) + { + var uri = redmineManager.RedmineApiUrls.ProjectWikiPageDelete(projectId, pageName); + + uri = Uri.EscapeDataString(uri); + + redmineManager.ApiClient.Delete(uri, requestOptions); + } + + /// + /// Updates the attachment. + /// + /// + /// The issue identifier. + /// The attachment. + /// + public static void UpdateIssueAttachment(this RedmineManager redmineManager, int issueId, Attachment attachment, RequestOptions requestOptions = null) + { + var attachments = new Attachments + { + {attachment.Id, attachment} + }; + + var data = redmineManager.Serializer.Serialize(attachments); + + var uri = redmineManager.RedmineApiUrls.AttachmentUpdate(issueId.ToString(CultureInfo.InvariantCulture)); + + redmineManager.ApiClient.Patch(uri, data, requestOptions); + } + + /// + /// + /// + /// + /// query strings. enable to specify multiple values separated by a space " ". + /// number of results in response. + /// skip this number of results in response + /// Optional filters. + /// + /// + /// Returns the search results by the specified condition parameters. + /// + public static PagedResults Search(this RedmineManager redmineManager, string q, int limit = RedmineConstants.DEFAULT_PAGE_SIZE_VALUE, int offset = 0, SearchFilterBuilder searchFilter = null, string impersonateUserName = null) + { + var parameters = CreateSearchParameters(q, limit, offset, searchFilter); + + var response = redmineManager.GetPaginatedObjects(parameters); + + return response; } + private static NameValueCollection CreateSearchParameters(string q, int limit, int offset, SearchFilterBuilder searchFilter) + { + if (q.IsNullOrWhiteSpace()) + { + throw new ArgumentNullException(nameof(q)); + } + + var parameters = new NameValueCollection + { + {RedmineKeys.Q, q}, + {RedmineKeys.LIMIT, limit.ToString(CultureInfo.InvariantCulture)}, + {RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)}, + }; + + return searchFilter != null ? searchFilter.Build(parameters) : parameters; + } + + #if !(NET20) + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static async Task> SearchAsync(this RedmineManager redmineManager, string q, int limit = RedmineManager.DEFAULT_PAGE_SIZE_VALUE, int offset = 0, SearchFilterBuilder searchFilter = null, CancellationToken cancellationToken = default) + { + var parameters = CreateSearchParameters(q, limit, offset, searchFilter); + + var response = await redmineManager.ApiClient.GetPagedAsync("", new RequestOptions() + { + QueryString = parameters + }, cancellationToken).ConfigureAwait(false); + + return response.DeserializeToPagedResults(redmineManager.Serializer); + } + /// /// /// /// - /// - /// + /// + /// /// - /// - public static PagedResults GetProjectFiles(this RedmineManager redmineManager, string projectIdentifier, NameValueCollection nameValueCollection) + public static async Task GetCurrentUserAsync(this RedmineManager redmineManager, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.CurrentUser(); + + var response = await redmineManager.ApiClient.GetAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); + + return response.DeserializeTo(redmineManager.Serializer); + } + + /// + /// Creates the or update wiki page asynchronous. + /// + /// The redmine manager. + /// The project identifier. + /// Name of the page. + /// The wiki page. + /// + /// + /// + public static async Task CreateWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var payload = redmineManager.Serializer.Serialize(wikiPage); + + if (string.IsNullOrEmpty(payload)) + { + throw new RedmineException("The payload is empty"); + } + + var url = redmineManager.RedmineApiUrls.ProjectWikiPageUpdate(projectId, pageName); + + var uri = Uri.EscapeDataString(url); + + var response = await redmineManager.ApiClient.CreateAsync(uri, payload,requestOptions, cancellationToken).ConfigureAwait(false); + + return response.DeserializeTo(redmineManager.Serializer); + } + + /// + /// Creates the or update wiki page asynchronous. + /// + /// The redmine manager. + /// The project identifier. + /// Name of the page. + /// The wiki page. + /// + /// + /// + public static async Task UpdateWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) { - if (projectIdentifier.IsNullOrWhiteSpace()) + var payload = redmineManager.Serializer.Serialize(wikiPage); + + if (string.IsNullOrEmpty(payload)) { - throw new RedmineException($"Argument '{nameof(projectIdentifier)}' is null"); + return; } + + var url = redmineManager.RedmineApiUrls.ProjectWikiPageUpdate(projectId, pageName); + + var uri = Uri.EscapeDataString(url); + + await redmineManager.ApiClient.PatchAsync(uri, payload, requestOptions, cancellationToken).ConfigureAwait(false); + } + + /// + /// Deletes the wiki page asynchronous. + /// + /// The redmine manager. + /// The project identifier. + /// Name of the page. + /// + /// + /// + public static async Task DeleteWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.ProjectWikiPageDelete(projectId, pageName); + + uri = Uri.EscapeDataString(uri); + + await redmineManager.ApiClient.DeleteAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); + } + + /// + /// Support for adding attachments through the REST API is added in Redmine 1.4.0. + /// Upload a file to server. This method does not block the calling thread. + /// + /// The redmine manager. + /// The content of the file that will be uploaded on server. + /// + /// + /// + /// . + /// + public static async Task UploadFileAsync(this RedmineManager redmineManager, byte[] data, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var url = redmineManager.RedmineApiUrls.UploadFragment(); + + var response = await redmineManager.ApiClient.UploadFileAsync(url, data,requestOptions , cancellationToken: cancellationToken).ConfigureAwait(false); - return WebApiHelper.ExecuteDownloadList(redmineManager, Uri.EscapeUriString($"{redmineManager.Host}/project/{projectIdentifier}/files.{redmineManager.Format}"), nameValueCollection); + return response.DeserializeTo(redmineManager.Serializer); + } + + /// + /// Downloads the file asynchronous. + /// + /// The redmine manager. + /// The address. + /// + /// + /// + public static async Task DownloadFileAsync(this RedmineManager redmineManager, string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var response = await redmineManager.ApiClient.DownloadAsync(address, requestOptions,cancellationToken: cancellationToken).ConfigureAwait(false); + return response.Content; + } + + /// + /// Gets the wiki page asynchronous. + /// + /// The redmine manager. + /// The project identifier. + /// Name of the page. + /// + /// The version. + /// + /// + public static async Task GetWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, RequestOptions requestOptions = null, uint version = 0, CancellationToken cancellationToken = default) + { + var uri = version == 0 + ? redmineManager.RedmineApiUrls.ProjectWikiPage(projectId, pageName) + : redmineManager.RedmineApiUrls.ProjectWikiPageVersion(projectId, pageName, version.ToString(CultureInfo.InvariantCulture)); + + uri = Uri.EscapeDataString(uri); + + var response = await redmineManager.ApiClient.GetAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); + + return response.DeserializeTo(redmineManager.Serializer); + } + + /// + /// Gets all wiki pages asynchronous. + /// + /// The redmine manager. + /// The project identifier. + /// + /// + /// + public static async Task> GetAllWikiPagesAsync(this RedmineManager redmineManager, string projectId, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.ProjectWikiIndex(projectId); + + var response = await redmineManager.ApiClient.GetPagedAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); + + return response.DeserializeToList(redmineManager.Serializer); + } + + /// + /// Adds an existing user to a group. This method does not block the calling thread. + /// + /// The redmine manager. + /// The group id. + /// The user id. + /// + /// + /// + /// Returns the Guid associated with the async request. + /// + public static async Task AddUserToGroupAsync(this RedmineManager redmineManager, int groupId, int userId, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.GroupUserAdd(groupId.ToString(CultureInfo.InvariantCulture)); + + var payload = SerializationHelper.SerializeUserId(userId, redmineManager.MimeFormat); + + await redmineManager.ApiClient.CreateAsync(uri, payload, requestOptions, cancellationToken).ConfigureAwait(false); + } + + /// + /// Removes an user from a group. This method does not block the calling thread. + /// + /// The redmine manager. + /// The group id. + /// The user id. + /// + /// + /// + public static async Task RemoveUserFromGroupAsync(this RedmineManager redmineManager, int groupId, int userId, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.GroupUserRemove(groupId.ToString(CultureInfo.InvariantCulture), userId.ToString(CultureInfo.InvariantCulture)); + + await redmineManager.ApiClient.DeleteAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); + } + + /// + /// Adds the watcher asynchronous. + /// + /// The redmine manager. + /// The issue identifier. + /// The user identifier. + /// + /// + /// + public static async Task AddWatcherToIssueAsync(this RedmineManager redmineManager, int issueId, int userId, RequestOptions requestOptions = null , CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.IssueWatcherAdd(issueId.ToString(CultureInfo.InvariantCulture)); + + var payload = SerializationHelper.SerializeUserId(userId, redmineManager.MimeFormat); + + await redmineManager.ApiClient.CreateAsync(uri, payload, requestOptions, cancellationToken).ConfigureAwait(false); + } + + /// + /// Removes the watcher asynchronous. + /// + /// The redmine manager. + /// The issue identifier. + /// The user identifier. + /// + /// + /// + public static async Task RemoveWatcherFromIssueAsync(this RedmineManager redmineManager, int issueId, int userId, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.IssueWatcherRemove(issueId.ToString(CultureInfo.InvariantCulture), userId.ToString(CultureInfo.InvariantCulture)); + + await redmineManager.ApiClient.DeleteAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); + } + + /// + /// + /// + /// + /// + /// + /// + public static async Task CountAsync(this RedmineManager redmineManager, params string[] include) where T : class, new() + { + RequestOptions requestOptions = null; + + if (include is {Length: > 0}) + { + requestOptions = new RequestOptions() + { + QueryString = new NameValueCollection + { + {RedmineKeys.INCLUDE, string.Join(",", include)} + } + }; + } + + return await CountAsync(redmineManager, requestOptions).ConfigureAwait(false); + } + + /// + /// + /// + /// + /// + /// + /// + public static async Task CountAsync(this RedmineManager redmineManager, RequestOptions requestOptions) where T : class, new() + { + var totalCount = 0; + const int PAGE_SIZE = 1; + const int OFFSET = 0; + + if (requestOptions == null) + { + requestOptions = new RequestOptions(); + } + + requestOptions.QueryString.AddPagingParameters(PAGE_SIZE, OFFSET); + + var tempResult = await GetPagedAsync(redmineManager, requestOptions).ConfigureAwait(false); + if (tempResult != null) + { + totalCount = tempResult.TotalItems; + } + + return totalCount; + } + + + /// + /// Gets the paginated objects asynchronous. + /// + /// + /// The redmine manager. + /// + /// + /// + public static async Task> GetPagedAsync(this RedmineManager redmineManager, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new() + { + var url = redmineManager.RedmineApiUrls.GetListFragment(); + + var response= await redmineManager.ApiClient.GetAsync(url, requestOptions, cancellationToken).ConfigureAwait(false); + + return response.DeserializeToPagedResults(redmineManager.Serializer); + } + + /// + /// Gets the objects asynchronous. + /// + /// + /// The redmine manager. + /// + /// + /// + public static async Task> GetObjectsAsync(this RedmineManager redmineManager, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new() + { + int pageSize = 0, offset = 0; + var isLimitSet = false; + List resultList = null; + + if (requestOptions == null) + { + requestOptions = new RequestOptions(); + } + + if (requestOptions.QueryString == null) + { + requestOptions.QueryString = new NameValueCollection(); + } + else + { + isLimitSet = int.TryParse(requestOptions.QueryString[RedmineKeys.LIMIT], out pageSize); + int.TryParse(requestOptions.QueryString[RedmineKeys.OFFSET], out offset); + } + + if (pageSize == default) + { + pageSize = redmineManager.PageSize > 0 + ? redmineManager.PageSize + : RedmineManager.DEFAULT_PAGE_SIZE_VALUE; + requestOptions.QueryString.Set(RedmineKeys.LIMIT, pageSize.ToString(CultureInfo.InvariantCulture)); + } + + try + { + var hasOffset = RedmineManager.TypesWithOffset.ContainsKey(typeof(T)); + if (hasOffset) + { + int totalCount; + do + { + requestOptions.QueryString.Set(RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)); + + var tempResult = await redmineManager.GetPagedAsync(requestOptions, cancellationToken: cancellationToken).ConfigureAwait(false); + + totalCount = isLimitSet ? pageSize : tempResult.TotalItems; + + if (tempResult?.Items != null) + { + if (resultList == null) + { + resultList = new List(tempResult.Items); + } + else + { + resultList.AddRange(tempResult.Items); + } + } + + offset += pageSize; + } while (offset < totalCount); + } + else + { + var result = await redmineManager.GetPagedAsync(requestOptions, cancellationToken: cancellationToken).ConfigureAwait(false); + if (result?.Items != null) + { + return new List(result.Items); + } + } + } + catch (WebException wex) + { + wex.HandleWebException(redmineManager.Serializer); + } + + return resultList; + } + + /// + /// Gets a Redmine object. This method does not block the calling thread. + /// + /// The type of objects to retrieve. + /// The redmine manager. + /// The id of the object. + /// + /// + /// + public static async Task GetObjectAsync(this RedmineManager redmineManager, string id, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new() + { + var url = redmineManager.RedmineApiUrls.GetFragment(id); + + var response = await redmineManager.ApiClient.GetAsync(url,requestOptions, cancellationToken).ConfigureAwait(false); + + return response.DeserializeTo(redmineManager.Serializer); + } + + /// + /// Creates a new Redmine object. This method does not block the calling thread. + /// + /// The type of object to create. + /// The redmine manager. + /// The object to create. + /// + /// + /// + public static async Task CreateObjectAsync(this RedmineManager redmineManager, T entity, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new() + { + return await redmineManager.CreateObjectAsync( entity, null, requestOptions, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + /// Creates a new Redmine object. This method does not block the calling thread. + /// + /// The type of object to create. + /// The redmine manager. + /// The object to create. + /// The owner identifier. + /// + /// + /// + public static async Task CreateObjectAsync(this RedmineManager redmineManager, T entity, string ownerId, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new() + { + var url = redmineManager.RedmineApiUrls.CreateEntityFragment(ownerId); + + var payload = redmineManager.Serializer.Serialize(entity); + + var response = await redmineManager.ApiClient.CreateAsync(url, payload, requestOptions, cancellationToken).ConfigureAwait(false); + + return response.DeserializeTo(redmineManager.Serializer); + } + + /// + /// Updates the object asynchronous. + /// + /// + /// The redmine manager. + /// The identifier. + /// The object. + /// + /// + /// + public static async Task UpdateObjectAsync(this RedmineManager redmineManager, string id, T entity, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new() + { + var url = redmineManager.RedmineApiUrls.UpdateFragment(id); + + var payload = redmineManager.Serializer.Serialize(entity); + + await redmineManager.ApiClient.UpdateAsync(url, payload, requestOptions,cancellationToken: cancellationToken).ConfigureAwait(false); + // data = Regex.Replace(data, @"\r\n|\r|\n", "\r\n"); + } + + /// + /// Deletes the Redmine object. This method does not block the calling thread. + /// + /// The type of objects to delete. + /// The redmine manager. + /// The id of the object to delete + /// + /// + /// + public static async Task DeleteObjectAsync(this RedmineManager redmineManager, string id, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new() + { + var url = redmineManager.RedmineApiUrls.DeleteFragment(id); + + await redmineManager.ApiClient.DeleteAsync(url, requestOptions, cancellationToken).ConfigureAwait((false)); + } + #endif + + internal static RequestOptions CreateRequestOptions(NameValueCollection parameters = null, string impersonateUserName = null) + { + RequestOptions requestOptions = null; + if (parameters != null || !impersonateUserName.IsNullOrWhiteSpace()) + { + requestOptions = new RequestOptions() + { + QueryString = parameters, + ImpersonateUser = impersonateUserName + }; + } + + return requestOptions; } } } \ No newline at end of file From b6150c258e722b239dbe1535dad239a53575706a Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 3 Jan 2024 17:58:04 +0200 Subject: [PATCH 325/549] [RedmineApiExtension] Code arrange --- .../Exceptions/RedmineApiException.cs | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/redmine-net-api/Exceptions/RedmineApiException.cs b/src/redmine-net-api/Exceptions/RedmineApiException.cs index ad3b114b..9a544503 100644 --- a/src/redmine-net-api/Exceptions/RedmineApiException.cs +++ b/src/redmine-net-api/Exceptions/RedmineApiException.cs @@ -13,18 +13,14 @@ public sealed class RedmineApiException : RedmineException /// /// public RedmineApiException() - : this(errorCode: null, false) - { - } + : this(errorCode: null, false) { } /// /// /// /// public RedmineApiException(string message) - : this(message, errorCode: null, false) - { - } + : this(message, errorCode: null, false) { } /// /// @@ -32,9 +28,7 @@ public RedmineApiException(string message) /// /// public RedmineApiException(string message, Exception innerException) - : this(message, innerException, errorCode: null, false) - { - } + : this(message, innerException, errorCode: null, false) { } /// /// @@ -42,9 +36,7 @@ public RedmineApiException(string message, Exception innerException) /// /// public RedmineApiException(string errorCode, bool isTransient) - : this(string.Empty, errorCode, isTransient) - { - } + : this(string.Empty, errorCode, isTransient) { } /// /// @@ -53,9 +45,7 @@ public RedmineApiException(string errorCode, bool isTransient) /// /// public RedmineApiException(string message, string errorCode, bool isTransient) - : this(message, null, errorCode, isTransient) - { - } + : this(message, null, errorCode, isTransient) { } /// /// From dc776b1475891574f9effd61af8c4130503e9068 Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 3 Jan 2024 18:00:40 +0200 Subject: [PATCH 326/549] [GitActions] Use multiple os --- .github/workflows/ci-cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 029bbc5f..74fb1cfd 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -53,7 +53,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ ubuntu-latest]#, windows-latest, macos-latest ] + os: [ ubuntu-latest, windows-latest, macos-latest ] dotnet: [ '7.x.x' ] steps: From 6528c5a6dcad89f05cc19dfd3653bfa783319509 Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 3 Jan 2024 18:03:06 +0200 Subject: [PATCH 327/549] [Sln] Update --- redmine-net-api.sln | 2 ++ 1 file changed, 2 insertions(+) diff --git a/redmine-net-api.sln b/redmine-net-api.sln index cd0dfab4..332b18aa 100644 --- a/redmine-net-api.sln +++ b/redmine-net-api.sln @@ -26,6 +26,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionFolder", "SolutionF releasenotes.props = releasenotes.props signing.props = signing.props version.props = version.props + .github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml + .github\workflows\ci-cd.yml = .github\workflows\ci-cd.yml EndProjectSection EndProject Global From d086fbea1880202755a5d78ad4fafc60dbeefc1b Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 3 Jan 2024 18:43:56 +0200 Subject: [PATCH 328/549] [MimeFormat] Mark as obsolete --- .../Serialization/{MimeFormat.cs => MimeFormatObsolete.cs} | 3 +++ 1 file changed, 3 insertions(+) rename src/redmine-net-api/Serialization/{MimeFormat.cs => MimeFormatObsolete.cs} (89%) diff --git a/src/redmine-net-api/Serialization/MimeFormat.cs b/src/redmine-net-api/Serialization/MimeFormatObsolete.cs similarity index 89% rename from src/redmine-net-api/Serialization/MimeFormat.cs rename to src/redmine-net-api/Serialization/MimeFormatObsolete.cs index 5f755688..16d54fd3 100755 --- a/src/redmine-net-api/Serialization/MimeFormat.cs +++ b/src/redmine-net-api/Serialization/MimeFormatObsolete.cs @@ -14,11 +14,14 @@ You may obtain a copy of the License at limitations under the License. */ +using System; + namespace Redmine.Net.Api { /// /// /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use SerializationType instead")] public enum MimeFormat { /// From f64f6c9437fb35ad5f143d5c10f61931fd219d23 Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 3 Jan 2024 18:45:27 +0200 Subject: [PATCH 329/549] [SerializationHelper] Replace MimeFormat argument with IRedmineSerializer --- .../Extensions/RedmineManagerExtensions.cs | 8 ++++---- src/redmine-net-api/Serialization/SerializationHelper.cs | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs index cdba6637..6d185825 100644 --- a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs +++ b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs @@ -138,7 +138,7 @@ public static void AddWatcherToIssue(this RedmineManager redmineManager, int iss { var uri = redmineManager.RedmineApiUrls.IssueWatcherAdd(issueId.ToString(CultureInfo.InvariantCulture)); - var payload = SerializationHelper.SerializeUserId(userId, redmineManager.MimeFormat); + var payload = SerializationHelper.SerializeUserId(userId, redmineManager.Serializer); redmineManager.ApiClient.Create(uri, payload, requestOptions); } @@ -168,7 +168,7 @@ public static void AddUserToGroup(this RedmineManager redmineManager, int groupI { var uri = redmineManager.RedmineApiUrls.GroupUserAdd(groupId.ToString(CultureInfo.InvariantCulture)); - var payload = SerializationHelper.SerializeUserId(userId, redmineManager.MimeFormat); + var payload = SerializationHelper.SerializeUserId(userId, redmineManager.Serializer); redmineManager.ApiClient.Create(uri, payload, requestOptions); } @@ -553,7 +553,7 @@ public static async Task AddUserToGroupAsync(this RedmineManager redmineManager, { var uri = redmineManager.RedmineApiUrls.GroupUserAdd(groupId.ToString(CultureInfo.InvariantCulture)); - var payload = SerializationHelper.SerializeUserId(userId, redmineManager.MimeFormat); + var payload = SerializationHelper.SerializeUserId(userId, redmineManager.Serializer); await redmineManager.ApiClient.CreateAsync(uri, payload, requestOptions, cancellationToken).ConfigureAwait(false); } @@ -587,7 +587,7 @@ public static async Task AddWatcherToIssueAsync(this RedmineManager redmineManag { var uri = redmineManager.RedmineApiUrls.IssueWatcherAdd(issueId.ToString(CultureInfo.InvariantCulture)); - var payload = SerializationHelper.SerializeUserId(userId, redmineManager.MimeFormat); + var payload = SerializationHelper.SerializeUserId(userId, redmineManager.Serializer); await redmineManager.ApiClient.CreateAsync(uri, payload, requestOptions, cancellationToken).ConfigureAwait(false); } diff --git a/src/redmine-net-api/Serialization/SerializationHelper.cs b/src/redmine-net-api/Serialization/SerializationHelper.cs index 678b22c7..779b801c 100644 --- a/src/redmine-net-api/Serialization/SerializationHelper.cs +++ b/src/redmine-net-api/Serialization/SerializationHelper.cs @@ -1,6 +1,6 @@ using System.Globalization; -namespace Redmine.Net.Api +namespace Redmine.Net.Api.Serialization { /// /// @@ -10,12 +10,12 @@ internal static class SerializationHelper /// /// /// - /// /// + /// /// - public static string SerializeUserId(int userId, MimeFormat mimeFormat) + public static string SerializeUserId(int userId, IRedmineSerializer redmineSerializer) { - return mimeFormat == MimeFormat.Xml + return redmineSerializer is XmlRedmineSerializer ? $"{userId.ToString(CultureInfo.InvariantCulture)}" : $"{{\"user_id\":\"{userId.ToString(CultureInfo.InvariantCulture)}\"}}"; } From f6b65d71730c7d573d0460549d61a2f46feaa3a8 Mon Sep 17 00:00:00 2001 From: zapadi Date: Thu, 4 Jan 2024 13:59:03 +0200 Subject: [PATCH 330/549] [RedmineApiUrlExtensions] Fix CurrentUser --- src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs b/src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs index 4ccd918b..6725c085 100644 --- a/src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs +++ b/src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs @@ -12,7 +12,7 @@ public static string MyAccount(this RedmineApiUrls redmineApiUrls) public static string CurrentUser(this RedmineApiUrls redmineApiUrls) { - return $"{RedmineKeys.CURRENT}.{redmineApiUrls.Format}"; + return $"{RedmineKeys.USERS}/{RedmineKeys.CURRENT}.{redmineApiUrls.Format}"; } public static string ProjectNews(this RedmineApiUrls redmineApiUrls, string projectIdentifier) From 5c727861bdfbe633e4c89f8ce5db7170536a300f Mon Sep 17 00:00:00 2001 From: zapadi Date: Thu, 4 Jan 2024 14:00:18 +0200 Subject: [PATCH 331/549] [StringExtensions] Add ToInvariantString of T --- .../Extensions/StringExtensions.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/redmine-net-api/Extensions/StringExtensions.cs b/src/redmine-net-api/Extensions/StringExtensions.cs index 1fd04cdf..c02836e7 100644 --- a/src/redmine-net-api/Extensions/StringExtensions.cs +++ b/src/redmine-net-api/Extensions/StringExtensions.cs @@ -16,6 +16,7 @@ limitations under the License. using System; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Security; namespace Redmine.Net.Api.Extensions @@ -131,5 +132,29 @@ internal static string ValueOrFallback(this string value, string fallback) { return !value.IsNullOrWhiteSpace() ? value : fallback; } + + internal static string ToInvariantString(this T value) where T : struct + { + return value switch + { + sbyte v => v.ToString(CultureInfo.InvariantCulture), + byte v => v.ToString(CultureInfo.InvariantCulture), + short v => v.ToString(CultureInfo.InvariantCulture), + ushort v => v.ToString(CultureInfo.InvariantCulture), + int v => v.ToString(CultureInfo.InvariantCulture), + uint v => v.ToString(CultureInfo.InvariantCulture), + long v => v.ToString(CultureInfo.InvariantCulture), + ulong v => v.ToString(CultureInfo.InvariantCulture), + float v => v.ToString("G7", CultureInfo.InvariantCulture), // Specify precision explicitly for backward compatibility + double v => v.ToString("G15", CultureInfo.InvariantCulture), // Specify precision explicitly for backward compatibility + decimal v => v.ToString(CultureInfo.InvariantCulture), + TimeSpan ts => ts.ToString(), + DateTime d => d.ToString(CultureInfo.InvariantCulture), + #pragma warning disable CA1308 + bool b => b.ToString().ToLowerInvariant(), + #pragma warning restore CA1308 + _ => value.ToString(), + }; + } } } \ No newline at end of file From 4a3dc25c156157fc2460784c0b90941e9f417c0a Mon Sep 17 00:00:00 2001 From: zapadi Date: Fri, 5 Jan 2024 12:54:09 +0200 Subject: [PATCH 332/549] [Header] Header info added --- .../Authentication/IRedmineAuthentication.cs | 18 +++++++++++++++++- .../RedmineApiKeyAuthentication.cs | 16 ++++++++++++++++ .../RedmineBasicAuthentication.cs | 16 ++++++++++++++++ .../Authentication/RedmineNoAuthentication.cs | 16 ++++++++++++++++ .../Extensions/RedmineManagerExtensions.cs | 16 ++++++++++++++++ src/redmine-net-api/Net/ApiRequestMessage.cs | 16 ++++++++++++++++ .../Net/ApiRequestMessageContent.cs | 16 ++++++++++++++++ src/redmine-net-api/Net/ApiResponseMessage.cs | 16 ++++++++++++++++ .../Net/ApiResponseMessageExtensions.cs | 16 ++++++++++++++++ src/redmine-net-api/Net/IRedmineApiClient.cs | 16 ++++++++++++++++ .../Net/IRedmineApiClientOptions.cs | 16 ++++++++++++++++ src/redmine-net-api/Net/RedmineApiUrls.cs | 16 ++++++++++++++++ .../Net/RedmineApiUrlsExtensions.cs | 16 ++++++++++++++++ src/redmine-net-api/Net/RequestOptions.cs | 16 ++++++++++++++++ .../Net/WebClient/RedmineWebClientOptions.cs | 16 ++++++++++++++++ src/redmine-net-api/RedmineConstants.cs | 16 ++++++++++++++++ src/redmine-net-api/RedmineManagerOptions.cs | 17 +++++++++++++++++ .../RedmineManagerOptionsBuilder.cs | 18 +++++++++++++++++- .../Serialization/RedmineSerializerFactory.cs | 16 ++++++++++++++++ .../Serialization/SerializationHelper.cs | 16 ++++++++++++++++ .../Serialization/SerializationType.cs | 16 ++++++++++++++++ 21 files changed, 339 insertions(+), 2 deletions(-) diff --git a/src/redmine-net-api/Authentication/IRedmineAuthentication.cs b/src/redmine-net-api/Authentication/IRedmineAuthentication.cs index 9604c83f..6823243a 100644 --- a/src/redmine-net-api/Authentication/IRedmineAuthentication.cs +++ b/src/redmine-net-api/Authentication/IRedmineAuthentication.cs @@ -1,6 +1,22 @@ +/* + Copyright 2011 - 2023 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + using System.Net; -namespace Redmine.Net.Api; +namespace Redmine.Net.Api.Authentication; /// /// diff --git a/src/redmine-net-api/Authentication/RedmineApiKeyAuthentication.cs b/src/redmine-net-api/Authentication/RedmineApiKeyAuthentication.cs index 084752d3..5be2a87f 100644 --- a/src/redmine-net-api/Authentication/RedmineApiKeyAuthentication.cs +++ b/src/redmine-net-api/Authentication/RedmineApiKeyAuthentication.cs @@ -1,3 +1,19 @@ +/* + Copyright 2011 - 2023 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + using System.Net; namespace Redmine.Net.Api.Authentication; diff --git a/src/redmine-net-api/Authentication/RedmineBasicAuthentication.cs b/src/redmine-net-api/Authentication/RedmineBasicAuthentication.cs index 58c740ba..88a70cd9 100644 --- a/src/redmine-net-api/Authentication/RedmineBasicAuthentication.cs +++ b/src/redmine-net-api/Authentication/RedmineBasicAuthentication.cs @@ -1,3 +1,19 @@ +/* + Copyright 2011 - 2023 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + using System; using System.Net; using System.Text; diff --git a/src/redmine-net-api/Authentication/RedmineNoAuthentication.cs b/src/redmine-net-api/Authentication/RedmineNoAuthentication.cs index f568f1bc..2849d703 100644 --- a/src/redmine-net-api/Authentication/RedmineNoAuthentication.cs +++ b/src/redmine-net-api/Authentication/RedmineNoAuthentication.cs @@ -1,3 +1,19 @@ +/* + Copyright 2011 - 2023 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + using System.Net; namespace Redmine.Net.Api.Authentication; diff --git a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs index 6d185825..e162e155 100644 --- a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs +++ b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs @@ -1,3 +1,19 @@ +/* + Copyright 2011 - 2023 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + using System; using System.Collections.Generic; using System.Collections.Specialized; diff --git a/src/redmine-net-api/Net/ApiRequestMessage.cs b/src/redmine-net-api/Net/ApiRequestMessage.cs index b4a2e11e..c3bdb891 100644 --- a/src/redmine-net-api/Net/ApiRequestMessage.cs +++ b/src/redmine-net-api/Net/ApiRequestMessage.cs @@ -1,3 +1,19 @@ +/* + Copyright 2011 - 2023 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + using System.Collections.Specialized; namespace Redmine.Net.Api.Net; diff --git a/src/redmine-net-api/Net/ApiRequestMessageContent.cs b/src/redmine-net-api/Net/ApiRequestMessageContent.cs index f3e6fb48..e484c81a 100644 --- a/src/redmine-net-api/Net/ApiRequestMessageContent.cs +++ b/src/redmine-net-api/Net/ApiRequestMessageContent.cs @@ -1,3 +1,19 @@ +/* + Copyright 2011 - 2023 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + namespace Redmine.Net.Api.Net; internal abstract class ApiRequestMessageContent diff --git a/src/redmine-net-api/Net/ApiResponseMessage.cs b/src/redmine-net-api/Net/ApiResponseMessage.cs index c6f6431c..4cdf66c0 100644 --- a/src/redmine-net-api/Net/ApiResponseMessage.cs +++ b/src/redmine-net-api/Net/ApiResponseMessage.cs @@ -1,3 +1,19 @@ +/* + Copyright 2011 - 2023 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + using System.Collections.Specialized; namespace Redmine.Net.Api.Net; diff --git a/src/redmine-net-api/Net/ApiResponseMessageExtensions.cs b/src/redmine-net-api/Net/ApiResponseMessageExtensions.cs index 7efdfd95..36aeaf6e 100644 --- a/src/redmine-net-api/Net/ApiResponseMessageExtensions.cs +++ b/src/redmine-net-api/Net/ApiResponseMessageExtensions.cs @@ -1,3 +1,19 @@ +/* + Copyright 2011 - 2023 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + using System.Collections.Generic; using System.Text; using Redmine.Net.Api.Serialization; diff --git a/src/redmine-net-api/Net/IRedmineApiClient.cs b/src/redmine-net-api/Net/IRedmineApiClient.cs index 00a28bd7..586a001a 100644 --- a/src/redmine-net-api/Net/IRedmineApiClient.cs +++ b/src/redmine-net-api/Net/IRedmineApiClient.cs @@ -1,3 +1,19 @@ +/* + Copyright 2011 - 2023 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + using System.Threading; #if!(NET20) using System.Threading.Tasks; diff --git a/src/redmine-net-api/Net/IRedmineApiClientOptions.cs b/src/redmine-net-api/Net/IRedmineApiClientOptions.cs index 56013ec2..263c703a 100644 --- a/src/redmine-net-api/Net/IRedmineApiClientOptions.cs +++ b/src/redmine-net-api/Net/IRedmineApiClientOptions.cs @@ -1,3 +1,19 @@ +/* + Copyright 2011 - 2023 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + using System; using System.Collections.Generic; using System.Net; diff --git a/src/redmine-net-api/Net/RedmineApiUrls.cs b/src/redmine-net-api/Net/RedmineApiUrls.cs index 4b3e5470..34417919 100644 --- a/src/redmine-net-api/Net/RedmineApiUrls.cs +++ b/src/redmine-net-api/Net/RedmineApiUrls.cs @@ -1,3 +1,19 @@ +/* + Copyright 2011 - 2023 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + using System; using System.Collections.Generic; using Redmine.Net.Api.Exceptions; diff --git a/src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs b/src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs index 6725c085..aa415007 100644 --- a/src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs +++ b/src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs @@ -1,3 +1,19 @@ +/* + Copyright 2011 - 2023 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; diff --git a/src/redmine-net-api/Net/RequestOptions.cs b/src/redmine-net-api/Net/RequestOptions.cs index eaaef3b8..1b514c8a 100644 --- a/src/redmine-net-api/Net/RequestOptions.cs +++ b/src/redmine-net-api/Net/RequestOptions.cs @@ -1,3 +1,19 @@ +/* + Copyright 2011 - 2023 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + using System.Collections.Specialized; namespace Redmine.Net.Api.Net; diff --git a/src/redmine-net-api/Net/WebClient/RedmineWebClientOptions.cs b/src/redmine-net-api/Net/WebClient/RedmineWebClientOptions.cs index 05b0c766..0a2953e8 100644 --- a/src/redmine-net-api/Net/WebClient/RedmineWebClientOptions.cs +++ b/src/redmine-net-api/Net/WebClient/RedmineWebClientOptions.cs @@ -1,3 +1,19 @@ +/* + Copyright 2011 - 2023 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + using System; using System.Collections.Generic; using System.Net; diff --git a/src/redmine-net-api/RedmineConstants.cs b/src/redmine-net-api/RedmineConstants.cs index dea7584a..4d715422 100644 --- a/src/redmine-net-api/RedmineConstants.cs +++ b/src/redmine-net-api/RedmineConstants.cs @@ -1,3 +1,19 @@ +/* + Copyright 2011 - 2023 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + namespace Redmine.Net.Api { /// diff --git a/src/redmine-net-api/RedmineManagerOptions.cs b/src/redmine-net-api/RedmineManagerOptions.cs index 828f1422..aadf9916 100644 --- a/src/redmine-net-api/RedmineManagerOptions.cs +++ b/src/redmine-net-api/RedmineManagerOptions.cs @@ -1,5 +1,22 @@ +/* + Copyright 2011 - 2023 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + using System; using System.Net; +using Redmine.Net.Api.Authentication; using Redmine.Net.Api.Net; using Redmine.Net.Api.Serialization; diff --git a/src/redmine-net-api/RedmineManagerOptionsBuilder.cs b/src/redmine-net-api/RedmineManagerOptionsBuilder.cs index 56a2d195..e59afb5c 100644 --- a/src/redmine-net-api/RedmineManagerOptionsBuilder.cs +++ b/src/redmine-net-api/RedmineManagerOptionsBuilder.cs @@ -1,6 +1,22 @@ +/* + Copyright 2011 - 2023 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + using System; using System.Net; -using System.Xml.Serialization; +using Redmine.Net.Api.Authentication; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Net; diff --git a/src/redmine-net-api/Serialization/RedmineSerializerFactory.cs b/src/redmine-net-api/Serialization/RedmineSerializerFactory.cs index d371f59b..240d1af8 100644 --- a/src/redmine-net-api/Serialization/RedmineSerializerFactory.cs +++ b/src/redmine-net-api/Serialization/RedmineSerializerFactory.cs @@ -1,3 +1,19 @@ +/* + Copyright 2011 - 2023 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + using System; namespace Redmine.Net.Api.Serialization; diff --git a/src/redmine-net-api/Serialization/SerializationHelper.cs b/src/redmine-net-api/Serialization/SerializationHelper.cs index 779b801c..5ecd4b8f 100644 --- a/src/redmine-net-api/Serialization/SerializationHelper.cs +++ b/src/redmine-net-api/Serialization/SerializationHelper.cs @@ -1,3 +1,19 @@ +/* + Copyright 2011 - 2023 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + using System.Globalization; namespace Redmine.Net.Api.Serialization diff --git a/src/redmine-net-api/Serialization/SerializationType.cs b/src/redmine-net-api/Serialization/SerializationType.cs index e57dd054..c46591f8 100644 --- a/src/redmine-net-api/Serialization/SerializationType.cs +++ b/src/redmine-net-api/Serialization/SerializationType.cs @@ -1,3 +1,19 @@ +/* + Copyright 2011 - 2023 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + namespace Redmine.Net.Api.Serialization { /// From 33ad0ed3029425666376a3b88db086ac12b0aea4 Mon Sep 17 00:00:00 2001 From: zapadi Date: Fri, 5 Jan 2024 13:01:30 +0200 Subject: [PATCH 333/549] Code arrange --- .../Authentication/RedmineApiKeyAuthentication.cs | 2 +- .../Authentication/RedmineBasicAuthentication.cs | 2 +- src/redmine-net-api/Authentication/RedmineNoAuthentication.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/redmine-net-api/Authentication/RedmineApiKeyAuthentication.cs b/src/redmine-net-api/Authentication/RedmineApiKeyAuthentication.cs index 5be2a87f..c0a34580 100644 --- a/src/redmine-net-api/Authentication/RedmineApiKeyAuthentication.cs +++ b/src/redmine-net-api/Authentication/RedmineApiKeyAuthentication.cs @@ -24,7 +24,7 @@ namespace Redmine.Net.Api.Authentication; public sealed class RedmineApiKeyAuthentication: IRedmineAuthentication { /// - public string AuthenticationType { get; } = "X-Redmine-API-Key"; + public string AuthenticationType => "X-Redmine-API-Key"; /// public string Token { get; init; } diff --git a/src/redmine-net-api/Authentication/RedmineBasicAuthentication.cs b/src/redmine-net-api/Authentication/RedmineBasicAuthentication.cs index 88a70cd9..2e8da6cb 100644 --- a/src/redmine-net-api/Authentication/RedmineBasicAuthentication.cs +++ b/src/redmine-net-api/Authentication/RedmineBasicAuthentication.cs @@ -27,7 +27,7 @@ namespace Redmine.Net.Api.Authentication public sealed class RedmineBasicAuthentication: IRedmineAuthentication { /// - public string AuthenticationType { get; } = "Basic"; + public string AuthenticationType => "Basic"; /// public string Token { get; init; } diff --git a/src/redmine-net-api/Authentication/RedmineNoAuthentication.cs b/src/redmine-net-api/Authentication/RedmineNoAuthentication.cs index 2849d703..6fb7fe8a 100644 --- a/src/redmine-net-api/Authentication/RedmineNoAuthentication.cs +++ b/src/redmine-net-api/Authentication/RedmineNoAuthentication.cs @@ -24,7 +24,7 @@ namespace Redmine.Net.Api.Authentication; public sealed class RedmineNoAuthentication: IRedmineAuthentication { /// - public string AuthenticationType { get; } = "NoAuth"; + public string AuthenticationType => "NoAuth"; /// public string Token { get; init; } From ad95d151c16a83ffaebb1efe9c208c094f0164e9 Mon Sep 17 00:00:00 2001 From: zapadi Date: Fri, 5 Jan 2024 13:02:43 +0200 Subject: [PATCH 334/549] [RedmineManagerOptionsBuilder] Add With[Api/Basic]Authentication methods --- src/redmine-net-api/RedmineManagerObsolete.cs | 6 ++---- .../RedmineManagerOptionsBuilder.cs | 21 ++++++++++++++----- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/redmine-net-api/RedmineManagerObsolete.cs b/src/redmine-net-api/RedmineManagerObsolete.cs index 34764d20..f7b40ebb 100644 --- a/src/redmine-net-api/RedmineManagerObsolete.cs +++ b/src/redmine-net-api/RedmineManagerObsolete.cs @@ -20,7 +20,6 @@ limitations under the License. using System.Net; using System.Net.Security; using System.Security.Cryptography.X509Certificates; -using Redmine.Net.Api.Authentication; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Net.WebClient; using Redmine.Net.Api.Serialization; @@ -53,7 +52,6 @@ public RedmineManager(string host, MimeFormat mimeFormat = MimeFormat.Xml, bool IWebProxy proxy = null, SecurityProtocolType securityProtocolType = default, string scheme = "https", TimeSpan? timeout = null) :this(new RedmineManagerOptionsBuilder() .WithHost(host) - .WithAuthentication(new RedmineNoAuthentication()) .WithSerializationType(mimeFormat) .WithVerifyServerCert(verifyServerCert) .WithClientOptions(new RedmineWebClientOptions() @@ -87,7 +85,7 @@ public RedmineManager(string host, string apiKey, MimeFormat mimeFormat = MimeFo SecurityProtocolType securityProtocolType = default, string scheme = "https", TimeSpan? timeout = null) : this(new RedmineManagerOptionsBuilder() .WithHost(host) - .WithAuthentication(new RedmineApiKeyAuthentication(apiKey)) + .WithApiKeyAuthentication(apiKey) .WithSerializationType(mimeFormat) .WithVerifyServerCert(verifyServerCert) .WithClientOptions(new RedmineWebClientOptions() @@ -116,7 +114,7 @@ public RedmineManager(string host, string login, string password, MimeFormat mim SecurityProtocolType securityProtocolType = default, string scheme = "https", TimeSpan? timeout = null) : this(new RedmineManagerOptionsBuilder() .WithHost(host) - .WithAuthentication(new RedmineBasicAuthentication(login, password)) + .WithBasicAuthentication(login, password) .WithSerializationType(mimeFormat) .WithVerifyServerCert(verifyServerCert) .WithClientOptions(new RedmineWebClientOptions() diff --git a/src/redmine-net-api/RedmineManagerOptionsBuilder.cs b/src/redmine-net-api/RedmineManagerOptionsBuilder.cs index e59afb5c..8e682f0b 100644 --- a/src/redmine-net-api/RedmineManagerOptionsBuilder.cs +++ b/src/redmine-net-api/RedmineManagerOptionsBuilder.cs @@ -100,11 +100,23 @@ public RedmineManagerOptionsBuilder WithSerializationType(SerializationType seri /// /// /// - /// + /// /// - public RedmineManagerOptionsBuilder WithAuthentication(IRedmineAuthentication authentication) + public RedmineManagerOptionsBuilder WithApiKeyAuthentication(string apiKey) { - this.Authentication = authentication; + this.Authentication = new RedmineApiKeyAuthentication(apiKey); + return this; + } + + /// + /// + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithBasicAuthentication(string login, string password) + { + this.Authentication = new RedmineBasicAuthentication(login, password); return this; } @@ -198,14 +210,13 @@ internal RedmineManagerOptions Build() VerifyServerCert = VerifyServerCert, Serializer = RedmineSerializerFactory.CreateSerializer(SerializationType), RedmineVersion = Version, - Authentication = Authentication, + Authentication = Authentication ?? new RedmineNoAuthentication(), ClientOptions = ClientOptions }; return options; } - internal static void EnsureDomainNameIsValid(string domainName) { if (domainName.IsNullOrWhiteSpace()) From 0b0381132f5b522486e8423fdaf72ba224ea011e Mon Sep 17 00:00:00 2001 From: zapadi Date: Fri, 5 Jan 2024 13:03:50 +0200 Subject: [PATCH 335/549] [New][RedmineConstants] xml keyword --- src/redmine-net-api/RedmineConstants.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/redmine-net-api/RedmineConstants.cs b/src/redmine-net-api/RedmineConstants.cs index 4d715422..f8fdad88 100644 --- a/src/redmine-net-api/RedmineConstants.cs +++ b/src/redmine-net-api/RedmineConstants.cs @@ -47,5 +47,10 @@ public static class RedmineConstants /// /// public const string IMPERSONATE_HEADER_KEY = "X-Redmine-Switch-User"; + + /// + /// + /// + public const string XML = "xml"; } } \ No newline at end of file From ff8e7512088b973d8252bca819cc916463e6cbc1 Mon Sep 17 00:00:00 2001 From: zapadi Date: Fri, 5 Jan 2024 13:04:44 +0200 Subject: [PATCH 336/549] [Csproj] Add net80 TargetFramework --- src/redmine-net-api/redmine-net-api.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 7bd68c94..2588c72e 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -4,7 +4,7 @@ Redmine.Net.Api redmine-net-api - net20;net40;net45;net451;net452;net46;net461;net462;net47;net471;net472;net48;net481;net60;net70; + net20;net40;net45;net451;net452;net46;net461;net462;net47;net471;net472;net48;net481;net60;net70;net80 false True true From ef14dda02927e4735513005dcc091146cf01b6d7 Mon Sep 17 00:00:00 2001 From: zapadi Date: Fri, 5 Jan 2024 13:05:52 +0200 Subject: [PATCH 337/549] [Csproj] Bump up Newtonsoft.Json to 13.0.3 --- src/redmine-net-api/redmine-net-api.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 2588c72e..a2a4a4d2 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -35,7 +35,7 @@ - + From 01dc695f6d2902622e61f50e3c82440d70440baa Mon Sep 17 00:00:00 2001 From: zapadi Date: Fri, 5 Jan 2024 13:21:41 +0200 Subject: [PATCH 338/549] Group message content files under MessageContent folder --- .../ByteArrayApiRequestMessageContent.cs | 9 ------- .../ByteArrayApiRequestMessageContent.cs} | 14 ++++------- .../StreamApiRequestMessageContent.cs | 25 +++++++++++++++++++ .../StringApiRequestMessageContent.cs | 18 ++++++++++++- .../StreamApiRequestMessageContent.cs | 9 ------- 5 files changed, 47 insertions(+), 28 deletions(-) delete mode 100644 src/redmine-net-api/Net/WebClient/ByteArrayApiRequestMessageContent.cs rename src/redmine-net-api/{Serialization/ISerialization.cs => Net/WebClient/MessageContent/ByteArrayApiRequestMessageContent.cs} (65%) create mode 100644 src/redmine-net-api/Net/WebClient/MessageContent/StreamApiRequestMessageContent.cs rename src/redmine-net-api/Net/WebClient/{ => MessageContent}/StringApiRequestMessageContent.cs (58%) delete mode 100644 src/redmine-net-api/Net/WebClient/StreamApiRequestMessageContent.cs diff --git a/src/redmine-net-api/Net/WebClient/ByteArrayApiRequestMessageContent.cs b/src/redmine-net-api/Net/WebClient/ByteArrayApiRequestMessageContent.cs deleted file mode 100644 index 66650e04..00000000 --- a/src/redmine-net-api/Net/WebClient/ByteArrayApiRequestMessageContent.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Redmine.Net.Api.Net.WebClient; - -internal class ByteArrayApiRequestMessageContent : ApiRequestMessageContent -{ - public ByteArrayApiRequestMessageContent(byte[] content) - { - Body = content; - } -} \ No newline at end of file diff --git a/src/redmine-net-api/Serialization/ISerialization.cs b/src/redmine-net-api/Net/WebClient/MessageContent/ByteArrayApiRequestMessageContent.cs similarity index 65% rename from src/redmine-net-api/Serialization/ISerialization.cs rename to src/redmine-net-api/Net/WebClient/MessageContent/ByteArrayApiRequestMessageContent.cs index 52a0c317..4f72fc83 100644 --- a/src/redmine-net-api/Serialization/ISerialization.cs +++ b/src/redmine-net-api/Net/WebClient/MessageContent/ByteArrayApiRequestMessageContent.cs @@ -14,16 +14,12 @@ You may obtain a copy of the License at limitations under the License. */ -namespace Redmine.Net.Api.Serialization +namespace Redmine.Net.Api.Net.WebClient.MessageContent; + +internal class ByteArrayApiRequestMessageContent : ApiRequestMessageContent { - internal interface IRedmineSerializer + public ByteArrayApiRequestMessageContent(byte[] content) { - string Type { get; } - - string Serialize(T obj) where T : class; - - PagedResults DeserializeToPagedResults(string response) where T : class, new(); - - T Deserialize(string response) where T : new(); + Body = content; } } \ No newline at end of file diff --git a/src/redmine-net-api/Net/WebClient/MessageContent/StreamApiRequestMessageContent.cs b/src/redmine-net-api/Net/WebClient/MessageContent/StreamApiRequestMessageContent.cs new file mode 100644 index 00000000..e7527234 --- /dev/null +++ b/src/redmine-net-api/Net/WebClient/MessageContent/StreamApiRequestMessageContent.cs @@ -0,0 +1,25 @@ +/* + Copyright 2011 - 2023 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +namespace Redmine.Net.Api.Net.WebClient.MessageContent; + +internal sealed class StreamApiRequestMessageContent : ByteArrayApiRequestMessageContent +{ + public StreamApiRequestMessageContent(byte[] content) : base(content) + { + ContentType = RedmineConstants.CONTENT_TYPE_APPLICATION_STREAM; + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Net/WebClient/StringApiRequestMessageContent.cs b/src/redmine-net-api/Net/WebClient/MessageContent/StringApiRequestMessageContent.cs similarity index 58% rename from src/redmine-net-api/Net/WebClient/StringApiRequestMessageContent.cs rename to src/redmine-net-api/Net/WebClient/MessageContent/StringApiRequestMessageContent.cs index 8806a562..9d02d69a 100644 --- a/src/redmine-net-api/Net/WebClient/StringApiRequestMessageContent.cs +++ b/src/redmine-net-api/Net/WebClient/MessageContent/StringApiRequestMessageContent.cs @@ -1,7 +1,23 @@ +/* + Copyright 2011 - 2023 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + using System; using System.Text; -namespace Redmine.Net.Api.Net.WebClient; +namespace Redmine.Net.Api.Net.WebClient.MessageContent; internal sealed class StringApiRequestMessageContent : ByteArrayApiRequestMessageContent { diff --git a/src/redmine-net-api/Net/WebClient/StreamApiRequestMessageContent.cs b/src/redmine-net-api/Net/WebClient/StreamApiRequestMessageContent.cs deleted file mode 100644 index c04820df..00000000 --- a/src/redmine-net-api/Net/WebClient/StreamApiRequestMessageContent.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Redmine.Net.Api.Net.WebClient; - -internal sealed class StreamApiRequestMessageContent : ByteArrayApiRequestMessageContent -{ - public StreamApiRequestMessageContent(byte[] content) : base(content) - { - ContentType = RedmineConstants.CONTENT_TYPE_APPLICATION_STREAM; - } -} \ No newline at end of file From 0d4eb5cd14d14909d7930094e8e8b7206a1b1353 Mon Sep 17 00:00:00 2001 From: zapadi Date: Fri, 5 Jan 2024 13:23:43 +0200 Subject: [PATCH 339/549] [Rename] RedmineApiClient to InternalRedmineApiWebClient --- ...ient.cs => InternalRedmineApiWebClient.cs} | 38 ++++++++++++++++--- 1 file changed, 32 insertions(+), 6 deletions(-) rename src/redmine-net-api/Net/WebClient/{RedmineApiClient.cs => InternalRedmineApiWebClient.cs} (88%) diff --git a/src/redmine-net-api/Net/WebClient/RedmineApiClient.cs b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs similarity index 88% rename from src/redmine-net-api/Net/WebClient/RedmineApiClient.cs rename to src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs index 2bb19404..5a9b9c45 100644 --- a/src/redmine-net-api/Net/WebClient/RedmineApiClient.cs +++ b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs @@ -1,12 +1,30 @@ +/* + Copyright 2011 - 2023 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + using System; using System.Collections.Specialized; using System.Net; using System.Text; using System.Threading; +using Redmine.Net.Api.Authentication; #if!(NET20) using System.Threading.Tasks; #endif using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Net.WebClient.MessageContent; using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api.Net.WebClient @@ -14,19 +32,19 @@ namespace Redmine.Net.Api.Net.WebClient /// /// /// - internal sealed class RedmineApiClient : IRedmineApiClient + internal sealed class InternalRedmineApiWebClient : IRedmineApiClient { private readonly Func _webClientFunc; private readonly IRedmineAuthentication _credentials; private readonly IRedmineSerializer _serializer; - public RedmineApiClient(RedmineManagerOptions redmineManagerOptions) - : this(() => new InternalRedmineWebClient(redmineManagerOptions), redmineManagerOptions.Authentication, redmineManagerOptions.Serializer) + public InternalRedmineApiWebClient(RedmineManagerOptions redmineManagerOptions) + : this(() => new InternalWebClient(redmineManagerOptions), redmineManagerOptions.Authentication, redmineManagerOptions.Serializer) { ConfigureServicePointManager(redmineManagerOptions.ClientOptions); } - public RedmineApiClient(Func webClientFunc, IRedmineAuthentication authentication, IRedmineSerializer serializer) + public InternalRedmineApiWebClient(Func webClientFunc, IRedmineAuthentication authentication, IRedmineSerializer serializer) { _webClientFunc = webClientFunc; _credentials = authentication; @@ -306,7 +324,15 @@ private void SetWebClientHeaders(System.Net.WebClient webClient, ApiRequestMessa webClient.QueryString = requestMessage.QueryString; } - webClient.Headers.Add(_credentials.AuthenticationType, _credentials.Token); + switch (_credentials) + { + case RedmineApiKeyAuthentication: + webClient.Headers.Add(_credentials.AuthenticationType,_credentials.Token); + break; + case RedmineBasicAuthentication: + webClient.Headers.Add("Authorization", $"{_credentials.AuthenticationType} {_credentials.Token}"); + break; + } if (!requestMessage.ImpersonateUser.IsNullOrWhiteSpace()) { @@ -321,7 +347,7 @@ private static bool IsGetOrDownload(string method) private static string GetContentType(IRedmineSerializer serializer) { - return serializer.Format == "xml" ? RedmineConstants.CONTENT_TYPE_APPLICATION_XML : RedmineConstants.CONTENT_TYPE_APPLICATION_JSON; + return serializer.Format == RedmineConstants.XML ? RedmineConstants.CONTENT_TYPE_APPLICATION_XML : RedmineConstants.CONTENT_TYPE_APPLICATION_JSON; } } } \ No newline at end of file From 0907336609528ce6e912f8cf343067f062147cce Mon Sep 17 00:00:00 2001 From: zapadi Date: Fri, 5 Jan 2024 13:24:28 +0200 Subject: [PATCH 340/549] [Rename] InternalRedmineWebClient to InternalwebClient --- ...dmineWebClient.cs => InternalWebClient.cs} | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) rename src/redmine-net-api/Net/WebClient/{InternalRedmineWebClient.cs => InternalWebClient.cs} (81%) diff --git a/src/redmine-net-api/Net/WebClient/InternalRedmineWebClient.cs b/src/redmine-net-api/Net/WebClient/InternalWebClient.cs similarity index 81% rename from src/redmine-net-api/Net/WebClient/InternalRedmineWebClient.cs rename to src/redmine-net-api/Net/WebClient/InternalWebClient.cs index c2f35660..27051719 100644 --- a/src/redmine-net-api/Net/WebClient/InternalRedmineWebClient.cs +++ b/src/redmine-net-api/Net/WebClient/InternalWebClient.cs @@ -1,3 +1,18 @@ +/* + Copyright 2011 - 2023 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ using System; using System.Net; using Redmine.Net.Api.Exceptions; @@ -5,15 +20,17 @@ namespace Redmine.Net.Api.Net.WebClient; -internal sealed class InternalRedmineWebClient : System.Net.WebClient +internal sealed class InternalWebClient : System.Net.WebClient { private readonly IRedmineApiClientOptions _webClientSettings; - public InternalRedmineWebClient(RedmineManagerOptions redmineManagerOptions) + #pragma warning disable SYSLIB0014 + public InternalWebClient(RedmineManagerOptions redmineManagerOptions) { _webClientSettings = redmineManagerOptions.ClientOptions; BaseAddress = redmineManagerOptions.BaseAddress.ToString(); } + #pragma warning restore SYSLIB0014 protected override WebRequest GetWebRequest(Uri address) { From 7eb221e9e262fe9d7f8f0f9f5ec4e8c6baa52e48 Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 9 Jan 2024 20:03:01 +0200 Subject: [PATCH 341/549] [Exceptions] Add preprocessor directive for NET8 or greater --- src/redmine-net-api/Exceptions/ConflictException.cs | 2 ++ src/redmine-net-api/Exceptions/ForbiddenException.cs | 2 ++ src/redmine-net-api/Exceptions/InternalServerErrorException.cs | 2 ++ .../Exceptions/NameResolutionFailureException.cs | 2 ++ src/redmine-net-api/Exceptions/NotAcceptableException.cs | 2 ++ src/redmine-net-api/Exceptions/NotFoundException.cs | 2 ++ src/redmine-net-api/Exceptions/RedmineApiException.cs | 2 ++ src/redmine-net-api/Exceptions/RedmineException.cs | 2 ++ src/redmine-net-api/Exceptions/RedmineTimeoutException.cs | 2 ++ src/redmine-net-api/Exceptions/UnauthorizedException.cs | 3 ++- 10 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/redmine-net-api/Exceptions/ConflictException.cs b/src/redmine-net-api/Exceptions/ConflictException.cs index 1cb1dcf9..bc098687 100644 --- a/src/redmine-net-api/Exceptions/ConflictException.cs +++ b/src/redmine-net-api/Exceptions/ConflictException.cs @@ -73,6 +73,7 @@ public ConflictException(string format, Exception innerException, params object[ { } +#if !(NET8_0_OR_GREATER) /// /// /// @@ -82,5 +83,6 @@ private ConflictException(SerializationInfo serializationInfo, StreamingContext { } +#endif } } \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/ForbiddenException.cs b/src/redmine-net-api/Exceptions/ForbiddenException.cs index 6821eb19..75e6192b 100644 --- a/src/redmine-net-api/Exceptions/ForbiddenException.cs +++ b/src/redmine-net-api/Exceptions/ForbiddenException.cs @@ -73,6 +73,7 @@ public ForbiddenException(string format, Exception innerException, params object { } +#if !(NET8_0_OR_GREATER) /// /// /// @@ -83,5 +84,6 @@ private ForbiddenException(SerializationInfo serializationInfo, StreamingContext { } +#endif } } \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/InternalServerErrorException.cs b/src/redmine-net-api/Exceptions/InternalServerErrorException.cs index bf6dda64..ccf3d5aa 100644 --- a/src/redmine-net-api/Exceptions/InternalServerErrorException.cs +++ b/src/redmine-net-api/Exceptions/InternalServerErrorException.cs @@ -73,6 +73,7 @@ public InternalServerErrorException(string format, Exception innerException, par { } +#if !(NET8_0_OR_GREATER) /// /// /// @@ -83,5 +84,6 @@ private InternalServerErrorException(SerializationInfo serializationInfo, Stream { } +#endif } } \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/NameResolutionFailureException.cs b/src/redmine-net-api/Exceptions/NameResolutionFailureException.cs index 3651d6db..81da3053 100644 --- a/src/redmine-net-api/Exceptions/NameResolutionFailureException.cs +++ b/src/redmine-net-api/Exceptions/NameResolutionFailureException.cs @@ -73,6 +73,7 @@ public NameResolutionFailureException(string format, Exception innerException, p { } +#if !(NET8_0_OR_GREATER) /// /// /// @@ -82,5 +83,6 @@ private NameResolutionFailureException(SerializationInfo serializationInfo, Stre { } +#endif } } \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/NotAcceptableException.cs b/src/redmine-net-api/Exceptions/NotAcceptableException.cs index 6b1ef5ea..0c865fbc 100644 --- a/src/redmine-net-api/Exceptions/NotAcceptableException.cs +++ b/src/redmine-net-api/Exceptions/NotAcceptableException.cs @@ -73,6 +73,7 @@ public NotAcceptableException(string format, Exception innerException, params ob { } +#if !(NET8_0_OR_GREATER) /// /// /// @@ -82,5 +83,6 @@ private NotAcceptableException(SerializationInfo serializationInfo, StreamingCon { } +#endif } } \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/NotFoundException.cs b/src/redmine-net-api/Exceptions/NotFoundException.cs index 71722e93..cde236b1 100644 --- a/src/redmine-net-api/Exceptions/NotFoundException.cs +++ b/src/redmine-net-api/Exceptions/NotFoundException.cs @@ -74,6 +74,7 @@ public NotFoundException(string format, Exception innerException, params object[ { } +#if !(NET8_0_OR_GREATER) /// /// /// @@ -83,5 +84,6 @@ private NotFoundException(SerializationInfo serializationInfo, StreamingContext { } +#endif } } \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/RedmineApiException.cs b/src/redmine-net-api/Exceptions/RedmineApiException.cs index 9a544503..a858e304 100644 --- a/src/redmine-net-api/Exceptions/RedmineApiException.cs +++ b/src/redmine-net-api/Exceptions/RedmineApiException.cs @@ -73,6 +73,7 @@ public RedmineApiException(string message, Exception inner, string errorCode, bo /// Value indicating whether the exception is transient or not. public bool IsTransient { get; } + #if !(NET8_0_OR_GREATER) /// public override void GetObjectData(SerializationInfo info, StreamingContext context) { @@ -81,5 +82,6 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont info.AddValue(nameof(this.ErrorCode), this.ErrorCode); info.AddValue(nameof(this.IsTransient), this.IsTransient); } + #endif } } \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/RedmineException.cs b/src/redmine-net-api/Exceptions/RedmineException.cs index 9ce2af67..ccb10313 100644 --- a/src/redmine-net-api/Exceptions/RedmineException.cs +++ b/src/redmine-net-api/Exceptions/RedmineException.cs @@ -74,6 +74,7 @@ public RedmineException(string format, Exception innerException, params object[] { } + #if !(NET8_0_OR_GREATER) /// /// /// @@ -83,5 +84,6 @@ protected RedmineException(SerializationInfo serializationInfo, StreamingContext { } + #endif } } \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/RedmineTimeoutException.cs b/src/redmine-net-api/Exceptions/RedmineTimeoutException.cs index 0f4d89c4..a919fd96 100644 --- a/src/redmine-net-api/Exceptions/RedmineTimeoutException.cs +++ b/src/redmine-net-api/Exceptions/RedmineTimeoutException.cs @@ -76,6 +76,7 @@ public RedmineTimeoutException(string format, Exception innerException, params o { } +#if !(NET8_0_OR_GREATER) /// /// /// @@ -85,5 +86,6 @@ private RedmineTimeoutException(SerializationInfo serializationInfo, StreamingCo { } +#endif } } \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/UnauthorizedException.cs b/src/redmine-net-api/Exceptions/UnauthorizedException.cs index 7e063c58..c77c37f8 100644 --- a/src/redmine-net-api/Exceptions/UnauthorizedException.cs +++ b/src/redmine-net-api/Exceptions/UnauthorizedException.cs @@ -76,7 +76,7 @@ public UnauthorizedException(string format, Exception innerException, params obj : base(string.Format(CultureInfo.InvariantCulture,format, args), innerException) { } - +#if !(NET8_0_OR_GREATER) /// /// /// @@ -87,5 +87,6 @@ private UnauthorizedException(SerializationInfo serializationInfo, StreamingCont { } +#endif } } \ No newline at end of file From 7898d66df037df2595c738952d51293a54ff2d0b Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 9 Jan 2024 20:07:27 +0200 Subject: [PATCH 342/549] [Obsolete] Mark methods & extensions --- ... RedmineManagerAsyncExtensionsObsolete.cs} | 14 +- src/redmine-net-api/IRedmineManager.cs | 326 +++++------------ .../IRedmineManagerObsolete.cs | 306 ++++++++++++++++ ...Client.cs => IRedmineWebClientObsolete.cs} | 1 + ...bClient.cs => RedmineWebClientObsolete.cs} | 7 + src/redmine-net-api/RedmineManager.cs | 333 +++++------------- src/redmine-net-api/RedmineManagerObsolete.cs | 275 ++++++++++++++- ...sync.cs => RedmineManagerAsyncObsolete.cs} | 4 +- 8 files changed, 749 insertions(+), 517 deletions(-) rename src/redmine-net-api/Extensions/{RedmineManagerAsyncExtensions.cs => RedmineManagerAsyncExtensionsObsolete.cs} (95%) create mode 100644 src/redmine-net-api/IRedmineManagerObsolete.cs rename src/redmine-net-api/Net/WebClient/{IRedmineWebClient.cs => IRedmineWebClientObsolete.cs} (97%) rename src/redmine-net-api/Net/WebClient/{RedmineWebClient.cs => RedmineWebClientObsolete.cs} (96%) rename src/redmine-net-api/_net20/{RedmineManagerAsync.cs => RedmineManagerAsyncObsolete.cs} (98%) diff --git a/src/redmine-net-api/Extensions/RedmineManagerAsyncExtensions.cs b/src/redmine-net-api/Extensions/RedmineManagerAsyncExtensionsObsolete.cs similarity index 95% rename from src/redmine-net-api/Extensions/RedmineManagerAsyncExtensions.cs rename to src/redmine-net-api/Extensions/RedmineManagerAsyncExtensionsObsolete.cs index fad99217..dd605bef 100644 --- a/src/redmine-net-api/Extensions/RedmineManagerAsyncExtensions.cs +++ b/src/redmine-net-api/Extensions/RedmineManagerAsyncExtensionsObsolete.cs @@ -220,7 +220,7 @@ public static async Task RemoveWatcherFromIssueAsync(this RedmineManager redmine [Obsolete(RedmineConstants.OBSOLETE_TEXT)] public static async Task CountAsync(this RedmineManager redmineManager, params string[] include) where T : class, new() { - return await RedmineManagerExtensions.CountAsync(redmineManager, include).ConfigureAwait(false); + return await redmineManager.CountAsync(null, CancellationToken.None).ConfigureAwait(false); } /// @@ -265,7 +265,7 @@ public static async Task> GetObjectsAsync(this RedmineManager redmine where T : class, new() { var requestOptions = RedmineManagerExtensions.CreateRequestOptions(parameters); - return await redmineManager.GetObjectsAsync(requestOptions).ConfigureAwait(false); + return await redmineManager.GetAsync(requestOptions).ConfigureAwait(false); } /// @@ -281,7 +281,7 @@ public static async Task GetObjectAsync(this RedmineManager redmineManager where T : class, new() { var requestOptions = RedmineManagerExtensions.CreateRequestOptions(parameters); - return await redmineManager.GetObjectAsync(id, requestOptions).ConfigureAwait(false); + return await redmineManager.GetAsync(id, requestOptions).ConfigureAwait(false); } /// @@ -296,7 +296,7 @@ public static async Task CreateObjectAsync(this RedmineManager redmineMana where T : class, new() { var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); - return await redmineManager.CreateObjectAsync(entity, null, requestOptions).ConfigureAwait(false); + return await redmineManager.CreateAsync(entity, null, requestOptions).ConfigureAwait(false); } /// @@ -312,7 +312,7 @@ public static async Task CreateObjectAsync(this RedmineManager redmineMana where T : class, new() { var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); - return await redmineManager.CreateObjectAsync(entity, ownerId, requestOptions, CancellationToken.None).ConfigureAwait(false); + return await redmineManager.CreateAsync(entity, ownerId, requestOptions, CancellationToken.None).ConfigureAwait(false); } /// @@ -328,7 +328,7 @@ public static async Task UpdateObjectAsync(this RedmineManager redmineManager where T : class, new() { var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); - await redmineManager.UpdateObjectAsync(id, entity, requestOptions).ConfigureAwait(false); + await redmineManager.UpdateAsync(id, entity, requestOptions).ConfigureAwait(false); } /// @@ -343,7 +343,7 @@ public static async Task DeleteObjectAsync(this RedmineManager redmineManager where T : class, new() { var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); - await redmineManager.DeleteObjectAsync(id, requestOptions).ConfigureAwait(false); + await redmineManager.DeleteAsync(id, requestOptions).ConfigureAwait(false); } /// diff --git a/src/redmine-net-api/IRedmineManager.cs b/src/redmine-net-api/IRedmineManager.cs index c3519e8b..b8148141 100644 --- a/src/redmine-net-api/IRedmineManager.cs +++ b/src/redmine-net-api/IRedmineManager.cs @@ -14,260 +14,102 @@ You may obtain a copy of the License at limitations under the License. */ -using System; using System.Collections.Generic; -using System.Collections.Specialized; -using System.Net; -using System.Net.Security; -using System.Security.Cryptography.X509Certificates; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Net; using Redmine.Net.Api.Serialization; -namespace Redmine.Net.Api.Types +namespace Redmine.Net.Api.Types; + +/// +/// +/// +public partial interface IRedmineManager { /// /// /// - public interface IRedmineManager - { - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)]string Host { get; } - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)]string ApiKey { get; } - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)]int PageSize { get; set; } - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)]string ImpersonateUser { get; set; } - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)]MimeFormat MimeFormat { get; } - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)]IWebProxy Proxy { get; } - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)]SecurityProtocolType SecurityProtocolType { get; } - - /// - /// - /// - /// - /// - User GetCurrentUser(NameValueCollection parameters = null); - - /// - /// - /// - /// - /// - void AddUserToGroup(int groupId, int userId); - /// - /// - /// - /// - /// - void RemoveUserFromGroup(int groupId, int userId); - - /// - /// - /// - /// - /// - void AddWatcherToIssue(int issueId, int userId); - /// - /// - /// - /// - /// - void RemoveWatcherFromIssue(int issueId, int userId); - - /// - /// - /// - /// - /// - /// - /// - WikiPage CreateWikiPage(string projectId, string pageName, WikiPage wikiPage); - - /// - /// - /// - /// - /// - /// - void UpdateWikiPage(string projectId, string pageName, WikiPage wikiPage); - - /// - /// - /// - /// - /// - /// - /// - /// - WikiPage GetWikiPage(string projectId, NameValueCollection parameters, string pageName, uint version = 0); - /// - /// - /// - /// - /// - List GetAllWikiPages(string projectId); - /// - /// - /// - /// - /// - void DeleteWikiPage(string projectId, string pageName); + /// + /// + /// + int Count(RequestOptions requestOptions = null) + where T : class, new(); - /// - /// - /// - /// - /// - Upload UploadFile(byte[] data); - /// - /// - /// - /// - /// - void UpdateAttachment(int issueId, Attachment attachment); + /// + /// + /// + /// + /// + /// + /// + T Get(string id, RequestOptions requestOptions = null) + where T : class, new(); - /// - /// - /// - /// query strings. enable to specify multiple values separated by a space " ". - /// number of results in response. - /// skip this number of results in response - /// Optional filters. - /// - /// Returns the search results by the specified condition parameters. - /// - PagedResults Search(string q, int limit , int offset = 0, - SearchFilterBuilder searchFilter = null); - - /// - /// - /// - /// - /// - byte[] DownloadFile(string address); - - /// - /// - /// - /// - /// - /// - PagedResults GetPaginatedObjects(NameValueCollection parameters) where T : class, new(); + /// + /// + /// + /// + /// + /// + List Get(RequestOptions requestOptions = null) + where T : class, new(); - /// - /// - /// - /// - /// - /// - int Count(params string[] include) where T : class, new(); - - /// - /// - /// - /// - /// - /// - int Count(NameValueCollection parameters = null) where T : class, new(); + /// + /// + /// + /// + /// + /// + PagedResults GetPaginated(RequestOptions requestOptions = null) + where T : class, new(); - /// - /// - /// - /// - /// - /// - /// - T GetObject(string id, NameValueCollection parameters) where T : class, new(); - /// - /// - /// - /// - /// - /// - /// - /// - List GetObjects(int limit, int offset, params string[] include) where T : class, new(); - /// - /// - /// - /// - /// - /// - List GetObjects(params string[] include) where T : class, new(); + /// + /// + /// + /// + /// + /// + /// + /// + T Create(T entity, string ownerId = null,RequestOptions requestOptions = null) + where T : class, new(); - /// - /// - /// - /// - /// - /// - List GetObjects(NameValueCollection parameters) where T : class, new(); + /// + /// + /// + /// + /// + /// + /// + /// + void Update(string id, T entity, string projectId = null, RequestOptions requestOptions = null) + where T : class, new(); - /// - /// - /// - /// - /// - /// - T CreateObject(T entity) where T : class, new(); - /// - /// - /// - /// - /// - /// - /// - T CreateObject(T entity, string ownerId) where T : class, new(); - - /// - /// - /// - /// - /// - /// - /// - void UpdateObject(string id, T entity, string projectId = null) where T : class, new(); + /// + /// + /// + /// + /// + /// + void Delete(string id, RequestOptions requestOptions = null) + where T : class, new(); + + /// + /// Support for adding attachments through the REST API is added in Redmine 1.4.0. + /// Upload a file to server. + /// + /// The content of the file that will be uploaded on server. + /// + /// Returns the token for uploaded file. + /// + /// + Upload UploadFile(byte[] data); - /// - /// - /// - /// - /// - /// - void DeleteObject(string id, NameValueCollection parameters = null) where T : class, new(); - - /// - /// - /// - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)]RedmineWebClient CreateWebClient(NameValueCollection parameters, bool uploadFile = false); - /// - /// - /// - /// - /// - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)]bool RemoteCertValidate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors sslPolicyErrors); - } + /// + /// Downloads a file from the specified address. + /// + /// The address. + /// The content of the downloaded file as a byte array. + /// + byte[] DownloadFile(string address); } \ No newline at end of file diff --git a/src/redmine-net-api/IRedmineManagerObsolete.cs b/src/redmine-net-api/IRedmineManagerObsolete.cs new file mode 100644 index 00000000..553627f9 --- /dev/null +++ b/src/redmine-net-api/IRedmineManagerObsolete.cs @@ -0,0 +1,306 @@ +/* + Copyright 2011 - 2023 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Net; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using Redmine.Net.Api.Serialization; + +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + public partial interface IRedmineManager + { + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + string Host { get; } + + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + string ApiKey { get; } + + /// + /// Maximum page-size when retrieving complete object lists + /// + /// By default only 25 results can be retrieved per request. Maximum is 100. To change the maximum value set + /// in your Settings -> General, "Objects per page options".By adding (for instance) 9999 there would make you + /// able to get that many results per request. + /// + /// + /// + /// The size of the page. + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + int PageSize { get; set; } + + /// + /// As of Redmine 2.2.0 you can impersonate user setting user login (eg. jsmith). This only works when using the API + /// with an administrator account, this header will be ignored when using the API with a regular user account. + /// + /// + /// The impersonate user. + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + string ImpersonateUser { get; set; } + + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + MimeFormat MimeFormat { get; } + + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + IWebProxy Proxy { get; } + + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + SecurityProtocolType SecurityProtocolType { get; } + + /// + /// + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'GetCurrentUser' extension instead")] + User GetCurrentUser(NameValueCollection parameters = null); + + /// + /// + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'AddUserToGroup' extension instead")] + void AddUserToGroup(int groupId, int userId); + + /// + /// + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'RemoveUserFromGroup' extension instead")] + void RemoveUserFromGroup(int groupId, int userId); + + /// + /// + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'AddWatcherToIssue' extension instead")] + void AddWatcherToIssue(int issueId, int userId); + + /// + /// + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'RemoveWatcherFromIssue' extension instead")] + void RemoveWatcherFromIssue(int issueId, int userId); + + /// + /// + /// + /// + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'CreateWikiPage' extension instead")] + WikiPage CreateWikiPage(string projectId, string pageName, WikiPage wikiPage); + + /// + /// + /// + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'UpdateWikiPage' extension instead")] + void UpdateWikiPage(string projectId, string pageName, WikiPage wikiPage); + + /// + /// + /// + /// + /// + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'GetWikiPage' extension instead")] + WikiPage GetWikiPage(string projectId, NameValueCollection parameters, string pageName, uint version = 0); + + /// + /// + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'GetAllWikiPages' extension instead")] + List GetAllWikiPages(string projectId); + + /// + /// + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'DeleteWikiPage' extension instead")] + void DeleteWikiPage(string projectId, string pageName); + + /// + /// + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'UpdateAttachment' extension instead")] + void UpdateAttachment(int issueId, Attachment attachment); + + /// + /// + /// + /// query strings. enable to specify multiple values separated by a space " ". + /// number of results in response. + /// skip this number of results in response + /// Optional filters. + /// + /// Returns the search results by the specified condition parameters. + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'Search' extension instead")] + PagedResults Search(string q, int limit , int offset = 0, SearchFilterBuilder searchFilter = null); + + /// + /// + /// + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'GetPaginated' method instead")] + PagedResults GetPaginatedObjects(NameValueCollection parameters) where T : class, new(); + + /// + /// + /// + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'Count' method instead")] + int Count(params string[] include) where T : class, new(); + + /// + /// + /// + /// + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'Get' method instead")] + T GetObject(string id, NameValueCollection parameters) where T : class, new(); + + /// + /// + /// + /// + /// + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'Get' method instead")] + List GetObjects(int limit, int offset, params string[] include) where T : class, new(); + + /// + /// + /// + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'Get' method instead")] + List GetObjects(params string[] include) where T : class, new(); + + /// + /// + /// + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'Get' method instead")] + List GetObjects(NameValueCollection parameters) where T : class, new(); + + /// + /// + /// + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'Create' method instead")] + T CreateObject(T entity) where T : class, new(); + /// + /// + /// + /// + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'Create' method instead")] + T CreateObject(T entity, string ownerId) where T : class, new(); + + /// + /// + /// + /// + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'Update' method instead")] + void UpdateObject(string id, T entity, string projectId = null) where T : class, new(); + + /// + /// + /// + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'Delete' method instead")] + void DeleteObject(string id, NameValueCollection parameters = null) where T : class, new(); + + /// + /// + /// + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + RedmineWebClient CreateWebClient(NameValueCollection parameters, bool uploadFile = false); + /// + /// + /// + /// + /// + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + bool RemoteCertValidate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors sslPolicyErrors); + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Net/WebClient/IRedmineWebClient.cs b/src/redmine-net-api/Net/WebClient/IRedmineWebClientObsolete.cs similarity index 97% rename from src/redmine-net-api/Net/WebClient/IRedmineWebClient.cs rename to src/redmine-net-api/Net/WebClient/IRedmineWebClientObsolete.cs index 91cf5bb2..1f6be22c 100644 --- a/src/redmine-net-api/Net/WebClient/IRedmineWebClient.cs +++ b/src/redmine-net-api/Net/WebClient/IRedmineWebClientObsolete.cs @@ -24,6 +24,7 @@ namespace Redmine.Net.Api.Types /// /// /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] public interface IRedmineWebClient { /// diff --git a/src/redmine-net-api/Net/WebClient/RedmineWebClient.cs b/src/redmine-net-api/Net/WebClient/RedmineWebClientObsolete.cs similarity index 96% rename from src/redmine-net-api/Net/WebClient/RedmineWebClient.cs rename to src/redmine-net-api/Net/WebClient/RedmineWebClientObsolete.cs index 6db3ee4d..688a499e 100644 --- a/src/redmine-net-api/Net/WebClient/RedmineWebClient.cs +++ b/src/redmine-net-api/Net/WebClient/RedmineWebClientObsolete.cs @@ -24,6 +24,8 @@ namespace Redmine.Net.Api /// /// /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + #pragma warning disable SYSLIB0014 public class RedmineWebClient : WebClient { private string redirectUrl = string.Empty; @@ -203,7 +205,11 @@ protected void HandleRedirect(WebRequest request, WebResponse response) } // Have to make sure that the "/" symbol is between the "host" and "redirect" strings + #if NET5_0_OR_GREATER + if (!redirectUrl.StartsWith('/') && !host.EndsWith('/')) + #else if (!redirectUrl.StartsWith("/", StringComparison.OrdinalIgnoreCase) && !host.EndsWith("/", StringComparison.OrdinalIgnoreCase)) + #endif { redirectUrl = $"/{redirectUrl}"; } @@ -250,4 +256,5 @@ protected void HandleCookies(WebRequest request, WebResponse response) CookieContainer.Add(col); } } + #pragma warning restore SYSLIB0014 } \ No newline at end of file diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index cac2c338..f683dc3a 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -19,20 +19,12 @@ limitations under the License. using System.Collections.Specialized; using System.Globalization; using System.Net; -using System.Net.Security; -using System.Security.Cryptography.X509Certificates; -using System.Text; -using System.Text.RegularExpressions; using Redmine.Net.Api.Authentication; -using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Internals; using Redmine.Net.Api.Net; using Redmine.Net.Api.Net.WebClient; using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Types; -using Group = Redmine.Net.Api.Types.Group; -using Version = Redmine.Net.Api.Types.Version; namespace Redmine.Net.Api { @@ -76,7 +68,7 @@ public RedmineManager(RedmineManagerOptionsBuilder optionsBuilder) Scheme = _redmineManagerOptions.BaseAddress.Scheme; Proxy = _redmineManagerOptions.ClientOptions.Proxy; Timeout = _redmineManagerOptions.ClientOptions.Timeout; - MimeFormat = "xml".Equals(Serializer.Format, StringComparison.OrdinalIgnoreCase) ? MimeFormat.Xml : MimeFormat.Json; + MimeFormat = RedmineConstants.XML.Equals(Serializer.Format, StringComparison.OrdinalIgnoreCase) ? MimeFormat.Xml : MimeFormat.Json; _redmineManagerOptions.ClientOptions.SecurityProtocolType ??= ServicePointManager.SecurityProtocol; @@ -88,59 +80,25 @@ public RedmineManager(RedmineManagerOptionsBuilder optionsBuilder) } RedmineApiUrls = new RedmineApiUrls(Serializer.Format); - ApiClient = new RedmineApiClient(_redmineManagerOptions); + ApiClient = new InternalRedmineApiWebClient(_redmineManagerOptions); } - - /// - /// Maximum page-size when retrieving complete object lists - /// - /// By default only 25 results can be retrieved per request. Maximum is 100. To change the maximum value set - /// in your Settings -> General, "Objects per page options".By adding (for instance) 9999 there would make you - /// able to get that many results per request. - /// - /// - /// - /// The size of the page. - /// - public int PageSize { get; set; } - - /// - /// As of Redmine 2.2.0 you can impersonate user setting user login (eg. jsmith). This only works when using the API - /// with an administrator account, this header will be ignored when using the API with a regular user account. - /// - /// - /// The impersonate user. - /// - public string ImpersonateUser { get; set; } - /// - /// - /// - /// - /// - /// - public int Count(params string[] include) where T : class, new() - { - var parameters = NameValueCollectionExtensions.AddParamsIfExist(null, include); - - return Count(parameters); - } - - /// - /// - /// - /// - /// - /// - public int Count(NameValueCollection parameters) where T : class, new() + /// + public int Count(RequestOptions requestOptions = null) + where T : class, new() { var totalCount = 0; const int PAGE_SIZE = 1; const int OFFSET = 0; - parameters.AddPagingParameters(PAGE_SIZE, OFFSET); + if (requestOptions == null) + { + requestOptions = new RequestOptions(); + } - var tempResult = GetPaginatedObjects(parameters); + requestOptions.QueryString = requestOptions.QueryString.AddPagingParameters(PAGE_SIZE, OFFSET); + + var tempResult = GetPaginatedObjects(requestOptions.QueryString); if (tempResult != null) { @@ -150,98 +108,95 @@ public RedmineManager(RedmineManagerOptionsBuilder optionsBuilder) return totalCount; } - /// - /// Gets the redmine object based on id. - /// - /// The type of objects to retrieve. - /// The id of the object. - /// Optional filters and/or optional fetched data. - /// - /// Returns the object of type T. - /// - /// - /// - /// string issueId = "927"; - /// NameValueCollection parameters = null; - /// Issue issue = redmineManager.GetObject<Issue>(issueId, parameters); - /// - /// - public T GetObject(string id, NameValueCollection parameters) where T : class, new() + /// + public T Get(string id, RequestOptions requestOptions = null) + where T : class, new() { var url = RedmineApiUrls.GetFragment(id); - var response = ApiClient.Get(url, parameters != null ? new RequestOptions { QueryString = parameters } : null); + var response = ApiClient.Get(url, requestOptions); return response.DeserializeTo(Serializer); } - /// - /// Returns the complete list of objects. - /// - /// - /// Optional fetched data. - /// - /// Optional fetched data: - /// Project: trackers, issue_categories, enabled_modules (since Redmine 2.6.0) - /// Issue: children, attachments, relations, changesets, journals, watchers (since Redmine 2.3.0) - /// Users: memberships, groups (since Redmine 2.1) - /// Groups: users, memberships - /// - /// Returns the complete list of objects. - public List GetObjects(params string[] include) where T : class, new() + /// + public List Get(RequestOptions requestOptions = null) + where T : class, new() + { + var uri = RedmineApiUrls.GetListFragment(); + + return GetObjects(uri, requestOptions); + } + + /// + public PagedResults GetPaginated(RequestOptions requestOptions = null) + where T : class, new() + { + var url = RedmineApiUrls.GetListFragment(); + + return GetPaginatedObjects(url, requestOptions); + } + + /// + public T Create(T entity, string ownerId = null, RequestOptions requestOptions = null) + where T : class, new() { - var parameters = NameValueCollectionExtensions.AddParamsIfExist(null, include); + var url = RedmineApiUrls.CreateEntityFragment(ownerId); - return GetObjects(parameters); + var payload = Serializer.Serialize(entity); + + var response = ApiClient.Create(url, payload, requestOptions); + + return response.DeserializeTo(Serializer); } - - /// - /// Returns the complete list of objects. - /// - /// - /// The page size. - /// The offset. - /// Optional fetched data. - /// - /// Optional fetched data: - /// Project: trackers, issue_categories, enabled_modules (since 2.6.0) - /// Issue: children, attachments, relations, changesets, journals, watchers - Since 2.3.0 - /// Users: memberships, groups (added in 2.1) - /// Groups: users, memberships - /// - /// Returns the complete list of objects. - public List GetObjects(int limit, int offset, params string[] include) where T : class, new() + + /// + public void Update(string id, T entity, string projectId = null, RequestOptions requestOptions = null) + where T : class, new() + { + var url = RedmineApiUrls.UpdateFragment(id); + + var payload = Serializer.Serialize(entity); + + ApiClient.Update(url, payload, requestOptions); + } + + /// + public void Delete(string id, RequestOptions requestOptions = null) + where T : class, new() { - var parameters = NameValueCollectionExtensions - .AddParamsIfExist(null, include) - .AddPagingParameters(limit, offset); + var url = RedmineApiUrls.DeleteFragment(id); - return GetObjects(parameters); + ApiClient.Delete(url, requestOptions); } - /// - /// Returns the complete list of objects. - /// - /// The type of objects to retrieve. - /// Optional filters and/or optional fetched data. - /// - /// Returns a complete list of objects. - /// - public List GetObjects(NameValueCollection parameters = null) where T : class, new() + /// + public Upload UploadFile(byte[] data) { - var uri = RedmineApiUrls.GetListFragment(); + var url = RedmineApiUrls.UploadFragment(); + + var response = ApiClient.Upload(url, data); - return GetObjects(uri, parameters != null ? new RequestOptions { QueryString = parameters } : null); + return response.DeserializeTo(Serializer); } - /// + /// + public byte[] DownloadFile(string address) + { + var response = ApiClient.Download(address); + + return response.Content; + } + + /// /// /// /// /// /// /// - internal List GetObjects(string uri, RequestOptions requestOptions = null) where T : class, new() + internal List GetObjects(string uri, RequestOptions requestOptions = null) + where T : class, new() { int pageSize = 0, offset = 0; var isLimitSet = false; @@ -304,20 +259,7 @@ public RedmineManager(RedmineManagerOptionsBuilder optionsBuilder) return resultList; } - - /// - /// Gets the paginated objects. - /// - /// - /// The parameters. - /// - public PagedResults GetPaginatedObjects(NameValueCollection parameters) where T : class, new() - { - var url = RedmineApiUrls.GetListFragment(); - - return GetPaginatedObjects(url, parameters != null ? new RequestOptions { QueryString = parameters } : null); - } - + /// /// /// @@ -325,7 +267,8 @@ public RedmineManager(RedmineManagerOptionsBuilder optionsBuilder) /// /// /// - internal PagedResults GetPaginatedObjects(string uri = null, RequestOptions requestOptions = null) where T : class, new() + internal PagedResults GetPaginatedObjects(string uri = null, RequestOptions requestOptions = null) + where T : class, new() { uri = uri.IsNullOrWhiteSpace() ? RedmineApiUrls.GetListFragment() : uri; @@ -333,121 +276,5 @@ public RedmineManager(RedmineManagerOptionsBuilder optionsBuilder) return response.DeserializeToPagedResults(Serializer); } - - /// - /// Creates a new Redmine object. - /// - /// The type of object to create. - /// The object to create. - /// - /// - /// - /// When trying to create an object with invalid or missing attribute parameters, you will get a 422 Unprocessable - /// Entity response. That means that the object could not be created. - /// - public T CreateObject(T entity) where T : class, new() - { - return CreateObject(entity, null); - } - - /// - /// Creates a new Redmine object. - /// - /// The type of object to create. - /// The object to create. - /// The owner identifier. - /// - /// - /// - /// When trying to create an object with invalid or missing attribute parameters, you will get a 422 Unprocessable - /// Entity response. That means that the object could not be created. - /// - /// - /// - /// var project = new Project(); - /// project.Name = "test"; - /// project.Identifier = "the project identifier"; - /// project.Description = "the project description"; - /// redmineManager.CreateObject(project); - /// - /// - public T CreateObject(T entity, string ownerId) where T : class, new() - { - var url = RedmineApiUrls.CreateEntityFragment(ownerId); - - var payload = Serializer.Serialize(entity); - - var response = ApiClient.Create(url, payload); - - return response.DeserializeTo(Serializer); - } - - /// - /// Updates a Redmine object. - /// - /// The type of object to be update. - /// The id of the object to be update. - /// The object to be update. - /// The project identifier. - /// - /// - /// When trying to update an object with invalid or missing attribute parameters, you will get a - /// 422(RedmineException) Unprocessable Entity response. That means that the object could not be updated. - /// - /// - /// - public void UpdateObject(string id, T entity, string projectId = null) where T : class, new() - { - var url = RedmineApiUrls.UpdateFragment(id); - - var payload = Serializer.Serialize(entity); - - ApiClient.Update(url, payload); - } - - /// - /// Deletes the Redmine object. - /// - /// The type of objects to delete. - /// The id of the object to delete - /// The parameters - /// - /// - public void DeleteObject(string id, NameValueCollection parameters = null) where T : class, new() - { - var url = RedmineApiUrls.DeleteFragment(id); - - ApiClient.Delete(url, parameters != null ? new RequestOptions { QueryString = parameters } : null); - } - - /// - /// Support for adding attachments through the REST API is added in Redmine 1.4.0. - /// Upload a file to server. - /// - /// The content of the file that will be uploaded on server. - /// - /// Returns the token for uploaded file. - /// - /// - public Upload UploadFile(byte[] data) - { - var url = RedmineApiUrls.UploadFragment(); - - var response = ApiClient.Upload(url, data); - - return response.DeserializeTo(Serializer); - } - - /// - /// Downloads a file from the specified address. - /// - /// The address. - /// The content of the downloaded file as a byte array. - /// - public byte[] DownloadFile(string address) - { - var response = ApiClient.Download(address); - return response.Content; - } } } \ No newline at end of file diff --git a/src/redmine-net-api/RedmineManagerObsolete.cs b/src/redmine-net-api/RedmineManagerObsolete.cs index f7b40ebb..c3bdfec6 100644 --- a/src/redmine-net-api/RedmineManagerObsolete.cs +++ b/src/redmine-net-api/RedmineManagerObsolete.cs @@ -20,7 +20,9 @@ limitations under the License. using System.Net; using System.Net.Security; using System.Security.Cryptography.X509Certificates; +using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Net; using Redmine.Net.Api.Net.WebClient; using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Types; @@ -34,7 +36,8 @@ public partial class RedmineManager { /// /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use RedmineConstants.DEFAULT_PAGE_SIZE")]public const int DEFAULT_PAGE_SIZE_VALUE = 25; + [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use RedmineConstants.DEFAULT_PAGE_SIZE")] + public const int DEFAULT_PAGE_SIZE_VALUE = 25; /// /// Initializes a new instance of the class. @@ -125,29 +128,33 @@ public RedmineManager(string host, string login, string password, MimeFormat mim SecurityProtocolType = securityProtocolType })) {} - #region Obsolete + /// /// Gets the suffixes. /// /// /// The suffixes. /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)]public static Dictionary Suffixes => null; + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + public static Dictionary Suffixes => null; /// /// /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)]public string Format { get; } + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + public string Format { get; } /// /// /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)]public string Scheme { get; } + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + public string Scheme { get; } /// /// /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)]public TimeSpan? Timeout { get; } + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + public TimeSpan? Timeout { get; } /// /// Gets the host. @@ -155,7 +162,8 @@ public RedmineManager(string host, string login, string password, MimeFormat mim /// /// The host. /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)]public string Host { get; } + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + public string Host { get; } /// /// The ApiKey used to authenticate. @@ -163,7 +171,8 @@ public RedmineManager(string host, string login, string password, MimeFormat mim /// /// The API key. /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)]public string ApiKey { get; } + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + public string ApiKey { get; } /// /// Gets the MIME format. @@ -171,7 +180,8 @@ public RedmineManager(string host, string login, string password, MimeFormat mim /// /// The MIME format. /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)]public MimeFormat MimeFormat { get; } + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + public MimeFormat MimeFormat { get; } /// /// Gets the proxy. @@ -179,7 +189,8 @@ public RedmineManager(string host, string login, string password, MimeFormat mim /// /// The proxy. /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)]public IWebProxy Proxy { get; } + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + public IWebProxy Proxy { get; } /// /// Gets the type of the security protocol. @@ -187,14 +198,32 @@ public RedmineManager(string host, string login, string password, MimeFormat mim /// /// The type of the security protocol. /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)]public SecurityProtocolType SecurityProtocolType { get; } - #endregion + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + public SecurityProtocolType SecurityProtocolType { get; } + + + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + public int PageSize { get; set; } + + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + public string ImpersonateUser { get; set; } /// /// /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Returns null")] - public static readonly Dictionary TypesWithOffset = null; + [Obsolete(RedmineConstants.OBSOLETE_TEXT )] + public static readonly Dictionary TypesWithOffset = new Dictionary{ + {typeof(Issue), true}, + {typeof(Project), true}, + {typeof(User), true}, + {typeof(News), true}, + {typeof(Query), true}, + {typeof(TimeEntry), true}, + {typeof(ProjectMembership), true}, + {typeof(Search), true} + }; /// /// Returns the user whose credentials are used to access the API. @@ -356,6 +385,224 @@ public PagedResults Search(string q, int limit = DEFAULT_PAGE_SIZE_VALUE return RedmineManagerExtensions.Search(this, q, limit, offset, searchFilter); } + /// + /// + /// + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + public int Count(params string[] include) where T : class, new() + { + var parameters = NameValueCollectionExtensions.AddParamsIfExist(null, include); + + return Count(parameters); + } + + /// + /// + /// + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + public int Count(NameValueCollection parameters) where T : class, new() + { + return Count(parameters != null ? new RequestOptions { QueryString = parameters } : null); + } + + /// + /// Gets the redmine object based on id. + /// + /// The type of objects to retrieve. + /// The id of the object. + /// Optional filters and/or optional fetched data. + /// + /// Returns the object of type T. + /// + /// + /// + /// string issueId = "927"; + /// NameValueCollection parameters = null; + /// Issue issue = redmineManager.GetObject<Issue>(issueId, parameters); + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + public T GetObject(string id, NameValueCollection parameters) where T : class, new() + { + var url = RedmineApiUrls.GetFragment(id); + + var response = ApiClient.Get(url, parameters != null ? new RequestOptions { QueryString = parameters } : null); + + return response.DeserializeTo(Serializer); + } + + /// + /// Returns the complete list of objects. + /// + /// + /// Optional fetched data. + /// + /// Optional fetched data: + /// Project: trackers, issue_categories, enabled_modules (since Redmine 2.6.0) + /// Issue: children, attachments, relations, changesets, journals, watchers (since Redmine 2.3.0) + /// Users: memberships, groups (since Redmine 2.1) + /// Groups: users, memberships + /// + /// Returns the complete list of objects. + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + public List GetObjects(params string[] include) where T : class, new() + { + var parameters = NameValueCollectionExtensions.AddParamsIfExist(null, include); + + return GetObjects(parameters); + } + + /// + /// Returns the complete list of objects. + /// + /// + /// The page size. + /// The offset. + /// Optional fetched data. + /// + /// Optional fetched data: + /// Project: trackers, issue_categories, enabled_modules (since 2.6.0) + /// Issue: children, attachments, relations, changesets, journals, watchers - Since 2.3.0 + /// Users: memberships, groups (added in 2.1) + /// Groups: users, memberships + /// + /// Returns the complete list of objects. + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + public List GetObjects(int limit, int offset, params string[] include) where T : class, new() + { + var parameters = NameValueCollectionExtensions + .AddParamsIfExist(null, include) + .AddPagingParameters(limit, offset); + + return GetObjects(parameters); + } + + /// + /// Returns the complete list of objects. + /// + /// The type of objects to retrieve. + /// Optional filters and/or optional fetched data. + /// + /// Returns a complete list of objects. + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + public List GetObjects(NameValueCollection parameters = null) where T : class, new() + { + var uri = RedmineApiUrls.GetListFragment(); + + return GetObjects(uri, parameters != null ? new RequestOptions { QueryString = parameters } : null); + } + + /// + /// Gets the paginated objects. + /// + /// + /// The parameters. + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + public PagedResults GetPaginatedObjects(NameValueCollection parameters) where T : class, new() + { + var url = RedmineApiUrls.GetListFragment(); + + return GetPaginatedObjects(url, parameters != null ? new RequestOptions { QueryString = parameters } : null); + } + + /// + /// Creates a new Redmine object. + /// + /// The type of object to create. + /// The object to create. + /// + /// + /// + /// When trying to create an object with invalid or missing attribute parameters, you will get a 422 Unprocessable + /// Entity response. That means that the object could not be created. + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + public T CreateObject(T entity) where T : class, new() + { + return CreateObject(entity, null); + } + + /// + /// Creates a new Redmine object. + /// + /// The type of object to create. + /// The object to create. + /// The owner identifier. + /// + /// + /// + /// When trying to create an object with invalid or missing attribute parameters, you will get a 422 Unprocessable + /// Entity response. That means that the object could not be created. + /// + /// + /// + /// var project = new Project(); + /// project.Name = "test"; + /// project.Identifier = "the project identifier"; + /// project.Description = "the project description"; + /// redmineManager.CreateObject(project); + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + public T CreateObject(T entity, string ownerId) where T : class, new() + { + var url = RedmineApiUrls.CreateEntityFragment(ownerId); + + var payload = Serializer.Serialize(entity); + + var response = ApiClient.Create(url, payload); + + return response.DeserializeTo(Serializer); + } + + /// + /// Updates a Redmine object. + /// + /// The type of object to be update. + /// The id of the object to be update. + /// The object to be update. + /// The project identifier. + /// + /// + /// When trying to update an object with invalid or missing attribute parameters, you will get a + /// 422(RedmineException) Unprocessable Entity response. That means that the object could not be updated. + /// + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + public void UpdateObject(string id, T entity, string projectId = null) where T : class, new() + { + var url = RedmineApiUrls.UpdateFragment(id); + + var payload = Serializer.Serialize(entity); + + ApiClient.Update(url, payload); + } + + /// + /// Deletes the Redmine object. + /// + /// The type of objects to delete. + /// The id of the object to delete + /// The parameters + /// + /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + public void DeleteObject(string id, NameValueCollection parameters = null) where T : class, new() + { + var url = RedmineApiUrls.DeleteFragment(id); + + ApiClient.Delete(url, parameters != null ? new RequestOptions { QueryString = parameters } : null); + } + /// /// Creates the Redmine web client. /// diff --git a/src/redmine-net-api/_net20/RedmineManagerAsync.cs b/src/redmine-net-api/_net20/RedmineManagerAsyncObsolete.cs similarity index 98% rename from src/redmine-net-api/_net20/RedmineManagerAsync.cs rename to src/redmine-net-api/_net20/RedmineManagerAsyncObsolete.cs index acceb9b5..5a8a2fcc 100644 --- a/src/redmine-net-api/_net20/RedmineManagerAsync.cs +++ b/src/redmine-net-api/_net20/RedmineManagerAsyncObsolete.cs @@ -29,6 +29,7 @@ namespace Redmine.Net.Api.Async /// /// /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] public delegate void Task(); /// @@ -36,11 +37,12 @@ namespace Redmine.Net.Api.Async /// /// The type of the resource. /// - public delegate TRes Task(); + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] public delegate TRes Task(); /// /// /// + [Obsolete(RedmineConstants.OBSOLETE_TEXT)] public static class RedmineManagerAsync { /// From 6ea053798bea19cd24f8c53913eb2fd241a1b61b Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 9 Jan 2024 20:10:05 +0200 Subject: [PATCH 343/549] [Code] Arrange & clean up --- .../Extensions/RedmineManagerExtensions.cs | 436 +++++------------- .../Internals/HashCodeHelper.cs | 12 +- 2 files changed, 136 insertions(+), 312 deletions(-) diff --git a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs index e162e155..043404d9 100644 --- a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs +++ b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs @@ -44,9 +44,11 @@ public static class RedmineManagerExtensions /// public static PagedResults GetProjectNews(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null) { - var uri = Uri.EscapeDataString(redmineManager.RedmineApiUrls.ProjectNews(projectIdentifier)); + var uri = redmineManager.RedmineApiUrls.ProjectNews(projectIdentifier); - var response = redmineManager.GetPaginatedObjects(uri, requestOptions); + var escapedUri = Uri.EscapeDataString(uri); + + var response = redmineManager.GetPaginatedObjects(escapedUri, requestOptions); return response; } @@ -71,12 +73,14 @@ public static News AddProjectNews(this RedmineManager redmineManager, string pro { throw new RedmineException("News title cannot be blank"); } - - var uri = Uri.EscapeDataString(redmineManager.RedmineApiUrls.ProjectNews(projectIdentifier)); - + var payload = redmineManager.Serializer.Serialize(news); + + var uri = Uri.EscapeDataString(redmineManager.RedmineApiUrls.ProjectNews(projectIdentifier)); + + var escapedUri = Uri.EscapeDataString(uri); - var response = redmineManager.ApiClient.Create(uri, payload, requestOptions); + var response = redmineManager.ApiClient.Create(escapedUri, payload, requestOptions); return response.DeserializeTo(redmineManager.Serializer); } @@ -223,9 +227,9 @@ public static void UpdateWikiPage(this RedmineManager redmineManager, string pro var uri = redmineManager.RedmineApiUrls.ProjectWikiPageUpdate(projectId, pageName); - uri = Uri.EscapeDataString(uri); + var escapedUri = Uri.EscapeDataString(uri); - redmineManager.ApiClient.Patch(uri, payload, requestOptions); + redmineManager.ApiClient.Patch(escapedUri, payload, requestOptions); } /// @@ -248,9 +252,9 @@ public static WikiPage CreateWikiPage(this RedmineManager redmineManager, string var uri = redmineManager.RedmineApiUrls.ProjectWikiPageUpdate(projectId, pageName); - uri = Uri.EscapeDataString(uri); + var escapedUri = Uri.EscapeDataString(uri); - var response = redmineManager.ApiClient.Create(uri, payload, requestOptions); + var response = redmineManager.ApiClient.Create(escapedUri, payload, requestOptions); return response.DeserializeTo(redmineManager.Serializer); } @@ -270,9 +274,9 @@ public static WikiPage GetWikiPage(this RedmineManager redmineManager, string pr ? redmineManager.RedmineApiUrls.ProjectWikiPage(projectId, pageName) : redmineManager.RedmineApiUrls.ProjectWikiPageVersion(projectId, pageName, version.ToString(CultureInfo.InvariantCulture)); - uri = Uri.EscapeDataString(uri); + var escapedUri = Uri.EscapeDataString(uri); - var response = redmineManager.ApiClient.Get(uri, requestOptions); + var response = redmineManager.ApiClient.Get(escapedUri, requestOptions); return response.DeserializeTo(redmineManager.Serializer); } @@ -305,9 +309,9 @@ public static void DeleteWikiPage(this RedmineManager redmineManager, string pro { var uri = redmineManager.RedmineApiUrls.ProjectWikiPageDelete(projectId, pageName); - uri = Uri.EscapeDataString(uri); + var escapedUri = Uri.EscapeDataString(uri); - redmineManager.ApiClient.Delete(uri, requestOptions); + redmineManager.ApiClient.Delete(escapedUri, requestOptions); } /// @@ -370,6 +374,96 @@ private static NameValueCollection CreateSearchParameters(string q, int limit, i } #if !(NET20) + + /// + /// + /// + /// + /// + /// + /// + /// + public static async Task> GetProjectNewsAsync(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.ProjectNews(projectIdentifier); + + var escapedUri = Uri.EscapeDataString(uri); + + var response = await redmineManager.ApiClient.GetPagedAsync(escapedUri, requestOptions, cancellationToken).ConfigureAwait(false); + + return response.DeserializeToPagedResults(redmineManager.Serializer);; + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static async Task AddProjectNewsAsync(this RedmineManager redmineManager, string projectIdentifier, News news, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + if (news == null) + { + throw new RedmineException("Argument news is null"); + } + + if (news.Title.IsNullOrWhiteSpace()) + { + throw new RedmineException("News title cannot be blank"); + } + + var payload = redmineManager.Serializer.Serialize(news); + + var uri = Uri.EscapeDataString(redmineManager.RedmineApiUrls.ProjectNews(projectIdentifier)); + + var escapedUri = Uri.EscapeDataString(uri); + + var response = await redmineManager.ApiClient.CreateAsync(escapedUri, payload, requestOptions, cancellationToken).ConfigureAwait(false); + + return response.DeserializeTo(redmineManager.Serializer); + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static async Task> GetProjectMembershipsAsync(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.ProjectMemberships(projectIdentifier); + + var response = await redmineManager.ApiClient.GetPagedAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); + + return response.DeserializeToPagedResults(redmineManager.Serializer);; + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static async Task> GetProjectFilesAsync(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.ProjectFilesFragment(projectIdentifier); + + var response = await redmineManager.ApiClient.GetPagedAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); + + return response.DeserializeToPagedResults(redmineManager.Serializer);; + } + + /// /// /// @@ -429,9 +523,9 @@ public static async Task CreateWikiPageAsync(this RedmineManager redmi var url = redmineManager.RedmineApiUrls.ProjectWikiPageUpdate(projectId, pageName); - var uri = Uri.EscapeDataString(url); + var escapedUri = Uri.EscapeDataString(url); - var response = await redmineManager.ApiClient.CreateAsync(uri, payload,requestOptions, cancellationToken).ConfigureAwait(false); + var response = await redmineManager.ApiClient.CreateAsync(escapedUri, payload,requestOptions, cancellationToken).ConfigureAwait(false); return response.DeserializeTo(redmineManager.Serializer); } @@ -457,9 +551,9 @@ public static async Task UpdateWikiPageAsync(this RedmineManager redmineManager, var url = redmineManager.RedmineApiUrls.ProjectWikiPageUpdate(projectId, pageName); - var uri = Uri.EscapeDataString(url); + var escapedUri = Uri.EscapeDataString(url); - await redmineManager.ApiClient.PatchAsync(uri, payload, requestOptions, cancellationToken).ConfigureAwait(false); + await redmineManager.ApiClient.PatchAsync(escapedUri, payload, requestOptions, cancellationToken).ConfigureAwait(false); } /// @@ -475,45 +569,11 @@ public static async Task DeleteWikiPageAsync(this RedmineManager redmineManager, { var uri = redmineManager.RedmineApiUrls.ProjectWikiPageDelete(projectId, pageName); - uri = Uri.EscapeDataString(uri); + var escapedUri = Uri.EscapeDataString(uri); - await redmineManager.ApiClient.DeleteAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); - } - - /// - /// Support for adding attachments through the REST API is added in Redmine 1.4.0. - /// Upload a file to server. This method does not block the calling thread. - /// - /// The redmine manager. - /// The content of the file that will be uploaded on server. - /// - /// - /// - /// . - /// - public static async Task UploadFileAsync(this RedmineManager redmineManager, byte[] data, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) - { - var url = redmineManager.RedmineApiUrls.UploadFragment(); - - var response = await redmineManager.ApiClient.UploadFileAsync(url, data,requestOptions , cancellationToken: cancellationToken).ConfigureAwait(false); - - return response.DeserializeTo(redmineManager.Serializer); - } - - /// - /// Downloads the file asynchronous. - /// - /// The redmine manager. - /// The address. - /// - /// - /// - public static async Task DownloadFileAsync(this RedmineManager redmineManager, string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) - { - var response = await redmineManager.ApiClient.DownloadAsync(address, requestOptions,cancellationToken: cancellationToken).ConfigureAwait(false); - return response.Content; + await redmineManager.ApiClient.DeleteAsync(escapedUri, requestOptions, cancellationToken).ConfigureAwait(false); } - + /// /// Gets the wiki page asynchronous. /// @@ -530,9 +590,9 @@ public static async Task GetWikiPageAsync(this RedmineManager redmineM ? redmineManager.RedmineApiUrls.ProjectWikiPage(projectId, pageName) : redmineManager.RedmineApiUrls.ProjectWikiPageVersion(projectId, pageName, version.ToString(CultureInfo.InvariantCulture)); - uri = Uri.EscapeDataString(uri); + var escapedUri = Uri.EscapeDataString(uri); - var response = await redmineManager.ApiClient.GetAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); + var response = await redmineManager.ApiClient.GetAsync(escapedUri, requestOptions, cancellationToken).ConfigureAwait(false); return response.DeserializeTo(redmineManager.Serializer); } @@ -623,270 +683,26 @@ public static async Task RemoveWatcherFromIssueAsync(this RedmineManager redmine await redmineManager.ApiClient.DeleteAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); } - - /// - /// - /// - /// - /// - /// - /// - public static async Task CountAsync(this RedmineManager redmineManager, params string[] include) where T : class, new() + #endif + + internal static RequestOptions CreateRequestOptions(NameValueCollection parameters = null, string impersonateUserName = null) { RequestOptions requestOptions = null; - - if (include is {Length: > 0}) + if (parameters != null) { requestOptions = new RequestOptions() { - QueryString = new NameValueCollection - { - {RedmineKeys.INCLUDE, string.Join(",", include)} - } + QueryString = parameters }; } - return await CountAsync(redmineManager, requestOptions).ConfigureAwait(false); - } - - /// - /// - /// - /// - /// - /// - /// - public static async Task CountAsync(this RedmineManager redmineManager, RequestOptions requestOptions) where T : class, new() - { - var totalCount = 0; - const int PAGE_SIZE = 1; - const int OFFSET = 0; - - if (requestOptions == null) + if (impersonateUserName.IsNullOrWhiteSpace()) { - requestOptions = new RequestOptions(); + return requestOptions; } - requestOptions.QueryString.AddPagingParameters(PAGE_SIZE, OFFSET); - - var tempResult = await GetPagedAsync(redmineManager, requestOptions).ConfigureAwait(false); - if (tempResult != null) - { - totalCount = tempResult.TotalItems; - } - - return totalCount; - } - - - /// - /// Gets the paginated objects asynchronous. - /// - /// - /// The redmine manager. - /// - /// - /// - public static async Task> GetPagedAsync(this RedmineManager redmineManager, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) - where T : class, new() - { - var url = redmineManager.RedmineApiUrls.GetListFragment(); - - var response= await redmineManager.ApiClient.GetAsync(url, requestOptions, cancellationToken).ConfigureAwait(false); - - return response.DeserializeToPagedResults(redmineManager.Serializer); - } - - /// - /// Gets the objects asynchronous. - /// - /// - /// The redmine manager. - /// - /// - /// - public static async Task> GetObjectsAsync(this RedmineManager redmineManager, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) - where T : class, new() - { - int pageSize = 0, offset = 0; - var isLimitSet = false; - List resultList = null; - - if (requestOptions == null) - { - requestOptions = new RequestOptions(); - } - - if (requestOptions.QueryString == null) - { - requestOptions.QueryString = new NameValueCollection(); - } - else - { - isLimitSet = int.TryParse(requestOptions.QueryString[RedmineKeys.LIMIT], out pageSize); - int.TryParse(requestOptions.QueryString[RedmineKeys.OFFSET], out offset); - } - - if (pageSize == default) - { - pageSize = redmineManager.PageSize > 0 - ? redmineManager.PageSize - : RedmineManager.DEFAULT_PAGE_SIZE_VALUE; - requestOptions.QueryString.Set(RedmineKeys.LIMIT, pageSize.ToString(CultureInfo.InvariantCulture)); - } - - try - { - var hasOffset = RedmineManager.TypesWithOffset.ContainsKey(typeof(T)); - if (hasOffset) - { - int totalCount; - do - { - requestOptions.QueryString.Set(RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)); - - var tempResult = await redmineManager.GetPagedAsync(requestOptions, cancellationToken: cancellationToken).ConfigureAwait(false); - - totalCount = isLimitSet ? pageSize : tempResult.TotalItems; - - if (tempResult?.Items != null) - { - if (resultList == null) - { - resultList = new List(tempResult.Items); - } - else - { - resultList.AddRange(tempResult.Items); - } - } - - offset += pageSize; - } while (offset < totalCount); - } - else - { - var result = await redmineManager.GetPagedAsync(requestOptions, cancellationToken: cancellationToken).ConfigureAwait(false); - if (result?.Items != null) - { - return new List(result.Items); - } - } - } - catch (WebException wex) - { - wex.HandleWebException(redmineManager.Serializer); - } - - return resultList; - } - - /// - /// Gets a Redmine object. This method does not block the calling thread. - /// - /// The type of objects to retrieve. - /// The redmine manager. - /// The id of the object. - /// - /// - /// - public static async Task GetObjectAsync(this RedmineManager redmineManager, string id, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) - where T : class, new() - { - var url = redmineManager.RedmineApiUrls.GetFragment(id); - - var response = await redmineManager.ApiClient.GetAsync(url,requestOptions, cancellationToken).ConfigureAwait(false); - - return response.DeserializeTo(redmineManager.Serializer); - } - - /// - /// Creates a new Redmine object. This method does not block the calling thread. - /// - /// The type of object to create. - /// The redmine manager. - /// The object to create. - /// - /// - /// - public static async Task CreateObjectAsync(this RedmineManager redmineManager, T entity, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) - where T : class, new() - { - return await redmineManager.CreateObjectAsync( entity, null, requestOptions, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - /// - /// Creates a new Redmine object. This method does not block the calling thread. - /// - /// The type of object to create. - /// The redmine manager. - /// The object to create. - /// The owner identifier. - /// - /// - /// - public static async Task CreateObjectAsync(this RedmineManager redmineManager, T entity, string ownerId, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) - where T : class, new() - { - var url = redmineManager.RedmineApiUrls.CreateEntityFragment(ownerId); - - var payload = redmineManager.Serializer.Serialize(entity); - - var response = await redmineManager.ApiClient.CreateAsync(url, payload, requestOptions, cancellationToken).ConfigureAwait(false); - - return response.DeserializeTo(redmineManager.Serializer); - } - - /// - /// Updates the object asynchronous. - /// - /// - /// The redmine manager. - /// The identifier. - /// The object. - /// - /// - /// - public static async Task UpdateObjectAsync(this RedmineManager redmineManager, string id, T entity, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) - where T : class, new() - { - var url = redmineManager.RedmineApiUrls.UpdateFragment(id); - - var payload = redmineManager.Serializer.Serialize(entity); - - await redmineManager.ApiClient.UpdateAsync(url, payload, requestOptions,cancellationToken: cancellationToken).ConfigureAwait(false); - // data = Regex.Replace(data, @"\r\n|\r|\n", "\r\n"); - } - - /// - /// Deletes the Redmine object. This method does not block the calling thread. - /// - /// The type of objects to delete. - /// The redmine manager. - /// The id of the object to delete - /// - /// - /// - public static async Task DeleteObjectAsync(this RedmineManager redmineManager, string id, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) - where T : class, new() - { - var url = redmineManager.RedmineApiUrls.DeleteFragment(id); - - await redmineManager.ApiClient.DeleteAsync(url, requestOptions, cancellationToken).ConfigureAwait((false)); - } - #endif - - internal static RequestOptions CreateRequestOptions(NameValueCollection parameters = null, string impersonateUserName = null) - { - RequestOptions requestOptions = null; - if (parameters != null || !impersonateUserName.IsNullOrWhiteSpace()) - { - requestOptions = new RequestOptions() - { - QueryString = parameters, - ImpersonateUser = impersonateUserName - }; - } + requestOptions ??= new RequestOptions(); + requestOptions.ImpersonateUser = impersonateUserName; return requestOptions; } diff --git a/src/redmine-net-api/Internals/HashCodeHelper.cs b/src/redmine-net-api/Internals/HashCodeHelper.cs index 5f8982c8..ba8595c4 100755 --- a/src/redmine-net-api/Internals/HashCodeHelper.cs +++ b/src/redmine-net-api/Internals/HashCodeHelper.cs @@ -38,12 +38,20 @@ public static int GetHashCode(IList list, int hash) where T : class unchecked { var hashCode = hash; - if (list == null) return hashCode; + if (list == null) + { + return hashCode; + } + hashCode = (hashCode * 13) + list.Count; + foreach (var t in list) { hashCode *= 13; - if (t != null) hashCode += t.GetHashCode(); + if (t != null) + { + hashCode += t.GetHashCode(); + } } return hashCode; From 9cb1b9db050b38d4bc7f8bc1cb0a2d117db0cbbd Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 9 Jan 2024 20:11:18 +0200 Subject: [PATCH 344/549] [RedmineKeys] Add account, index & my keys --- src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs | 4 ++-- src/redmine-net-api/RedmineKeys.cs | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs b/src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs index aa415007..b16ff47f 100644 --- a/src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs +++ b/src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs @@ -23,7 +23,7 @@ internal static class RedmineApiUrlsExtensions { public static string MyAccount(this RedmineApiUrls redmineApiUrls) { - return $"my/account.{redmineApiUrls.Format}"; + return $"{RedmineKeys.MY}/{RedmineKeys.ACCOUNT}.{redmineApiUrls.Format}"; } public static string CurrentUser(this RedmineApiUrls redmineApiUrls) @@ -53,7 +53,7 @@ public static string ProjectMemberships(this RedmineApiUrls redmineApiUrls, stri public static string ProjectWikiIndex(this RedmineApiUrls redmineApiUrls, string projectId) { - return $"{RedmineKeys.PROJECTS}/{projectId}/{RedmineKeys.WIKI}/index.{redmineApiUrls.Format}"; + return $"{RedmineKeys.PROJECTS}/{projectId}/{RedmineKeys.WIKI}/{RedmineKeys.INDEX}.{redmineApiUrls.Format}"; } public static string ProjectWikiPage(this RedmineApiUrls redmineApiUrls, string projectId, string wikiPageName) diff --git a/src/redmine-net-api/RedmineKeys.cs b/src/redmine-net-api/RedmineKeys.cs index f5863407..5bd7661e 100644 --- a/src/redmine-net-api/RedmineKeys.cs +++ b/src/redmine-net-api/RedmineKeys.cs @@ -20,6 +20,10 @@ namespace Redmine.Net.Api /// public static class RedmineKeys { + /// + /// + /// + public const string ACCOUNT = "account"; /// /// /// @@ -329,6 +333,10 @@ public static class RedmineKeys /// /// /// + public const string INDEX = "index"; + /// + /// + /// public const string INHERITED = "inherited"; /// /// @@ -479,6 +487,10 @@ public static class RedmineKeys /// /// /// + public const string MY = "my"; + /// + /// + /// public const string NAME = "name"; /// /// From ab3755bbc64997ae10ff2037873f8d9d88e06ecd Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 9 Jan 2024 20:12:14 +0200 Subject: [PATCH 345/549] [New] [RedmineManagerAsync] Interface & implementation --- src/redmine-net-api/IRedmineManagerAsync.cs | 154 +++++++++++++++ src/redmine-net-api/RedmineManagerAsync.cs | 209 ++++++++++++++++++++ 2 files changed, 363 insertions(+) create mode 100644 src/redmine-net-api/IRedmineManagerAsync.cs create mode 100644 src/redmine-net-api/RedmineManagerAsync.cs diff --git a/src/redmine-net-api/IRedmineManagerAsync.cs b/src/redmine-net-api/IRedmineManagerAsync.cs new file mode 100644 index 00000000..7c77d334 --- /dev/null +++ b/src/redmine-net-api/IRedmineManagerAsync.cs @@ -0,0 +1,154 @@ +/* + Copyright 2011 - 2023 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#if !(NET20) +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Redmine.Net.Api.Net; +using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api +{ + /// + /// + /// + public interface IRedmineManagerAsync + { + /// + /// Returns the count of items asynchronously for a given type T. + /// + /// The type of the results. + /// Optional request options. + /// Optional cancellation token. + /// The count of items as an integer. + Task CountAsync(RequestOptions requestOptions, CancellationToken cancellationToken = default) + where T : class, new(); + + /// + /// Gets the paginated objects asynchronous. + /// + /// The type of the results. + /// Optional request options. + /// Optional cancellation token. + /// A task representing the asynchronous operation that returns the paged results. + Task> GetPagedAsync(RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new(); + + /// + /// Gets the objects asynchronous. + /// + /// + /// + /// + /// + Task> GetAsync(RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new(); + + /// + /// Gets a Redmine object asynchronous. + /// + /// The type of object to retrieve. + /// The ID of the object to retrieve. + /// Optional request options. + /// Optional cancellation token. + /// The retrieved object of type T. + Task GetAsync(string id, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new(); + + /// + /// Creates a new Redmine object asynchronous. + /// + /// The type of the entity. + /// The entity to create. + /// The optional request options. + /// The cancellation token. + /// A Task representing the asynchronous operation, returning the created entity. + /// + /// This method creates an entity of type T asynchronously. It accepts an entity object, along with optional request options and cancellation token. + /// The method is generic and constrained to accept only classes that have a default constructor. + /// It uses the CreateAsync method to create the entity, passing the entity, request options, and cancellation token as arguments. + /// The method is awaited and returns a Task of type T representing the asynchronous operation. + /// + Task CreateAsync(T entity, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new(); + + /// + /// Creates a new Redmine object. This method does not block the calling thread. + /// + /// The type of the entity. + /// The entity object to create. + /// The ID of the owner. + /// Optional request options. + /// Optional cancellation token. + /// The created entity. + /// Thrown when an error occurs during the creation process. + Task CreateAsync(T entity, string ownerId, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new(); + + /// + /// Updates the object asynchronous. + /// + /// The type of the entity. + /// The ID of the entity to update. + /// The entity to update. + /// Optional request options. + /// Optional cancellation token. + /// A task representing the asynchronous update operation. + /// + /// This method sends an update request to the Redmine API to update the entity with the specified ID. + /// + Task UpdateAsync(string id, T entity, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new(); + + /// + /// Deletes the Redmine object asynchronous. + /// + /// The type of the resource to delete. + /// The ID of the resource to delete. + /// Optional request options. + /// Cancellation token. + /// A task representing the asynchronous delete operation. + /// + /// This method sends a DELETE request to the Redmine API to delete a resource identified by the given ID. + /// + Task DeleteAsync(string id, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new(); + + /// + /// Support for adding attachments through the REST API is added in Redmine 1.4.0. + /// Upload a file to server. This method does not block the calling thread. + /// + /// The content of the file that will be uploaded on server. + /// + /// + /// + /// . + /// + Task UploadFileAsync(byte[] data, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + + /// + /// Downloads the file asynchronous. + /// + /// The address. + /// + /// + /// + Task DownloadFileAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net-api/RedmineManagerAsync.cs b/src/redmine-net-api/RedmineManagerAsync.cs new file mode 100644 index 00000000..50192c5a --- /dev/null +++ b/src/redmine-net-api/RedmineManagerAsync.cs @@ -0,0 +1,209 @@ +/* + Copyright 2011 - 2023 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#if !(NET20) +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Globalization; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Net; +using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api; + +public partial class RedmineManager: IRedmineManagerAsync +{ + /// + public async Task CountAsync(RequestOptions requestOptions, CancellationToken cancellationToken = default) where T : class, new() + { + const int PAGE_SIZE = 1; + const int OFFSET = 0; + var totalCount = 0; + + requestOptions ??= new RequestOptions(); + + requestOptions.QueryString.AddPagingParameters(PAGE_SIZE, OFFSET); + + var tempResult = await GetPagedAsync(requestOptions, cancellationToken).ConfigureAwait(false); + if (tempResult != null) + { + totalCount = tempResult.TotalItems; + } + + return totalCount; + } + + /// + public async Task> GetPagedAsync(RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new() + { + var url = RedmineApiUrls.GetListFragment(); + + var response = await ApiClient.GetAsync(url, requestOptions, cancellationToken).ConfigureAwait(false); + + return response.DeserializeToPagedResults(Serializer); + } + + /// + public async Task> GetAsync(RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new() + { + int pageSize = 0, offset = 0; + var isLimitSet = false; + List resultList = null; + + requestOptions ??= new RequestOptions(); + + if (requestOptions.QueryString == null) + { + requestOptions.QueryString = new NameValueCollection(); + } + else + { + isLimitSet = int.TryParse(requestOptions.QueryString[RedmineKeys.LIMIT], out pageSize); + int.TryParse(requestOptions.QueryString[RedmineKeys.OFFSET], out offset); + } + + if (pageSize == default) + { + pageSize = PageSize > 0 + ? PageSize + : RedmineConstants.DEFAULT_PAGE_SIZE_VALUE; + requestOptions.QueryString.Set(RedmineKeys.LIMIT, pageSize.ToString(CultureInfo.InvariantCulture)); + } + + try + { + var hasOffset = RedmineManager.TypesWithOffset.ContainsKey(typeof(T)); + if (hasOffset) + { + int totalCount; + do + { + requestOptions.QueryString.Set(RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)); + + var tempResult = await GetPagedAsync(requestOptions, cancellationToken: cancellationToken).ConfigureAwait(false); + + totalCount = isLimitSet ? pageSize : tempResult.TotalItems; + + if (tempResult?.Items != null) + { + if (resultList == null) + { + resultList = new List(tempResult.Items); + } + else + { + resultList.AddRange(tempResult.Items); + } + } + + offset += pageSize; + } while (offset < totalCount); + } + else + { + var result = await GetPagedAsync(requestOptions, cancellationToken: cancellationToken).ConfigureAwait(false); + if (result?.Items != null) + { + return new List(result.Items); + } + } + } + catch (WebException wex) + { + wex.HandleWebException(Serializer); + } + + return resultList; + } + + /// + public async Task GetAsync(string id, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new() + { + var url = RedmineApiUrls.GetFragment(id); + + var response = await ApiClient.GetAsync(url, requestOptions, cancellationToken).ConfigureAwait(false); + + return response.DeserializeTo(Serializer); + } + + + /// + public async Task CreateAsync(T entity, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new() + { + return await CreateAsync(entity, null, requestOptions, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + public async Task CreateAsync(T entity, string ownerId, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new() + { + var url = RedmineApiUrls.CreateEntityFragment(ownerId); + + var payload = Serializer.Serialize(entity); + + var response = await ApiClient.CreateAsync(url, payload, requestOptions, cancellationToken).ConfigureAwait(false); + + return response.DeserializeTo(Serializer); + } + + /// + public async Task UpdateAsync(string id, T entity, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new() + { + var url = RedmineApiUrls.UpdateFragment(id); + + var payload = Serializer.Serialize(entity); + + // payload = Regex.Replace(payload, @"\r\n|\r|\n", "\r\n"); + + await ApiClient.UpdateAsync(url, payload, requestOptions, cancellationToken: cancellationToken).ConfigureAwait(false); + } + + /// + public async Task DeleteAsync(string id, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new() + { + var url = RedmineApiUrls.DeleteFragment(id); + + await ApiClient.DeleteAsync(url, requestOptions, cancellationToken).ConfigureAwait((false)); + } + + /// + public async Task UploadFileAsync(byte[] data, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var url = RedmineApiUrls.UploadFragment(); + + var response = await ApiClient.UploadFileAsync(url, data,requestOptions , cancellationToken: cancellationToken).ConfigureAwait(false); + + return response.DeserializeTo(Serializer); + } + + /// + public async Task DownloadFileAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var response = await ApiClient.DownloadAsync(address, requestOptions,cancellationToken: cancellationToken).ConfigureAwait(false); + return response.Content; + } +} +#endif \ No newline at end of file From c95c6e957561bbfc4d6aa2b3b56c638e5d203120 Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 9 Jan 2024 20:13:51 +0200 Subject: [PATCH 346/549] [New] [IntExtensions] --- .../Extensions/IntExtensions.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/redmine-net-api/Extensions/IntExtensions.cs diff --git a/src/redmine-net-api/Extensions/IntExtensions.cs b/src/redmine-net-api/Extensions/IntExtensions.cs new file mode 100644 index 00000000..5f838547 --- /dev/null +++ b/src/redmine-net-api/Extensions/IntExtensions.cs @@ -0,0 +1,45 @@ +/* + Copyright 2011 - 2023 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +namespace Redmine.Net.Api.Extensions; + +internal static class IntExtensions +{ + public static bool Between(this int val, int from, int to) + { + return val >= from && val <= to; + } + + public static bool Greater(this int val, int than) + { + return val > than; + } + + public static bool GreaterOrEqual(this int val, int than) + { + return val >= than; + } + + public static bool Lower(this int val, int than) + { + return val < than; + } + + public static bool LowerOrEqual(this int val, int than) + { + return val <= than; + } +} \ No newline at end of file From 64f9a3a2c1658f9c80f4aa009432a58821179c2f Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 9 Jan 2024 20:17:03 +0200 Subject: [PATCH 347/549] [Attachments] Mark as sealed --- src/redmine-net-api/Types/Attachments.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/redmine-net-api/Types/Attachments.cs b/src/redmine-net-api/Types/Attachments.cs index 7797e7e7..15024d91 100644 --- a/src/redmine-net-api/Types/Attachments.cs +++ b/src/redmine-net-api/Types/Attachments.cs @@ -24,7 +24,7 @@ namespace Redmine.Net.Api.Types /// /// /// - internal class Attachments : Dictionary, IJsonSerializable + internal sealed class Attachments : Dictionary, IJsonSerializable { /// /// From 7c1413f03970135664657a09684c6158772d9ffa Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 9 Jan 2024 20:18:01 +0200 Subject: [PATCH 348/549] [Permission] Disable warning CA1711 - name ending in 'permission' --- src/redmine-net-api/Types/Permission.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/redmine-net-api/Types/Permission.cs b/src/redmine-net-api/Types/Permission.cs index 13fb4e9d..3a658453 100644 --- a/src/redmine-net-api/Types/Permission.cs +++ b/src/redmine-net-api/Types/Permission.cs @@ -30,7 +30,9 @@ namespace Redmine.Net.Api.Types /// [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.PERMISSION)] + #pragma warning disable CA1711 public sealed class Permission : IXmlSerializable, IJsonSerializable, IEquatable + #pragma warning restore CA1711 { #region Properties /// From 3395e2269f55c008735880041d7a2908e764b8c9 Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 9 Jan 2024 20:20:01 +0200 Subject: [PATCH 349/549] [VersionSharing] Add enum default option - Unknown --- src/redmine-net-api/Types/Version.cs | 10 +++++++--- src/redmine-net-api/Types/VersionSharing.cs | 4 ++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/redmine-net-api/Types/Version.cs b/src/redmine-net-api/Types/Version.cs index e094ee27..ca0ed402 100644 --- a/src/redmine-net-api/Types/Version.cs +++ b/src/redmine-net-api/Types/Version.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -141,8 +141,12 @@ public override void ReadXml(XmlReader reader) public override void WriteXml(XmlWriter writer) { writer.WriteElementString(RedmineKeys.NAME, Name); - writer.WriteElementString(RedmineKeys.STATUS, Status.ToString().ToLowerInv()); - writer.WriteElementString(RedmineKeys.SHARING, Sharing.ToString().ToLowerInv()); + writer.WriteElementString(RedmineKeys.STATUS, Status.ToInvariantString()); + if (Sharing != VersionSharing.Unknown) + { + writer.WriteElementString(RedmineKeys.SHARING, Sharing.ToInvariantString()); + } + writer.WriteDateOrEmpty(RedmineKeys.DUE_DATE, DueDate); writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); if (CustomFields != null) diff --git a/src/redmine-net-api/Types/VersionSharing.cs b/src/redmine-net-api/Types/VersionSharing.cs index 1fbf63b0..125f3f1c 100644 --- a/src/redmine-net-api/Types/VersionSharing.cs +++ b/src/redmine-net-api/Types/VersionSharing.cs @@ -21,6 +21,10 @@ namespace Redmine.Net.Api.Types /// public enum VersionSharing { + /// + /// + /// + Unknown = 0, /// /// /// From e625819dafadcd64b0154619300a7326bd82ef0c Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 9 Jan 2024 20:20:43 +0200 Subject: [PATCH 350/549] [VersionStatus] Add enum default option - None --- src/redmine-net-api/Types/VersionStatus.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/redmine-net-api/Types/VersionStatus.cs b/src/redmine-net-api/Types/VersionStatus.cs index 60a4684b..7e452f41 100644 --- a/src/redmine-net-api/Types/VersionStatus.cs +++ b/src/redmine-net-api/Types/VersionStatus.cs @@ -21,6 +21,10 @@ namespace Redmine.Net.Api.Types /// public enum VersionStatus { + /// + /// value of zero - Not set/unknown + /// + None, /// /// /// From 360b75735c2e74d4e293b572f18c2f9dba048fc7 Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 9 Jan 2024 20:21:00 +0200 Subject: [PATCH 351/549] [ProjectStatus] Add enum default option - None --- src/redmine-net-api/Types/ProjectStatus.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/redmine-net-api/Types/ProjectStatus.cs b/src/redmine-net-api/Types/ProjectStatus.cs index 355a3092..ecd504a2 100755 --- a/src/redmine-net-api/Types/ProjectStatus.cs +++ b/src/redmine-net-api/Types/ProjectStatus.cs @@ -21,6 +21,10 @@ namespace Redmine.Net.Api.Types /// public enum ProjectStatus { + /// + /// value of zero - Not set/unknown + /// + None, /// /// /// From 7f0e4bd2a6f51bc1e1dc147b3be69eb833a1752e Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 9 Jan 2024 20:24:30 +0200 Subject: [PATCH 352/549] [Tests] Fix RedmineFixture --- tests/redmine-net-api.Tests/Infrastructure/RedmineFixture.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/redmine-net-api.Tests/Infrastructure/RedmineFixture.cs b/tests/redmine-net-api.Tests/Infrastructure/RedmineFixture.cs index f4e7e2c1..cbcfbc63 100644 --- a/tests/redmine-net-api.Tests/Infrastructure/RedmineFixture.cs +++ b/tests/redmine-net-api.Tests/Infrastructure/RedmineFixture.cs @@ -18,7 +18,7 @@ public RedmineFixture () _redmineManagerOptionsBuilder = new RedmineManagerOptionsBuilder() .WithHost(Credentials.Uri) - .WithAuthentication(new RedmineApiKeyAuthentication(Credentials.ApiKey)); + .WithApiKeyAuthentication(Credentials.ApiKey); SetMimeTypeXml(); SetMimeTypeJson(); From d6e24a771ac8f81fa8c1d71419d51e0eee4f7d53 Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 9 Jan 2024 20:24:57 +0200 Subject: [PATCH 353/549] [New][Tests] HostValidation --- .../Tests/HostValidationTests.cs | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 tests/redmine-net-api.Tests/Tests/HostValidationTests.cs diff --git a/tests/redmine-net-api.Tests/Tests/HostValidationTests.cs b/tests/redmine-net-api.Tests/Tests/HostValidationTests.cs new file mode 100644 index 00000000..32448c72 --- /dev/null +++ b/tests/redmine-net-api.Tests/Tests/HostValidationTests.cs @@ -0,0 +1,94 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Redmine.Net.Api; +using Redmine.Net.Api.Exceptions; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Tests +{ + [Trait("Redmine-api", "Host")] + [Order(1)] + public sealed class HostValidationTests + { + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("string.Empty")] + [InlineData("localhost")] + [InlineData("http://")] + [InlineData("")] + [InlineData("xyztuv")] + [InlineData("ftp://example.com")] + [InlineData("ftp://localhost:3000")] + [InlineData("\"https://localhost:3000/"")] + [InlineData("C:/test/path/file.txt")] + [InlineData(@"\\host\share\some\directory\name\")] + [InlineData("xyz:c:\abc")] + [InlineData("file:///C:/test/path/file.txt")] + [InlineData("file://server/filename.ext")] + [InlineData("ftp://myUrl/../..")] + [InlineData("ftp://myUrl/%2E%2E/%2E%2E")] + [InlineData("example--domain.com")] + [InlineData("-example.com")] + [InlineData("example.com-")] + [InlineData("example.com/-")] + [InlineData("invalid-host")] + public void Should_Throw_Redmine_Exception_When_Host_Is_Invalid(string host) + { + // Arrange + var optionsBuilder = new RedmineManagerOptionsBuilder().WithHost(host); + + // Act and Assert + Assert.Throws(() => optionsBuilder.Build()); + } + + [Theory] + [InlineData("192.168.0.1", "https://192.168.0.1/")] + [InlineData("127.0.0.1", "https://127.0.0.1/")] + [InlineData("localhost:3000", "https://localhost:3000/")] + [InlineData("localhost:3000/", "https://localhost:3000/")] + [InlineData("https://localhost:3000/", "https://localhost:3000/")] + [InlineData("example.com", "https://example.com/")] + [InlineData("www.example.com", "https://www.example.com/")] + [InlineData("www.domain.com/", "https://www.domain.com/")] + [InlineData("www.domain.com:3000", "https://www.domain.com:3000/")] + [InlineData("https://www.google.com/", "https://www.google.com/")] + [InlineData("http://example.com:8080/", "http://example.com:8080/")] + [InlineData("http://example.com/path", "http://example.com/")] + [InlineData("http://example.com/?param=value", "http://example.com/")] + [InlineData("http://example.com/#fragment", "http://example.com/")] + [InlineData("http://example.com/", "http://example.com/")] + [InlineData("http://example.com/?param=value", "http://example.com/")] + [InlineData("http://example.com/#fragment", "http://example.com/")] + [InlineData("http://example.com/path/page", "http://example.com/")] + [InlineData("http://example.com/path/page?param=value", "http://example.com/")] + [InlineData("http://example.com/path/page#fragment","http://example.com/")] + [InlineData("http://[::1]:8080/", "http://[::1]/")] + [InlineData("http://www.domain.com/title/index.htm", "http://www.domain.com/")] + [InlineData("http://www.localhost.com/", "http://www.localhost.com/")] + [InlineData("https://www.localhost.com/", "https://www.localhost.com/")] + [InlineData("http://www.domain.com/", "http://www.domain.com/")] + [InlineData("http://www.domain.com/catalog/shownew.htm?date=today", "http://www.domain.com/")] + [InlineData("HTTP://www.domain.com:80//thick%20and%20thin.htm", "http://www.domain.com/")] + [InlineData("http://www.domain.com/index.htm#search", "http://www.domain.com/")] + [InlineData("http://www.domain.com:8080/", "http://www.domain.com:8080/")] + [InlineData("https://www.domain.com:8080/", "https://www.domain.com:8080/")] + [InlineData("http://[fe80::200:39ff:fe36:1a2d%254]/", "http://[fe80::200:39ff:fe36:1a2d]/")] + [InlineData("http://myurl/", "http://myurl/")] + [InlineData("http://[fe80::200:39ff:fe36:1a2d%254]/temp/example.htm", "http://[fe80::200:39ff:fe36:1a2d]/")] + [InlineData("http://myurl/", "http://myurl/")] + [InlineData("http://user:password@www.localhost.com/index.htm", "http://www.localhost.com/")] + public void Should_Not_Throw_Redmine_Exception_When_Host_Is_Valid(string host, string expected) + { + // Arrange + var optionsBuilder = new RedmineManagerOptionsBuilder().WithHost(host); + + // Act + var options = optionsBuilder.Build(); + + // Assert + Assert.NotNull(options); + Assert.Equal(expected, options.BaseAddress.ToString()); + } + } +} \ No newline at end of file From 8eda132357834a922e5d089e327b9bc3967ce708 Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 9 Jan 2024 20:35:21 +0200 Subject: [PATCH 354/549] [Serialization] Code arrange & clean up --- .../Serialization/IRedmineSerializer.cs | 44 +++++++++++++++++++ .../Json/JsonRedmineSerializer.cs | 4 +- .../Serialization/Xml/XmlRedmineSerializer.cs | 19 ++++---- 3 files changed, 54 insertions(+), 13 deletions(-) create mode 100644 src/redmine-net-api/Serialization/IRedmineSerializer.cs diff --git a/src/redmine-net-api/Serialization/IRedmineSerializer.cs b/src/redmine-net-api/Serialization/IRedmineSerializer.cs new file mode 100644 index 00000000..1c2e5eec --- /dev/null +++ b/src/redmine-net-api/Serialization/IRedmineSerializer.cs @@ -0,0 +1,44 @@ +/* + Copyright 2011 - 2023 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +namespace Redmine.Net.Api.Serialization +{ + /// + /// Serialization interface that supports serialize and deserialize methods. + /// + internal interface IRedmineSerializer + { + /// + /// Gets the application format this serializer supports (e.g. "json", "xml"). + /// + string Format { get; } + + /// + /// Serializes the specified object into a string. + /// + string Serialize(T obj) where T : class; + + /// + /// Deserializes the string into a PageResult of T object. + /// + PagedResults DeserializeToPagedResults(string response) where T : class, new(); + + /// + /// Deserializes the string into an object. + /// + T Deserialize(string input) where T : new(); + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs b/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs index 43cf4e8a..42807e3f 100644 --- a/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs +++ b/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs @@ -135,7 +135,7 @@ internal sealed class JsonRedmineSerializer : IRedmineSerializer } #pragma warning restore CA1822 - public string Type { get; } = "json"; + public string Format { get; } = "json"; public string Serialize(T entity) where T : class { @@ -155,7 +155,7 @@ public string Serialize(T entity) where T : class { using (var writer = new JsonTextWriter(sw)) { - writer.Formatting = Newtonsoft.Json.Formatting.Indented; + writer.Formatting = Formatting.Indented; writer.DateFormatHandling = DateFormatHandling.IsoDateFormat; jsonSerializable.WriteJson(writer); diff --git a/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs b/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs index be1764a0..179eaec0 100644 --- a/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs +++ b/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs @@ -27,20 +27,17 @@ namespace Redmine.Net.Api.Serialization internal sealed class XmlRedmineSerializer : IRedmineSerializer { - public XmlRedmineSerializer() + public XmlRedmineSerializer(): this(new XmlWriterSettings { - xmlWriterSettings = new XmlWriterSettings - { - OmitXmlDeclaration = true - }; - } + OmitXmlDeclaration = true + }) { } public XmlRedmineSerializer(XmlWriterSettings xmlWriterSettings) { - this.xmlWriterSettings = xmlWriterSettings; + this._xmlWriterSettings = xmlWriterSettings; } - private readonly XmlWriterSettings xmlWriterSettings; + private readonly XmlWriterSettings _xmlWriterSettings; public T Deserialize(string response) where T : new() { @@ -53,7 +50,7 @@ public XmlRedmineSerializer(XmlWriterSettings xmlWriterSettings) throw new RedmineException(ex.GetBaseException().Message, ex); } } - + public PagedResults DeserializeToPagedResults(string response) where T : class, new() { try @@ -81,7 +78,7 @@ public XmlRedmineSerializer(XmlWriterSettings xmlWriterSettings) } #pragma warning restore CA1822 - public string Type { get; } = "xml"; + public string Format => RedmineConstants.XML; public string Serialize(T entity) where T : class { @@ -153,7 +150,7 @@ private string ToXML(T entity) where T : class using (var stringWriter = new StringWriter()) { - using (var xmlWriter = XmlWriter.Create(stringWriter, xmlWriterSettings)) + using (var xmlWriter = XmlWriter.Create(stringWriter, _xmlWriterSettings)) { var serializer = new XmlSerializer(typeof(T)); From 0776d21fdeb73ee629c745cc2b13ccc1faa90275 Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 9 Jan 2024 22:11:40 +0200 Subject: [PATCH 355/549] [Csproj] Fix --- Directory.Build.props | 13 ------- src/redmine-net-api/redmine-net-api.csproj | 6 +++ .../redmine-net-api.Tests.csproj | 38 ++++++++++--------- 3 files changed, 27 insertions(+), 30 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 2f24981b..fb133faf 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,22 +1,9 @@ - - 11 strict true - embedded - false - $(SolutionDir)/artifacts - - - - - - - - \ No newline at end of file diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index a2a4a4d2..e6e154d0 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -34,6 +34,12 @@ latest + + embedded + false + $(SolutionDir)/artifacts + + diff --git a/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj index 6f78956b..8632e5f9 100644 --- a/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj +++ b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj @@ -1,20 +1,22 @@ + Padi.DotNet.RedmineAPI.Tests $(AssemblyName) false net481 - net40;net451;net452;net46;net461;net462;net47;net471;net472;net48;net481; false f8b9e946-b547-42f1-861c-f719dca00a84 Release;Debug;DebugJson - |net45|net451|net452|net46|net461| - |net40|net45|net451|net452|net46|net461| + |net40|net45|net451|net452|net46|net461| + |net45|net451|net452|net46|net461| + |net40|net45|net451|net452|net46|net461|net462|net470|net471|net472|net48| + |net45|net451|net452|net46|net461|net462|net470|net471|net472|net48| @@ -25,14 +27,6 @@ DEBUG;TRACE;DEBUG_JSON - - - - - - - - @@ -42,22 +36,32 @@ - + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive - + - + - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive From cf7531c0c5482d1951e291b656af93dbbd489a06 Mon Sep 17 00:00:00 2001 From: Padi Date: Tue, 9 Jan 2024 22:15:33 +0200 Subject: [PATCH 356/549] Delete redmine-net-api.sln.DotSettings --- redmine-net-api.sln.DotSettings | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 redmine-net-api.sln.DotSettings diff --git a/redmine-net-api.sln.DotSettings b/redmine-net-api.sln.DotSettings deleted file mode 100644 index 9134cb35..00000000 --- a/redmine-net-api.sln.DotSettings +++ /dev/null @@ -1,4 +0,0 @@ - - True - True - True \ No newline at end of file From bc62d10a4272d47dfc91b717ff1fad115076bbac Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 9 Jan 2024 22:22:45 +0200 Subject: [PATCH 357/549] [Types] [StringComparison] InvariantCultureIgnoreCase To OrdinalIgnoreCase --- src/redmine-net-api/Types/Detail.cs | 8 ++++---- src/redmine-net-api/Types/Error.cs | 2 +- src/redmine-net-api/Types/Search.cs | 8 ++++---- src/redmine-net-api/Types/Version.cs | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/redmine-net-api/Types/Detail.cs b/src/redmine-net-api/Types/Detail.cs index 298e93b7..9d74336a 100644 --- a/src/redmine-net-api/Types/Detail.cs +++ b/src/redmine-net-api/Types/Detail.cs @@ -173,10 +173,10 @@ public void ReadJson(JsonReader reader) public bool Equals(Detail other) { if (other == null) return false; - return string.Equals(Property, other.Property, StringComparison.InvariantCultureIgnoreCase) - && string.Equals(Name, other.Name, StringComparison.InvariantCultureIgnoreCase) - && string.Equals(OldValue, other.OldValue, StringComparison.InvariantCultureIgnoreCase) - && string.Equals(NewValue, other.NewValue, StringComparison.InvariantCultureIgnoreCase); + return string.Equals(Property, other.Property, StringComparison.OrdinalIgnoreCase) + && string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase) + && string.Equals(OldValue, other.OldValue, StringComparison.OrdinalIgnoreCase) + && string.Equals(NewValue, other.NewValue, StringComparison.OrdinalIgnoreCase); } /// diff --git a/src/redmine-net-api/Types/Error.cs b/src/redmine-net-api/Types/Error.cs index ef6b7b7e..96621f51 100644 --- a/src/redmine-net-api/Types/Error.cs +++ b/src/redmine-net-api/Types/Error.cs @@ -119,7 +119,7 @@ public bool Equals(Error other) { if (other == null) return false; - return string.Equals(Info,other.Info, StringComparison.InvariantCultureIgnoreCase); + return string.Equals(Info,other.Info, StringComparison.OrdinalIgnoreCase); } /// diff --git a/src/redmine-net-api/Types/Search.cs b/src/redmine-net-api/Types/Search.cs index d9355764..9f35e29a 100644 --- a/src/redmine-net-api/Types/Search.cs +++ b/src/redmine-net-api/Types/Search.cs @@ -123,10 +123,10 @@ public void ReadJson(JsonReader reader) public bool Equals(Search other) { if (other == null) return false; - return Id == other.Id && string.Equals(Title, other.Title, StringComparison.InvariantCultureIgnoreCase) - && string.Equals(Description, other.Description, StringComparison.InvariantCultureIgnoreCase) - && string.Equals(Url, other.Url, StringComparison.InvariantCultureIgnoreCase) - && string.Equals(Type, other.Type, StringComparison.InvariantCultureIgnoreCase) + return Id == other.Id && string.Equals(Title, other.Title, StringComparison.OrdinalIgnoreCase) + && string.Equals(Description, other.Description, StringComparison.OrdinalIgnoreCase) + && string.Equals(Url, other.Url, StringComparison.OrdinalIgnoreCase) + && string.Equals(Type, other.Type, StringComparison.OrdinalIgnoreCase) && DateTime == other.DateTime; } diff --git a/src/redmine-net-api/Types/Version.cs b/src/redmine-net-api/Types/Version.cs index ca0ed402..a9c74321 100644 --- a/src/redmine-net-api/Types/Version.cs +++ b/src/redmine-net-api/Types/Version.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -232,7 +232,7 @@ public override bool Equals(Version other) && CreatedOn == other.CreatedOn && UpdatedOn == other.UpdatedOn && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null) - && string.Equals(WikiPageTitle,other.WikiPageTitle, StringComparison.InvariantCultureIgnoreCase) + && string.Equals(WikiPageTitle,other.WikiPageTitle, StringComparison.OrdinalIgnoreCase) && EstimatedHours == other.EstimatedHours && SpentHours == other.SpentHours ; From 64856a29ac1030f8ba250cd051808f60b4611200 Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 6 Feb 2024 19:28:00 +0200 Subject: [PATCH 358/549] [Solution] Group solution folder files --- redmine-net-api.sln | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/redmine-net-api.sln b/redmine-net-api.sln index 332b18aa..935b397c 100644 --- a/redmine-net-api.sln +++ b/redmine-net-api.sln @@ -12,22 +12,42 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "redmine-net-api.Tests", "te EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionFolder", "SolutionFolder", "{E8C35EC2-DD90-46E8-9B63-84EFD5F2FDE3}" ProjectSection(SolutionItems) = preProject - appveyor.yml = appveyor.yml CHANGELOG.md = CHANGELOG.md CONTRIBUTING.md = CONTRIBUTING.md - Directory.Build.props = Directory.Build.props - docker-compose.yml = docker-compose.yml ISSUE_TEMPLATE.md = ISSUE_TEMPLATE.md LICENSE = LICENSE - logo.png = logo.png PULL_REQUEST_TEMPLATE.md = PULL_REQUEST_TEMPLATE.md README.md = README.md - redmine-net-api.snk = redmine-net-api.snk + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitActions", "GitActions", "{79119F8B-C468-4DC8-BE6F-6E7102BD2079}" + ProjectSection(SolutionItems) = preProject + .github\workflows\ci-cd.yml = .github\workflows\ci-cd.yml + .github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AppVeyor", "AppVeyor", "{F20AEA6C-B957-4A83-9616-B91548B4C561}" + ProjectSection(SolutionItems) = preProject + appveyor.yml = appveyor.yml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{707B6A3F-1A2C-4EFE-851F-1DB0E68CFFFB}" + ProjectSection(SolutionItems) = preProject + Directory.Build.props = Directory.Build.props releasenotes.props = releasenotes.props signing.props = signing.props version.props = version.props - .github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml - .github\workflows\ci-cd.yml = .github\workflows\ci-cd.yml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docker", "Docker", "{1D340EEB-C535-45D4-80D7-ADD4434D7B77}" + ProjectSection(SolutionItems) = preProject + docker-compose.yml = docker-compose.yml + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Others", "Others", "{4ADECA2A-4D7B-4F05-85A2-0C0963A83689}" + ProjectSection(SolutionItems) = preProject + logo.png = logo.png + redmine-net-api.snk = redmine-net-api.snk EndProjectSection EndProject Global @@ -55,6 +75,11 @@ Global GlobalSection(NestedProjects) = preSolution {0E6B9B72-445D-4E71-8D29-48C4A009AB03} = {0DFF4758-5C19-4D8F-BA6C-76E618323F6A} {900EF0B3-0233-45DA-811F-4C59483E8452} = {F3F4278D-6271-4F77-BA88-41555D53CBD1} + {79119F8B-C468-4DC8-BE6F-6E7102BD2079} = {E8C35EC2-DD90-46E8-9B63-84EFD5F2FDE3} + {F20AEA6C-B957-4A83-9616-B91548B4C561} = {E8C35EC2-DD90-46E8-9B63-84EFD5F2FDE3} + {707B6A3F-1A2C-4EFE-851F-1DB0E68CFFFB} = {E8C35EC2-DD90-46E8-9B63-84EFD5F2FDE3} + {1D340EEB-C535-45D4-80D7-ADD4434D7B77} = {E8C35EC2-DD90-46E8-9B63-84EFD5F2FDE3} + {4ADECA2A-4D7B-4F05-85A2-0C0963A83689} = {E8C35EC2-DD90-46E8-9B63-84EFD5F2FDE3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4AA87D90-ABD0-4793-BE47-955B35FAE2BB} From 50217b8b61732d5a1040eae535add79a4a87c1aa Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 6 Feb 2024 19:29:04 +0200 Subject: [PATCH 359/549] [RedmineManagerExtension] Remove extra semicolons --- src/redmine-net-api/Extensions/RedmineManagerExtensions.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs index 043404d9..75a778df 100644 --- a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs +++ b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs @@ -18,7 +18,6 @@ limitations under the License. using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; -using System.Net; #if !(NET20) using System.Threading; using System.Threading.Tasks; @@ -391,7 +390,7 @@ public static async Task> GetProjectNewsAsync(this RedmineMan var response = await redmineManager.ApiClient.GetPagedAsync(escapedUri, requestOptions, cancellationToken).ConfigureAwait(false); - return response.DeserializeToPagedResults(redmineManager.Serializer);; + return response.DeserializeToPagedResults(redmineManager.Serializer); } /// @@ -442,7 +441,7 @@ public static async Task> GetProjectMembershipsA var response = await redmineManager.ApiClient.GetPagedAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); - return response.DeserializeToPagedResults(redmineManager.Serializer);; + return response.DeserializeToPagedResults(redmineManager.Serializer); } /// @@ -460,7 +459,7 @@ public static async Task> GetProjectFilesAsync(this RedmineMa var response = await redmineManager.ApiClient.GetPagedAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); - return response.DeserializeToPagedResults(redmineManager.Serializer);; + return response.DeserializeToPagedResults(redmineManager.Serializer); } From 68debaccfcc7f2b5ee5078d2a9cf7c880ab5dbe2 Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 6 Feb 2024 20:13:00 +0200 Subject: [PATCH 360/549] [Docker-Compose] Change redmine image to 5.1.1-alpine version --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index e7a53c7d..3ffac57c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: redmine: ports: - '8089:3000' - image: 'redmine:4.1.1-alpine' + image: 'redmine:5.1.1-alpine' container_name: 'redmine-web' depends_on: - db-postgres From 9088e4d5646b9c8bff33687578b53ffde461db9c Mon Sep 17 00:00:00 2001 From: zapadi Date: Tue, 6 Feb 2024 20:13:38 +0200 Subject: [PATCH 361/549] [Docker-Compose] Change postgres image to 16-alpine version --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 3ffac57c..5a788f19 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,7 +33,7 @@ services: POSTGRES_USER: redmine-usr POSTGRES_PASSWORD: redmine-pswd container_name: 'redmine-db' - image: 'postgres:11.1-alpine' + image: 'postgres:16-alpine' healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 20s From d7d59067c358774593d9ea79a48bceb7f9f76dcd Mon Sep 17 00:00:00 2001 From: zapadi Date: Thu, 8 Feb 2024 19:29:16 +0200 Subject: [PATCH 362/549] [Csproj] Remove 'system.Net.Http' reference for NET20 --- src/redmine-net-api/redmine-net-api.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index e6e154d0..cc750f06 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -68,7 +68,6 @@ - From d2a67e8ab596690b10dc188684baff1513e66953 Mon Sep 17 00:00:00 2001 From: zapadi Date: Thu, 8 Feb 2024 19:27:38 +0200 Subject: [PATCH 363/549] [GitActions] Remove ci-cd.yml --- .github/workflows/ci-cd.yml | 117 ------------------------------------ 1 file changed, 117 deletions(-) delete mode 100644 .github/workflows/ci-cd.yml diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml deleted file mode 100644 index 74fb1cfd..00000000 --- a/.github/workflows/ci-cd.yml +++ /dev/null @@ -1,117 +0,0 @@ - -name: "CI/CD" -on: - workflow_dispatch: - push: - branches: [ master ] - paths-ignore: - - '**/*.md' - - '**/*.gif' - - '**/*.png' - - '**/*.gitignore' - - '**/*.gitattributes' - - LICENSE - - tests/* - tags: - - '[0-9]+.[0-9]+.[0-9]+' - pull_request: - branches: [ master ] - paths-ignore: - - '**/*.md' - - '**/*.gif' - - '**/*.png' - - '**/*.gitignore' - - '**/*.gitattributes' - - LICENSE - - tests/* - -env: - # Disable the .NET logo in the console output. - DOTNET_NOLOGO: true - - # Stop wasting time caching packages - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true - - # Disable sending usage data to Microsoft - DOTNET_CLI_TELEMETRY_OPTOUT: true - - DOTNET_ADD_GLOBAL_TOOLS_TO_PATH: false - - DOTNET_MULTILEVEL_LOOKUP: 0 - - # Project name to pack and publish - PROJECT_NAME: redmine-net-api - - BUILD_CONFIGURATION: Release - - # Set the build number in MinVer. - MINVERBUILDMETADATA: build.${{github.run_number}} - -jobs: - build: - name: OS ${{ matrix.os }} - dotnet ${{ matrix.dotnet }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ ubuntu-latest, windows-latest, macos-latest ] - dotnet: [ '7.x.x' ] - - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - lfs: true - fetch-depth: 0 - - - name: Setup .NET Core - uses: actions/setup-dotnet@v3 - with: - dotnet-version: ${{ matrix.dotnet }} - - - name: Get Version - run: | - echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV - - - name: Restore - run: dotnet restore - - - name: Build - run: | - dotnet build --no-restore --configuration $BUILD_CONFIGURATION -p:Version=$VERSION - - - name: Build Signed - if: runner.os == 'Linux' - run: dotnet build redmine-net-api.sln --no-restore --configuration $BUILD_CONFIGURATION -p:Version=$VERSION -p:Sign=true - - - name: Test - run: dotnet test --no-restore --no-build --configuration $BUILD_CONFIGURATION - - - name: Pack && startsWith(github.ref, 'refs/tags') - if: runner.os == 'Linux' - run: | - dotnet pack ./src/redmine-net-api/redmine-net-api.csproj -o ./artifacts --configuration $BUILD_CONFIGURATION -p:Version=$VERSION --include-symbols --include-source -p:SymbolPackageFormat=snupkg - - - name: Pack Signed && startsWith(github.ref, 'refs/tags') - if: runner.os == 'Linux' - run: | - dotnet pack ./src/redmine-net-api/redmine-net-api.csproj -o ./artifacts --configuration $BUILD_CONFIGURATION -p:Version=$VERSION --include-symbols --include-source -p:Sign=true -p:SymbolPackageFormat=snupkg - - - uses: actions/upload-artifact@v3 - if: runner.os == 'Linux' - with: - name: artifacts - path: ./artifacts - - deploy: - runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags') - needs: build - name: Deploy Packages - steps: - - uses: actions/download-artifact@v3 - with: - name: artifacts - path: ./artifacts - - - name: Publish packages - run: dotnet nuget push ./artifacts/**.nupkg --source nuget.org -k ${{secrets.NUGET_API_KEY}} \ No newline at end of file From 6d3e6dbfa25a71254fc4d7b73724275f790c06fc Mon Sep 17 00:00:00 2001 From: zapadi Date: Thu, 8 Feb 2024 19:31:44 +0200 Subject: [PATCH 364/549] [New][GitActions] build.yml --- .github/workflows/build.yml | 84 +++++++++++++++++++++++++++++++++++++ redmine-net-api.sln | 2 +- 2 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..acc154cf --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,84 @@ +name: build + +on: + workflow_dispatch: + inputs: + reason: + description: 'The reason for running the workflow' + required: false + default: 'Manual run' + push: + pull_request: + branches: [ main ] + paths: + - '**.cs' + - '**.csproj' + +env: + # Disable the .NET logo in the console output. + DOTNET_NOLOGO: true + + # Stop wasting time caching packages + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + + # Disable sending usage data to Microsoft + DOTNET_CLI_TELEMETRY_OPTOUT: true + + DOTNET_ADD_GLOBAL_TOOLS_TO_PATH: false + + DOTNET_MULTILEVEL_LOOKUP: 0 + + PROJECT_PATH: "." + + PROJECT_NAME: redmine-net-api + + CONFIGURATION: 'Release' + + # Set the build number in MinVer. + MINVERBUILDMETADATA: build.${{github.run_number}} + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build ${{ matrix.os }} - dotnet ${{ matrix.dotnet }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ ubuntu-latest, windows-latest, macos-latest ] + + steps: + - name: Print manual run reason + if: ${{ github.event_name == 'workflow_dispatch' }} + run: | + echo 'Reason: ${{ github.event.inputs.reason }}' + + - name: Checkout + uses: actions/checkout@v4 + with: + lfs: true + fetch-depth: 0 + + - name: Setup .NET (global.json) + uses: actions/setup-dotnet@v4 + + - uses: actions/cache@v4 + with: + path: ~/.nuget/packages + # Look to see if there is a cache hit for the corresponding requirements file + key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} + restore-keys: | + ${{ runner.os }}-nuget + + - name: Restore + run: dotnet restore "${{ env.PROJECT_PATH }}" + + - name: Build + run: | + dotnet build "${{ env.PROJECT_PATH }}" \ + --configuration "${{ env.CONFIGURATION }}" \ + --no-restore \ + /p:ContinuousIntegrationBuild=true \ No newline at end of file diff --git a/redmine-net-api.sln b/redmine-net-api.sln index 935b397c..9d76a38e 100644 --- a/redmine-net-api.sln +++ b/redmine-net-api.sln @@ -22,8 +22,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionFolder", "SolutionF EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitActions", "GitActions", "{79119F8B-C468-4DC8-BE6F-6E7102BD2079}" ProjectSection(SolutionItems) = preProject - .github\workflows\ci-cd.yml = .github\workflows\ci-cd.yml .github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml + .github\workflows\build.yml = .github\workflows\build.yml EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AppVeyor", "AppVeyor", "{F20AEA6C-B957-4A83-9616-B91548B4C561}" From 472c3f98fa8d93b8f8203e0e6844a77024465c3c Mon Sep 17 00:00:00 2001 From: zapadi Date: Thu, 8 Feb 2024 19:52:24 +0200 Subject: [PATCH 365/549] [GitActions][build] Remove quotes --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index acc154cf..4971c9a7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,7 +32,7 @@ env: PROJECT_NAME: redmine-net-api - CONFIGURATION: 'Release' + CONFIGURATION: Release # Set the build number in MinVer. MINVERBUILDMETADATA: build.${{github.run_number}} From faaf41d195fded141cab3bae7b00c33320d7eb09 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 10 Feb 2024 20:45:41 +0200 Subject: [PATCH 366/549] [GitActions][build] Change '|' character with '>-' --- .github/workflows/build.yml | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4971c9a7..54e69e8e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,10 +28,8 @@ env: DOTNET_MULTILEVEL_LOOKUP: 0 - PROJECT_PATH: "." + PROJECT_PATH: . - PROJECT_NAME: redmine-net-api - CONFIGURATION: Release # Set the build number in MinVer. @@ -77,8 +75,8 @@ jobs: run: dotnet restore "${{ env.PROJECT_PATH }}" - name: Build - run: | - dotnet build "${{ env.PROJECT_PATH }}" \ - --configuration "${{ env.CONFIGURATION }}" \ - --no-restore \ - /p:ContinuousIntegrationBuild=true \ No newline at end of file + run: >- + dotnet build "${{ env.PROJECT_PATH }}" + --configuration "${{ env.CONFIGURATION }}" + --no-restore + -p:ContinuousIntegrationBuild=true \ No newline at end of file From 41622ea0a290e03283b8bc8eb45ef611900ae751 Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 12 Feb 2024 16:58:48 +0200 Subject: [PATCH 367/549] [GitActions][New] build & test workflow --- .github/workflows/build-and-test.yml | 35 ++++++++++++++++++++++++++++ redmine-net-api.sln | 1 + 2 files changed, 36 insertions(+) create mode 100644 .github/workflows/build-and-test.yml diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml new file mode 100644 index 00000000..1c546e0d --- /dev/null +++ b/.github/workflows/build-and-test.yml @@ -0,0 +1,35 @@ +name: build and test + +on: + workflow_dispatch: + inputs: + reason: + description: 'The reason for running the workflow' + required: false + default: 'Manual build and run tests' + workflow_run: + workflows: [ Build ] + types: + - completed + +jobs: + build: + uses: ./.github/workflows/build.yml + test: + name: Test - ${{matrix.os}} + needs: [build] + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ ubuntu-latest, windows-latest, macOS-latest ] + steps: + - name: Test + if: ${{ github.event.workflow_run.conclusion == 'success' }} + timeout-minutes: 60 + run: >- + dotnet test "${{ env.PROJECT_PATH }}" + --no-restore + --no-build + --verbosity normal + --logger "trx;LogFileName=test-results.trx" || true + \ No newline at end of file diff --git a/redmine-net-api.sln b/redmine-net-api.sln index 9d76a38e..36df19c4 100644 --- a/redmine-net-api.sln +++ b/redmine-net-api.sln @@ -24,6 +24,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitActions", "GitActions", ProjectSection(SolutionItems) = preProject .github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml .github\workflows\build.yml = .github\workflows\build.yml + .github\workflows\build-and-test.yml = .github\workflows\build-and-test.yml EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AppVeyor", "AppVeyor", "{F20AEA6C-B957-4A83-9616-B91548B4C561}" From c3ec525dfd532588249901ae75997c0bd23cd724 Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 12 Feb 2024 16:59:05 +0200 Subject: [PATCH 368/549] [New] global.json --- global.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 global.json diff --git a/global.json b/global.json new file mode 100644 index 00000000..88e2f39a --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "8.0.101", + "allowPrerelease": false, + "rollForward": "latestMajor" + } +} \ No newline at end of file From b6c3d081e2c77aadc507ac59378da142f41684bc Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 12 Feb 2024 16:59:26 +0200 Subject: [PATCH 369/549] [LangVersion] Set to 12 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index fb133faf..3d16a055 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@ - 11 + 12 strict true From 4c77e7719a94fe92d5e8e785a16d6e83c52fedcd Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 12 Feb 2024 17:22:39 +0200 Subject: [PATCH 370/549] [GitActions][build] Set to be reusable & to trigger on 'master' PR --- .github/workflows/build.yml | 6 +++++- redmine-net-api.sln | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 54e69e8e..9d49289f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,7 @@ name: build on: + workflow_call: workflow_dispatch: inputs: reason: @@ -8,8 +9,11 @@ on: required: false default: 'Manual run' push: + paths: + - '**.cs' + - '**.csproj' pull_request: - branches: [ main ] + branches: [ master ] paths: - '**.cs' - '**.csproj' diff --git a/redmine-net-api.sln b/redmine-net-api.sln index 36df19c4..3a5d83e1 100644 --- a/redmine-net-api.sln +++ b/redmine-net-api.sln @@ -49,6 +49,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Others", "Others", "{4ADECA ProjectSection(SolutionItems) = preProject logo.png = logo.png redmine-net-api.snk = redmine-net-api.snk + global.json = global.json EndProjectSection EndProject Global From b0d217efff17edbaa408b55c80100b794e4add85 Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 12 Feb 2024 18:35:53 +0200 Subject: [PATCH 371/549] [New][GitActions] pack & publish workflows --- .github/workflows/pack.yml | 103 ++++++++++++++++++++++++++++++++++ .github/workflows/publish.yml | 47 ++++++++++++++++ redmine-net-api.sln | 2 + 3 files changed, 152 insertions(+) create mode 100644 .github/workflows/pack.yml create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/pack.yml b/.github/workflows/pack.yml new file mode 100644 index 00000000..a813fb3d --- /dev/null +++ b/.github/workflows/pack.yml @@ -0,0 +1,103 @@ +name: 'Pack' + +on: + workflow_run: + workflows: [ 'Build' ] + types: [ requested ] + branches: [ master ] + + workflow_dispatch: + inputs: + reason: + description: 'The reason for running the workflow' + required: false + default: 'Manual pack' + version: + description: 'Version' + required: true + +env: + CONFIGURATION: 'Release' + + PROJECT_PATH: "." + + PROJECT_NAME: redmine-net-api + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + pack: + name: Pack + if: github.ref == 'refs/heads/master' + runs-on: ubuntu-latest + steps: + - name: Determine Version + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + echo "VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV + else + echo "VERSION=$(git describe --tags `git rev-list --tags --max-count=1`)" >> $GITHUB_ENV + fi + echo "$GITHUB_ENV" + + - name: Print Version + run: | + echo "$VERSION" + + - name: Validate Version matches SemVer format + run: | + if [[ ! "$VERSION" =~ ^([0-9]+\.){2}[0-9]+$ ]]; then + echo "The version does not match the SemVer format (X.Y.Z). Please provide a valid version." + exit 1 + fi + + - name: Checkout + uses: actions/checkout@v4 + with: + lfs: true + fetch-depth: 0 + + - name: Setup .NET Core (global.json) + uses: actions/setup-dotnet@v4 + + - name: Install dependencies + run: dotnet restore "${{ env.PROJECT_PATH }}" + + - name: Pack + run: >- + dotnet pack ./src/redmine-net-api/redmine-net-api.csproj + --output ./artifacts + --configuration "${{ env.CONFIGURATION }}" + -p:Version=$VERSION + -p:PackageVersion=${{ env.VERSION }} + -p:SymbolPackageFormat=snupkg + + - name: Pack Signed + run: >- + dotnet pack ./src/redmine-net-api/redmine-net-api.csproj + --output ./artifacts + --configuration "${{ env.CONFIGURATION }}" + --include-symbols + --include-source + -p:Version=$VERSION + -p:PackageVersion=${{ env.VERSION }} + -p:SymbolPackageFormat=snupkg + -p:Sign=true + + - name: Install dotnet-validate + run: >- + dotnet tool install + --global dotnet-validate + --version 0.0.1-preview.304 + + - name: Validate NuGet package + run: | + dotnet-validate package local ./artifacts/**.nupkg + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: artifacts + path: ./artifacts diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..605ba8b3 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,47 @@ +name: 'Publish to NuGet' + +on: + workflow_dispatch: + inputs: + reason: + description: 'The reason for running the workflow' + required: false + default: 'Manual publish to nuget' + push: + tags: + - '[0-9]+.[0-9]+.[0-9]+' + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + publish: + name: Publish to Nuget + if: github.ref == 'refs/heads/master' + runs-on: ubuntu-latest + steps: + - name: Print manual run reason + if: ${{ github.event_name == 'workflow_dispatch' }} + run: | + echo 'Reason: ${{ github.event.inputs.reason }}' + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: artifacts + path: ./artifacts + + - name: Publish packages + run: >- + dotnet nuget push ./artifacts/**.nupkg + --source 'https://api.nuget.org/v3/index.json' + --api-key ${{secrets.NUGET_TOKEN}} + --skip-duplicate + + - name: Upload artifacts to the GitHub release + uses: Roang-zero1/github-upload-release-artifacts-action@v3.0.0 + with: + args: ./artifacts + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/redmine-net-api.sln b/redmine-net-api.sln index 3a5d83e1..2715270a 100644 --- a/redmine-net-api.sln +++ b/redmine-net-api.sln @@ -23,8 +23,10 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitActions", "GitActions", "{79119F8B-C468-4DC8-BE6F-6E7102BD2079}" ProjectSection(SolutionItems) = preProject .github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml + .github\workflows\pack.yml = .github\workflows\pack.yml .github\workflows\build.yml = .github\workflows\build.yml .github\workflows\build-and-test.yml = .github\workflows\build-and-test.yml + .github\workflows\publish.yml = .github\workflows\publish.yml EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AppVeyor", "AppVeyor", "{F20AEA6C-B957-4A83-9616-B91548B4C561}" From b5d7c48378386c5b68012c35a4c7ce19e4957f3f Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 12 Feb 2024 18:37:33 +0200 Subject: [PATCH 372/549] [GitActions] Add quotes to workflow names --- .github/workflows/build-and-test.yml | 2 +- .github/workflows/build.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 1c546e0d..ee0b6a3f 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -1,4 +1,4 @@ -name: build and test +name: 'Build and Test' on: workflow_dispatch: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9d49289f..d40f5180 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,13 +1,13 @@ -name: build +name: 'Build' on: - workflow_call: workflow_dispatch: inputs: reason: description: 'The reason for running the workflow' required: false default: 'Manual run' + workflow_call: push: paths: - '**.cs' From 1f1cf0cd5ee47b90b30e79143c7440531d1d89d5 Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 12 Feb 2024 18:41:49 +0200 Subject: [PATCH 373/549] [GitActions][build] Commented out on push paths: --- .github/workflows/build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d40f5180..39c3dd2b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,11 +7,11 @@ on: description: 'The reason for running the workflow' required: false default: 'Manual run' - workflow_call: +# workflow_call: push: - paths: - - '**.cs' - - '**.csproj' +# paths: +# - '**.cs' +# - '**.csproj' pull_request: branches: [ master ] paths: From b1cc6a5e5560487aebfc19ad0128c3be091af1ac Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 12 Feb 2024 19:00:59 +0200 Subject: [PATCH 374/549] [GitActions][build] Add option to be reusable --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 39c3dd2b..d6dbffc4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ on: description: 'The reason for running the workflow' required: false default: 'Manual run' -# workflow_call: + workflow_call: push: # paths: # - '**.cs' From 8f10995c0f140cfbd408897f9798e49b041baeef Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 12 Feb 2024 20:43:49 +0200 Subject: [PATCH 375/549] [GitActions] Updates --- .github/workflows/build-and-test.yml | 4 ++-- .github/workflows/build.yml | 6 +++--- .github/workflows/pack.yml | 8 +++++--- .github/workflows/publish.yml | 7 ++++++- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index ee0b6a3f..524903c9 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -8,7 +8,7 @@ on: required: false default: 'Manual build and run tests' workflow_run: - workflows: [ Build ] + workflows: [ 'Build' ] types: - completed @@ -24,7 +24,7 @@ jobs: os: [ ubuntu-latest, windows-latest, macOS-latest ] steps: - name: Test - if: ${{ github.event.workflow_run.conclusion == 'success' }} + # if: ${{ github.event.workflow_run.conclusion == 'success' }} timeout-minutes: 60 run: >- dotnet test "${{ env.PROJECT_PATH }}" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d6dbffc4..d40f5180 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,9 +9,9 @@ on: default: 'Manual run' workflow_call: push: -# paths: -# - '**.cs' -# - '**.csproj' + paths: + - '**.cs' + - '**.csproj' pull_request: branches: [ master ] paths: diff --git a/.github/workflows/pack.yml b/.github/workflows/pack.yml index a813fb3d..710e4df4 100644 --- a/.github/workflows/pack.yml +++ b/.github/workflows/pack.yml @@ -2,10 +2,12 @@ name: 'Pack' on: workflow_run: - workflows: [ 'Build' ] + workflows: [ 'Build and Test' ] types: [ requested ] branches: [ master ] + workflow_call: + workflow_dispatch: inputs: reason: @@ -48,7 +50,7 @@ jobs: - name: Validate Version matches SemVer format run: | - if [[ ! "$VERSION" =~ ^([0-9]+\.){2}[0-9]+$ ]]; then + if [[ ! "$VERSION" =~ ^([0-9]+\.){2}[0-9]+(-[\w.]+)?$ ]]; then echo "The version does not match the SemVer format (X.Y.Z). Please provide a valid version." exit 1 fi @@ -93,7 +95,7 @@ jobs: --version 0.0.1-preview.304 - name: Validate NuGet package - run: | + run: >- dotnet-validate package local ./artifacts/**.nupkg - name: Upload artifacts diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 605ba8b3..8f8c8a8f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -7,9 +7,14 @@ on: description: 'The reason for running the workflow' required: false default: 'Manual publish to nuget' + + workflow_run: + workflows: [ 'Pack' ] + types: + - completed push: tags: - - '[0-9]+.[0-9]+.[0-9]+' + - '[0-9]+.[0-9]+.[0-9]+(-[\w.]+)?' concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} From 84eb283022ae6160071757a3afc9ecbd013a96db Mon Sep 17 00:00:00 2001 From: zapadi Date: Thu, 22 Feb 2024 17:53:53 +0200 Subject: [PATCH 376/549] [GitActions][Delete] build & pack workflows --- .github/workflows/build.yml | 86 ----------------------------- .github/workflows/pack.yml | 105 ------------------------------------ redmine-net-api.sln | 2 - 3 files changed, 193 deletions(-) delete mode 100644 .github/workflows/build.yml delete mode 100644 .github/workflows/pack.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index d40f5180..00000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: 'Build' - -on: - workflow_dispatch: - inputs: - reason: - description: 'The reason for running the workflow' - required: false - default: 'Manual run' - workflow_call: - push: - paths: - - '**.cs' - - '**.csproj' - pull_request: - branches: [ master ] - paths: - - '**.cs' - - '**.csproj' - -env: - # Disable the .NET logo in the console output. - DOTNET_NOLOGO: true - - # Stop wasting time caching packages - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true - - # Disable sending usage data to Microsoft - DOTNET_CLI_TELEMETRY_OPTOUT: true - - DOTNET_ADD_GLOBAL_TOOLS_TO_PATH: false - - DOTNET_MULTILEVEL_LOOKUP: 0 - - PROJECT_PATH: . - - CONFIGURATION: Release - - # Set the build number in MinVer. - MINVERBUILDMETADATA: build.${{github.run_number}} - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - build: - name: Build ${{ matrix.os }} - dotnet ${{ matrix.dotnet }} - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ ubuntu-latest, windows-latest, macos-latest ] - - steps: - - name: Print manual run reason - if: ${{ github.event_name == 'workflow_dispatch' }} - run: | - echo 'Reason: ${{ github.event.inputs.reason }}' - - - name: Checkout - uses: actions/checkout@v4 - with: - lfs: true - fetch-depth: 0 - - - name: Setup .NET (global.json) - uses: actions/setup-dotnet@v4 - - - uses: actions/cache@v4 - with: - path: ~/.nuget/packages - # Look to see if there is a cache hit for the corresponding requirements file - key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} - restore-keys: | - ${{ runner.os }}-nuget - - - name: Restore - run: dotnet restore "${{ env.PROJECT_PATH }}" - - - name: Build - run: >- - dotnet build "${{ env.PROJECT_PATH }}" - --configuration "${{ env.CONFIGURATION }}" - --no-restore - -p:ContinuousIntegrationBuild=true \ No newline at end of file diff --git a/.github/workflows/pack.yml b/.github/workflows/pack.yml deleted file mode 100644 index 710e4df4..00000000 --- a/.github/workflows/pack.yml +++ /dev/null @@ -1,105 +0,0 @@ -name: 'Pack' - -on: - workflow_run: - workflows: [ 'Build and Test' ] - types: [ requested ] - branches: [ master ] - - workflow_call: - - workflow_dispatch: - inputs: - reason: - description: 'The reason for running the workflow' - required: false - default: 'Manual pack' - version: - description: 'Version' - required: true - -env: - CONFIGURATION: 'Release' - - PROJECT_PATH: "." - - PROJECT_NAME: redmine-net-api - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - pack: - name: Pack - if: github.ref == 'refs/heads/master' - runs-on: ubuntu-latest - steps: - - name: Determine Version - run: | - if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then - echo "VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV - else - echo "VERSION=$(git describe --tags `git rev-list --tags --max-count=1`)" >> $GITHUB_ENV - fi - echo "$GITHUB_ENV" - - - name: Print Version - run: | - echo "$VERSION" - - - name: Validate Version matches SemVer format - run: | - if [[ ! "$VERSION" =~ ^([0-9]+\.){2}[0-9]+(-[\w.]+)?$ ]]; then - echo "The version does not match the SemVer format (X.Y.Z). Please provide a valid version." - exit 1 - fi - - - name: Checkout - uses: actions/checkout@v4 - with: - lfs: true - fetch-depth: 0 - - - name: Setup .NET Core (global.json) - uses: actions/setup-dotnet@v4 - - - name: Install dependencies - run: dotnet restore "${{ env.PROJECT_PATH }}" - - - name: Pack - run: >- - dotnet pack ./src/redmine-net-api/redmine-net-api.csproj - --output ./artifacts - --configuration "${{ env.CONFIGURATION }}" - -p:Version=$VERSION - -p:PackageVersion=${{ env.VERSION }} - -p:SymbolPackageFormat=snupkg - - - name: Pack Signed - run: >- - dotnet pack ./src/redmine-net-api/redmine-net-api.csproj - --output ./artifacts - --configuration "${{ env.CONFIGURATION }}" - --include-symbols - --include-source - -p:Version=$VERSION - -p:PackageVersion=${{ env.VERSION }} - -p:SymbolPackageFormat=snupkg - -p:Sign=true - - - name: Install dotnet-validate - run: >- - dotnet tool install - --global dotnet-validate - --version 0.0.1-preview.304 - - - name: Validate NuGet package - run: >- - dotnet-validate package local ./artifacts/**.nupkg - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: artifacts - path: ./artifacts diff --git a/redmine-net-api.sln b/redmine-net-api.sln index 2715270a..7cbed138 100644 --- a/redmine-net-api.sln +++ b/redmine-net-api.sln @@ -23,8 +23,6 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitActions", "GitActions", "{79119F8B-C468-4DC8-BE6F-6E7102BD2079}" ProjectSection(SolutionItems) = preProject .github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml - .github\workflows\pack.yml = .github\workflows\pack.yml - .github\workflows\build.yml = .github\workflows\build.yml .github\workflows\build-and-test.yml = .github\workflows\build-and-test.yml .github\workflows\publish.yml = .github\workflows\publish.yml EndProjectSection From a14530fa7a5fbbf62b76455bfbf7f2c16bac7873 Mon Sep 17 00:00:00 2001 From: zapadi Date: Thu, 22 Feb 2024 17:54:58 +0200 Subject: [PATCH 377/549] [GitActions] Improve build, test & pack workflows --- .github/workflows/build-and-test.yml | 110 +++++++++++++--- .github/workflows/publish.yml | 180 +++++++++++++++++++++++---- 2 files changed, 244 insertions(+), 46 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 524903c9..1cc0e016 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -1,35 +1,107 @@ name: 'Build and Test' on: + workflow_call: workflow_dispatch: inputs: reason: description: 'The reason for running the workflow' required: false default: 'Manual build and run tests' - workflow_run: - workflows: [ 'Build' ] - types: - - completed + push: + tags-ignore: + - '[0-9]+.[0-9]+.[0-9]+*' + paths: + - '**.cs' + - '**.csproj' + - '**.sln' + pull_request: + branches: [ master ] + paths: + - '**.cs' + - '**.csproj' + - '**.sln' + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + # Disable the .NET logo in the console output. + DOTNET_NOLOGO: true + + # Stop wasting time caching packages + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + + # Disable sending usage data to Microsoft + DOTNET_CLI_TELEMETRY_OPTOUT: true + + DOTNET_ADD_GLOBAL_TOOLS_TO_PATH: false + DOTNET_MULTILEVEL_LOOKUP: 0 + + PROJECT_PATH: . + + CONFIGURATION: Release + jobs: build: - uses: ./.github/workflows/build.yml - test: - name: Test - ${{matrix.os}} - needs: [build] + needs: before + name: Build ${{ matrix.os }} - dotnet ${{ matrix.dotnet }} runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: - os: [ ubuntu-latest, windows-latest, macOS-latest ] + os: [ ubuntu-latest, windows-latest, macos-latest ] + steps: - - name: Test - # if: ${{ github.event.workflow_run.conclusion == 'success' }} - timeout-minutes: 60 - run: >- - dotnet test "${{ env.PROJECT_PATH }}" - --no-restore - --no-build - --verbosity normal - --logger "trx;LogFileName=test-results.trx" || true - \ No newline at end of file + - name: Print manual run reason + if: ${{ github.event_name == 'workflow_dispatch' }} + run: | + echo 'Reason: ${{ github.event.inputs.reason }}' + + - name: Checkout + uses: actions/checkout@v4 + with: + lfs: true + fetch-depth: 0 + + - name: Setup .NET (global.json) + uses: actions/setup-dotnet@v4 + + - name: Display dotnet version + run: dotnet --version + + - uses: actions/cache@v4 + with: + path: ~/.nuget/packages + # Look to see if there is a cache hit for the corresponding requirements file + key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} + restore-keys: | + ${{ runner.os }}-nuget + + - name: Restore + run: dotnet restore "${{ env.PROJECT_PATH }}" + + - name: Build + run: >- + dotnet build "${{ env.PROJECT_PATH }}" + --configuration "${{ env.CONFIGURATION }}" + --no-restore + + - name: Test + timeout-minutes: 60 + run: >- + dotnet test "${{ env.PROJECT_PATH }}" + --no-restore + --no-build + --verbosity normal + --logger trx + --results-directory "TestResults-${{ matrix.os }}" || true + + - name: Upload test results + if: ${{ always() }} + uses: actions/upload-artifact@v4 + with: + name: test-results-${{ matrix.os }} + path: TestResults-${{ matrix.os }} \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8f8c8a8f..91fdaed1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,52 +1,178 @@ name: 'Publish to NuGet' -on: +on: workflow_dispatch: inputs: reason: description: 'The reason for running the workflow' required: false - default: 'Manual publish to nuget' - - workflow_run: - workflows: [ 'Pack' ] - types: - - completed + default: 'Manual publish' + version: + description: 'Version' + required: true push: tags: - - '[0-9]+.[0-9]+.[0-9]+(-[\w.]+)?' - + - '[0-9]+.[0-9]+.[0-9]+*' + +env: + # Disable the .NET logo in the console output. + DOTNET_NOLOGO: true + + # Disable sending usage data to Microsoft + DOTNET_CLI_TELEMETRY_OPTOUT: true + + # Set working directory + PROJECT_PATH: ./src/redmine-net-api/redmine-net-api.csproj + + # Configuration + CONFIGURATION: Release + concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true - -jobs: + +jobs: + check-tag-branch: + name: Check Tag and Master Branch hashes + # This job is based on replies in https://github.community/t/how-to-create-filter-on-both-tag-and-branch/16936/6 + runs-on: ubuntu-latest + outputs: + ver: ${{ steps.set-version.outputs.VERSION }} + steps: + - name: Get tag commit hash + id: tag-commit-hash + run: | + hash=${{ github.sha }} + echo "{name}=tag-hash::${hash}" >> $GITHUB_OUTPUT + echo $hash + + - name: Checkout master + uses: actions/checkout@v4 + with: + ref: master + + - name: Get latest master commit hash + id: master-commit-hash + run: | + hash=$(git log -n1 --format=format:"%H") + echo "{name}=master-hash::${hash}" >> $GITHUB_OUTPUT + echo $hash + + - name: Verify tag commit matches master commit - exit if they don't match + if: steps.tag-commit-hash.outputs.tag-hash != steps.master-commit-hash.outputs.master-hash + run: | + echo "Tag was not on the master branch. Exiting." + exit 1 + + - name: Get Dispatched Version + if: github.event_name == 'workflow_dispatch' + run: | + echo "VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV + + - name: Get Tag Version + if: github.event_name == 'push' + run: | + echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV + + - name: Set Version + id: set-version + run: | + echo "VERSION=${{ env.VERSION }}" >> "$GITHUB_OUTPUT" + + validate-version: + name: Validate Version + needs: check-tag-branch + runs-on: ubuntu-latest + steps: + - name: Get Version + run: echo "VERSION=${{ needs.check-tag-branch.outputs.ver }}" >> $GITHUB_ENV + + - name: Display Version + run: echo "$VERSION" + + - name: Check Version Is Declared + run: | + if [[ -z "$VERSION" ]]; then + echo "Version is not declared." + exit 1 + fi + + - name: Validate Version matches SemVer format + run: | + if [[ ! "$VERSION" =~ ^([0-9]+\.){2,3}[0-9]+(-[a-zA-Z0-9.-]+)*$ ]]; then + echo "The version does not match the SemVer format (X.Y.Z). Please provide a valid version." + exit 1 + fi + + call-build-and-test: + name: Call Build and Test + needs: validate-version + uses: ./.github/workflows/build-and-test.yml + + pack: + name: Pack + needs: [check-tag-branch, validate-version, call-build-and-test] + runs-on: ubuntu-latest + steps: + - name: Get Version + run: echo "VERSION=${{ needs.check-tag-branch.outputs.ver }}" >> $GITHUB_ENV + + - name: Checkout + uses: actions/checkout@v4 + with: + lfs: true + fetch-depth: 0 + + - name: Setup .NET Core (global.json) + uses: actions/setup-dotnet@v4 + + - name: Display dotnet version + run: dotnet --version + + - name: Install dependencies + run: dotnet restore "${{ env.PROJECT_PATH }}" + + - name: Create the package + run: >- + dotnet pack "${{ env.PROJECT_PATH }}" + --output ./artifacts + --configuration "${{ env.CONFIGURATION }}" + -p:Version=$VERSION + -p:PackageVersion=$VERSION + -p:SymbolPackageFormat=snupkg + + - name: Create the package - Signed + run: >- + dotnet pack "${{ env.PROJECT_PATH }}" + --output ./artifacts + --configuration "${{ env.CONFIGURATION }}" + --include-symbols + --include-source + -p:Version=$VERSION + -p:PackageVersion=$VERSION + -p:SymbolPackageFormat=snupkg + -p:Sign=true + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: artifacts + path: ./artifacts + publish: name: Publish to Nuget - if: github.ref == 'refs/heads/master' + needs: pack runs-on: ubuntu-latest steps: - - name: Print manual run reason - if: ${{ github.event_name == 'workflow_dispatch' }} - run: | - echo 'Reason: ${{ github.event.inputs.reason }}' - - name: Download artifacts uses: actions/download-artifact@v4 with: name: artifacts - path: ./artifacts - + path: ./artifacts + - name: Publish packages run: >- dotnet nuget push ./artifacts/**.nupkg --source 'https://api.nuget.org/v3/index.json' --api-key ${{secrets.NUGET_TOKEN}} - --skip-duplicate - - - name: Upload artifacts to the GitHub release - uses: Roang-zero1/github-upload-release-artifacts-action@v3.0.0 - with: - args: ./artifacts - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + --skip-duplicate \ No newline at end of file From c7abcf9cbfbf0991d7a1f00ffbf52e06088e298c Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 28 Feb 2024 20:51:04 +0200 Subject: [PATCH 378/549] [New][SemaphoreSlimExtensions] - WaitAsync for .net 4.0 --- .../Extensions/SemaphoreSlimExtensions.cs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/redmine-net-api/Extensions/SemaphoreSlimExtensions.cs diff --git a/src/redmine-net-api/Extensions/SemaphoreSlimExtensions.cs b/src/redmine-net-api/Extensions/SemaphoreSlimExtensions.cs new file mode 100644 index 00000000..a47dbb9b --- /dev/null +++ b/src/redmine-net-api/Extensions/SemaphoreSlimExtensions.cs @@ -0,0 +1,35 @@ +/* +Copyright 2011 - 2023 Adrian Popescu + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#if !(NET20) +using System.Threading; +using System.Threading.Tasks; + +namespace Redmine.Net.Api.Extensions; +#if !(NET45_OR_GREATER || NETCOREAPP) +internal static class SemaphoreSlimExtensions +{ + + public static Task WaitAsync(this SemaphoreSlim semaphore, CancellationToken cancellationToken = default) + { + return Task.Factory.StartNew(() => semaphore.Wait(cancellationToken) + , CancellationToken.None + , TaskCreationOptions.None + , TaskScheduler.Default); + } +} +#endif +#endif From 3346e36aeff43c064a733bc99db0a836bff55c21 Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 28 Feb 2024 20:52:08 +0200 Subject: [PATCH 379/549] [New][TaskExtensions] --- .../Extensions/TaskExtensions.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/redmine-net-api/Extensions/TaskExtensions.cs diff --git a/src/redmine-net-api/Extensions/TaskExtensions.cs b/src/redmine-net-api/Extensions/TaskExtensions.cs new file mode 100644 index 00000000..865ebc42 --- /dev/null +++ b/src/redmine-net-api/Extensions/TaskExtensions.cs @@ -0,0 +1,45 @@ +/* +Copyright 2011 - 2023 Adrian Popescu + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#if !(NET20) +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Redmine.Net.Api.Extensions; + +internal static class TaskExtensions +{ + public static T GetAwaiterResult(this Task task) + { + return task.GetAwaiter().GetResult(); + } + + public static TResult Synchronize(Func> function) + { + return Task.Factory.StartNew(function, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default) + .Unwrap().GetAwaiter().GetResult(); + } + + public static void Synchronize(Func function) + { + Task.Factory.StartNew(function, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default) + .Unwrap().GetAwaiter().GetResult(); + } +} +#endif \ No newline at end of file From 86c803fc1c46b23838e31ac897ee56a14b64e786 Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 28 Feb 2024 22:11:48 +0200 Subject: [PATCH 380/549] [RedmineManagerAsync] Add ReplaceEndingsRegex for NET70 onwards --- src/redmine-net-api/RedmineManagerAsync.cs | 27 +++++++++++++++------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/redmine-net-api/RedmineManagerAsync.cs b/src/redmine-net-api/RedmineManagerAsync.cs index 50192c5a..86dc0e27 100644 --- a/src/redmine-net-api/RedmineManagerAsync.cs +++ b/src/redmine-net-api/RedmineManagerAsync.cs @@ -15,10 +15,12 @@ limitations under the License. */ #if !(NET20) +using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; using System.Net; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Redmine.Net.Api.Extensions; @@ -30,16 +32,17 @@ namespace Redmine.Net.Api; public partial class RedmineManager: IRedmineManagerAsync { + private const string CRLR = "\r\n"; + /// - public async Task CountAsync(RequestOptions requestOptions, CancellationToken cancellationToken = default) where T : class, new() + public async Task CountAsync(RequestOptions requestOptions, CancellationToken cancellationToken = default) + where T : class, new() { - const int PAGE_SIZE = 1; - const int OFFSET = 0; var totalCount = 0; requestOptions ??= new RequestOptions(); - requestOptions.QueryString.AddPagingParameters(PAGE_SIZE, OFFSET); + requestOptions.QueryString.AddPagingParameters(pageSize: 1, offset: 0); var tempResult = await GetPagedAsync(requestOptions, cancellationToken).ConfigureAwait(false); if (tempResult != null) @@ -145,8 +148,7 @@ public async Task GetAsync(string id, RequestOptions requestOptions = null return response.DeserializeTo(Serializer); } - - + /// public async Task CreateAsync(T entity, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) where T : class, new() @@ -174,8 +176,12 @@ public async Task UpdateAsync(string id, T entity, RequestOptions requestOpti var url = RedmineApiUrls.UpdateFragment(id); var payload = Serializer.Serialize(entity); - - // payload = Regex.Replace(payload, @"\r\n|\r|\n", "\r\n"); + + #if NET7_0_OR_GREATER + payload = ReplaceEndingsRegex().Replace(payload, CRLR); + #else + payload = Regex.Replace(payload, "\r\n|\r|\n",CRLR); + #endif await ApiClient.UpdateAsync(url, payload, requestOptions, cancellationToken: cancellationToken).ConfigureAwait(false); } @@ -205,5 +211,10 @@ public async Task DownloadFileAsync(string address, RequestOptions reque var response = await ApiClient.DownloadAsync(address, requestOptions,cancellationToken: cancellationToken).ConfigureAwait(false); return response.Content; } + + #if NET7_0_OR_GREATER + [GeneratedRegex(@"\r\n|\r|\n")] + private static partial Regex ReplaceEndingsRegex(); + #endif } #endif \ No newline at end of file From aa88782326671dcde688a9253dc3382d65b3eb2e Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 28 Feb 2024 23:03:28 +0200 Subject: [PATCH 381/549] [TaskExtensions] Add WhenAll for .net 4.0 --- src/redmine-net-api/Extensions/TaskExtensions.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/redmine-net-api/Extensions/TaskExtensions.cs b/src/redmine-net-api/Extensions/TaskExtensions.cs index 865ebc42..c74b6d4d 100644 --- a/src/redmine-net-api/Extensions/TaskExtensions.cs +++ b/src/redmine-net-api/Extensions/TaskExtensions.cs @@ -41,5 +41,20 @@ public static void Synchronize(Func function) Task.Factory.StartNew(function, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default) .Unwrap().GetAwaiter().GetResult(); } + + #if !(NET45_OR_GREATER || NETCOREAPP) + public static Task WhenAll(IEnumerable> tasks) + { + var clone = tasks.ToArray(); + + var x = Task.Factory.StartNew(() => + { + Task.WaitAll(clone); + return clone.Select(t => t.Result).ToArray(); + }, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); + + return default; + } + #endif } #endif \ No newline at end of file From d29da3f7cf2acb680370987ea1b9119069abc27b Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 28 Feb 2024 23:05:03 +0200 Subject: [PATCH 382/549] [RedmineManagerAsync] Improve GetAsync --- src/redmine-net-api/RedmineManagerAsync.cs | 116 +++++++++++++++------ 1 file changed, 83 insertions(+), 33 deletions(-) diff --git a/src/redmine-net-api/RedmineManagerAsync.cs b/src/redmine-net-api/RedmineManagerAsync.cs index 86dc0e27..20485248 100644 --- a/src/redmine-net-api/RedmineManagerAsync.cs +++ b/src/redmine-net-api/RedmineManagerAsync.cs @@ -18,8 +18,6 @@ limitations under the License. using System; using System.Collections.Generic; using System.Collections.Specialized; -using System.Globalization; -using System.Net; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -27,6 +25,7 @@ limitations under the License. using Redmine.Net.Api.Net; using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Types; +using TaskExtensions = Redmine.Net.Api.Extensions.TaskExtensions; namespace Redmine.Net.Api; @@ -63,7 +62,8 @@ public async Task> GetPagedAsync(RequestOptions requestOption return response.DeserializeToPagedResults(Serializer); } - + + /// public async Task> GetAsync(RequestOptions requestOptions = null, CancellationToken cancellationToken = default) where T : class, new() @@ -89,52 +89,83 @@ public async Task> GetAsync(RequestOptions requestOptions = null, Can pageSize = PageSize > 0 ? PageSize : RedmineConstants.DEFAULT_PAGE_SIZE_VALUE; - requestOptions.QueryString.Set(RedmineKeys.LIMIT, pageSize.ToString(CultureInfo.InvariantCulture)); + requestOptions.QueryString.Set(RedmineKeys.LIMIT, pageSize.ToInvariantString()); } - - try + + var hasOffset = RedmineManager.TypesWithOffset.ContainsKey(typeof(T)); + if (hasOffset) { - var hasOffset = RedmineManager.TypesWithOffset.ContainsKey(typeof(T)); - if (hasOffset) + requestOptions.QueryString.Set(RedmineKeys.OFFSET, offset.ToInvariantString()); + + var tempResult = await GetPagedAsync(requestOptions, cancellationToken).ConfigureAwait(false); + + var totalCount = isLimitSet ? pageSize : tempResult.TotalItems; + + if (tempResult?.Items != null) { - int totalCount; - do - { - requestOptions.QueryString.Set(RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)); + resultList = new List(tempResult.Items); + } + + var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); - var tempResult = await GetPagedAsync(requestOptions, cancellationToken: cancellationToken).ConfigureAwait(false); + var remainingPages = totalPages - offset / pageSize; - totalCount = isLimitSet ? pageSize : tempResult.TotalItems; + if (remainingPages <= 0) + { + return resultList; + } + + using (var semaphore = new SemaphoreSlim(MAX_CONCURRENT_TASKS)) + { + var pageFetchTasks = new List>>(); + + for (int page = 0; page < remainingPages; page++) + { + await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - if (tempResult?.Items != null) + var innerOffset = (page * pageSize) + offset; + + pageFetchTasks.Add(GetPagedInternalAsync(semaphore, new RequestOptions() { - if (resultList == null) + QueryString = new NameValueCollection() { - resultList = new List(tempResult.Items); + {RedmineKeys.OFFSET, innerOffset.ToInvariantString()}, + {RedmineKeys.LIMIT, pageSize.ToInvariantString()} } - else - { - resultList.AddRange(tempResult.Items); - } - } + }, cancellationToken)); + } - offset += pageSize; - } while (offset < totalCount); - } - else - { - var result = await GetPagedAsync(requestOptions, cancellationToken: cancellationToken).ConfigureAwait(false); - if (result?.Items != null) + var pageResults = await + #if(NET45_OR_GREATER || NETCOREAPP) + Task.WhenAll(pageFetchTasks) + #else + TaskExtensions.WhenAll(pageFetchTasks) + #endif + .ConfigureAwait(false); + + foreach (var pageResult in pageResults) { - return new List(result.Items); + if (pageResult?.Items == null) + { + continue; + } + + resultList ??= new List(); + + resultList.AddRange(pageResult.Items); } } } - catch (WebException wex) + else { - wex.HandleWebException(Serializer); + var result = await GetPagedAsync(requestOptions, cancellationToken: cancellationToken) + .ConfigureAwait(false); + if (result?.Items != null) + { + return new List(result.Items); + } } - + return resultList; } @@ -216,5 +247,24 @@ public async Task DownloadFileAsync(string address, RequestOptions reque [GeneratedRegex(@"\r\n|\r|\n")] private static partial Regex ReplaceEndingsRegex(); #endif + + private const int MAX_CONCURRENT_TASKS = 3; + + private async Task> GetPagedInternalAsync(SemaphoreSlim semaphore, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new() + { + try + { + var url = RedmineApiUrls.GetListFragment(); + + var response = await ApiClient.GetAsync(url, requestOptions, cancellationToken).ConfigureAwait(false); + + return response.DeserializeToPagedResults(Serializer); + } + finally + { + semaphore.Release(); + } + } } #endif \ No newline at end of file From b81d4e755ac9ca0db538c10676fa5fe3cce887bc Mon Sep 17 00:00:00 2001 From: kasperk81 <83082615+kasperk81@users.noreply.github.com> Date: Sun, 16 Jun 2024 16:59:58 +0300 Subject: [PATCH 383/549] use correct framework (#350) --- src/redmine-net-api/redmine-net-api.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index cc750f06..df2e9234 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -4,7 +4,7 @@ Redmine.Net.Api redmine-net-api - net20;net40;net45;net451;net452;net46;net461;net462;net47;net471;net472;net48;net481;net60;net70;net80 + net20;net40;net45;net451;net452;net46;net461;net462;net47;net471;net472;net48;net481;net6.0;net7.0;net8.0 false True true From b5bee3c066f83979c954611993b8aa4bb2a08a2b Mon Sep 17 00:00:00 2001 From: Padi Date: Sat, 3 Aug 2024 19:03:37 +0300 Subject: [PATCH 384/549] Update pack.yml --- .github/workflows/pack.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.github/workflows/pack.yml b/.github/workflows/pack.yml index 710e4df4..eb967b1d 100644 --- a/.github/workflows/pack.yml +++ b/.github/workflows/pack.yml @@ -88,16 +88,6 @@ jobs: -p:SymbolPackageFormat=snupkg -p:Sign=true - - name: Install dotnet-validate - run: >- - dotnet tool install - --global dotnet-validate - --version 0.0.1-preview.304 - - - name: Validate NuGet package - run: >- - dotnet-validate package local ./artifacts/**.nupkg - - name: Upload artifacts uses: actions/upload-artifact@v4 with: From 948bd0610bb78632286a1e2e8dfcc76470dea657 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 3 Aug 2024 19:25:51 +0300 Subject: [PATCH 385/549] [Global.Json] Update the sdk version to 8.0.303 --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 88e2f39a..ab747bba 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.101", + "version": "8.0.303", "allowPrerelease": false, "rollForward": "latestMajor" } From e021336237a4b92d1dc2f4ce0c09b3f3ffa7b917 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 3 Aug 2024 19:26:36 +0300 Subject: [PATCH 386/549] [Logo] Update logo --- logo.png | Bin 2034 -> 1631 bytes 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 logo.png diff --git a/logo.png b/logo.png old mode 100755 new mode 100644 index 0e88a5f479a6db483627c0bbc806bdcb7415ca04..83433adf960e5b583c62eb00c943ba4da7cd5350 GIT binary patch literal 1631 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 Qn*aa+07*qoM6N<$g5C%Ega7~l From c7433fc66730acaa7e852048a427949277fca4bd Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 3 Aug 2024 19:28:18 +0300 Subject: [PATCH 387/549] [Csproj] Build symbol package (.snupkg) --- src/redmine-net-api/redmine-net-api.csproj | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index cc750f06..87d799d9 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -64,6 +64,12 @@ git https://github.com/zapadi/redmine-net-api Redmine .NET API Client + + true + + true + snupkg + true From 1c8ce6e515a7df7e46601dc647b0e068f8a00e0a Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 3 Aug 2024 19:28:56 +0300 Subject: [PATCH 388/549] [Appveyor] Cahnge VS version to 2022 --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index fcb1ad07..8fc91681 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,6 +1,6 @@ version: '{build}' image: - - Visual Studio 2019 + - Visual Studio 2022 - Ubuntu environment: From ba922b8cf0df62efdce5a4c0f2e3d5e155347684 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 3 Aug 2024 19:31:04 +0300 Subject: [PATCH 389/549] [IssueCustomField] Serialization improvements --- src/redmine-net-api/Types/CustomFieldValue.cs | 9 +++------ src/redmine-net-api/Types/IssueCustomField.cs | 14 +++++++++----- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/redmine-net-api/Types/CustomFieldValue.cs b/src/redmine-net-api/Types/CustomFieldValue.cs index 0a0f1033..a345acdf 100644 --- a/src/redmine-net-api/Types/CustomFieldValue.cs +++ b/src/redmine-net-api/Types/CustomFieldValue.cs @@ -35,9 +35,7 @@ public class CustomFieldValue : IXmlSerializable, IJsonSerializable, IEquatable< /// /// /// - public CustomFieldValue() - { - } + public CustomFieldValue() { } /// /// @@ -47,7 +45,7 @@ public CustomFieldValue(string value) { Info = value; } - + #region Properties /// @@ -144,7 +142,7 @@ public void WriteJson(JsonWriter writer) public bool Equals(CustomFieldValue other) { if (other == null) return false; - return string.Equals(Info,other.Info,StringComparison.OrdinalIgnoreCase); + return string.Equals(Info, other.Info, StringComparison.OrdinalIgnoreCase); } /// @@ -195,6 +193,5 @@ public object Clone() /// /// private string DebuggerDisplay => $"[{nameof(CustomFieldValue)}: {Info}]"; - } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/IssueCustomField.cs b/src/redmine-net-api/Types/IssueCustomField.cs index d278d5fc..65ad1049 100644 --- a/src/redmine-net-api/Types/IssueCustomField.cs +++ b/src/redmine-net-api/Types/IssueCustomField.cs @@ -112,7 +112,9 @@ public override void WriteXml(XmlWriter writer) writer.WriteAttributeString(RedmineKeys.ID, Id.ToString(CultureInfo.InvariantCulture)); - if (itemsCount > 1) + Multiple = itemsCount > 1; + + if (Multiple) { writer.WriteArrayStringElement(RedmineKeys.VALUE, Values, GetValue); } @@ -120,6 +122,8 @@ public override void WriteXml(XmlWriter writer) { writer.WriteElementString(RedmineKeys.VALUE, itemsCount > 0 ? Values[0].Info : null); } + + writer.WriteBoolean(RedmineKeys.MULTIPLE, Multiple); } #endregion @@ -136,12 +140,14 @@ public override void WriteJson(JsonWriter writer) } var itemsCount = Values.Count; + Multiple = itemsCount > 1; writer.WriteStartObject(); writer.WriteProperty(RedmineKeys.ID, Id); writer.WriteProperty(RedmineKeys.NAME, Name); - - if (itemsCount > 1) + writer.WriteBoolean(RedmineKeys.MULTIPLE, Multiple); + + if (Multiple) { writer.WritePropertyName(RedmineKeys.VALUE); writer.WriteStartArray(); @@ -150,8 +156,6 @@ public override void WriteJson(JsonWriter writer) writer.WriteValue(cfv.Info); } writer.WriteEndArray(); - - writer.WriteBoolean(RedmineKeys.MULTIPLE, Multiple); } else { From 2413a2f45d1e59d62d1c08d295115ae1aa517287 Mon Sep 17 00:00:00 2001 From: zapadi Date: Thu, 22 Feb 2024 17:53:53 +0200 Subject: [PATCH 390/549] [GitActions][Delete] build & pack workflows --- .github/workflows/build.yml | 86 --------------------------------- .github/workflows/pack.yml | 95 ------------------------------------- redmine-net-api.sln | 2 - 3 files changed, 183 deletions(-) delete mode 100644 .github/workflows/build.yml delete mode 100644 .github/workflows/pack.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index d40f5180..00000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: 'Build' - -on: - workflow_dispatch: - inputs: - reason: - description: 'The reason for running the workflow' - required: false - default: 'Manual run' - workflow_call: - push: - paths: - - '**.cs' - - '**.csproj' - pull_request: - branches: [ master ] - paths: - - '**.cs' - - '**.csproj' - -env: - # Disable the .NET logo in the console output. - DOTNET_NOLOGO: true - - # Stop wasting time caching packages - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true - - # Disable sending usage data to Microsoft - DOTNET_CLI_TELEMETRY_OPTOUT: true - - DOTNET_ADD_GLOBAL_TOOLS_TO_PATH: false - - DOTNET_MULTILEVEL_LOOKUP: 0 - - PROJECT_PATH: . - - CONFIGURATION: Release - - # Set the build number in MinVer. - MINVERBUILDMETADATA: build.${{github.run_number}} - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - build: - name: Build ${{ matrix.os }} - dotnet ${{ matrix.dotnet }} - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ ubuntu-latest, windows-latest, macos-latest ] - - steps: - - name: Print manual run reason - if: ${{ github.event_name == 'workflow_dispatch' }} - run: | - echo 'Reason: ${{ github.event.inputs.reason }}' - - - name: Checkout - uses: actions/checkout@v4 - with: - lfs: true - fetch-depth: 0 - - - name: Setup .NET (global.json) - uses: actions/setup-dotnet@v4 - - - uses: actions/cache@v4 - with: - path: ~/.nuget/packages - # Look to see if there is a cache hit for the corresponding requirements file - key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} - restore-keys: | - ${{ runner.os }}-nuget - - - name: Restore - run: dotnet restore "${{ env.PROJECT_PATH }}" - - - name: Build - run: >- - dotnet build "${{ env.PROJECT_PATH }}" - --configuration "${{ env.CONFIGURATION }}" - --no-restore - -p:ContinuousIntegrationBuild=true \ No newline at end of file diff --git a/.github/workflows/pack.yml b/.github/workflows/pack.yml deleted file mode 100644 index eb967b1d..00000000 --- a/.github/workflows/pack.yml +++ /dev/null @@ -1,95 +0,0 @@ -name: 'Pack' - -on: - workflow_run: - workflows: [ 'Build and Test' ] - types: [ requested ] - branches: [ master ] - - workflow_call: - - workflow_dispatch: - inputs: - reason: - description: 'The reason for running the workflow' - required: false - default: 'Manual pack' - version: - description: 'Version' - required: true - -env: - CONFIGURATION: 'Release' - - PROJECT_PATH: "." - - PROJECT_NAME: redmine-net-api - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - pack: - name: Pack - if: github.ref == 'refs/heads/master' - runs-on: ubuntu-latest - steps: - - name: Determine Version - run: | - if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then - echo "VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV - else - echo "VERSION=$(git describe --tags `git rev-list --tags --max-count=1`)" >> $GITHUB_ENV - fi - echo "$GITHUB_ENV" - - - name: Print Version - run: | - echo "$VERSION" - - - name: Validate Version matches SemVer format - run: | - if [[ ! "$VERSION" =~ ^([0-9]+\.){2}[0-9]+(-[\w.]+)?$ ]]; then - echo "The version does not match the SemVer format (X.Y.Z). Please provide a valid version." - exit 1 - fi - - - name: Checkout - uses: actions/checkout@v4 - with: - lfs: true - fetch-depth: 0 - - - name: Setup .NET Core (global.json) - uses: actions/setup-dotnet@v4 - - - name: Install dependencies - run: dotnet restore "${{ env.PROJECT_PATH }}" - - - name: Pack - run: >- - dotnet pack ./src/redmine-net-api/redmine-net-api.csproj - --output ./artifacts - --configuration "${{ env.CONFIGURATION }}" - -p:Version=$VERSION - -p:PackageVersion=${{ env.VERSION }} - -p:SymbolPackageFormat=snupkg - - - name: Pack Signed - run: >- - dotnet pack ./src/redmine-net-api/redmine-net-api.csproj - --output ./artifacts - --configuration "${{ env.CONFIGURATION }}" - --include-symbols - --include-source - -p:Version=$VERSION - -p:PackageVersion=${{ env.VERSION }} - -p:SymbolPackageFormat=snupkg - -p:Sign=true - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: artifacts - path: ./artifacts diff --git a/redmine-net-api.sln b/redmine-net-api.sln index 2715270a..7cbed138 100644 --- a/redmine-net-api.sln +++ b/redmine-net-api.sln @@ -23,8 +23,6 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitActions", "GitActions", "{79119F8B-C468-4DC8-BE6F-6E7102BD2079}" ProjectSection(SolutionItems) = preProject .github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml - .github\workflows\pack.yml = .github\workflows\pack.yml - .github\workflows\build.yml = .github\workflows\build.yml .github\workflows\build-and-test.yml = .github\workflows\build-and-test.yml .github\workflows\publish.yml = .github\workflows\publish.yml EndProjectSection From bb637e31971af7080637849860dbfef4e5858103 Mon Sep 17 00:00:00 2001 From: zapadi Date: Thu, 22 Feb 2024 17:54:58 +0200 Subject: [PATCH 391/549] [GitActions] Improve build, test & pack workflows --- .github/workflows/build-and-test.yml | 110 +++++++++++++--- .github/workflows/publish.yml | 180 +++++++++++++++++++++++---- 2 files changed, 244 insertions(+), 46 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 524903c9..1cc0e016 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -1,35 +1,107 @@ name: 'Build and Test' on: + workflow_call: workflow_dispatch: inputs: reason: description: 'The reason for running the workflow' required: false default: 'Manual build and run tests' - workflow_run: - workflows: [ 'Build' ] - types: - - completed + push: + tags-ignore: + - '[0-9]+.[0-9]+.[0-9]+*' + paths: + - '**.cs' + - '**.csproj' + - '**.sln' + pull_request: + branches: [ master ] + paths: + - '**.cs' + - '**.csproj' + - '**.sln' + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + # Disable the .NET logo in the console output. + DOTNET_NOLOGO: true + + # Stop wasting time caching packages + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + + # Disable sending usage data to Microsoft + DOTNET_CLI_TELEMETRY_OPTOUT: true + + DOTNET_ADD_GLOBAL_TOOLS_TO_PATH: false + DOTNET_MULTILEVEL_LOOKUP: 0 + + PROJECT_PATH: . + + CONFIGURATION: Release + jobs: build: - uses: ./.github/workflows/build.yml - test: - name: Test - ${{matrix.os}} - needs: [build] + needs: before + name: Build ${{ matrix.os }} - dotnet ${{ matrix.dotnet }} runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: - os: [ ubuntu-latest, windows-latest, macOS-latest ] + os: [ ubuntu-latest, windows-latest, macos-latest ] + steps: - - name: Test - # if: ${{ github.event.workflow_run.conclusion == 'success' }} - timeout-minutes: 60 - run: >- - dotnet test "${{ env.PROJECT_PATH }}" - --no-restore - --no-build - --verbosity normal - --logger "trx;LogFileName=test-results.trx" || true - \ No newline at end of file + - name: Print manual run reason + if: ${{ github.event_name == 'workflow_dispatch' }} + run: | + echo 'Reason: ${{ github.event.inputs.reason }}' + + - name: Checkout + uses: actions/checkout@v4 + with: + lfs: true + fetch-depth: 0 + + - name: Setup .NET (global.json) + uses: actions/setup-dotnet@v4 + + - name: Display dotnet version + run: dotnet --version + + - uses: actions/cache@v4 + with: + path: ~/.nuget/packages + # Look to see if there is a cache hit for the corresponding requirements file + key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} + restore-keys: | + ${{ runner.os }}-nuget + + - name: Restore + run: dotnet restore "${{ env.PROJECT_PATH }}" + + - name: Build + run: >- + dotnet build "${{ env.PROJECT_PATH }}" + --configuration "${{ env.CONFIGURATION }}" + --no-restore + + - name: Test + timeout-minutes: 60 + run: >- + dotnet test "${{ env.PROJECT_PATH }}" + --no-restore + --no-build + --verbosity normal + --logger trx + --results-directory "TestResults-${{ matrix.os }}" || true + + - name: Upload test results + if: ${{ always() }} + uses: actions/upload-artifact@v4 + with: + name: test-results-${{ matrix.os }} + path: TestResults-${{ matrix.os }} \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8f8c8a8f..91fdaed1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,52 +1,178 @@ name: 'Publish to NuGet' -on: +on: workflow_dispatch: inputs: reason: description: 'The reason for running the workflow' required: false - default: 'Manual publish to nuget' - - workflow_run: - workflows: [ 'Pack' ] - types: - - completed + default: 'Manual publish' + version: + description: 'Version' + required: true push: tags: - - '[0-9]+.[0-9]+.[0-9]+(-[\w.]+)?' - + - '[0-9]+.[0-9]+.[0-9]+*' + +env: + # Disable the .NET logo in the console output. + DOTNET_NOLOGO: true + + # Disable sending usage data to Microsoft + DOTNET_CLI_TELEMETRY_OPTOUT: true + + # Set working directory + PROJECT_PATH: ./src/redmine-net-api/redmine-net-api.csproj + + # Configuration + CONFIGURATION: Release + concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true - -jobs: + +jobs: + check-tag-branch: + name: Check Tag and Master Branch hashes + # This job is based on replies in https://github.community/t/how-to-create-filter-on-both-tag-and-branch/16936/6 + runs-on: ubuntu-latest + outputs: + ver: ${{ steps.set-version.outputs.VERSION }} + steps: + - name: Get tag commit hash + id: tag-commit-hash + run: | + hash=${{ github.sha }} + echo "{name}=tag-hash::${hash}" >> $GITHUB_OUTPUT + echo $hash + + - name: Checkout master + uses: actions/checkout@v4 + with: + ref: master + + - name: Get latest master commit hash + id: master-commit-hash + run: | + hash=$(git log -n1 --format=format:"%H") + echo "{name}=master-hash::${hash}" >> $GITHUB_OUTPUT + echo $hash + + - name: Verify tag commit matches master commit - exit if they don't match + if: steps.tag-commit-hash.outputs.tag-hash != steps.master-commit-hash.outputs.master-hash + run: | + echo "Tag was not on the master branch. Exiting." + exit 1 + + - name: Get Dispatched Version + if: github.event_name == 'workflow_dispatch' + run: | + echo "VERSION=${{ github.event.inputs.version }}" >> $GITHUB_ENV + + - name: Get Tag Version + if: github.event_name == 'push' + run: | + echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV + + - name: Set Version + id: set-version + run: | + echo "VERSION=${{ env.VERSION }}" >> "$GITHUB_OUTPUT" + + validate-version: + name: Validate Version + needs: check-tag-branch + runs-on: ubuntu-latest + steps: + - name: Get Version + run: echo "VERSION=${{ needs.check-tag-branch.outputs.ver }}" >> $GITHUB_ENV + + - name: Display Version + run: echo "$VERSION" + + - name: Check Version Is Declared + run: | + if [[ -z "$VERSION" ]]; then + echo "Version is not declared." + exit 1 + fi + + - name: Validate Version matches SemVer format + run: | + if [[ ! "$VERSION" =~ ^([0-9]+\.){2,3}[0-9]+(-[a-zA-Z0-9.-]+)*$ ]]; then + echo "The version does not match the SemVer format (X.Y.Z). Please provide a valid version." + exit 1 + fi + + call-build-and-test: + name: Call Build and Test + needs: validate-version + uses: ./.github/workflows/build-and-test.yml + + pack: + name: Pack + needs: [check-tag-branch, validate-version, call-build-and-test] + runs-on: ubuntu-latest + steps: + - name: Get Version + run: echo "VERSION=${{ needs.check-tag-branch.outputs.ver }}" >> $GITHUB_ENV + + - name: Checkout + uses: actions/checkout@v4 + with: + lfs: true + fetch-depth: 0 + + - name: Setup .NET Core (global.json) + uses: actions/setup-dotnet@v4 + + - name: Display dotnet version + run: dotnet --version + + - name: Install dependencies + run: dotnet restore "${{ env.PROJECT_PATH }}" + + - name: Create the package + run: >- + dotnet pack "${{ env.PROJECT_PATH }}" + --output ./artifacts + --configuration "${{ env.CONFIGURATION }}" + -p:Version=$VERSION + -p:PackageVersion=$VERSION + -p:SymbolPackageFormat=snupkg + + - name: Create the package - Signed + run: >- + dotnet pack "${{ env.PROJECT_PATH }}" + --output ./artifacts + --configuration "${{ env.CONFIGURATION }}" + --include-symbols + --include-source + -p:Version=$VERSION + -p:PackageVersion=$VERSION + -p:SymbolPackageFormat=snupkg + -p:Sign=true + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: artifacts + path: ./artifacts + publish: name: Publish to Nuget - if: github.ref == 'refs/heads/master' + needs: pack runs-on: ubuntu-latest steps: - - name: Print manual run reason - if: ${{ github.event_name == 'workflow_dispatch' }} - run: | - echo 'Reason: ${{ github.event.inputs.reason }}' - - name: Download artifacts uses: actions/download-artifact@v4 with: name: artifacts - path: ./artifacts - + path: ./artifacts + - name: Publish packages run: >- dotnet nuget push ./artifacts/**.nupkg --source 'https://api.nuget.org/v3/index.json' --api-key ${{secrets.NUGET_TOKEN}} - --skip-duplicate - - - name: Upload artifacts to the GitHub release - uses: Roang-zero1/github-upload-release-artifacts-action@v3.0.0 - with: - args: ./artifacts - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + --skip-duplicate \ No newline at end of file From 0bb4cf70443738c26ae7a04e138cee024957187b Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 28 Feb 2024 20:51:04 +0200 Subject: [PATCH 392/549] [New][SemaphoreSlimExtensions] - WaitAsync for .net 4.0 --- .../Extensions/SemaphoreSlimExtensions.cs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/redmine-net-api/Extensions/SemaphoreSlimExtensions.cs diff --git a/src/redmine-net-api/Extensions/SemaphoreSlimExtensions.cs b/src/redmine-net-api/Extensions/SemaphoreSlimExtensions.cs new file mode 100644 index 00000000..a47dbb9b --- /dev/null +++ b/src/redmine-net-api/Extensions/SemaphoreSlimExtensions.cs @@ -0,0 +1,35 @@ +/* +Copyright 2011 - 2023 Adrian Popescu + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#if !(NET20) +using System.Threading; +using System.Threading.Tasks; + +namespace Redmine.Net.Api.Extensions; +#if !(NET45_OR_GREATER || NETCOREAPP) +internal static class SemaphoreSlimExtensions +{ + + public static Task WaitAsync(this SemaphoreSlim semaphore, CancellationToken cancellationToken = default) + { + return Task.Factory.StartNew(() => semaphore.Wait(cancellationToken) + , CancellationToken.None + , TaskCreationOptions.None + , TaskScheduler.Default); + } +} +#endif +#endif From 54a74381f460e0f085cc133943926a581dd52eab Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 28 Feb 2024 20:52:08 +0200 Subject: [PATCH 393/549] [New][TaskExtensions] --- .../Extensions/TaskExtensions.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/redmine-net-api/Extensions/TaskExtensions.cs diff --git a/src/redmine-net-api/Extensions/TaskExtensions.cs b/src/redmine-net-api/Extensions/TaskExtensions.cs new file mode 100644 index 00000000..865ebc42 --- /dev/null +++ b/src/redmine-net-api/Extensions/TaskExtensions.cs @@ -0,0 +1,45 @@ +/* +Copyright 2011 - 2023 Adrian Popescu + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +#if !(NET20) +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Redmine.Net.Api.Extensions; + +internal static class TaskExtensions +{ + public static T GetAwaiterResult(this Task task) + { + return task.GetAwaiter().GetResult(); + } + + public static TResult Synchronize(Func> function) + { + return Task.Factory.StartNew(function, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default) + .Unwrap().GetAwaiter().GetResult(); + } + + public static void Synchronize(Func function) + { + Task.Factory.StartNew(function, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default) + .Unwrap().GetAwaiter().GetResult(); + } +} +#endif \ No newline at end of file From 3435bd293a8403541fa88b0f6d1397a9a0768097 Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 28 Feb 2024 22:11:48 +0200 Subject: [PATCH 394/549] [RedmineManagerAsync] Add ReplaceEndingsRegex for NET70 onwards --- src/redmine-net-api/RedmineManagerAsync.cs | 27 +++++++++++++++------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/src/redmine-net-api/RedmineManagerAsync.cs b/src/redmine-net-api/RedmineManagerAsync.cs index 50192c5a..86dc0e27 100644 --- a/src/redmine-net-api/RedmineManagerAsync.cs +++ b/src/redmine-net-api/RedmineManagerAsync.cs @@ -15,10 +15,12 @@ limitations under the License. */ #if !(NET20) +using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; using System.Net; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Redmine.Net.Api.Extensions; @@ -30,16 +32,17 @@ namespace Redmine.Net.Api; public partial class RedmineManager: IRedmineManagerAsync { + private const string CRLR = "\r\n"; + /// - public async Task CountAsync(RequestOptions requestOptions, CancellationToken cancellationToken = default) where T : class, new() + public async Task CountAsync(RequestOptions requestOptions, CancellationToken cancellationToken = default) + where T : class, new() { - const int PAGE_SIZE = 1; - const int OFFSET = 0; var totalCount = 0; requestOptions ??= new RequestOptions(); - requestOptions.QueryString.AddPagingParameters(PAGE_SIZE, OFFSET); + requestOptions.QueryString.AddPagingParameters(pageSize: 1, offset: 0); var tempResult = await GetPagedAsync(requestOptions, cancellationToken).ConfigureAwait(false); if (tempResult != null) @@ -145,8 +148,7 @@ public async Task GetAsync(string id, RequestOptions requestOptions = null return response.DeserializeTo(Serializer); } - - + /// public async Task CreateAsync(T entity, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) where T : class, new() @@ -174,8 +176,12 @@ public async Task UpdateAsync(string id, T entity, RequestOptions requestOpti var url = RedmineApiUrls.UpdateFragment(id); var payload = Serializer.Serialize(entity); - - // payload = Regex.Replace(payload, @"\r\n|\r|\n", "\r\n"); + + #if NET7_0_OR_GREATER + payload = ReplaceEndingsRegex().Replace(payload, CRLR); + #else + payload = Regex.Replace(payload, "\r\n|\r|\n",CRLR); + #endif await ApiClient.UpdateAsync(url, payload, requestOptions, cancellationToken: cancellationToken).ConfigureAwait(false); } @@ -205,5 +211,10 @@ public async Task DownloadFileAsync(string address, RequestOptions reque var response = await ApiClient.DownloadAsync(address, requestOptions,cancellationToken: cancellationToken).ConfigureAwait(false); return response.Content; } + + #if NET7_0_OR_GREATER + [GeneratedRegex(@"\r\n|\r|\n")] + private static partial Regex ReplaceEndingsRegex(); + #endif } #endif \ No newline at end of file From 9621e3ee495072996f0a46c9ae67df98cf19a1c8 Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 28 Feb 2024 23:03:28 +0200 Subject: [PATCH 395/549] [TaskExtensions] Add WhenAll for .net 4.0 --- src/redmine-net-api/Extensions/TaskExtensions.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/redmine-net-api/Extensions/TaskExtensions.cs b/src/redmine-net-api/Extensions/TaskExtensions.cs index 865ebc42..c74b6d4d 100644 --- a/src/redmine-net-api/Extensions/TaskExtensions.cs +++ b/src/redmine-net-api/Extensions/TaskExtensions.cs @@ -41,5 +41,20 @@ public static void Synchronize(Func function) Task.Factory.StartNew(function, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default) .Unwrap().GetAwaiter().GetResult(); } + + #if !(NET45_OR_GREATER || NETCOREAPP) + public static Task WhenAll(IEnumerable> tasks) + { + var clone = tasks.ToArray(); + + var x = Task.Factory.StartNew(() => + { + Task.WaitAll(clone); + return clone.Select(t => t.Result).ToArray(); + }, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default); + + return default; + } + #endif } #endif \ No newline at end of file From 451b3b10714f3f99c402abf0a8cef5ea3aeeae3a Mon Sep 17 00:00:00 2001 From: zapadi Date: Wed, 28 Feb 2024 23:05:03 +0200 Subject: [PATCH 396/549] [RedmineManagerAsync] Improve GetAsync --- src/redmine-net-api/RedmineManagerAsync.cs | 116 +++++++++++++++------ 1 file changed, 83 insertions(+), 33 deletions(-) diff --git a/src/redmine-net-api/RedmineManagerAsync.cs b/src/redmine-net-api/RedmineManagerAsync.cs index 86dc0e27..20485248 100644 --- a/src/redmine-net-api/RedmineManagerAsync.cs +++ b/src/redmine-net-api/RedmineManagerAsync.cs @@ -18,8 +18,6 @@ limitations under the License. using System; using System.Collections.Generic; using System.Collections.Specialized; -using System.Globalization; -using System.Net; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -27,6 +25,7 @@ limitations under the License. using Redmine.Net.Api.Net; using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Types; +using TaskExtensions = Redmine.Net.Api.Extensions.TaskExtensions; namespace Redmine.Net.Api; @@ -63,7 +62,8 @@ public async Task> GetPagedAsync(RequestOptions requestOption return response.DeserializeToPagedResults(Serializer); } - + + /// public async Task> GetAsync(RequestOptions requestOptions = null, CancellationToken cancellationToken = default) where T : class, new() @@ -89,52 +89,83 @@ public async Task> GetAsync(RequestOptions requestOptions = null, Can pageSize = PageSize > 0 ? PageSize : RedmineConstants.DEFAULT_PAGE_SIZE_VALUE; - requestOptions.QueryString.Set(RedmineKeys.LIMIT, pageSize.ToString(CultureInfo.InvariantCulture)); + requestOptions.QueryString.Set(RedmineKeys.LIMIT, pageSize.ToInvariantString()); } - - try + + var hasOffset = RedmineManager.TypesWithOffset.ContainsKey(typeof(T)); + if (hasOffset) { - var hasOffset = RedmineManager.TypesWithOffset.ContainsKey(typeof(T)); - if (hasOffset) + requestOptions.QueryString.Set(RedmineKeys.OFFSET, offset.ToInvariantString()); + + var tempResult = await GetPagedAsync(requestOptions, cancellationToken).ConfigureAwait(false); + + var totalCount = isLimitSet ? pageSize : tempResult.TotalItems; + + if (tempResult?.Items != null) { - int totalCount; - do - { - requestOptions.QueryString.Set(RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)); + resultList = new List(tempResult.Items); + } + + var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); - var tempResult = await GetPagedAsync(requestOptions, cancellationToken: cancellationToken).ConfigureAwait(false); + var remainingPages = totalPages - offset / pageSize; - totalCount = isLimitSet ? pageSize : tempResult.TotalItems; + if (remainingPages <= 0) + { + return resultList; + } + + using (var semaphore = new SemaphoreSlim(MAX_CONCURRENT_TASKS)) + { + var pageFetchTasks = new List>>(); + + for (int page = 0; page < remainingPages; page++) + { + await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - if (tempResult?.Items != null) + var innerOffset = (page * pageSize) + offset; + + pageFetchTasks.Add(GetPagedInternalAsync(semaphore, new RequestOptions() { - if (resultList == null) + QueryString = new NameValueCollection() { - resultList = new List(tempResult.Items); + {RedmineKeys.OFFSET, innerOffset.ToInvariantString()}, + {RedmineKeys.LIMIT, pageSize.ToInvariantString()} } - else - { - resultList.AddRange(tempResult.Items); - } - } + }, cancellationToken)); + } - offset += pageSize; - } while (offset < totalCount); - } - else - { - var result = await GetPagedAsync(requestOptions, cancellationToken: cancellationToken).ConfigureAwait(false); - if (result?.Items != null) + var pageResults = await + #if(NET45_OR_GREATER || NETCOREAPP) + Task.WhenAll(pageFetchTasks) + #else + TaskExtensions.WhenAll(pageFetchTasks) + #endif + .ConfigureAwait(false); + + foreach (var pageResult in pageResults) { - return new List(result.Items); + if (pageResult?.Items == null) + { + continue; + } + + resultList ??= new List(); + + resultList.AddRange(pageResult.Items); } } } - catch (WebException wex) + else { - wex.HandleWebException(Serializer); + var result = await GetPagedAsync(requestOptions, cancellationToken: cancellationToken) + .ConfigureAwait(false); + if (result?.Items != null) + { + return new List(result.Items); + } } - + return resultList; } @@ -216,5 +247,24 @@ public async Task DownloadFileAsync(string address, RequestOptions reque [GeneratedRegex(@"\r\n|\r|\n")] private static partial Regex ReplaceEndingsRegex(); #endif + + private const int MAX_CONCURRENT_TASKS = 3; + + private async Task> GetPagedInternalAsync(SemaphoreSlim semaphore, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + where T : class, new() + { + try + { + var url = RedmineApiUrls.GetListFragment(); + + var response = await ApiClient.GetAsync(url, requestOptions, cancellationToken).ConfigureAwait(false); + + return response.DeserializeToPagedResults(Serializer); + } + finally + { + semaphore.Release(); + } + } } #endif \ No newline at end of file From 466f227fdbf56d4d98871bc9301ff9f3a04baa21 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 3 Aug 2024 19:25:51 +0300 Subject: [PATCH 397/549] [Global.Json] Update the sdk version to 8.0.303 --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 88e2f39a..ab747bba 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.101", + "version": "8.0.303", "allowPrerelease": false, "rollForward": "latestMajor" } From aefcd6ee4cab94908b7cfb90836e5a2e5c9cfd31 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 3 Aug 2024 19:26:36 +0300 Subject: [PATCH 398/549] [Logo] Update logo --- logo.png | Bin 2034 -> 1631 bytes 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 logo.png diff --git a/logo.png b/logo.png old mode 100755 new mode 100644 index 0e88a5f479a6db483627c0bbc806bdcb7415ca04..83433adf960e5b583c62eb00c943ba4da7cd5350 GIT binary patch literal 1631 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 Qn*aa+07*qoM6N<$g5C%Ega7~l From c1f9ac2b64d23042662f036d6d3becddbc2cd054 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 3 Aug 2024 19:28:18 +0300 Subject: [PATCH 399/549] [Csproj] Build symbol package (.snupkg) --- src/redmine-net-api/redmine-net-api.csproj | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index df2e9234..3d19d477 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -64,6 +64,12 @@ git https://github.com/zapadi/redmine-net-api Redmine .NET API Client + + true + + true + snupkg + true From 611c504f75851b29fa70003417cf90d6b709f112 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 3 Aug 2024 19:28:56 +0300 Subject: [PATCH 400/549] [Appveyor] Cahnge VS version to 2022 --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index fcb1ad07..8fc91681 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,6 +1,6 @@ version: '{build}' image: - - Visual Studio 2019 + - Visual Studio 2022 - Ubuntu environment: From 8750f39c20a0a701be8142d0ac190d39017738bf Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 3 Aug 2024 19:31:04 +0300 Subject: [PATCH 401/549] [IssueCustomField] Serialization improvements --- src/redmine-net-api/Types/CustomFieldValue.cs | 9 +++------ src/redmine-net-api/Types/IssueCustomField.cs | 14 +++++++++----- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/redmine-net-api/Types/CustomFieldValue.cs b/src/redmine-net-api/Types/CustomFieldValue.cs index 0a0f1033..a345acdf 100644 --- a/src/redmine-net-api/Types/CustomFieldValue.cs +++ b/src/redmine-net-api/Types/CustomFieldValue.cs @@ -35,9 +35,7 @@ public class CustomFieldValue : IXmlSerializable, IJsonSerializable, IEquatable< /// /// /// - public CustomFieldValue() - { - } + public CustomFieldValue() { } /// /// @@ -47,7 +45,7 @@ public CustomFieldValue(string value) { Info = value; } - + #region Properties /// @@ -144,7 +142,7 @@ public void WriteJson(JsonWriter writer) public bool Equals(CustomFieldValue other) { if (other == null) return false; - return string.Equals(Info,other.Info,StringComparison.OrdinalIgnoreCase); + return string.Equals(Info, other.Info, StringComparison.OrdinalIgnoreCase); } /// @@ -195,6 +193,5 @@ public object Clone() /// /// private string DebuggerDisplay => $"[{nameof(CustomFieldValue)}: {Info}]"; - } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/IssueCustomField.cs b/src/redmine-net-api/Types/IssueCustomField.cs index d278d5fc..65ad1049 100644 --- a/src/redmine-net-api/Types/IssueCustomField.cs +++ b/src/redmine-net-api/Types/IssueCustomField.cs @@ -112,7 +112,9 @@ public override void WriteXml(XmlWriter writer) writer.WriteAttributeString(RedmineKeys.ID, Id.ToString(CultureInfo.InvariantCulture)); - if (itemsCount > 1) + Multiple = itemsCount > 1; + + if (Multiple) { writer.WriteArrayStringElement(RedmineKeys.VALUE, Values, GetValue); } @@ -120,6 +122,8 @@ public override void WriteXml(XmlWriter writer) { writer.WriteElementString(RedmineKeys.VALUE, itemsCount > 0 ? Values[0].Info : null); } + + writer.WriteBoolean(RedmineKeys.MULTIPLE, Multiple); } #endregion @@ -136,12 +140,14 @@ public override void WriteJson(JsonWriter writer) } var itemsCount = Values.Count; + Multiple = itemsCount > 1; writer.WriteStartObject(); writer.WriteProperty(RedmineKeys.ID, Id); writer.WriteProperty(RedmineKeys.NAME, Name); - - if (itemsCount > 1) + writer.WriteBoolean(RedmineKeys.MULTIPLE, Multiple); + + if (Multiple) { writer.WritePropertyName(RedmineKeys.VALUE); writer.WriteStartArray(); @@ -150,8 +156,6 @@ public override void WriteJson(JsonWriter writer) writer.WriteValue(cfv.Info); } writer.WriteEndArray(); - - writer.WriteBoolean(RedmineKeys.MULTIPLE, Multiple); } else { From b7d1e656c50ed4187dbf5e67c6fe758e08706281 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sat, 3 Aug 2024 20:02:25 +0300 Subject: [PATCH 402/549] [GitActions] Add missing job (before) --- .github/workflows/build-and-test.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 1cc0e016..a78cc6e2 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -45,6 +45,22 @@ env: CONFIGURATION: Release jobs: + before: + name: Before + runs-on: ubuntu-latest + steps: + - name: Info Before + run: | + echo "[${{ github.event_name }}] event automatically triggered this job." + echo "branch name is ${{ github.ref }}" + echo "This job has a '${{ job.status }}' status." + - name: Run a one-line script + run: | + echo "Is true: $( [ \"$EVENT_NAME\" = 'push' ] && [ \"$GITHUB_REF\" != 'refs/tags/' ] ) || [ \"$EVENT_NAME\" = 'workflow_dispatch' ]" + env: + EVENT_NAME: ${{ github.event_name }} + GITHUB_REF: ${{ github.ref }} + build: needs: before name: Build ${{ matrix.os }} - dotnet ${{ matrix.dotnet }} From 7f0f0b245d1ec3d743dd196c5d5b784e0eaa6f82 Mon Sep 17 00:00:00 2001 From: Padi Date: Sat, 3 Aug 2024 20:14:44 +0300 Subject: [PATCH 403/549] Update publish.yml --- .github/workflows/publish.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 91fdaed1..01a911f1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -27,10 +27,6 @@ env: # Configuration CONFIGURATION: Release -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - jobs: check-tag-branch: name: Check Tag and Master Branch hashes @@ -175,4 +171,4 @@ jobs: dotnet nuget push ./artifacts/**.nupkg --source 'https://api.nuget.org/v3/index.json' --api-key ${{secrets.NUGET_TOKEN}} - --skip-duplicate \ No newline at end of file + --skip-duplicate From ba7b7f601b8b459d7cf83a51f39d4b6cb5f8282f Mon Sep 17 00:00:00 2001 From: Padi Date: Thu, 15 Aug 2024 01:00:20 +0300 Subject: [PATCH 404/549] Update publish.yml --- .github/workflows/publish.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 01a911f1..c6ed19cf 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -133,6 +133,8 @@ jobs: dotnet pack "${{ env.PROJECT_PATH }}" --output ./artifacts --configuration "${{ env.CONFIGURATION }}" + --include-symbols + --include-source -p:Version=$VERSION -p:PackageVersion=$VERSION -p:SymbolPackageFormat=snupkg From 71a3cf9d9e7ca9db9f3ae2341ccb71b46011006a Mon Sep 17 00:00:00 2001 From: Padi Date: Thu, 15 Aug 2024 01:01:06 +0300 Subject: [PATCH 405/549] Update build-and-test.yml --- .github/workflows/build-and-test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index a78cc6e2..ef7d0fcc 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -22,9 +22,9 @@ on: - '**.csproj' - '**.sln' -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true +# concurrency: +# group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} +# cancel-in-progress: true env: # Disable the .NET logo in the console output. @@ -120,4 +120,4 @@ jobs: uses: actions/upload-artifact@v4 with: name: test-results-${{ matrix.os }} - path: TestResults-${{ matrix.os }} \ No newline at end of file + path: TestResults-${{ matrix.os }} From cdaa0aff55ab47e576d07972acce99dd12c0d531 Mon Sep 17 00:00:00 2001 From: Padi Date: Thu, 15 Aug 2024 01:11:07 +0300 Subject: [PATCH 406/549] Update codeql-analysis.yml --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index e47ceb46..1ce884aa 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -42,7 +42,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -53,7 +53,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -67,4 +67,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 From 8c24137b55218210a9400b8dd8516985e3c160e7 Mon Sep 17 00:00:00 2001 From: Padi Date: Mon, 26 Aug 2024 12:23:31 +0300 Subject: [PATCH 407/549] [Fix] Upload file for attachment (#356) --- .../Net/WebClient/InternalRedmineApiWebClient.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs index 5a9b9c45..b4c00970 100644 --- a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs +++ b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs @@ -131,7 +131,7 @@ public ApiResponseMessage Download(string address, RequestOptions requestOptions public ApiResponseMessage Upload(string address, byte[] data, RequestOptions requestOptions = null) { var content = new ByteArrayApiRequestMessageContent(data); - return HandleRequest(address, HttpVerbs.UPLOAD, requestOptions, content); + return HandleRequest(address, HttpVerbs.POST, requestOptions, content); } #if !(NET20) @@ -160,7 +160,7 @@ public async Task UpdateAsync(string address, string payload public async Task UploadFileAsync(string address, byte[] data, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) { var content = new ByteArrayApiRequestMessageContent(data); - return await HandleRequestAsync(address, HttpVerbs.UPLOAD, requestOptions, content, cancellationToken:cancellationToken).ConfigureAwait(false); + return await HandleRequestAsync(address, HttpVerbs.POST, requestOptions, content, cancellationToken:cancellationToken).ConfigureAwait(false); } public async Task PatchAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) @@ -350,4 +350,4 @@ private static string GetContentType(IRedmineSerializer serializer) return serializer.Format == RedmineConstants.XML ? RedmineConstants.CONTENT_TYPE_APPLICATION_XML : RedmineConstants.CONTENT_TYPE_APPLICATION_JSON; } } -} \ No newline at end of file +} From 27b4da6dd6b82c9c7f7304ae42dd5a74fabfa1f4 Mon Sep 17 00:00:00 2001 From: Padi Date: Mon, 26 Aug 2024 12:49:42 +0300 Subject: [PATCH 408/549] Update publish.yml --- .github/workflows/publish.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c6ed19cf..f3c32f01 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -137,6 +137,7 @@ jobs: --include-source -p:Version=$VERSION -p:PackageVersion=$VERSION + -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg - name: Create the package - Signed @@ -148,6 +149,7 @@ jobs: --include-source -p:Version=$VERSION -p:PackageVersion=$VERSION + -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg -p:Sign=true From 096d25c6c642e820d7a62d1b442e0e90173e8473 Mon Sep 17 00:00:00 2001 From: Padi Date: Sat, 31 Aug 2024 20:37:51 +0300 Subject: [PATCH 409/549] Update InternalRedmineApiWebClient.cs --- .../Net/WebClient/InternalRedmineApiWebClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs index b4c00970..0ee22f25 100644 --- a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs +++ b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs @@ -130,7 +130,7 @@ public ApiResponseMessage Download(string address, RequestOptions requestOptions public ApiResponseMessage Upload(string address, byte[] data, RequestOptions requestOptions = null) { - var content = new ByteArrayApiRequestMessageContent(data); + var content = new StreamApiRequestMessageContent(data); return HandleRequest(address, HttpVerbs.POST, requestOptions, content); } @@ -159,7 +159,7 @@ public async Task UpdateAsync(string address, string payload public async Task UploadFileAsync(string address, byte[] data, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) { - var content = new ByteArrayApiRequestMessageContent(data); + var content = new StreamApiRequestMessageContent(data); return await HandleRequestAsync(address, HttpVerbs.POST, requestOptions, content, cancellationToken:cancellationToken).ConfigureAwait(false); } From 3d067a3d59b95ed8ba047c7489d1f3d882c50e2f Mon Sep 17 00:00:00 2001 From: Padi Date: Sat, 31 Aug 2024 21:00:21 +0300 Subject: [PATCH 410/549] Update codeql-analysis.yml --- .github/workflows/codeql-analysis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 1ce884aa..4c4bd893 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -38,11 +38,11 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -53,7 +53,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -67,4 +67,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 From 4ec711f5d43a9d41c77e371c6d1e90e4edaa06db Mon Sep 17 00:00:00 2001 From: Padi Date: Sat, 31 Aug 2024 22:41:09 +0300 Subject: [PATCH 411/549] Update redmine-net-api.csproj --- src/redmine-net-api/redmine-net-api.csproj | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 3d19d477..55dc139d 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -35,7 +35,8 @@ - embedded + full + portable false $(SolutionDir)/artifacts @@ -68,6 +69,7 @@ true true + true snupkg true @@ -105,4 +107,4 @@ - \ No newline at end of file + From 409af54d9365ee42bf1340c61ec4a8bfd07c0abc Mon Sep 17 00:00:00 2001 From: Koichi Kobayashi Date: Tue, 26 Nov 2024 23:05:59 +0900 Subject: [PATCH 412/549] Update NuGet Package (#360) --- .../redmine-net-api.Tests.csproj | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj index 8632e5f9..90f40281 100644 --- a/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj +++ b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj @@ -54,14 +54,17 @@ - - - - - - - - + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + runtime; build; native; contentfiles; analyzers; buildtransitive From ac206ee617eb7713eb49a930fde5f53e983b48cb Mon Sep 17 00:00:00 2001 From: Padi Date: Sat, 4 Jan 2025 15:59:36 +0200 Subject: [PATCH 413/549] Update README.md --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 5d79c737..11bb8f6c 100755 --- a/README.md +++ b/README.md @@ -58,6 +58,13 @@ A good way to get started (flow): The API is released under Apache 2 open-source license. You can use it for both personal and commercial purposes, build upon it and modify it. +## Contributors +Thanks to all the people who already contributed! + + + + + ## Thanks I would like to thank: From 17ab59ebf1be3b1d2737adfab82ac9846e5d82fa Mon Sep 17 00:00:00 2001 From: Martin Hey Date: Tue, 7 Jan 2025 15:27:16 +0100 Subject: [PATCH 414/549] fixed memberships url (#365) according to https://www.redmine.org/projects/redmine/wiki/Rest_Memberships the url is /projects/:project_id --- src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs b/src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs index b16ff47f..12f896f9 100644 --- a/src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs +++ b/src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs @@ -48,7 +48,7 @@ public static string ProjectMemberships(this RedmineApiUrls redmineApiUrls, stri throw new RedmineException($"Argument '{nameof(projectIdentifier)}' is null or whitespace"); } - return $"{RedmineKeys.PROJECT}/{projectIdentifier}/{RedmineKeys.MEMBERSHIPS}.{redmineApiUrls.Format}"; + return $"{RedmineKeys.PROJECTS}/{projectIdentifier}/{RedmineKeys.MEMBERSHIPS}.{redmineApiUrls.Format}"; } public static string ProjectWikiIndex(this RedmineApiUrls redmineApiUrls, string projectId) From cbc5bd007862eb6a5d25a1768ee50ff3c0dde402 Mon Sep 17 00:00:00 2001 From: Padi Date: Thu, 9 Jan 2025 19:38:10 +0200 Subject: [PATCH 415/549] Fix #363 (#366) --- src/redmine-net-api/RedmineManagerOptionsBuilder.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/redmine-net-api/RedmineManagerOptionsBuilder.cs b/src/redmine-net-api/RedmineManagerOptionsBuilder.cs index 8e682f0b..0b6dd5d5 100644 --- a/src/redmine-net-api/RedmineManagerOptionsBuilder.cs +++ b/src/redmine-net-api/RedmineManagerOptionsBuilder.cs @@ -361,6 +361,10 @@ internal static Uri CreateRedmineUri(string host, string scheme = null) uriBuilder.Scheme = uri.Scheme; uriBuilder.Port = int.TryParse(uri.LocalPath, out var port) ? port : uri.Port; uriBuilder.Host = uri.Host; + if (!uri.LocalPath.IsNullOrWhiteSpace() && !uri.LocalPath.Contains(".")) + { + uriBuilder.Path = uri.LocalPath; + } } } From c57e7702b205f6dabe78387eec13a6298cb69e13 Mon Sep 17 00:00:00 2001 From: Padi Date: Thu, 9 Jan 2025 20:59:05 +0200 Subject: [PATCH 416/549] Fix #364 (#367) --- src/redmine-net-api/RedmineKeys.cs | 2 +- .../Xml/Extensions/XmlReaderExtensions.cs | 17 +++++++++++++++++ src/redmine-net-api/Types/Permission.cs | 18 ++---------------- src/redmine-net-api/Types/Role.cs | 13 ++++++++++--- 4 files changed, 30 insertions(+), 20 deletions(-) diff --git a/src/redmine-net-api/RedmineKeys.cs b/src/redmine-net-api/RedmineKeys.cs index 5bd7661e..8cc70a16 100644 --- a/src/redmine-net-api/RedmineKeys.cs +++ b/src/redmine-net-api/RedmineKeys.cs @@ -67,7 +67,7 @@ public static class RedmineKeys /// /// /// - public const string ASSIGNABLE = "Assignable"; + public const string ASSIGNABLE = "assignable"; /// /// /// diff --git a/src/redmine-net-api/Serialization/Xml/Extensions/XmlReaderExtensions.cs b/src/redmine-net-api/Serialization/Xml/Extensions/XmlReaderExtensions.cs index f0312351..1c762b1a 100644 --- a/src/redmine-net-api/Serialization/Xml/Extensions/XmlReaderExtensions.cs +++ b/src/redmine-net-api/Serialization/Xml/Extensions/XmlReaderExtensions.cs @@ -87,6 +87,23 @@ public static bool ReadAttributeAsBoolean(this XmlReader reader, string attribut return result; } + /// + /// Reads the element content as nullable boolean. + /// + /// The reader. + /// + public static bool? ReadElementContentAsNullableBoolean(this XmlReader reader) + { + var content = reader.ReadElementContentAsString(); + + if (content.IsNullOrWhiteSpace() || !bool.TryParse(content, out var result)) + { + return null; + } + + return result; + } + /// /// Reads the element content as nullable date time. /// diff --git a/src/redmine-net-api/Types/Permission.cs b/src/redmine-net-api/Types/Permission.cs index 3a658453..fcfcbbf1 100644 --- a/src/redmine-net-api/Types/Permission.cs +++ b/src/redmine-net-api/Types/Permission.cs @@ -77,23 +77,9 @@ public void WriteXml(XmlWriter writer) { } /// public void ReadJson(JsonReader reader) { - while (reader.Read()) + if (reader.TokenType == JsonToken.String) { - if (reader.TokenType == JsonToken.EndObject) - { - return; - } - - if (reader.TokenType != JsonToken.PropertyName) - { - continue; - } - - switch (reader.Value) - { - case RedmineKeys.PERMISSION: Info = reader.ReadAsString(); break; - default: reader.Read(); break; - } + Info = reader.Value as string; } } diff --git a/src/redmine-net-api/Types/Role.cs b/src/redmine-net-api/Types/Role.cs index 366ce69e..40ac4122 100644 --- a/src/redmine-net-api/Types/Role.cs +++ b/src/redmine-net-api/Types/Role.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -59,7 +59,7 @@ public sealed class Role : IdentifiableName, IEquatable /// /// /// - public bool IsAssignable { get; set; } + public bool? IsAssignable { get; set; } #endregion #region Implementation of IXmlSerialization @@ -82,6 +82,7 @@ public override void ReadXml(XmlReader reader) { case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; + case RedmineKeys.ASSIGNABLE: IsAssignable = reader.ReadElementContentAsNullableBoolean(); break; case RedmineKeys.PERMISSIONS: Permissions = reader.ReadElementContentAsCollection(); break; default: reader.Read(); break; } @@ -113,6 +114,7 @@ public override void ReadJson(JsonReader reader) { case RedmineKeys.ID: Id = reader.ReadAsInt(); break; case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + case RedmineKeys.ASSIGNABLE: IsAssignable = reader.ReadAsBoolean(); break; case RedmineKeys.PERMISSIONS: Permissions = reader.ReadAsCollection(); break; default: reader.Read(); break; } @@ -129,7 +131,11 @@ public override void ReadJson(JsonReader reader) public bool Equals(Role other) { if (other == null) return false; - return Id == other.Id && Name == other.Name; + return EqualityComparer.Default.Equals(Id, other.Id) && + EqualityComparer.Default.Equals(Name, other.Name) && + IsAssignable == other.IsAssignable && + EqualityComparer>.Default.Equals(Permissions, other.Permissions); + } /// @@ -156,6 +162,7 @@ public override int GetHashCode() var hashCode = 13; hashCode = HashCodeHelper.GetHashCode(Id, hashCode); hashCode = HashCodeHelper.GetHashCode(Name, hashCode); + hashCode = HashCodeHelper.GetHashCode(IsAssignable, hashCode); hashCode = HashCodeHelper.GetHashCode(Permissions, hashCode); return hashCode; } From 9ce6a9d2ef300b1b44c3fe9efcc8059f2b1321c5 Mon Sep 17 00:00:00 2001 From: Padi Date: Thu, 9 Jan 2025 21:17:47 +0200 Subject: [PATCH 417/549] Improvements (#368) * [csproj] Add net9.0 to TargetFrameworks * [csproj] Add Tuple package for .NET 4.0 * [New] ArgumentNullThrowHelper * [RedmineManagerOptionsBuilder] Rewrite build method * [Obsolete] Adjustments * [RedmineManager] Replace GetxxxObjects calls with GetxxxInternal * [RedmineManagerAsync] Remove GeneratedRegex * [RedmineManager] Ctor improvements * [WebClient] RedmineWebClientOptions * [Upload] Add fileName * [global.json] Set Sdk version to 9.0.101 --- global.json | 2 +- ...RedmineManagerAsyncExtensions.Obsolete.cs} | 62 +++++--------- .../Extensions/RedmineManagerExtensions.cs | 10 +-- src/redmine-net-api/IRedmineManager.cs | 5 +- src/redmine-net-api/IRedmineManagerAsync.cs | 3 +- .../Internals/ArgumentNullThrowHelper.cs | 32 +++++++ src/redmine-net-api/Net/RedmineApiUrls.cs | 6 +- .../Net/WebClient/IRedmineWebClientOptions.cs | 83 +++++++++++++++++++ .../WebClient/InternalRedmineApiWebClient.cs | 47 ++++++----- .../Net/WebClient/InternalWebClient.cs | 46 +++++----- .../StringApiRequestMessageContent.cs | 10 +-- ...solete.cs => RedmineWebClient.Obsolete.cs} | 0 ...Obsolete.cs => RedmineManager.Obsolete.cs} | 55 ++++-------- src/redmine-net-api/RedmineManager.cs | 67 +++++++++------ src/redmine-net-api/RedmineManagerAsync.cs | 13 +-- .../RedmineManagerOptionsBuilder.cs | 50 +++++++---- src/redmine-net-api/redmine-net-api.csproj | 3 +- 17 files changed, 303 insertions(+), 191 deletions(-) rename src/redmine-net-api/Extensions/{RedmineManagerAsyncExtensionsObsolete.cs => RedmineManagerAsyncExtensions.Obsolete.cs} (87%) create mode 100644 src/redmine-net-api/Internals/ArgumentNullThrowHelper.cs create mode 100644 src/redmine-net-api/Net/WebClient/IRedmineWebClientOptions.cs rename src/redmine-net-api/Net/WebClient/{RedmineWebClientObsolete.cs => RedmineWebClient.Obsolete.cs} (100%) rename src/redmine-net-api/{RedmineManagerObsolete.cs => RedmineManager.Obsolete.cs} (93%) diff --git a/global.json b/global.json index ab747bba..6223f1b6 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.303", + "version": "9.0.101", "allowPrerelease": false, "rollForward": "latestMajor" } diff --git a/src/redmine-net-api/Extensions/RedmineManagerAsyncExtensionsObsolete.cs b/src/redmine-net-api/Extensions/RedmineManagerAsyncExtensions.Obsolete.cs similarity index 87% rename from src/redmine-net-api/Extensions/RedmineManagerAsyncExtensionsObsolete.cs rename to src/redmine-net-api/Extensions/RedmineManagerAsyncExtensions.Obsolete.cs index dd605bef..90f25fb8 100644 --- a/src/redmine-net-api/Extensions/RedmineManagerAsyncExtensionsObsolete.cs +++ b/src/redmine-net-api/Extensions/RedmineManagerAsyncExtensions.Obsolete.cs @@ -29,7 +29,7 @@ namespace Redmine.Net.Api.Async { /// /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use RedmineManger async methods instead")] + [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use RedmineManger async methods instead.")] public static class RedmineManagerAsyncExtensions { /// @@ -40,7 +40,7 @@ public static class RedmineManagerAsyncExtensions /// /// /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")] public static async Task GetCurrentUserAsync(this RedmineManager redmineManager, NameValueCollection parameters = null, string impersonateUserName = null, CancellationToken cancellationToken = default) { var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); @@ -55,7 +55,7 @@ public static async Task GetCurrentUserAsync(this RedmineManager redmineMa /// Name of the page. /// The wiki page. /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")] public static async Task CreateWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage) { var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); @@ -71,7 +71,7 @@ public static async Task CreateWikiPageAsync(this RedmineManager redmi /// Name of the page. /// The wiki page. /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")] public static async Task UpdateWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage) { var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); @@ -85,7 +85,7 @@ public static async Task UpdateWikiPageAsync(this RedmineManager redmineManager, /// The project identifier. /// Name of the page. /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")] public static async Task DeleteWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName) { var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); @@ -101,11 +101,11 @@ public static async Task DeleteWikiPageAsync(this RedmineManager redmineManager, /// /// . /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")] public static async Task UploadFileAsync(this RedmineManager redmineManager, byte[] data) { var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); - return await redmineManager.UploadFileAsync(data, requestOptions).ConfigureAwait(false); + return await redmineManager.UploadFileAsync(data, null, requestOptions).ConfigureAwait(false); } /// @@ -114,7 +114,7 @@ public static async Task UploadFileAsync(this RedmineManager redmineMana /// The redmine manager. /// The address. /// - [Obsolete("Use DownloadFileAsync instead")] + [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")] public static async Task DownloadFileAsync(this RedmineManager redmineManager, string address) { var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); @@ -131,7 +131,7 @@ public static async Task DownloadFileAsync(this RedmineManager redmineMa /// Name of the page. /// The version. /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")] public static async Task GetWikiPageAsync(this RedmineManager redmineManager, string projectId, NameValueCollection parameters, string pageName, uint version = 0) { var requestOptions = RedmineManagerExtensions.CreateRequestOptions(parameters); @@ -145,7 +145,7 @@ public static async Task GetWikiPageAsync(this RedmineManager redmineM /// The parameters. /// The project identifier. /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")] public static async Task> GetAllWikiPagesAsync(this RedmineManager redmineManager, NameValueCollection parameters, string projectId) { var requestOptions = RedmineManagerExtensions.CreateRequestOptions(parameters); @@ -161,7 +161,7 @@ public static async Task> GetAllWikiPagesAsync(this RedmineManage /// /// Returns the Guid associated with the async request. /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")] public static async Task AddUserToGroupAsync(this RedmineManager redmineManager, int groupId, int userId) { var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); @@ -175,7 +175,7 @@ public static async Task AddUserToGroupAsync(this RedmineManager redmineManager, /// The group id. /// The user id. /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")] public static async Task RemoveUserFromGroupAsync(this RedmineManager redmineManager, int groupId, int userId) { var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); @@ -189,7 +189,7 @@ public static async Task RemoveUserFromGroupAsync(this RedmineManager redmineMan /// The issue identifier. /// The user identifier. /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")] public static async Task AddWatcherToIssueAsync(this RedmineManager redmineManager, int issueId, int userId) { var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); @@ -203,7 +203,7 @@ public static async Task AddWatcherToIssueAsync(this RedmineManager redmineManag /// The issue identifier. /// The user identifier. /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")] public static async Task RemoveWatcherFromIssueAsync(this RedmineManager redmineManager, int issueId, int userId) { var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); @@ -217,7 +217,7 @@ public static async Task RemoveWatcherFromIssueAsync(this RedmineManager redmine /// /// /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")] public static async Task CountAsync(this RedmineManager redmineManager, params string[] include) where T : class, new() { return await redmineManager.CountAsync(null, CancellationToken.None).ConfigureAwait(false); @@ -230,7 +230,7 @@ public static async Task RemoveWatcherFromIssueAsync(this RedmineManager redmine /// /// /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use CountAsync method instead.")] public static async Task CountAsync(this RedmineManager redmineManager, NameValueCollection parameters) where T : class, new() { var requestOptions = RedmineManagerExtensions.CreateRequestOptions(parameters); @@ -245,7 +245,7 @@ public static async Task RemoveWatcherFromIssueAsync(this RedmineManager redmine /// The redmine manager. /// The parameters. /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use GetPagedAsync method instead.")] public static async Task> GetPaginatedObjectsAsync(this RedmineManager redmineManager, NameValueCollection parameters) where T : class, new() { @@ -260,7 +260,7 @@ public static async Task> GetPaginatedObjectsAsync(this Redmi /// The redmine manager. /// The parameters. /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use GetAsync method instead.")] public static async Task> GetObjectsAsync(this RedmineManager redmineManager, NameValueCollection parameters) where T : class, new() { @@ -276,7 +276,7 @@ public static async Task> GetObjectsAsync(this RedmineManager redmine /// The id of the object. /// Optional filters and/or optional fetched data. /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use GetAsync method instead.")] public static async Task GetObjectAsync(this RedmineManager redmineManager, string id, NameValueCollection parameters) where T : class, new() { @@ -291,7 +291,7 @@ public static async Task GetObjectAsync(this RedmineManager redmineManager /// The redmine manager. /// The object to create. /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use CreateAsync method instead.")] public static async Task CreateObjectAsync(this RedmineManager redmineManager, T entity) where T : class, new() { @@ -307,7 +307,7 @@ public static async Task CreateObjectAsync(this RedmineManager redmineMana /// The object to create. /// The owner identifier. /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use CreateAsync method instead.")] public static async Task CreateObjectAsync(this RedmineManager redmineManager, T entity, string ownerId) where T : class, new() { @@ -323,7 +323,7 @@ public static async Task CreateObjectAsync(this RedmineManager redmineMana /// The identifier. /// The object. /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use UpdateAsync method instead.")] public static async Task UpdateObjectAsync(this RedmineManager redmineManager, string id, T entity) where T : class, new() { @@ -338,29 +338,13 @@ public static async Task UpdateObjectAsync(this RedmineManager redmineManager /// The redmine manager. /// The id of the object to delete /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use DeleteAsync method instead.")] public static async Task DeleteObjectAsync(this RedmineManager redmineManager, string id) where T : class, new() { var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); await redmineManager.DeleteAsync(id, requestOptions).ConfigureAwait(false); } - - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - public static async Task> SearchAsync(this RedmineManager redmineManager, string q, int limit = RedmineManager.DEFAULT_PAGE_SIZE_VALUE, int offset = 0, SearchFilterBuilder searchFilter = null) - { - return await RedmineManagerExtensions.SearchAsync(redmineManager, q, limit, offset, searchFilter).ConfigureAwait(false); - } } } #endif \ No newline at end of file diff --git a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs index 75a778df..d4447c21 100644 --- a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs +++ b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs @@ -47,7 +47,7 @@ public static PagedResults GetProjectNews(this RedmineManager redmineManag var escapedUri = Uri.EscapeDataString(uri); - var response = redmineManager.GetPaginatedObjects(escapedUri, requestOptions); + var response = redmineManager.GetPaginatedInternal(escapedUri, requestOptions); return response; } @@ -96,7 +96,7 @@ public static PagedResults GetProjectMemberships(this Redmine { var uri = redmineManager.RedmineApiUrls.ProjectMemberships(projectIdentifier); - var response = redmineManager.GetPaginatedObjects(uri, requestOptions); + var response = redmineManager.GetPaginatedInternal(uri, requestOptions); return response; } @@ -113,7 +113,7 @@ public static PagedResults GetProjectFiles(this RedmineManager redmineMana { var uri = redmineManager.RedmineApiUrls.ProjectFilesFragment(projectIdentifier); - var response = redmineManager.GetPaginatedObjects(uri, requestOptions); + var response = redmineManager.GetPaginatedInternal(uri, requestOptions); return response; } @@ -291,7 +291,7 @@ public static List GetAllWikiPages(this RedmineManager redmineManager, { var uri = redmineManager.RedmineApiUrls.ProjectWikiIndex(projectId); - var response = redmineManager.GetObjects(uri, requestOptions); + var response = redmineManager.GetInternal(uri, requestOptions); return response; } @@ -530,7 +530,7 @@ public static async Task CreateWikiPageAsync(this RedmineManager redmi } /// - /// Creates the or update wiki page asynchronous. + /// Creates or update wiki page asynchronous. /// /// The redmine manager. /// The project identifier. diff --git a/src/redmine-net-api/IRedmineManager.cs b/src/redmine-net-api/IRedmineManager.cs index b8148141..e45ec6e7 100644 --- a/src/redmine-net-api/IRedmineManager.cs +++ b/src/redmine-net-api/IRedmineManager.cs @@ -93,17 +93,18 @@ void Update(string id, T entity, string projectId = null, RequestOptions requ /// void Delete(string id, RequestOptions requestOptions = null) where T : class, new(); - + /// /// Support for adding attachments through the REST API is added in Redmine 1.4.0. /// Upload a file to server. /// /// The content of the file that will be uploaded on server. + /// /// /// Returns the token for uploaded file. /// /// - Upload UploadFile(byte[] data); + Upload UploadFile(byte[] data, string fileName = null); /// /// Downloads a file from the specified address. diff --git a/src/redmine-net-api/IRedmineManagerAsync.cs b/src/redmine-net-api/IRedmineManagerAsync.cs index 7c77d334..adc9456b 100644 --- a/src/redmine-net-api/IRedmineManagerAsync.cs +++ b/src/redmine-net-api/IRedmineManagerAsync.cs @@ -134,12 +134,13 @@ Task DeleteAsync(string id, RequestOptions requestOptions = null, Cancellatio /// Upload a file to server. This method does not block the calling thread. /// /// The content of the file that will be uploaded on server. + /// /// /// /// /// . /// - Task UploadFileAsync(byte[] data, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + Task UploadFileAsync(byte[] data, string fileName = null, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); /// /// Downloads the file asynchronous. diff --git a/src/redmine-net-api/Internals/ArgumentNullThrowHelper.cs b/src/redmine-net-api/Internals/ArgumentNullThrowHelper.cs new file mode 100644 index 00000000..86f45107 --- /dev/null +++ b/src/redmine-net-api/Internals/ArgumentNullThrowHelper.cs @@ -0,0 +1,32 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +namespace Redmine.Net.Api.Internals; + +internal static class ArgumentNullThrowHelper +{ + public static void ThrowIfNull( + #if INTERNAL_NULLABLE_ATTRIBUTES || NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER + [NotNull] + #endif + object? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null) + { + #if !NET7_0_OR_GREATER || NETSTANDARD || NETFRAMEWORK + if (argument is null) + { + Throw(paramName); + } + #else + ArgumentNullException.ThrowIfNull(argument, paramName); + #endif + } + + #if !NET7_0_OR_GREATER || NETSTANDARD || NETFRAMEWORK + #if INTERNAL_NULLABLE_ATTRIBUTES || NETSTANDARD2_1_OR_GREATER || NET5_0_OR_GREATER + [DoesNotReturn] + #endif + internal static void Throw(string? paramName) => + throw new ArgumentNullException(paramName); + #endif +} \ No newline at end of file diff --git a/src/redmine-net-api/Net/RedmineApiUrls.cs b/src/redmine-net-api/Net/RedmineApiUrls.cs index 34417919..60788877 100644 --- a/src/redmine-net-api/Net/RedmineApiUrls.cs +++ b/src/redmine-net-api/Net/RedmineApiUrls.cs @@ -200,9 +200,11 @@ public string GetListFragment(Type type, string ownerId = null) return $"{TypeFragment(TypeUrlFragments, type)}.{Format}"; } - public string UploadFragment() + public string UploadFragment(string fileName = null) { - return $"{RedmineKeys.UPLOADS}.{Format}"; + return !fileName.IsNullOrWhiteSpace() + ? $"{RedmineKeys.UPLOADS}.{Format}?filename={Uri.EscapeDataString(fileName)}" + : $"{RedmineKeys.UPLOADS}.{Format}"; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Net/WebClient/IRedmineWebClientOptions.cs b/src/redmine-net-api/Net/WebClient/IRedmineWebClientOptions.cs new file mode 100644 index 00000000..809d9afb --- /dev/null +++ b/src/redmine-net-api/Net/WebClient/IRedmineWebClientOptions.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Net.Cache; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; + +namespace Redmine.Net.Api.Net.WebClient; + +/// +/// +/// +public interface IRedmineWebClientOptions : IRedmineApiClientOptions +{ +#if NET40_OR_GREATER || NETCOREAPP + /// + /// + /// + public X509CertificateCollection ClientCertificates { get; set; } +#endif + + /// + /// + /// + int? DefaultConnectionLimit { get; set; } + + /// + /// + /// + Dictionary DefaultHeaders { get; set; } + + /// + /// + /// + int? DnsRefreshTimeout { get; set; } + + /// + /// + /// + bool? EnableDnsRoundRobin { get; set; } + + /// + /// + /// + bool? KeepAlive { get; set; } + + /// + /// + /// + int? MaxServicePoints { get; set; } + + /// + /// + /// + int? MaxServicePointIdleTime { get; set; } + + /// + /// + /// + RequestCachePolicy RequestCachePolicy { get; set; } + +#if(NET46_OR_GREATER || NETCOREAPP) + /// + /// + /// + public bool? ReusePort { get; set; } +#endif + + /// + /// + /// + RemoteCertificateValidationCallback ServerCertificateValidationCallback { get; set; } + + /// + /// + /// + bool? UnsafeAuthenticatedConnectionSharing { get; set; } + + /// + /// + /// + /// Only HTTP/1.0 and HTTP/1.1 version requests are currently supported. + Version ProtocolVersion { get; set; } +} \ No newline at end of file diff --git a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs index 0ee22f25..a48b5aa3 100644 --- a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs +++ b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs @@ -34,6 +34,7 @@ namespace Redmine.Net.Api.Net.WebClient /// internal sealed class InternalRedmineApiWebClient : IRedmineApiClient { + private static readonly byte[] EmptyBytes = Encoding.UTF8.GetBytes(string.Empty); private readonly Func _webClientFunc; private readonly IRedmineAuthentication _credentials; private readonly IRedmineSerializer _serializer; @@ -44,48 +45,56 @@ public InternalRedmineApiWebClient(RedmineManagerOptions redmineManagerOptions) ConfigureServicePointManager(redmineManagerOptions.ClientOptions); } - public InternalRedmineApiWebClient(Func webClientFunc, IRedmineAuthentication authentication, IRedmineSerializer serializer) + public InternalRedmineApiWebClient( + Func webClientFunc, + IRedmineAuthentication authentication, + IRedmineSerializer serializer) { _webClientFunc = webClientFunc; _credentials = authentication; _serializer = serializer; } - private static void ConfigureServicePointManager(IRedmineApiClientOptions webClientSettings) + private static void ConfigureServicePointManager(IRedmineApiClientOptions options) { - if (webClientSettings.MaxServicePoints.HasValue) + if (options is not IRedmineWebClientOptions webClientOptions) { - ServicePointManager.MaxServicePoints = webClientSettings.MaxServicePoints.Value; + return; + } + + if (webClientOptions.MaxServicePoints.HasValue) + { + ServicePointManager.MaxServicePoints = webClientOptions.MaxServicePoints.Value; } - if (webClientSettings.MaxServicePointIdleTime.HasValue) + if (webClientOptions.MaxServicePointIdleTime.HasValue) { - ServicePointManager.MaxServicePointIdleTime = webClientSettings.MaxServicePointIdleTime.Value; + ServicePointManager.MaxServicePointIdleTime = webClientOptions.MaxServicePointIdleTime.Value; } - ServicePointManager.SecurityProtocol = webClientSettings.SecurityProtocolType ?? ServicePointManager.SecurityProtocol; + ServicePointManager.SecurityProtocol = webClientOptions.SecurityProtocolType ?? ServicePointManager.SecurityProtocol; - if (webClientSettings.DefaultConnectionLimit.HasValue) + if (webClientOptions.DefaultConnectionLimit.HasValue) { - ServicePointManager.DefaultConnectionLimit = webClientSettings.DefaultConnectionLimit.Value; + ServicePointManager.DefaultConnectionLimit = webClientOptions.DefaultConnectionLimit.Value; } - if (webClientSettings.DnsRefreshTimeout.HasValue) + if (webClientOptions.DnsRefreshTimeout.HasValue) { - ServicePointManager.DnsRefreshTimeout = webClientSettings.DnsRefreshTimeout.Value; + ServicePointManager.DnsRefreshTimeout = webClientOptions.DnsRefreshTimeout.Value; } - ServicePointManager.CheckCertificateRevocationList = webClientSettings.CheckCertificateRevocationList; + ServicePointManager.CheckCertificateRevocationList = webClientOptions.CheckCertificateRevocationList; - if (webClientSettings.EnableDnsRoundRobin.HasValue) + if (webClientOptions.EnableDnsRoundRobin.HasValue) { - ServicePointManager.EnableDnsRoundRobin = webClientSettings.EnableDnsRoundRobin.Value; + ServicePointManager.EnableDnsRoundRobin = webClientOptions.EnableDnsRoundRobin.Value; } #if(NET46_OR_GREATER || NETCOREAPP) - if (webClientSettings.ReusePort.HasValue) + if (webClientOptions.ReusePort.HasValue) { - ServicePointManager.ReusePort = webClientSettings.ReusePort.Value; + ServicePointManager.ReusePort = webClientOptions.ReusePort.Value; } #endif } @@ -197,7 +206,7 @@ private async Task SendAsync(ApiRequestMessage requestMessag SetWebClientHeaders(webClient, requestMessage); - if (requestMessage.Method is HttpVerbs.GET or HttpVerbs.DOWNLOAD) + if(IsGetOrDownload(requestMessage.Method)) { response = await webClient.DownloadDataTaskAsync(requestMessage.RequestUri).ConfigureAwait(false); } @@ -211,7 +220,7 @@ private async Task SendAsync(ApiRequestMessage requestMessag } else { - payload = Encoding.UTF8.GetBytes(string.Empty); + payload = EmptyBytes; } response = await webClient.UploadDataTaskAsync(requestMessage.RequestUri, requestMessage.Method, payload).ConfigureAwait(false); @@ -293,7 +302,7 @@ private ApiResponseMessage Send(ApiRequestMessage requestMessage) } else { - payload = Encoding.UTF8.GetBytes(string.Empty); + payload = EmptyBytes; } response = webClient.UploadData(requestMessage.RequestUri, requestMessage.Method, payload); diff --git a/src/redmine-net-api/Net/WebClient/InternalWebClient.cs b/src/redmine-net-api/Net/WebClient/InternalWebClient.cs index 27051719..87c7c602 100644 --- a/src/redmine-net-api/Net/WebClient/InternalWebClient.cs +++ b/src/redmine-net-api/Net/WebClient/InternalWebClient.cs @@ -22,12 +22,12 @@ namespace Redmine.Net.Api.Net.WebClient; internal sealed class InternalWebClient : System.Net.WebClient { - private readonly IRedmineApiClientOptions _webClientSettings; + private readonly IRedmineWebClientOptions _webClientOptions; #pragma warning disable SYSLIB0014 public InternalWebClient(RedmineManagerOptions redmineManagerOptions) { - _webClientSettings = redmineManagerOptions.ClientOptions; + _webClientOptions = redmineManagerOptions.ClientOptions as IRedmineWebClientOptions; BaseAddress = redmineManagerOptions.BaseAddress.ToString(); } #pragma warning restore SYSLIB0014 @@ -43,55 +43,55 @@ protected override WebRequest GetWebRequest(Uri address) return base.GetWebRequest(address); } - httpWebRequest.UserAgent = _webClientSettings.UserAgent.ValueOrFallback("RedmineDotNetAPIClient"); + httpWebRequest.UserAgent = _webClientOptions.UserAgent.ValueOrFallback("RedmineDotNetAPIClient"); - httpWebRequest.AutomaticDecompression = _webClientSettings.DecompressionFormat ?? DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.None; + httpWebRequest.AutomaticDecompression = _webClientOptions.DecompressionFormat ?? DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.None; - AssignIfHasValue(_webClientSettings.AutoRedirect, value => httpWebRequest.AllowAutoRedirect = value); + AssignIfHasValue(_webClientOptions.AutoRedirect, value => httpWebRequest.AllowAutoRedirect = value); - AssignIfHasValue(_webClientSettings.MaxAutomaticRedirections, value => httpWebRequest.MaximumAutomaticRedirections = value); + AssignIfHasValue(_webClientOptions.MaxAutomaticRedirections, value => httpWebRequest.MaximumAutomaticRedirections = value); - AssignIfHasValue(_webClientSettings.KeepAlive, value => httpWebRequest.KeepAlive = value); + AssignIfHasValue(_webClientOptions.KeepAlive, value => httpWebRequest.KeepAlive = value); - AssignIfHasValue(_webClientSettings.Timeout, value => httpWebRequest.Timeout = (int) value.TotalMilliseconds); + AssignIfHasValue(_webClientOptions.Timeout, value => httpWebRequest.Timeout = (int) value.TotalMilliseconds); - AssignIfHasValue(_webClientSettings.PreAuthenticate, value => httpWebRequest.PreAuthenticate = value); + AssignIfHasValue(_webClientOptions.PreAuthenticate, value => httpWebRequest.PreAuthenticate = value); - AssignIfHasValue(_webClientSettings.UseCookies, value => httpWebRequest.CookieContainer = _webClientSettings.CookieContainer); + AssignIfHasValue(_webClientOptions.UseCookies, value => httpWebRequest.CookieContainer = _webClientOptions.CookieContainer); - AssignIfHasValue(_webClientSettings.UnsafeAuthenticatedConnectionSharing, value => httpWebRequest.UnsafeAuthenticatedConnectionSharing = value); + AssignIfHasValue(_webClientOptions.UnsafeAuthenticatedConnectionSharing, value => httpWebRequest.UnsafeAuthenticatedConnectionSharing = value); - AssignIfHasValue(_webClientSettings.MaxResponseContentBufferSize, value => { }); + AssignIfHasValue(_webClientOptions.MaxResponseContentBufferSize, value => { }); - if (_webClientSettings.DefaultHeaders != null) + if (_webClientOptions.DefaultHeaders != null) { httpWebRequest.Headers = new WebHeaderCollection(); - foreach (var defaultHeader in _webClientSettings.DefaultHeaders) + foreach (var defaultHeader in _webClientOptions.DefaultHeaders) { httpWebRequest.Headers.Add(defaultHeader.Key, defaultHeader.Value); } } - httpWebRequest.CachePolicy = _webClientSettings.RequestCachePolicy; + httpWebRequest.CachePolicy = _webClientOptions.RequestCachePolicy; - httpWebRequest.Proxy = _webClientSettings.Proxy; + httpWebRequest.Proxy = _webClientOptions.Proxy; - httpWebRequest.Credentials = _webClientSettings.Credentials; + httpWebRequest.Credentials = _webClientOptions.Credentials; - #if !(NET20) - if (_webClientSettings.ClientCertificates != null) + #if NET40_OR_GREATER || NETCOREAPP + if (_webClientOptions.ClientCertificates != null) { - httpWebRequest.ClientCertificates = _webClientSettings.ClientCertificates; + httpWebRequest.ClientCertificates = _webClientOptions.ClientCertificates; } #endif #if (NET45_OR_GREATER || NETCOREAPP) - httpWebRequest.ServerCertificateValidationCallback = _webClientSettings.ServerCertificateValidationCallback; + httpWebRequest.ServerCertificateValidationCallback = _webClientOptions.ServerCertificateValidationCallback; #endif - if (_webClientSettings.ProtocolVersion != default) + if (_webClientOptions.ProtocolVersion != null) { - httpWebRequest.ProtocolVersion = _webClientSettings.ProtocolVersion; + httpWebRequest.ProtocolVersion = _webClientOptions.ProtocolVersion; } return httpWebRequest; diff --git a/src/redmine-net-api/Net/WebClient/MessageContent/StringApiRequestMessageContent.cs b/src/redmine-net-api/Net/WebClient/MessageContent/StringApiRequestMessageContent.cs index 9d02d69a..e69a5fee 100644 --- a/src/redmine-net-api/Net/WebClient/MessageContent/StringApiRequestMessageContent.cs +++ b/src/redmine-net-api/Net/WebClient/MessageContent/StringApiRequestMessageContent.cs @@ -16,6 +16,7 @@ limitations under the License. using System; using System.Text; +using Redmine.Net.Api.Internals; namespace Redmine.Net.Api.Net.WebClient.MessageContent; @@ -34,14 +35,7 @@ public StringApiRequestMessageContent(string content, string mediaType, Encoding private static byte[] GetContentByteArray(string content, Encoding encoding) { - #if NET5_0_OR_GREATER - ArgumentNullException.ThrowIfNull(content); - #else - if (content == null) - { - throw new ArgumentNullException(nameof(content)); - } - #endif + ArgumentNullThrowHelper.ThrowIfNull(content, nameof(content)); return (encoding ?? DefaultStringEncoding).GetBytes(content); } } \ No newline at end of file diff --git a/src/redmine-net-api/Net/WebClient/RedmineWebClientObsolete.cs b/src/redmine-net-api/Net/WebClient/RedmineWebClient.Obsolete.cs similarity index 100% rename from src/redmine-net-api/Net/WebClient/RedmineWebClientObsolete.cs rename to src/redmine-net-api/Net/WebClient/RedmineWebClient.Obsolete.cs diff --git a/src/redmine-net-api/RedmineManagerObsolete.cs b/src/redmine-net-api/RedmineManager.Obsolete.cs similarity index 93% rename from src/redmine-net-api/RedmineManagerObsolete.cs rename to src/redmine-net-api/RedmineManager.Obsolete.cs index c3bdfec6..2ac4a608 100644 --- a/src/redmine-net-api/RedmineManagerObsolete.cs +++ b/src/redmine-net-api/RedmineManager.Obsolete.cs @@ -57,7 +57,7 @@ public RedmineManager(string host, MimeFormat mimeFormat = MimeFormat.Xml, bool .WithHost(host) .WithSerializationType(mimeFormat) .WithVerifyServerCert(verifyServerCert) - .WithClientOptions(new RedmineWebClientOptions() + .WithWebClientOptions(new RedmineWebClientOptions() { Proxy = proxy, Scheme = scheme, @@ -91,7 +91,7 @@ public RedmineManager(string host, string apiKey, MimeFormat mimeFormat = MimeFo .WithApiKeyAuthentication(apiKey) .WithSerializationType(mimeFormat) .WithVerifyServerCert(verifyServerCert) - .WithClientOptions(new RedmineWebClientOptions() + .WithWebClientOptions(new RedmineWebClientOptions() { Proxy = proxy, Scheme = scheme, @@ -120,7 +120,7 @@ public RedmineManager(string host, string login, string password, MimeFormat mim .WithBasicAuthentication(login, password) .WithSerializationType(mimeFormat) .WithVerifyServerCert(verifyServerCert) - .WithClientOptions(new RedmineWebClientOptions() + .WithWebClientOptions(new RedmineWebClientOptions() { Proxy = proxy, Scheme = scheme, @@ -135,7 +135,7 @@ public RedmineManager(string host, string login, string password, MimeFormat mim /// /// The suffixes. /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + [Obsolete(RedmineConstants.OBSOLETE_TEXT + " It returns null.")] public static Dictionary Suffixes => null; /// @@ -385,7 +385,7 @@ public PagedResults Search(string q, int limit = DEFAULT_PAGE_SIZE_VALUE return RedmineManagerExtensions.Search(this, q, limit, offset, searchFilter); } - /// + /// /// /// /// @@ -427,14 +427,10 @@ public PagedResults Search(string q, int limit = DEFAULT_PAGE_SIZE_VALUE /// Issue issue = redmineManager.GetObject<Issue>(issueId, parameters); /// /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] + [Obsolete($"{RedmineConstants.OBSOLETE_TEXT} Use Get instead")] public T GetObject(string id, NameValueCollection parameters) where T : class, new() { - var url = RedmineApiUrls.GetFragment(id); - - var response = ApiClient.Get(url, parameters != null ? new RequestOptions { QueryString = parameters } : null); - - return response.DeserializeTo(Serializer); + return Get(id, parameters != null ? new RequestOptions { QueryString = parameters } : null); } /// @@ -494,9 +490,7 @@ public PagedResults Search(string q, int limit = DEFAULT_PAGE_SIZE_VALUE [Obsolete(RedmineConstants.OBSOLETE_TEXT)] public List GetObjects(NameValueCollection parameters = null) where T : class, new() { - var uri = RedmineApiUrls.GetListFragment(); - - return GetObjects(uri, parameters != null ? new RequestOptions { QueryString = parameters } : null); + return Get(parameters != null ? new RequestOptions { QueryString = parameters } : null); } /// @@ -508,9 +502,7 @@ public PagedResults Search(string q, int limit = DEFAULT_PAGE_SIZE_VALUE [Obsolete(RedmineConstants.OBSOLETE_TEXT)] public PagedResults GetPaginatedObjects(NameValueCollection parameters) where T : class, new() { - var url = RedmineApiUrls.GetListFragment(); - - return GetPaginatedObjects(url, parameters != null ? new RequestOptions { QueryString = parameters } : null); + return GetPaginated(parameters != null ? new RequestOptions { QueryString = parameters } : null); } /// @@ -527,7 +519,7 @@ public PagedResults Search(string q, int limit = DEFAULT_PAGE_SIZE_VALUE [Obsolete(RedmineConstants.OBSOLETE_TEXT)] public T CreateObject(T entity) where T : class, new() { - return CreateObject(entity, null); + return Create(entity); } /// @@ -554,21 +546,15 @@ public PagedResults Search(string q, int limit = DEFAULT_PAGE_SIZE_VALUE [Obsolete(RedmineConstants.OBSOLETE_TEXT)] public T CreateObject(T entity, string ownerId) where T : class, new() { - var url = RedmineApiUrls.CreateEntityFragment(ownerId); - - var payload = Serializer.Serialize(entity); - - var response = ApiClient.Create(url, payload); - - return response.DeserializeTo(Serializer); + return Create(entity, ownerId); } /// /// Updates a Redmine object. /// - /// The type of object to be update. - /// The id of the object to be update. - /// The object to be update. + /// The type of object to be updated. + /// The id of the object to be updated. + /// The object to be updated. /// The project identifier. /// /// @@ -580,11 +566,7 @@ public PagedResults Search(string q, int limit = DEFAULT_PAGE_SIZE_VALUE [Obsolete(RedmineConstants.OBSOLETE_TEXT)] public void UpdateObject(string id, T entity, string projectId = null) where T : class, new() { - var url = RedmineApiUrls.UpdateFragment(id); - - var payload = Serializer.Serialize(entity); - - ApiClient.Update(url, payload); + Update(id, entity, projectId); } /// @@ -598,9 +580,7 @@ public PagedResults Search(string q, int limit = DEFAULT_PAGE_SIZE_VALUE [Obsolete(RedmineConstants.OBSOLETE_TEXT)] public void DeleteObject(string id, NameValueCollection parameters = null) where T : class, new() { - var url = RedmineApiUrls.DeleteFragment(id); - - ApiClient.Delete(url, parameters != null ? new RequestOptions { QueryString = parameters } : null); + Delete(id, parameters != null ? new RequestOptions { QueryString = parameters } : null); } /// @@ -625,12 +605,13 @@ public virtual RedmineWebClient CreateWebClient(NameValueCollection parameters, /// The error. /// /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use WebClientSettings.ServerCertificateValidationCallback instead")] + [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use RedmineManagerOptions.ClientOptions.ServerCertificateValidationCallback instead")] public virtual bool RemoteCertValidate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors sslPolicyErrors) { const SslPolicyErrors IGNORED_ERRORS = SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNameMismatch; return (sslPolicyErrors & ~IGNORED_ERRORS) == SslPolicyErrors.None; + } } } \ No newline at end of file diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index f683dc3a..8f702eb1 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,6 +21,7 @@ limitations under the License. using System.Net; using Redmine.Net.Api.Authentication; using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; using Redmine.Net.Api.Net; using Redmine.Net.Api.Net.WebClient; using Redmine.Net.Api.Serialization; @@ -46,33 +47,34 @@ public partial class RedmineManager : IRedmineManager /// public RedmineManager(RedmineManagerOptionsBuilder optionsBuilder) { - #if NET5_0_OR_GREATER - ArgumentNullException.ThrowIfNull(optionsBuilder); - #else - if (optionsBuilder == null) - { - throw new ArgumentNullException(nameof(optionsBuilder)); - } - #endif + ArgumentNullThrowHelper.ThrowIfNull(optionsBuilder, nameof(optionsBuilder)); + _redmineManagerOptions = optionsBuilder.Build(); + #if NET45_OR_GREATER if (_redmineManagerOptions.VerifyServerCert) { _redmineManagerOptions.ClientOptions.ServerCertificateValidationCallback = RemoteCertValidate; } + #endif - Serializer = _redmineManagerOptions.Serializer; + if (_redmineManagerOptions.ClientOptions is RedmineWebClientOptions) + { + #pragma warning disable SYSLIB0014 + _redmineManagerOptions.ClientOptions.SecurityProtocolType ??= ServicePointManager.SecurityProtocol; + #pragma warning restore SYSLIB0014 + } + Serializer = _redmineManagerOptions.Serializer; Host = _redmineManagerOptions.BaseAddress.ToString(); PageSize = _redmineManagerOptions.PageSize; Format = Serializer.Format; Scheme = _redmineManagerOptions.BaseAddress.Scheme; Proxy = _redmineManagerOptions.ClientOptions.Proxy; Timeout = _redmineManagerOptions.ClientOptions.Timeout; - MimeFormat = RedmineConstants.XML.Equals(Serializer.Format, StringComparison.OrdinalIgnoreCase) ? MimeFormat.Xml : MimeFormat.Json; - - _redmineManagerOptions.ClientOptions.SecurityProtocolType ??= ServicePointManager.SecurityProtocol; - - SecurityProtocolType = _redmineManagerOptions.ClientOptions.SecurityProtocolType.Value; + MimeFormat = RedmineConstants.XML.Equals(Serializer.Format, StringComparison.Ordinal) + ? MimeFormat.Xml + : MimeFormat.Json; + SecurityProtocolType = _redmineManagerOptions.ClientOptions.SecurityProtocolType.GetValueOrDefault(); if (_redmineManagerOptions.Authentication is RedmineApiKeyAuthentication) { @@ -80,7 +82,22 @@ public RedmineManager(RedmineManagerOptionsBuilder optionsBuilder) } RedmineApiUrls = new RedmineApiUrls(Serializer.Format); - ApiClient = new InternalRedmineApiWebClient(_redmineManagerOptions); + #if NET45_OR_GREATER || NETCOREAPP + if (_redmineManagerOptions.ClientOptions is RedmineWebClientOptions) + { + ApiClient = _redmineManagerOptions.ClientFunc != null + ? new InternalRedmineApiWebClient(_redmineManagerOptions.ClientFunc, _redmineManagerOptions.Authentication, _redmineManagerOptions.Serializer) + : new InternalRedmineApiWebClient(_redmineManagerOptions); + } + else + { + + } + #else + ApiClient = _redmineManagerOptions.ClientFunc != null + ? new InternalRedmineApiWebClient(_redmineManagerOptions.ClientFunc, _redmineManagerOptions.Authentication, _redmineManagerOptions.Serializer) + : new InternalRedmineApiWebClient(_redmineManagerOptions); + #endif } /// @@ -98,7 +115,7 @@ public int Count(RequestOptions requestOptions = null) requestOptions.QueryString = requestOptions.QueryString.AddPagingParameters(PAGE_SIZE, OFFSET); - var tempResult = GetPaginatedObjects(requestOptions.QueryString); + var tempResult = GetPaginated(requestOptions); if (tempResult != null) { @@ -125,7 +142,7 @@ public List Get(RequestOptions requestOptions = null) { var uri = RedmineApiUrls.GetListFragment(); - return GetObjects(uri, requestOptions); + return GetInternal(uri, requestOptions); } /// @@ -134,7 +151,7 @@ public PagedResults GetPaginated(RequestOptions requestOptions = null) { var url = RedmineApiUrls.GetListFragment(); - return GetPaginatedObjects(url, requestOptions); + return GetPaginatedInternal(url, requestOptions); } /// @@ -171,9 +188,9 @@ public void Delete(string id, RequestOptions requestOptions = null) } /// - public Upload UploadFile(byte[] data) + public Upload UploadFile(byte[] data, string fileName = null) { - var url = RedmineApiUrls.UploadFragment(); + var url = RedmineApiUrls.UploadFragment(fileName); var response = ApiClient.Upload(url, data); @@ -195,7 +212,7 @@ public byte[] DownloadFile(string address) /// /// /// - internal List GetObjects(string uri, RequestOptions requestOptions = null) + internal List GetInternal(string uri, RequestOptions requestOptions = null) where T : class, new() { int pageSize = 0, offset = 0; @@ -228,7 +245,7 @@ internal List GetObjects(string uri, RequestOptions requestOptions = null) { requestOptions.QueryString.Set(RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)); - var tempResult = GetPaginatedObjects(uri, requestOptions); + var tempResult = GetPaginatedInternal(uri, requestOptions); totalCount = isLimitSet ? pageSize : tempResult.TotalItems; @@ -250,7 +267,7 @@ internal List GetObjects(string uri, RequestOptions requestOptions = null) } else { - var result = GetPaginatedObjects(uri, requestOptions); + var result = GetPaginatedInternal(uri, requestOptions); if (result?.Items != null) { return new List(result.Items); @@ -267,7 +284,7 @@ internal List GetObjects(string uri, RequestOptions requestOptions = null) /// /// /// - internal PagedResults GetPaginatedObjects(string uri = null, RequestOptions requestOptions = null) + internal PagedResults GetPaginatedInternal(string uri = null, RequestOptions requestOptions = null) where T : class, new() { uri = uri.IsNullOrWhiteSpace() ? RedmineApiUrls.GetListFragment() : uri; diff --git a/src/redmine-net-api/RedmineManagerAsync.cs b/src/redmine-net-api/RedmineManagerAsync.cs index 20485248..4039f028 100644 --- a/src/redmine-net-api/RedmineManagerAsync.cs +++ b/src/redmine-net-api/RedmineManagerAsync.cs @@ -208,11 +208,7 @@ public async Task UpdateAsync(string id, T entity, RequestOptions requestOpti var payload = Serializer.Serialize(entity); - #if NET7_0_OR_GREATER - payload = ReplaceEndingsRegex().Replace(payload, CRLR); - #else payload = Regex.Replace(payload, "\r\n|\r|\n",CRLR); - #endif await ApiClient.UpdateAsync(url, payload, requestOptions, cancellationToken: cancellationToken).ConfigureAwait(false); } @@ -227,9 +223,9 @@ public async Task DeleteAsync(string id, RequestOptions requestOptions = null } /// - public async Task UploadFileAsync(byte[] data, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + public async Task UploadFileAsync(byte[] data, string fileName = null, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) { - var url = RedmineApiUrls.UploadFragment(); + var url = RedmineApiUrls.UploadFragment(fileName); var response = await ApiClient.UploadFileAsync(url, data,requestOptions , cancellationToken: cancellationToken).ConfigureAwait(false); @@ -242,11 +238,6 @@ public async Task DownloadFileAsync(string address, RequestOptions reque var response = await ApiClient.DownloadAsync(address, requestOptions,cancellationToken: cancellationToken).ConfigureAwait(false); return response.Content; } - - #if NET7_0_OR_GREATER - [GeneratedRegex(@"\r\n|\r|\n")] - private static partial Regex ReplaceEndingsRegex(); - #endif private const int MAX_CONCURRENT_TASKS = 3; diff --git a/src/redmine-net-api/RedmineManagerOptionsBuilder.cs b/src/redmine-net-api/RedmineManagerOptionsBuilder.cs index 0b6dd5d5..fa7f2a13 100644 --- a/src/redmine-net-api/RedmineManagerOptionsBuilder.cs +++ b/src/redmine-net-api/RedmineManagerOptionsBuilder.cs @@ -32,10 +32,10 @@ public sealed class RedmineManagerOptionsBuilder { private enum ClientType { - None, WebClient, + HttpClient, } - private ClientType _clientType = ClientType.None; + private ClientType _clientType = ClientType.WebClient; /// /// @@ -93,7 +93,7 @@ public RedmineManagerOptionsBuilder WithSerializationType(SerializationType seri } /// - /// + /// Gets the current serialization type /// public SerializationType SerializationType { get; private set; } @@ -132,15 +132,7 @@ public RedmineManagerOptionsBuilder WithBasicAuthentication(string login, string /// public RedmineManagerOptionsBuilder WithWebClient(Func clientFunc) { - if (clientFunc != null) - { - _clientType = ClientType.WebClient; - } - - if (clientFunc == null && _clientType == ClientType.WebClient) - { - _clientType = ClientType.None; - } + _clientType = ClientType.WebClient; this.ClientFunc = clientFunc; return this; } @@ -155,8 +147,9 @@ public RedmineManagerOptionsBuilder WithWebClient(Func clientFunc) /// /// /// - public RedmineManagerOptionsBuilder WithClientOptions(IRedmineApiClientOptions clientOptions) + public RedmineManagerOptionsBuilder WithWebClientOptions(IRedmineApiClientOptions clientOptions) { + _clientType = ClientType.WebClient; this.ClientOptions = clientOptions; return this; } @@ -199,8 +192,30 @@ internal RedmineManagerOptionsBuilder WithVerifyServerCert(bool verifyServerCert /// internal RedmineManagerOptions Build() { - ClientOptions ??= new RedmineWebClientOptions(); - + const string defaultUserAgent = "Redmine.Net.Api.Net"; + var defaultDecompressionFormat = + #if NETFRAMEWORK + DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.None; + #else + DecompressionMethods.All; + #endif + #if NET45_OR_GREATER || NETCOREAPP + ClientOptions ??= _clientType switch + { + ClientType.WebClient => new RedmineWebClientOptions() + { + UserAgent = defaultUserAgent, + DecompressionFormat = defaultDecompressionFormat, + }, + _ => throw new ArgumentOutOfRangeException() + }; + #else + ClientOptions ??= new RedmineWebClientOptions() + { + UserAgent = defaultUserAgent, + DecompressionFormat = defaultDecompressionFormat, + }; + #endif var baseAddress = CreateRedmineUri(Host, ClientOptions.Scheme); var options = new RedmineManagerOptions() @@ -217,6 +232,8 @@ internal RedmineManagerOptions Build() return options; } + private static readonly char[] DotCharArray = ['.']; + internal static void EnsureDomainNameIsValid(string domainName) { if (domainName.IsNullOrWhiteSpace()) @@ -229,7 +246,7 @@ internal static void EnsureDomainNameIsValid(string domainName) throw new RedmineException("Domain name cannot be longer than 255 characters."); } - var labels = domainName.Split('.'); + var labels = domainName.Split(DotCharArray); if (labels.Length == 1) { throw new RedmineException("Domain name is not valid."); @@ -327,7 +344,6 @@ internal static Uri CreateRedmineUri(string host, string scheme = null) } else { - if (!IsSchemaHttpOrHttps(scheme)) { throw new RedmineException("Invalid host scheme. Only HTTP and HTTPS are supported."); diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 55dc139d..15c2aed6 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -4,7 +4,7 @@ Redmine.Net.Api redmine-net-api - net20;net40;net45;net451;net452;net46;net461;net462;net47;net471;net472;net48;net481;net6.0;net7.0;net8.0 + net9.0;net8.0;net7.0;net6.0;net5.0;net481;net48;net472;net471;net47;net462;net461;net46;net452;net451;net45;net40;net20 false True true @@ -79,6 +79,7 @@ + From 21a6bafe20254ec23b3f40a0e9333a5932116c70 Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 29 Jan 2025 23:47:44 +0200 Subject: [PATCH 418/549] Fix #369 (#370) --- ...bsolete.cs => IRedmineManager.Obsolete.cs} | 4 +-- .../WebClient/InternalRedmineApiWebClient.cs | 6 ++-- .../Net/WebClient/InternalWebClient.cs | 2 +- .../Net/WebClient/RedmineWebClientOptions.cs | 2 +- src/redmine-net-api/RedmineManager.cs | 26 +++++++------- src/redmine-net-api/RedmineManagerOptions.cs | 3 +- .../RedmineManagerOptionsBuilder.cs | 34 +++++++++++++++---- 7 files changed, 49 insertions(+), 28 deletions(-) rename src/redmine-net-api/{IRedmineManagerObsolete.cs => IRedmineManager.Obsolete.cs} (98%) diff --git a/src/redmine-net-api/IRedmineManagerObsolete.cs b/src/redmine-net-api/IRedmineManager.Obsolete.cs similarity index 98% rename from src/redmine-net-api/IRedmineManagerObsolete.cs rename to src/redmine-net-api/IRedmineManager.Obsolete.cs index 553627f9..8704367c 100644 --- a/src/redmine-net-api/IRedmineManagerObsolete.cs +++ b/src/redmine-net-api/IRedmineManager.Obsolete.cs @@ -44,7 +44,7 @@ public partial interface IRedmineManager /// /// Maximum page-size when retrieving complete object lists /// - /// By default only 25 results can be retrieved per request. Maximum is 100. To change the maximum value set + /// By default, only 25 results can be retrieved per request. Maximum is 100. To change the maximum value set /// in your Settings -> General, "Objects per page options".By adding (for instance) 9999 there would make you /// able to get that many results per request. /// @@ -56,7 +56,7 @@ public partial interface IRedmineManager int PageSize { get; set; } /// - /// As of Redmine 2.2.0 you can impersonate user setting user login (eg. jsmith). This only works when using the API + /// As of Redmine 2.2.0 you can impersonate user setting user login (e.g. jsmith). This only works when using the API /// with an administrator account, this header will be ignored when using the API with a regular user account. /// /// diff --git a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs index a48b5aa3..51b1ad7c 100644 --- a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs +++ b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs @@ -42,7 +42,7 @@ internal sealed class InternalRedmineApiWebClient : IRedmineApiClient public InternalRedmineApiWebClient(RedmineManagerOptions redmineManagerOptions) : this(() => new InternalWebClient(redmineManagerOptions), redmineManagerOptions.Authentication, redmineManagerOptions.Serializer) { - ConfigureServicePointManager(redmineManagerOptions.ClientOptions); + ConfigureServicePointManager(redmineManagerOptions.WebClientOptions); } public InternalRedmineApiWebClient( @@ -55,9 +55,9 @@ public InternalRedmineApiWebClient( _serializer = serializer; } - private static void ConfigureServicePointManager(IRedmineApiClientOptions options) + private static void ConfigureServicePointManager(IRedmineWebClientOptions webClientOptions) { - if (options is not IRedmineWebClientOptions webClientOptions) + if (webClientOptions == null) { return; } diff --git a/src/redmine-net-api/Net/WebClient/InternalWebClient.cs b/src/redmine-net-api/Net/WebClient/InternalWebClient.cs index 87c7c602..dbcdf193 100644 --- a/src/redmine-net-api/Net/WebClient/InternalWebClient.cs +++ b/src/redmine-net-api/Net/WebClient/InternalWebClient.cs @@ -27,7 +27,7 @@ internal sealed class InternalWebClient : System.Net.WebClient #pragma warning disable SYSLIB0014 public InternalWebClient(RedmineManagerOptions redmineManagerOptions) { - _webClientOptions = redmineManagerOptions.ClientOptions as IRedmineWebClientOptions; + _webClientOptions = redmineManagerOptions.WebClientOptions; BaseAddress = redmineManagerOptions.BaseAddress.ToString(); } #pragma warning restore SYSLIB0014 diff --git a/src/redmine-net-api/Net/WebClient/RedmineWebClientOptions.cs b/src/redmine-net-api/Net/WebClient/RedmineWebClientOptions.cs index 0a2953e8..cd76daf1 100644 --- a/src/redmine-net-api/Net/WebClient/RedmineWebClientOptions.cs +++ b/src/redmine-net-api/Net/WebClient/RedmineWebClientOptions.cs @@ -25,7 +25,7 @@ namespace Redmine.Net.Api.Net.WebClient; /// /// /// -public sealed class RedmineWebClientOptions: IRedmineApiClientOptions +public sealed class RedmineWebClientOptions: IRedmineWebClientOptions { /// /// diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index 8f702eb1..49cf2ee0 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -53,37 +53,37 @@ public RedmineManager(RedmineManagerOptionsBuilder optionsBuilder) #if NET45_OR_GREATER if (_redmineManagerOptions.VerifyServerCert) { - _redmineManagerOptions.ClientOptions.ServerCertificateValidationCallback = RemoteCertValidate; + _redmineManagerOptions.WebClientOptions.ServerCertificateValidationCallback = RemoteCertValidate; } #endif - if (_redmineManagerOptions.ClientOptions is RedmineWebClientOptions) + if (_redmineManagerOptions.WebClientOptions is RedmineWebClientOptions) { + Proxy = _redmineManagerOptions.WebClientOptions.Proxy; + Timeout = _redmineManagerOptions.WebClientOptions.Timeout; + SecurityProtocolType = _redmineManagerOptions.WebClientOptions.SecurityProtocolType.GetValueOrDefault(); #pragma warning disable SYSLIB0014 - _redmineManagerOptions.ClientOptions.SecurityProtocolType ??= ServicePointManager.SecurityProtocol; + _redmineManagerOptions.WebClientOptions.SecurityProtocolType ??= ServicePointManager.SecurityProtocol; #pragma warning restore SYSLIB0014 } + if (_redmineManagerOptions.Authentication is RedmineApiKeyAuthentication) + { + ApiKey = _redmineManagerOptions.Authentication.Token; + } + Serializer = _redmineManagerOptions.Serializer; Host = _redmineManagerOptions.BaseAddress.ToString(); PageSize = _redmineManagerOptions.PageSize; - Format = Serializer.Format; Scheme = _redmineManagerOptions.BaseAddress.Scheme; - Proxy = _redmineManagerOptions.ClientOptions.Proxy; - Timeout = _redmineManagerOptions.ClientOptions.Timeout; + Format = Serializer.Format; MimeFormat = RedmineConstants.XML.Equals(Serializer.Format, StringComparison.Ordinal) ? MimeFormat.Xml : MimeFormat.Json; - SecurityProtocolType = _redmineManagerOptions.ClientOptions.SecurityProtocolType.GetValueOrDefault(); - - if (_redmineManagerOptions.Authentication is RedmineApiKeyAuthentication) - { - ApiKey = _redmineManagerOptions.Authentication.Token; - } RedmineApiUrls = new RedmineApiUrls(Serializer.Format); #if NET45_OR_GREATER || NETCOREAPP - if (_redmineManagerOptions.ClientOptions is RedmineWebClientOptions) + if (_redmineManagerOptions.WebClientOptions is RedmineWebClientOptions) { ApiClient = _redmineManagerOptions.ClientFunc != null ? new InternalRedmineApiWebClient(_redmineManagerOptions.ClientFunc, _redmineManagerOptions.Authentication, _redmineManagerOptions.Serializer) diff --git a/src/redmine-net-api/RedmineManagerOptions.cs b/src/redmine-net-api/RedmineManagerOptions.cs index aadf9916..a0928c44 100644 --- a/src/redmine-net-api/RedmineManagerOptions.cs +++ b/src/redmine-net-api/RedmineManagerOptions.cs @@ -18,6 +18,7 @@ limitations under the License. using System.Net; using Redmine.Net.Api.Authentication; using Redmine.Net.Api.Net; +using Redmine.Net.Api.Net.WebClient; using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api @@ -60,7 +61,7 @@ internal sealed class RedmineManagerOptions /// /// Gets or sets the settings for configuring the Redmine web client. /// - public IRedmineApiClientOptions ClientOptions { get; init; } + public IRedmineWebClientOptions WebClientOptions { get; init; } /// /// Gets or sets the version of the Redmine server to which this client will connect. diff --git a/src/redmine-net-api/RedmineManagerOptionsBuilder.cs b/src/redmine-net-api/RedmineManagerOptionsBuilder.cs index fa7f2a13..417bcdec 100644 --- a/src/redmine-net-api/RedmineManagerOptionsBuilder.cs +++ b/src/redmine-net-api/RedmineManagerOptionsBuilder.cs @@ -69,7 +69,6 @@ public RedmineManagerOptionsBuilder WithHost(string baseAddress) /// public string Host { get; private set; } - /// /// /// @@ -147,17 +146,38 @@ public RedmineManagerOptionsBuilder WithWebClient(Func clientFunc) /// /// /// + [Obsolete("Use WithWebClientOptions(IRedmineWebClientOptions clientOptions) instead.")] public RedmineManagerOptionsBuilder WithWebClientOptions(IRedmineApiClientOptions clientOptions) + { + return WithWebClientOptions((IRedmineWebClientOptions)clientOptions); + } + + /// + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithWebClientOptions(IRedmineWebClientOptions clientOptions) { _clientType = ClientType.WebClient; - this.ClientOptions = clientOptions; + this.WebClientOptions = clientOptions; return this; } /// /// /// - public IRedmineApiClientOptions ClientOptions { get; private set; } + [Obsolete("Use WebClientOptions instead.")] + public IRedmineApiClientOptions ClientOptions + { + get => WebClientOptions; + private set { } + } + + /// + /// + /// + public IRedmineWebClientOptions WebClientOptions { get; private set; } /// /// @@ -200,7 +220,7 @@ internal RedmineManagerOptions Build() DecompressionMethods.All; #endif #if NET45_OR_GREATER || NETCOREAPP - ClientOptions ??= _clientType switch + WebClientOptions ??= _clientType switch { ClientType.WebClient => new RedmineWebClientOptions() { @@ -210,13 +230,13 @@ internal RedmineManagerOptions Build() _ => throw new ArgumentOutOfRangeException() }; #else - ClientOptions ??= new RedmineWebClientOptions() + WebClientOptions ??= new RedmineWebClientOptions() { UserAgent = defaultUserAgent, DecompressionFormat = defaultDecompressionFormat, }; #endif - var baseAddress = CreateRedmineUri(Host, ClientOptions.Scheme); + var baseAddress = CreateRedmineUri(Host, WebClientOptions.Scheme); var options = new RedmineManagerOptions() { @@ -226,7 +246,7 @@ internal RedmineManagerOptions Build() Serializer = RedmineSerializerFactory.CreateSerializer(SerializationType), RedmineVersion = Version, Authentication = Authentication ?? new RedmineNoAuthentication(), - ClientOptions = ClientOptions + WebClientOptions = WebClientOptions }; return options; From 6b94837ab4891c66f75cc0ddd12c69f330bf67c7 Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 30 Mar 2025 01:27:24 +0200 Subject: [PATCH 419/549] Fix #371 --- src/redmine-net-api/Net/RedmineApiUrls.cs | 60 ++++++++++++++++++- .../Bugs/RedmineApi-371.cs | 30 ++++++++++ 2 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 tests/redmine-net-api.Tests/Bugs/RedmineApi-371.cs diff --git a/src/redmine-net-api/Net/RedmineApiUrls.cs b/src/redmine-net-api/Net/RedmineApiUrls.cs index 60788877..493bfa6a 100644 --- a/src/redmine-net-api/Net/RedmineApiUrls.cs +++ b/src/redmine-net-api/Net/RedmineApiUrls.cs @@ -112,6 +112,27 @@ public string CreateEntityFragment(string ownerId = null) { var type = typeof(T); + return CreateEntityFragment(type, ownerId); + } + public string CreateEntityFragment(RequestOptions requestOptions) + { + var type = typeof(T); + + return CreateEntityFragment(type, requestOptions); + } + internal string CreateEntityFragment(Type type, RequestOptions requestOptions) + { + string ownerId = null; + if (requestOptions is { QueryString: not null }) + { + ownerId = requestOptions.QueryString.Get(RedmineKeys.PROJECT_ID) ?? + requestOptions.QueryString.Get(RedmineKeys.ISSUE_ID); + } + + return CreateEntityFragment(type, ownerId); + } + internal string CreateEntityFragment(Type type, string ownerId = null) + { if (type == typeof(Version) || type == typeof(IssueCategory) || type == typeof(ProjectMembership)) { return ProjectParentFragment(ownerId, TypeUrlFragments[type]); @@ -129,7 +150,7 @@ public string CreateEntityFragment(string ownerId = null) if (type == typeof(Upload)) { - return RedmineKeys.UPLOADS; + return $"{RedmineKeys.UPLOADS}.{Format}"; } if (type == typeof(Attachment) || type == typeof(Attachments)) @@ -144,6 +165,10 @@ public string CreateEntityFragment(string ownerId = null) { var type = typeof(T); + return GetFragment(type, id); + } + internal string GetFragment(Type type, string id) + { return $"{TypeFragment(TypeUrlFragments, type)}/{id}.{Format}"; } @@ -151,6 +176,10 @@ public string PatchFragment(string ownerId) { var type = typeof(T); + return PatchFragment(type, ownerId); + } + internal string PatchFragment(Type type, string ownerId) + { if (type == typeof(Attachment) || type == typeof(Attachments)) { return IssueAttachmentFragment(ownerId); @@ -163,6 +192,10 @@ public string DeleteFragment(string id) { var type = typeof(T); + return DeleteFragment(type, id); + } + internal string DeleteFragment(Type type,string id) + { return $"{TypeFragment(TypeUrlFragments, type)}/{id}.{Format}"; } @@ -170,6 +203,10 @@ public string UpdateFragment(string id) { var type = typeof(T); + return UpdateFragment(type, id); + } + internal string UpdateFragment(Type type, string id) + { return $"{TypeFragment(TypeUrlFragments, type)}/{id}.{Format}"; } @@ -179,8 +216,27 @@ public string UpdateFragment(string id) return GetListFragment(type, ownerId); } + + public string GetListFragment(RequestOptions requestOptions) where T : class, new() + { + var type = typeof(T); + + return GetListFragment(type, requestOptions); + } + + internal string GetListFragment(Type type, RequestOptions requestOptions) + { + string ownerId = null; + if (requestOptions is { QueryString: not null }) + { + ownerId = requestOptions.QueryString.Get(RedmineKeys.PROJECT_ID) ?? + requestOptions.QueryString.Get(RedmineKeys.ISSUE_ID); + } + + return GetListFragment(type, ownerId); + } - public string GetListFragment(Type type, string ownerId = null) + internal string GetListFragment(Type type, string ownerId = null) { if (type == typeof(Version) || type == typeof(IssueCategory) || type == typeof(ProjectMembership)) { diff --git a/tests/redmine-net-api.Tests/Bugs/RedmineApi-371.cs b/tests/redmine-net-api.Tests/Bugs/RedmineApi-371.cs new file mode 100644 index 00000000..c77ac1c1 --- /dev/null +++ b/tests/redmine-net-api.Tests/Bugs/RedmineApi-371.cs @@ -0,0 +1,30 @@ +using System.Collections.Specialized; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Net; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Bugs; + +public sealed class RedmineApi371 : IClassFixture +{ + private readonly RedmineFixture _fixture; + + public RedmineApi371(RedmineFixture fixture) + { + _fixture = fixture; + } + + [Fact] + public void Should_Return_IssueCategories() + { + var result = _fixture.RedmineManager.Get(new RequestOptions() + { + QueryString = new NameValueCollection() + { + { "project_id", 1.ToInvariantString() } + } + }); + } +} \ No newline at end of file From 406fee2344bdba5f3939510d80b5802df72d6e01 Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 30 Mar 2025 14:09:28 +0300 Subject: [PATCH 420/549] [Types] New Properties (#373) * [Version] Write wiki page title * [MyAccount] Write first name, last name & email * [Upload] Add Id * [Role] - Add Issue, time & user visibility * [New] TrackerCoreField * [Tracker] Enable standard fields * [Journal] Add Updated[On|By] * [CustomField] Add description * [User] Add Avatar URL * [Membership] Fetch Group & User * [IssuePriority] Add IsActive * [New] Document category --- src/redmine-net-api/RedmineKeys.cs | 27 ++- src/redmine-net-api/Types/CustomField.cs | 10 + src/redmine-net-api/Types/DocumentCategory.cs | 177 ++++++++++++++++++ src/redmine-net-api/Types/IssuePriority.cs | 11 +- src/redmine-net-api/Types/Journal.cs | 23 ++- src/redmine-net-api/Types/Membership.cs | 24 ++- src/redmine-net-api/Types/MyAccount.cs | 24 ++- src/redmine-net-api/Types/Role.cs | 14 +- src/redmine-net-api/Types/Tracker.cs | 8 + src/redmine-net-api/Types/TrackerCoreField.cs | 129 +++++++++++++ src/redmine-net-api/Types/Upload.cs | 7 + src/redmine-net-api/Types/User.cs | 10 + src/redmine-net-api/Types/Version.cs | 11 +- 13 files changed, 458 insertions(+), 17 deletions(-) create mode 100644 src/redmine-net-api/Types/DocumentCategory.cs create mode 100644 src/redmine-net-api/Types/TrackerCoreField.cs diff --git a/src/redmine-net-api/RedmineKeys.cs b/src/redmine-net-api/RedmineKeys.cs index 8cc70a16..996b7142 100644 --- a/src/redmine-net-api/RedmineKeys.cs +++ b/src/redmine-net-api/RedmineKeys.cs @@ -59,6 +59,10 @@ public static class RedmineKeys /// /// /// + public const string ARCHIVE = "archive"; + /// + /// + /// public const string ASSIGNED_TO = "assigned_to"; /// /// @@ -84,6 +88,11 @@ public static class RedmineKeys /// /// public const string AUTH_SOURCE_ID = "auth_source_id"; + + /// + /// + /// + public const string AVATAR_URL = "avatar_url"; /// /// /// @@ -224,6 +233,10 @@ public static class RedmineKeys /// /// /// + public const string DOCUMENT_CATEGORY = "document_category"; + /// + /// + /// public const string DOCUMENTS = "documents"; /// /// @@ -248,6 +261,10 @@ public static class RedmineKeys /// /// /// + public const string ENABLED_STANDARD_FIELDS = "enabled_standard_fields"; + /// + /// + /// public const string ENUMERATION_ISSUE_PRIORITIES = "enumerations/issue_priorities"; /// /// @@ -268,6 +285,10 @@ public static class RedmineKeys /// /// /// + public const string FIELD = "field"; + /// + /// + /// public const string FIELD_FORMAT = "field_format"; /// /// @@ -772,6 +793,10 @@ public static class RedmineKeys /// /// /// + public const string UPDATED_BY = "updated_by"; + /// + /// + /// public const string UPLOAD = "upload"; /// /// @@ -849,7 +874,5 @@ public static class RedmineKeys /// /// public const string WIKI_PAGES = "wiki_pages"; - - } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/CustomField.cs b/src/redmine-net-api/Types/CustomField.cs index 28573116..ad80e656 100644 --- a/src/redmine-net-api/Types/CustomField.cs +++ b/src/redmine-net-api/Types/CustomField.cs @@ -38,6 +38,11 @@ public sealed class CustomField : IdentifiableName, IEquatable /// /// public string CustomizedType { get; internal set; } + + /// + /// Added in Redmine 5.1.0 version + /// + public string Description { get; internal set; } /// /// @@ -125,6 +130,7 @@ public override void ReadXml(XmlReader reader) { case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; case RedmineKeys.CUSTOMIZED_TYPE: CustomizedType = reader.ReadElementContentAsString(); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadElementContentAsString(); break; case RedmineKeys.DEFAULT_VALUE: DefaultValue = reader.ReadElementContentAsString(); break; case RedmineKeys.FIELD_FORMAT: FieldFormat = reader.ReadElementContentAsString(); break; case RedmineKeys.IS_FILTER: IsFilter = reader.ReadElementContentAsBoolean(); break; @@ -170,6 +176,7 @@ public override void ReadJson(JsonReader reader) case RedmineKeys.ID: Id = reader.ReadAsInt(); break; case RedmineKeys.CUSTOMIZED_TYPE: CustomizedType = reader.ReadAsString(); break; case RedmineKeys.DEFAULT_VALUE: DefaultValue = reader.ReadAsString(); break; + case RedmineKeys.DESCRIPTION: Description = reader.ReadAsString(); break; case RedmineKeys.FIELD_FORMAT: FieldFormat = reader.ReadAsString(); break; case RedmineKeys.IS_FILTER: IsFilter = reader.ReadAsBool(); break; case RedmineKeys.IS_REQUIRED: IsRequired = reader.ReadAsBool(); break; @@ -207,6 +214,7 @@ public bool Equals(CustomField other) && Searchable == other.Searchable && Visible == other.Visible && string.Equals(CustomizedType,other.CustomizedType, StringComparison.OrdinalIgnoreCase) + && string.Equals(Description,other.Description, StringComparison.OrdinalIgnoreCase) && string.Equals(DefaultValue,other.DefaultValue, StringComparison.OrdinalIgnoreCase) && string.Equals(FieldFormat,other.FieldFormat, StringComparison.OrdinalIgnoreCase) && MaxLength == other.MaxLength @@ -243,6 +251,7 @@ public override int GetHashCode() hashCode = HashCodeHelper.GetHashCode(Id, hashCode); hashCode = HashCodeHelper.GetHashCode(IsFilter, hashCode); hashCode = HashCodeHelper.GetHashCode(IsRequired, hashCode); + hashCode = HashCodeHelper.GetHashCode(Description, hashCode); hashCode = HashCodeHelper.GetHashCode(Multiple, hashCode); hashCode = HashCodeHelper.GetHashCode(Searchable, hashCode); hashCode = HashCodeHelper.GetHashCode(Visible, hashCode); @@ -264,6 +273,7 @@ public override int GetHashCode() private string DebuggerDisplay => $@"[{nameof(CustomField)}: {ToString()} , CustomizedType={CustomizedType} +, Description={Description} , FieldFormat={FieldFormat} , Regexp={Regexp} , MinLength={MinLength?.ToString(CultureInfo.InvariantCulture)} diff --git a/src/redmine-net-api/Types/DocumentCategory.cs b/src/redmine-net-api/Types/DocumentCategory.cs new file mode 100644 index 00000000..490bfa13 --- /dev/null +++ b/src/redmine-net-api/Types/DocumentCategory.cs @@ -0,0 +1,177 @@ +/* + Copyright 2011 - 2023 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System; +using System.Diagnostics; +using System.Globalization; +using System.Xml; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; + +namespace Redmine.Net.Api.Types +{ + /// + /// Availability 2.2 + /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [XmlRoot(RedmineKeys.DOCUMENT_CATEGORY)] + public sealed class DocumentCategory : IdentifiableName, IEquatable + { + /// + /// + /// + public DocumentCategory() { } + + internal DocumentCategory(int id, string name) + : base(id, name) + { + } + + #region Properties + /// + /// + /// + public bool IsDefault { get; internal set; } + + /// + /// + /// + public bool IsActive { get; internal set; } + #endregion + + #region Implementation of IXmlSerializable + + /// + /// Generates an object from its XML representation. + /// + /// The stream from which the object is deserialized. + public override void ReadXml(XmlReader reader) + { + reader.Read(); + while (!reader.EOF) + { + if (reader.IsEmptyElement && !reader.HasAttributes) + { + reader.Read(); + continue; + } + + switch (reader.Name) + { + case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.IS_DEFAULT: IsDefault = reader.ReadElementContentAsBoolean(); break; + case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; + case RedmineKeys.ACTIVE: IsActive = reader.ReadElementContentAsBoolean(); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + public override void WriteXml(XmlWriter writer) { } + + #endregion + + #region Implementation of IJsonSerialization + /// + /// + /// + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.IS_DEFAULT: IsDefault = reader.ReadAsBool(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + case RedmineKeys.ACTIVE: IsActive = reader.ReadAsBool(); break; + default: reader.Read(); break; + } + } + } + #endregion + + #region Implementation of IEquatable + + /// + /// + /// + /// + /// + public bool Equals(DocumentCategory other) + { + if (other == null) return false; + + return Id == other.Id && Name == other.Name && IsDefault == other.IsDefault && IsActive == other.IsActive; + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as DocumentCategory); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + unchecked + { + var hashCode = 13; + hashCode = HashCodeHelper.GetHashCode(Id, hashCode); + hashCode = HashCodeHelper.GetHashCode(Name, hashCode); + hashCode = HashCodeHelper.GetHashCode(IsDefault, hashCode); + hashCode = HashCodeHelper.GetHashCode(IsActive, hashCode); + return hashCode; + } + } + + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[{nameof(TimeEntryActivity)}:{ToString()}, IsDefault={IsDefault.ToString(CultureInfo.InvariantCulture)}, IsActive={IsActive.ToString(CultureInfo.InvariantCulture)}]"; + + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/IssuePriority.cs b/src/redmine-net-api/Types/IssuePriority.cs index 80539b90..081e41c4 100644 --- a/src/redmine-net-api/Types/IssuePriority.cs +++ b/src/redmine-net-api/Types/IssuePriority.cs @@ -37,6 +37,10 @@ public sealed class IssuePriority : IdentifiableName, IEquatable /// /// public bool IsDefault { get; internal set; } + /// + /// + /// + public bool IsActive { get; internal set; } #endregion #region Implementation of IXmlSerializable @@ -59,6 +63,7 @@ public override void ReadXml(XmlReader reader) switch (reader.Name) { case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.ACTIVE: IsActive = reader.ReadElementContentAsBoolean(); break; case RedmineKeys.IS_DEFAULT: IsDefault = reader.ReadElementContentAsBoolean(); break; case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; default: reader.Read(); break; @@ -90,6 +95,7 @@ public override void ReadJson(JsonReader reader) switch (reader.Value) { case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.ACTIVE: IsActive = reader.ReadAsBool(); break; case RedmineKeys.IS_DEFAULT: IsDefault = reader.ReadAsBool(); break; case RedmineKeys.NAME: Name = reader.ReadAsString(); break; default: reader.Read(); break; @@ -108,7 +114,7 @@ public bool Equals(IssuePriority other) { if (other == null) return false; - return Id == other.Id && Name == other.Name && IsDefault == other.IsDefault; + return Id == other.Id && Name == other.Name && IsDefault == other.IsDefault && IsActive == other.IsActive; } /// @@ -136,6 +142,7 @@ public override int GetHashCode() hashCode = HashCodeHelper.GetHashCode(Id, hashCode); hashCode = HashCodeHelper.GetHashCode(Name, hashCode); hashCode = HashCodeHelper.GetHashCode(IsDefault, hashCode); + hashCode = HashCodeHelper.GetHashCode(IsActive, hashCode); return hashCode; } } @@ -145,7 +152,7 @@ public override int GetHashCode() /// /// /// - private string DebuggerDisplay => $"[IssuePriority: {ToString()}, IsDefault={IsDefault.ToString(CultureInfo.InvariantCulture)}]"; + private string DebuggerDisplay => $"[IssuePriority: {ToString()}, IsDefault={IsDefault.ToString(CultureInfo.InvariantCulture)}, IsActive={IsActive.ToString(CultureInfo.InvariantCulture)}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Journal.cs b/src/redmine-net-api/Types/Journal.cs index 9558fa8a..5ed9b608 100644 --- a/src/redmine-net-api/Types/Journal.cs +++ b/src/redmine-net-api/Types/Journal.cs @@ -35,7 +35,7 @@ public sealed class Journal : Identifiable { #region Properties /// - /// Gets or sets the user. + /// Gets the user. /// /// /// The user. @@ -53,12 +53,20 @@ public sealed class Journal : Identifiable public string Notes { get; set; } /// - /// Gets or sets the created on. + /// Gets the created on. /// /// /// The created on. /// public DateTime? CreatedOn { get; internal set; } + + /// + /// Gets the updated on. + /// + /// + /// The updated on. + /// + public DateTime? UpdatedOn { get; internal set; } /// /// @@ -66,12 +74,17 @@ public sealed class Journal : Identifiable public bool PrivateNotes { get; internal set; } /// - /// Gets or sets the details. + /// Gets the details. /// /// /// The details. /// public IList Details { get; internal set; } + + /// + /// + /// + public IdentifiableName UpdatedBy { get; internal set; } #endregion #region Implementation of IXmlSerialization @@ -95,10 +108,12 @@ public override void ReadXml(XmlReader reader) switch (reader.Name) { case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; + case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadElementContentAsNullableDateTime(); break; case RedmineKeys.DETAILS: Details = reader.ReadElementContentAsCollection(); break; case RedmineKeys.NOTES: Notes = reader.ReadElementContentAsString(); break; case RedmineKeys.PRIVATE_NOTES: PrivateNotes = reader.ReadElementContentAsBoolean(); break; case RedmineKeys.USER: User = new IdentifiableName(reader); break; + case RedmineKeys.UPDATED_BY: UpdatedBy = new IdentifiableName(reader); break; default: reader.Read(); break; } } @@ -137,10 +152,12 @@ public override void ReadJson(JsonReader reader) { case RedmineKeys.ID: Id = reader.ReadAsInt(); break; case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadAsDateTime(); break; + case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadAsDateTime(); break; case RedmineKeys.DETAILS: Details = reader.ReadAsCollection(); break; case RedmineKeys.NOTES: Notes = reader.ReadAsString(); break; case RedmineKeys.PRIVATE_NOTES: PrivateNotes = reader.ReadAsBool(); break; case RedmineKeys.USER: User = new IdentifiableName(reader); break; + case RedmineKeys.UPDATED_BY: UpdatedBy = new IdentifiableName(reader); break; default: reader.Read(); break; } } diff --git a/src/redmine-net-api/Types/Membership.cs b/src/redmine-net-api/Types/Membership.cs index b819c91b..d74e22c7 100644 --- a/src/redmine-net-api/Types/Membership.cs +++ b/src/redmine-net-api/Types/Membership.cs @@ -33,10 +33,19 @@ public sealed class Membership : Identifiable { #region Properties /// + /// Gets the group. + /// + public IdentifiableName Group { get; internal set; } + /// /// Gets or sets the project. /// /// The project. public IdentifiableName Project { get; internal set; } + + /// + /// Gets the user. + /// + public IdentifiableName User { get; internal set; } /// /// Gets or sets the type. @@ -64,7 +73,9 @@ public override void ReadXml(XmlReader reader) switch (reader.Name) { case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; + case RedmineKeys.GROUP: Group = new IdentifiableName(reader); break; case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; + case RedmineKeys.USER: User = new IdentifiableName(reader); break; case RedmineKeys.ROLES: Roles = reader.ReadElementContentAsCollection(); break; default: reader.Read(); break; } @@ -94,7 +105,9 @@ public override void ReadJson(JsonReader reader) switch (reader.Value) { case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.GROUP: Project = new IdentifiableName(reader); break; case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; + case RedmineKeys.USER: Project = new IdentifiableName(reader); break; case RedmineKeys.ROLES: Roles = reader.ReadAsCollection(); break; default: reader.Read(); break; } @@ -111,9 +124,9 @@ public override void ReadJson(JsonReader reader) public override bool Equals(Membership other) { if (other == null) return false; - return Id == other.Id && - Project != null ? Project.Equals(other.Project) : other.Project == null && - Roles != null ? Roles.Equals(other.Roles) : other.Roles == null; + return Id == other.Id + && Project != null ? Project.Equals(other.Project) : other.Project == null + && Roles != null ? Roles.Equals(other.Roles) : other.Roles == null; } /// @@ -126,7 +139,9 @@ public override int GetHashCode() { var hashCode = 13; hashCode = HashCodeHelper.GetHashCode(Id, hashCode); + hashCode = HashCodeHelper.GetHashCode(Group, hashCode); hashCode = HashCodeHelper.GetHashCode(Project, hashCode); + hashCode = HashCodeHelper.GetHashCode(User, hashCode); hashCode = HashCodeHelper.GetHashCode(Roles, hashCode); return hashCode; } @@ -137,7 +152,6 @@ public override int GetHashCode() /// /// /// - private string DebuggerDisplay => $"[{nameof(Membership)}: {ToString()}, Project={Project}, Roles={Roles.Dump()}]"; - + private string DebuggerDisplay => $"[{nameof(Membership)}: {ToString()}, Group={Group}, Project={Project}, User={User}, Roles={Roles.Dump()}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/MyAccount.cs b/src/redmine-net-api/Types/MyAccount.cs index 06189301..3138b036 100644 --- a/src/redmine-net-api/Types/MyAccount.cs +++ b/src/redmine-net-api/Types/MyAccount.cs @@ -23,6 +23,7 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api.Types { @@ -122,7 +123,15 @@ public override void ReadXml(XmlReader reader) } } } - + + /// + public override void WriteXml(XmlWriter writer) + { + writer.WriteElementString(RedmineKeys.FIRST_NAME, FirstName); + writer.WriteElementString(RedmineKeys.LAST_NAME, LastName); + writer.WriteElementString(RedmineKeys.MAIL, Email); + } + #endregion #region Implementation of IJsonSerializable @@ -158,7 +167,18 @@ public override void ReadJson(JsonReader reader) } } } - + + /// + public override void WriteJson(JsonWriter writer) + { + using (new JsonObject(writer, RedmineKeys.USER)) + { + writer.WriteProperty(RedmineKeys.FIRST_NAME, FirstName); + writer.WriteProperty(RedmineKeys.LAST_NAME, LastName); + writer.WriteProperty(RedmineKeys.MAIL, Email); + } + } + #endregion /// diff --git a/src/redmine-net-api/Types/Role.cs b/src/redmine-net-api/Types/Role.cs index 40ac4122..e2b6384a 100644 --- a/src/redmine-net-api/Types/Role.cs +++ b/src/redmine-net-api/Types/Role.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -83,6 +83,9 @@ public override void ReadXml(XmlReader reader) case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; case RedmineKeys.ASSIGNABLE: IsAssignable = reader.ReadElementContentAsNullableBoolean(); break; + case RedmineKeys.ISSUES_VISIBILITY: IssuesVisibility = reader.ReadElementContentAsString(); break; + case RedmineKeys.TIME_ENTRIES_VISIBILITY: TimeEntriesVisibility = reader.ReadElementContentAsString(); break; + case RedmineKeys.USERS_VISIBILITY: UsersVisibility = reader.ReadElementContentAsString(); break; case RedmineKeys.PERMISSIONS: Permissions = reader.ReadElementContentAsCollection(); break; default: reader.Read(); break; } @@ -115,6 +118,9 @@ public override void ReadJson(JsonReader reader) case RedmineKeys.ID: Id = reader.ReadAsInt(); break; case RedmineKeys.NAME: Name = reader.ReadAsString(); break; case RedmineKeys.ASSIGNABLE: IsAssignable = reader.ReadAsBoolean(); break; + case RedmineKeys.ISSUES_VISIBILITY: IssuesVisibility = reader.ReadAsString(); break; + case RedmineKeys.TIME_ENTRIES_VISIBILITY: TimeEntriesVisibility = reader.ReadAsString(); break; + case RedmineKeys.USERS_VISIBILITY: UsersVisibility = reader.ReadAsString(); break; case RedmineKeys.PERMISSIONS: Permissions = reader.ReadAsCollection(); break; default: reader.Read(); break; } @@ -134,6 +140,9 @@ public bool Equals(Role other) return EqualityComparer.Default.Equals(Id, other.Id) && EqualityComparer.Default.Equals(Name, other.Name) && IsAssignable == other.IsAssignable && + EqualityComparer.Default.Equals(IssuesVisibility, other.IssuesVisibility) && + EqualityComparer.Default.Equals(TimeEntriesVisibility, other.TimeEntriesVisibility) && + EqualityComparer.Default.Equals(UsersVisibility, other.UsersVisibility) && EqualityComparer>.Default.Equals(Permissions, other.Permissions); } @@ -163,6 +172,9 @@ public override int GetHashCode() hashCode = HashCodeHelper.GetHashCode(Id, hashCode); hashCode = HashCodeHelper.GetHashCode(Name, hashCode); hashCode = HashCodeHelper.GetHashCode(IsAssignable, hashCode); + hashCode = HashCodeHelper.GetHashCode(IssuesVisibility, hashCode); + hashCode = HashCodeHelper.GetHashCode(TimeEntriesVisibility, hashCode); + hashCode = HashCodeHelper.GetHashCode(UsersVisibility, hashCode); hashCode = HashCodeHelper.GetHashCode(Permissions, hashCode); return hashCode; } diff --git a/src/redmine-net-api/Types/Tracker.cs b/src/redmine-net-api/Types/Tracker.cs index 157b75aa..d203e6c2 100644 --- a/src/redmine-net-api/Types/Tracker.cs +++ b/src/redmine-net-api/Types/Tracker.cs @@ -15,6 +15,7 @@ limitations under the License. */ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Xml; using System.Xml.Serialization; @@ -40,6 +41,11 @@ public class Tracker : IdentifiableName, IEquatable /// Gets the description of this tracker. /// public string Description { get; internal set; } + + /// + /// Gets the list of enabled tracker's core fields + /// + public List EnabledStandardFields { get; internal set; } #region Implementation of IXmlSerialization /// @@ -63,6 +69,7 @@ public override void ReadXml(XmlReader reader) case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; case RedmineKeys.DEFAULT_STATUS: DefaultStatus = new IdentifiableName(reader); break; case RedmineKeys.DESCRIPTION: Description = reader.ReadElementContentAsString(); break; + case RedmineKeys.ENABLED_STANDARD_FIELDS: EnabledStandardFields = reader.ReadElementContentAsCollection(); break; default: reader.Read(); break; } } @@ -95,6 +102,7 @@ public override void ReadJson(JsonReader reader) case RedmineKeys.NAME: Name = reader.ReadAsString(); break; case RedmineKeys.DEFAULT_STATUS: DefaultStatus = new IdentifiableName(reader); break; case RedmineKeys.DESCRIPTION: Description = reader.ReadAsString(); break; + case RedmineKeys.ENABLED_STANDARD_FIELDS: EnabledStandardFields = reader.ReadAsCollection(); break; default: reader.Read(); break; } } diff --git a/src/redmine-net-api/Types/TrackerCoreField.cs b/src/redmine-net-api/Types/TrackerCoreField.cs new file mode 100644 index 00000000..1777883f --- /dev/null +++ b/src/redmine-net-api/Types/TrackerCoreField.cs @@ -0,0 +1,129 @@ +using System; +using System.Diagnostics; +using System.Xml; +using System.Xml.Schema; +using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization; + +namespace Redmine.Net.Api.Types +{ + /// + /// + /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [XmlRoot(RedmineKeys.TRACKER)] + public sealed class TrackerCoreField: IXmlSerializable, IJsonSerializable, IEquatable + { + /// + /// + /// + public string Name { get; private set; } + + /// + /// + /// + /// + private string DebuggerDisplay => $"[{nameof(TrackerCoreField)}: {ToString()}]"; + + /// + /// + /// + /// + public XmlSchema GetSchema() + { + return null; + } + + /// + /// + /// + /// + /// + public void ReadXml(XmlReader reader) + { + reader.Read(); + if (reader.NodeType == XmlNodeType.Text) + { + Name = reader.Value; + } + } + + /// + /// + /// + /// + public void WriteXml(XmlWriter writer) { } + + /// + /// + /// + /// + public void WriteJson(JsonWriter writer) { } + + /// + /// + /// + /// + /// + public void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) + { + case RedmineKeys.PERMISSION: Name = reader.ReadAsString(); break; + default: reader.Read(); break; + } + } + } + + /// + /// + /// + /// + /// + public bool Equals(TrackerCoreField other) + { + return other != null && Name == other.Name; + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as TrackerCoreField); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + unchecked + { + var hashCode = 13; + hashCode = HashCodeHelper.GetHashCode(Name, hashCode); + return hashCode; + } + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/Upload.cs b/src/redmine-net-api/Types/Upload.cs index 42b25a75..073690c8 100644 --- a/src/redmine-net-api/Types/Upload.cs +++ b/src/redmine-net-api/Types/Upload.cs @@ -34,6 +34,11 @@ namespace Redmine.Net.Api.Types public sealed class Upload : IXmlSerializable, IJsonSerializable, IEquatable { #region Properties + /// + /// Gets the uploaded id. + /// + public string Id { get; private set; } + /// /// Gets or sets the uploaded token. /// @@ -84,6 +89,7 @@ public void ReadXml(XmlReader reader) switch (reader.Name) { + case RedmineKeys.ID: Id = reader.ReadElementContentAsString(); break; case RedmineKeys.CONTENT_TYPE: ContentType = reader.ReadElementContentAsString(); break; case RedmineKeys.DESCRIPTION: Description = reader.ReadElementContentAsString(); break; case RedmineKeys.FILE_NAME: FileName = reader.ReadElementContentAsString(); break; @@ -127,6 +133,7 @@ public void ReadJson(JsonReader reader) switch (reader.Value) { + case RedmineKeys.ID: Id = reader.ReadAsString(); break; case RedmineKeys.CONTENT_TYPE: ContentType = reader.ReadAsString(); break; case RedmineKeys.DESCRIPTION: Description = reader.ReadAsString(); break; case RedmineKeys.FILE_NAME: FileName = reader.ReadAsString(); break; diff --git a/src/redmine-net-api/Types/User.cs b/src/redmine-net-api/Types/User.cs index 7a93bfcf..64225abb 100644 --- a/src/redmine-net-api/Types/User.cs +++ b/src/redmine-net-api/Types/User.cs @@ -35,6 +35,11 @@ namespace Redmine.Net.Api.Types public sealed class User : Identifiable { #region Properties + /// + /// Gets or sets the user avatar url. + /// + public string AvatarUrl { get; set; } + /// /// Gets or sets the user login. /// @@ -174,6 +179,7 @@ public override void ReadXml(XmlReader reader) case RedmineKeys.ADMIN: IsAdmin = reader.ReadElementContentAsBoolean(); break; case RedmineKeys.API_KEY: ApiKey = reader.ReadElementContentAsString(); break; case RedmineKeys.AUTH_SOURCE_ID: AuthenticationModeId = reader.ReadElementContentAsNullableInt(); break; + case RedmineKeys.AVATAR_URL: AvatarUrl = reader.ReadElementContentAsString(); break; case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadElementContentAsCollection(); break; case RedmineKeys.FIRST_NAME: FirstName = reader.ReadElementContentAsString(); break; @@ -255,6 +261,7 @@ public override void ReadJson(JsonReader reader) case RedmineKeys.ADMIN: IsAdmin = reader.ReadAsBool(); break; case RedmineKeys.API_KEY: ApiKey = reader.ReadAsString(); break; case RedmineKeys.AUTH_SOURCE_ID: AuthenticationModeId = reader.ReadAsInt32(); break; + case RedmineKeys.AVATAR_URL: AvatarUrl = reader.ReadAsString(); break; case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadAsDateTime(); break; case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadAsCollection(); break; case RedmineKeys.LAST_LOGIN_ON: LastLoginOn = reader.ReadAsDateTime(); break; @@ -324,6 +331,7 @@ public override bool Equals(User other) { if (other == null) return false; return Id == other.Id + && string.Equals(AvatarUrl,other.AvatarUrl, StringComparison.OrdinalIgnoreCase) && string.Equals(Login,other.Login, StringComparison.OrdinalIgnoreCase) && string.Equals(FirstName,other.FirstName, StringComparison.OrdinalIgnoreCase) && string.Equals(LastName,other.LastName, StringComparison.OrdinalIgnoreCase) @@ -354,6 +362,7 @@ public override int GetHashCode() unchecked { var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(AvatarUrl, hashCode); hashCode = HashCodeHelper.GetHashCode(Login, hashCode); hashCode = HashCodeHelper.GetHashCode(Password, hashCode); hashCode = HashCodeHelper.GetHashCode(FirstName, hashCode); @@ -387,6 +396,7 @@ public override int GetHashCode() Login={Login}, Password={Password}, FirstName={FirstName}, LastName={LastName}, +AvatarUrl={AvatarUrl}, IsAdmin={IsAdmin.ToString(CultureInfo.InvariantCulture)}, TwoFactorAuthenticationScheme={TwoFactorAuthenticationScheme} Email={Email}, diff --git a/src/redmine-net-api/Types/Version.cs b/src/redmine-net-api/Types/Version.cs index a9c74321..d3c52e6e 100644 --- a/src/redmine-net-api/Types/Version.cs +++ b/src/redmine-net-api/Types/Version.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -149,9 +149,10 @@ public override void WriteXml(XmlWriter writer) writer.WriteDateOrEmpty(RedmineKeys.DUE_DATE, DueDate); writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); + writer.WriteElementString(RedmineKeys.WIKI_PAGE_TITLE, WikiPageTitle); if (CustomFields != null) { - writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); + writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); } } #endregion @@ -210,6 +211,12 @@ public override void WriteJson(JsonWriter writer) writer.WriteProperty(RedmineKeys.SHARING, Sharing.ToString().ToLowerInv()); writer.WriteProperty(RedmineKeys.DESCRIPTION, Description); writer.WriteDateOrEmpty(RedmineKeys.DUE_DATE, DueDate); + if (CustomFields != null) + { + writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); + } + + writer.WriteProperty(RedmineKeys.WIKI_PAGE_TITLE, WikiPageTitle); } } #endregion From 286ae362ac486838fe76d28de082279875b2ab97 Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 30 Mar 2025 14:24:07 +0300 Subject: [PATCH 421/549] [Project] Add new actions (#374) * [Project] Add Archive * [Project] Add Unarchive * [Project] Add Reopen * [Project] Add Close * [Project][Repository] Add related issue * [Project][Repository] Remove related issue --- .../Extensions/RedmineManagerExtensions.cs | 202 +++++++++++++++++- .../Net/RedmineApiUrlsExtensions.cs | 30 +++ src/redmine-net-api/RedmineKeys.cs | 20 ++ 3 files changed, 251 insertions(+), 1 deletion(-) diff --git a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs index d4447c21..254e4855 100644 --- a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs +++ b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs @@ -34,7 +34,106 @@ namespace Redmine.Net.Api.Extensions /// public static class RedmineManagerExtensions { - /// + /// + /// + /// + /// + /// + /// + /// + public static void ArchiveProject(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null) + { + var uri = redmineManager.RedmineApiUrls.ProjectArchive(projectIdentifier); + + var escapedUri = Uri.EscapeDataString(uri); + + redmineManager.ApiClient.Update(escapedUri, string.Empty ,requestOptions); + } + + /// + /// + /// + /// + /// + /// + /// + public static void UnarchiveProject(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null) + { + var uri = redmineManager.RedmineApiUrls.ProjectUnarchive(projectIdentifier); + + var escapedUri = Uri.EscapeDataString(uri); + + redmineManager.ApiClient.Update(escapedUri, string.Empty ,requestOptions); + } + + /// + /// + /// + /// + /// + /// + /// + public static void ReopenProject(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null) + { + var uri = redmineManager.RedmineApiUrls.ProjectReopen(projectIdentifier); + + var escapedUri = Uri.EscapeDataString(uri); + + redmineManager.ApiClient.Update(escapedUri, string.Empty ,requestOptions); + } + + /// + /// + /// + /// + /// + /// + /// + public static void CloseProject(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null) + { + var uri = redmineManager.RedmineApiUrls.ProjectClose(projectIdentifier); + + var escapedUri = Uri.EscapeDataString(uri); + + redmineManager.ApiClient.Update(escapedUri,string.Empty, requestOptions); + } + + /// + /// + /// + /// + /// + /// + /// + /// + public static void ProjectRepositoryAddRelatedIssue(this RedmineManager redmineManager, string projectIdentifier, string repositoryIdentifier, string revision, RequestOptions requestOptions = null) + { + var uri = redmineManager.RedmineApiUrls.ProjectRepositoryAddRelatedIssue(projectIdentifier, repositoryIdentifier, revision); + + var escapedUri = Uri.EscapeDataString(uri); + + _ = redmineManager.ApiClient.Create(escapedUri,string.Empty, requestOptions); + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static void ProjectRepositoryRemoveRelatedIssue(this RedmineManager redmineManager, string projectIdentifier, string repositoryIdentifier, string revision, string issueIdentifier, RequestOptions requestOptions = null) + { + var uri = redmineManager.RedmineApiUrls.ProjectRepositoryRemoveRelatedIssue(projectIdentifier, repositoryIdentifier, revision, issueIdentifier); + + var escapedUri = Uri.EscapeDataString(uri); + + _ = redmineManager.ApiClient.Delete(escapedUri, requestOptions); + } + + /// /// /// /// @@ -374,6 +473,107 @@ private static NameValueCollection CreateSearchParameters(string q, int limit, i #if !(NET20) + /// + /// Archives the project asynchronously + /// + /// + /// + /// + /// + public static async Task ArchiveProjectAsync(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.ProjectArchive(projectIdentifier); + + var escapedUri = Uri.EscapeDataString(uri); + + await redmineManager.ApiClient.DeleteAsync(escapedUri, requestOptions, cancellationToken).ConfigureAwait(false); + } + + /// + /// Unarchives the project asynchronously + /// + /// + /// + /// + /// + public static async Task UnarchiveProjectAsync(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.ProjectUnarchive(projectIdentifier); + + var escapedUri = Uri.EscapeDataString(uri); + + await redmineManager.ApiClient.DeleteAsync(escapedUri, requestOptions, cancellationToken).ConfigureAwait(false); + } + + /// + /// Closes the project asynchronously + /// + /// + /// + /// + /// + public static async Task CloseProjectAsync(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.ProjectClose(projectIdentifier); + + var escapedUri = Uri.EscapeDataString(uri); + + await redmineManager.ApiClient.UpdateAsync(escapedUri, string.Empty, requestOptions, cancellationToken).ConfigureAwait(false); + } + + /// + /// Reopens the project asynchronously + /// + /// + /// + /// + /// + public static async Task ReopenProjectAsync(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.ProjectReopen(projectIdentifier); + + var escapedUri = Uri.EscapeDataString(uri); + + await redmineManager.ApiClient.UpdateAsync(escapedUri, string.Empty, requestOptions, cancellationToken).ConfigureAwait(false); + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static async Task ProjectRepositoryAddRelatedIssueAsync(this RedmineManager redmineManager, string projectIdentifier, string repositoryIdentifier, string revision, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.ProjectRepositoryAddRelatedIssue(projectIdentifier, repositoryIdentifier, revision); + + var escapedUri = Uri.EscapeDataString(uri); + + await redmineManager.ApiClient.CreateAsync(escapedUri, string.Empty ,requestOptions, cancellationToken).ConfigureAwait(false); + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static async Task ProjectRepositoryRemoveRelatedIssueAsync(this RedmineManager redmineManager, string projectIdentifier, string repositoryIdentifier, string revision, string issueIdentifier, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.ProjectRepositoryRemoveRelatedIssue(projectIdentifier, repositoryIdentifier, revision, issueIdentifier); + + var escapedUri = Uri.EscapeDataString(uri); + + await redmineManager.ApiClient.DeleteAsync(escapedUri, requestOptions, cancellationToken).ConfigureAwait(false); + } + /// /// /// diff --git a/src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs b/src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs index 12f896f9..763060aa 100644 --- a/src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs +++ b/src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs @@ -31,6 +31,36 @@ public static string CurrentUser(this RedmineApiUrls redmineApiUrls) return $"{RedmineKeys.USERS}/{RedmineKeys.CURRENT}.{redmineApiUrls.Format}"; } + public static string ProjectClose(this RedmineApiUrls redmineApiUrls, string projectIdentifier) + { + return $"{RedmineKeys.PROJECTS}/{projectIdentifier}/{RedmineKeys.CLOSE}.{redmineApiUrls.Format}"; + } + + public static string ProjectReopen(this RedmineApiUrls redmineApiUrls, string projectIdentifier) + { + return $"{RedmineKeys.PROJECTS}/{projectIdentifier}/{RedmineKeys.REOPEN}.{redmineApiUrls.Format}"; + } + + public static string ProjectArchive(this RedmineApiUrls redmineApiUrls, string projectIdentifier) + { + return $"{RedmineKeys.PROJECTS}/{projectIdentifier}/{RedmineKeys.ARCHIVE}.{redmineApiUrls.Format}"; + } + + public static string ProjectUnarchive(this RedmineApiUrls redmineApiUrls, string projectIdentifier) + { + return $"{RedmineKeys.PROJECTS}/{projectIdentifier}/{RedmineKeys.UNARCHIVE}.{redmineApiUrls.Format}"; + } + + public static string ProjectRepositoryAddRelatedIssue(this RedmineApiUrls redmineApiUrls, string projectIdentifier, string repositoryIdentifier, string revision) + { + return $"{RedmineKeys.PROJECTS}/{projectIdentifier}/{RedmineKeys.REPOSITORY}/{repositoryIdentifier}/{RedmineKeys.REVISIONS}/{revision}/{RedmineKeys.ISSUES}.{redmineApiUrls.Format}"; + } + + public static string ProjectRepositoryRemoveRelatedIssue(this RedmineApiUrls redmineApiUrls, string projectIdentifier, string repositoryIdentifier, string revision, string issueIdentifier) + { + return $"{RedmineKeys.PROJECTS}/{projectIdentifier}/{RedmineKeys.REPOSITORY}/{repositoryIdentifier}/{RedmineKeys.REVISIONS}/{revision}/{RedmineKeys.ISSUES}/{issueIdentifier}.{redmineApiUrls.Format}"; + } + public static string ProjectNews(this RedmineApiUrls redmineApiUrls, string projectIdentifier) { if (projectIdentifier.IsNullOrWhiteSpace()) diff --git a/src/redmine-net-api/RedmineKeys.cs b/src/redmine-net-api/RedmineKeys.cs index 996b7142..5ecae9fd 100644 --- a/src/redmine-net-api/RedmineKeys.cs +++ b/src/redmine-net-api/RedmineKeys.cs @@ -116,6 +116,10 @@ public static class RedmineKeys /// /// /// + public const string CLOSE = "close"; + /// + /// + /// public const string CLOSED_ON = "closed_on"; /// /// @@ -636,6 +640,14 @@ public static class RedmineKeys /// /// /// + public const string REOPEN = "reopen"; + /// + /// + /// + public const string REPOSITORY = "repository"; + /// + /// + /// public const string RESULT = "result"; /// /// @@ -644,6 +656,10 @@ public static class RedmineKeys /// /// /// + public const string REVISIONS = "revisions"; + /// + /// + /// public const string ROLE = "role"; /// /// @@ -789,6 +805,10 @@ public static class RedmineKeys /// /// /// + public const string UNARCHIVE = "unarchive"; + /// + /// + /// public const string UPDATED_ON = "updated_on"; /// /// From 2912b39b71e348c2497abc318276f7e2f280492c Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 30 Mar 2025 14:35:10 +0300 Subject: [PATCH 422/549] Small fixes (#375) * [IssueAllowedStatus] Implement Read Xml/Json * [TrackerCoreFiled] Set correct root * [Wiki] Fix get parent attribute value * [Xml] Set totalItems if total_count missing --- .../Serialization/Xml/XmlRedmineSerializer.cs | 7 ++- .../Types/IssueAllowedStatus.cs | 40 ++++++++++++ src/redmine-net-api/Types/TrackerCoreField.cs | 4 +- src/redmine-net-api/Types/WikiPage.cs | 61 ++++++++++++++++--- 4 files changed, 100 insertions(+), 12 deletions(-) diff --git a/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs b/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs index 179eaec0..74c26aea 100644 --- a/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs +++ b/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs @@ -27,7 +27,7 @@ namespace Redmine.Net.Api.Serialization internal sealed class XmlRedmineSerializer : IRedmineSerializer { - public XmlRedmineSerializer(): this(new XmlWriterSettings + public XmlRedmineSerializer() : this(new XmlWriterSettings { OmitXmlDeclaration = true }) { } @@ -126,6 +126,11 @@ public string Serialize(T entity) where T : class var limit = xmlReader.ReadAttributeAsInt(RedmineKeys.LIMIT); var result = xmlReader.ReadElementContentAsCollection(); + if (totalItems == 0 && result.Count > 0) + { + totalItems = result.Count; + } + return new PagedResults(result, totalItems, offset, limit); } } diff --git a/src/redmine-net-api/Types/IssueAllowedStatus.cs b/src/redmine-net-api/Types/IssueAllowedStatus.cs index 2df5b42a..35a1478c 100644 --- a/src/redmine-net-api/Types/IssueAllowedStatus.cs +++ b/src/redmine-net-api/Types/IssueAllowedStatus.cs @@ -15,7 +15,10 @@ limitations under the License. */ using System.Diagnostics; +using System.Xml; using System.Xml.Serialization; +using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; namespace Redmine.Net.Api.Types { @@ -26,6 +29,43 @@ namespace Redmine.Net.Api.Types [XmlRoot(RedmineKeys.STATUS)] public sealed class IssueAllowedStatus : IdentifiableName { + /// + /// + /// + public bool? IsClosed { get; internal set; } + + /// + public override void ReadXml(XmlReader reader) + { + Id = reader.ReadAttributeAsInt(RedmineKeys.ID); + Name = reader.GetAttribute(RedmineKeys.NAME); + IsClosed = reader.ReadAttributeAsBoolean(RedmineKeys.IS_CLOSED); + reader.Read(); + } + + /// + public override void ReadJson(JsonReader reader) + { + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) + { + return; + } + + if (reader.TokenType == JsonToken.PropertyName) + { + switch (reader.Value) + { + case RedmineKeys.ID: Id = reader.ReadAsInt(); break; + case RedmineKeys.NAME: Name = reader.ReadAsString(); break; + case RedmineKeys.IS_CLOSED: IsClosed = reader.ReadAsBoolean(); break; + default: reader.Read(); break; + } + } + } + } + private string DebuggerDisplay => $"[{nameof(IssueAllowedStatus)}: {ToString()}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/TrackerCoreField.cs b/src/redmine-net-api/Types/TrackerCoreField.cs index 1777883f..5f8edf54 100644 --- a/src/redmine-net-api/Types/TrackerCoreField.cs +++ b/src/redmine-net-api/Types/TrackerCoreField.cs @@ -13,7 +13,7 @@ namespace Redmine.Net.Api.Types /// /// [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] - [XmlRoot(RedmineKeys.TRACKER)] + [XmlRoot(RedmineKeys.FIELD)] public sealed class TrackerCoreField: IXmlSerializable, IJsonSerializable, IEquatable { /// @@ -40,7 +40,6 @@ public XmlSchema GetSchema() /// /// /// - /// public void ReadXml(XmlReader reader) { reader.Read(); @@ -66,7 +65,6 @@ public void WriteJson(JsonWriter writer) { } /// /// /// - /// public void ReadJson(JsonReader reader) { while (reader.Read()) diff --git a/src/redmine-net-api/Types/WikiPage.cs b/src/redmine-net-api/Types/WikiPage.cs index bba1fcf7..e5acd51b 100644 --- a/src/redmine-net-api/Types/WikiPage.cs +++ b/src/redmine-net-api/Types/WikiPage.cs @@ -123,7 +123,16 @@ public override void ReadXml(XmlReader reader) case RedmineKeys.TITLE: Title = reader.ReadElementContentAsString(); break; case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadElementContentAsNullableDateTime(); break; case RedmineKeys.VERSION: Version = reader.ReadElementContentAsInt(); break; - case RedmineKeys.PARENT: ParentTitle = reader.GetAttribute(RedmineKeys.PARENT); break; + case RedmineKeys.PARENT: + { + if (reader.HasAttributes) + { + ParentTitle = reader.GetAttribute(RedmineKeys.TITLE); + reader.Read(); + } + + break; + } default: reader.Read(); break; } } @@ -206,14 +215,28 @@ public override bool Equals(WikiPage other) { if (other == null) return false; - return Id == other.Id - && Title == other.Title - && Text == other.Text - && Comments == other.Comments + return base.Equals(other) + && string.Equals(Title, other.Title, StringComparison.Ordinal) + && string.Equals(Text, other.Text, StringComparison.Ordinal) + && string.Equals(Comments, other.Comments, StringComparison.Ordinal) && Version == other.Version - && Author == other.Author - && CreatedOn == other.CreatedOn - && UpdatedOn == other.UpdatedOn; + && Equals(Author, other.Author) + && CreatedOn.Equals(other.CreatedOn) + && UpdatedOn.Equals(other.UpdatedOn) + && Equals(Attachments, other.Attachments); + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as WikiPage); } /// @@ -236,6 +259,28 @@ public override int GetHashCode() return hashCode; } } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(WikiPage left, WikiPage right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(WikiPage left, WikiPage right) + { + return !Equals(left, right); + } #endregion /// From 582cfa5630aa438549426bf71cc61e2170b078fa Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 30 Mar 2025 20:36:53 +0300 Subject: [PATCH 423/549] [Types] Enhance equality comparison (#376) --- .../Internals/HashCodeHelper.cs | 4 +- src/redmine-net-api/Types/Attachment.cs | 63 +++++++++++---- src/redmine-net-api/Types/ChangeSet.cs | 26 +++++- src/redmine-net-api/Types/CustomField.cs | 79 +++++++++++------- .../Types/CustomFieldPossibleValue.cs | 28 ++++++- src/redmine-net-api/Types/CustomFieldValue.cs | 31 ++++++- src/redmine-net-api/Types/Detail.cs | 31 ++++++- src/redmine-net-api/Types/DocumentCategory.cs | 35 ++++++-- src/redmine-net-api/Types/Error.cs | 26 +++++- src/redmine-net-api/Types/File.cs | 65 +++++++++++---- src/redmine-net-api/Types/Group.cs | 35 ++++++-- src/redmine-net-api/Types/Identifiable.cs | 8 +- src/redmine-net-api/Types/IdentifiableName.cs | 37 ++++++++- src/redmine-net-api/Types/Issue.cs | 58 +++++++++++--- .../Types/IssueAllowedStatus.cs | 63 +++++++++++++++ src/redmine-net-api/Types/IssueCategory.cs | 40 +++++++++- src/redmine-net-api/Types/IssueChild.cs | 60 +++++++++++--- src/redmine-net-api/Types/IssueCustomField.cs | 59 ++++++++++---- src/redmine-net-api/Types/IssuePriority.cs | 34 ++++++-- src/redmine-net-api/Types/IssueRelation.cs | 43 +++++++++- .../Types/IssueRelationType.cs | 80 +------------------ src/redmine-net-api/Types/IssueStatus.cs | 28 ++++++- src/redmine-net-api/Types/Journal.cs | 59 +++++++++++--- src/redmine-net-api/Types/Membership.cs | 40 +++++++++- src/redmine-net-api/Types/MembershipRole.cs | 31 +++++-- src/redmine-net-api/Types/MyAccount.cs | 38 ++++++++- .../Types/MyAccountCustomField.cs | 48 +++++++++-- src/redmine-net-api/Types/News.cs | 59 +++++++++++--- src/redmine-net-api/Types/NewsComment.cs | 35 ++++++++ src/redmine-net-api/Types/Permission.cs | 26 +++++- src/redmine-net-api/Types/Project.cs | 80 ++++++++++++------- .../Types/ProjectMembership.cs | 43 +++++++++- src/redmine-net-api/Types/Query.cs | 49 +++++++++--- src/redmine-net-api/Types/Role.cs | 28 ++++++- src/redmine-net-api/Types/Search.cs | 44 ++++++++-- src/redmine-net-api/Types/TimeEntry.cs | 41 +++++++++- .../Types/TimeEntryActivity.cs | 25 +++++- src/redmine-net-api/Types/Tracker.cs | 32 ++++++-- src/redmine-net-api/Types/TrackerCoreField.cs | 24 +++++- src/redmine-net-api/Types/Upload.cs | 26 +++++- src/redmine-net-api/Types/User.cs | 46 +++++++++-- src/redmine-net-api/Types/Version.cs | 41 +++++++++- src/redmine-net-api/Types/Watcher.cs | 5 +- 43 files changed, 1412 insertions(+), 341 deletions(-) diff --git a/src/redmine-net-api/Internals/HashCodeHelper.cs b/src/redmine-net-api/Internals/HashCodeHelper.cs index ba8595c4..f2610116 100755 --- a/src/redmine-net-api/Internals/HashCodeHelper.cs +++ b/src/redmine-net-api/Internals/HashCodeHelper.cs @@ -43,11 +43,11 @@ public static int GetHashCode(IList list, int hash) where T : class return hashCode; } - hashCode = (hashCode * 13) + list.Count; + hashCode = (hashCode * 17) + list.Count; foreach (var t in list) { - hashCode *= 13; + hashCode *= 17; if (t != null) { hashCode += t.GetHashCode(); diff --git a/src/redmine-net-api/Types/Attachment.cs b/src/redmine-net-api/Types/Attachment.cs index b6e188a6..f831d647 100644 --- a/src/redmine-net-api/Types/Attachment.cs +++ b/src/redmine-net-api/Types/Attachment.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -31,7 +31,8 @@ namespace Redmine.Net.Api.Types /// [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.ATTACHMENT)] - public sealed class Attachment : Identifiable + public sealed class Attachment : + Identifiable { #region Properties /// @@ -186,15 +187,28 @@ public override void WriteJson(JsonWriter writer) public override bool Equals(Attachment other) { if (other == null) return false; - return Id == other.Id - && FileName == other.FileName - && FileSize == other.FileSize - && ContentType == other.ContentType - && Author == other.Author - && ThumbnailUrl == other.ThumbnailUrl - && CreatedOn == other.CreatedOn - && Description == other.Description - && ContentUrl == other.ContentUrl; + return base.Equals(other) + && string.Equals(FileName, other.FileName, StringComparison.OrdinalIgnoreCase) + && string.Equals(ContentType, other.ContentType, StringComparison.OrdinalIgnoreCase) + && string.Equals(Description, other.Description, StringComparison.OrdinalIgnoreCase) + && string.Equals(ContentUrl, other.ContentUrl, StringComparison.OrdinalIgnoreCase) + && string.Equals(ThumbnailUrl, other.ThumbnailUrl, StringComparison.OrdinalIgnoreCase) + && Equals(Author, other.Author) + && FileSize == other.FileSize + && CreatedOn == other.CreatedOn; + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as Attachment); } /// @@ -209,15 +223,36 @@ public override int GetHashCode() hashCode = HashCodeHelper.GetHashCode(FileName, hashCode); hashCode = HashCodeHelper.GetHashCode(FileSize, hashCode); hashCode = HashCodeHelper.GetHashCode(ContentType, hashCode); - hashCode = HashCodeHelper.GetHashCode(Author, hashCode); - hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); hashCode = HashCodeHelper.GetHashCode(Description, hashCode); hashCode = HashCodeHelper.GetHashCode(ContentUrl, hashCode); hashCode = HashCodeHelper.GetHashCode(ThumbnailUrl, hashCode); - + hashCode = HashCodeHelper.GetHashCode(Author, hashCode); + hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); return hashCode; } } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(Attachment left, Attachment right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(Attachment left, Attachment right) + { + return !Equals(left, right); + } #endregion private string DebuggerDisplay => diff --git a/src/redmine-net-api/Types/ChangeSet.cs b/src/redmine-net-api/Types/ChangeSet.cs index 5a362d68..bf07bc83 100644 --- a/src/redmine-net-api/Types/ChangeSet.cs +++ b/src/redmine-net-api/Types/ChangeSet.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -178,7 +178,7 @@ public override int GetHashCode() { unchecked { - var hashCode = 13; + var hashCode = 17; hashCode = HashCodeHelper.GetHashCode(Revision, hashCode); hashCode = HashCodeHelper.GetHashCode(User, hashCode); hashCode = HashCodeHelper.GetHashCode(Comments, hashCode); @@ -186,6 +186,28 @@ public override int GetHashCode() return hashCode; } } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(ChangeSet left, ChangeSet right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(ChangeSet left, ChangeSet right) + { + return !Equals(left, right); + } #endregion /// diff --git a/src/redmine-net-api/Types/CustomField.cs b/src/redmine-net-api/Types/CustomField.cs index ad80e656..486e492f 100644 --- a/src/redmine-net-api/Types/CustomField.cs +++ b/src/redmine-net-api/Types/CustomField.cs @@ -207,23 +207,22 @@ public bool Equals(CustomField other) { if (other == null) return false; - return Id == other.Id - && IsFilter == other.IsFilter - && IsRequired == other.IsRequired - && Multiple == other.Multiple - && Searchable == other.Searchable - && Visible == other.Visible - && string.Equals(CustomizedType,other.CustomizedType, StringComparison.OrdinalIgnoreCase) - && string.Equals(Description,other.Description, StringComparison.OrdinalIgnoreCase) - && string.Equals(DefaultValue,other.DefaultValue, StringComparison.OrdinalIgnoreCase) - && string.Equals(FieldFormat,other.FieldFormat, StringComparison.OrdinalIgnoreCase) - && MaxLength == other.MaxLength - && MinLength == other.MinLength - && string.Equals(Name,other.Name, StringComparison.OrdinalIgnoreCase) - && string.Equals(Regexp,other.Regexp, StringComparison.OrdinalIgnoreCase) - && PossibleValues.Equals(other.PossibleValues) - && Roles.Equals(other.Roles) - && Trackers.Equals(other.Trackers); + return base.Equals(other) + && string.Equals(CustomizedType, other.CustomizedType, StringComparison.OrdinalIgnoreCase) + && string.Equals(Description, other.Description, StringComparison.OrdinalIgnoreCase) + && string.Equals(FieldFormat, other.FieldFormat, StringComparison.OrdinalIgnoreCase) + && string.Equals(Regexp, other.Regexp, StringComparison.OrdinalIgnoreCase) + && string.Equals(DefaultValue, other.DefaultValue, StringComparison.Ordinal) + && MinLength == other.MinLength + && MaxLength == other.MaxLength + && IsRequired == other.IsRequired + && IsFilter == other.IsFilter + && Searchable == other.Searchable + && Multiple == other.Multiple + && Visible == other.Visible + && Equals(PossibleValues, other.PossibleValues) + && Equals(Trackers, other.Trackers) + && Equals(Roles, other.Roles); } /// @@ -247,27 +246,47 @@ public override int GetHashCode() { unchecked { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); - hashCode = HashCodeHelper.GetHashCode(IsFilter, hashCode); - hashCode = HashCodeHelper.GetHashCode(IsRequired, hashCode); - hashCode = HashCodeHelper.GetHashCode(Description, hashCode); - hashCode = HashCodeHelper.GetHashCode(Multiple, hashCode); - hashCode = HashCodeHelper.GetHashCode(Searchable, hashCode); - hashCode = HashCodeHelper.GetHashCode(Visible, hashCode); + var hashCode = base.GetHashCode(); hashCode = HashCodeHelper.GetHashCode(CustomizedType, hashCode); - hashCode = HashCodeHelper.GetHashCode(DefaultValue, hashCode); + hashCode = HashCodeHelper.GetHashCode(Description, hashCode); hashCode = HashCodeHelper.GetHashCode(FieldFormat, hashCode); - hashCode = HashCodeHelper.GetHashCode(MaxLength, hashCode); - hashCode = HashCodeHelper.GetHashCode(MinLength, hashCode); - hashCode = HashCodeHelper.GetHashCode(Name, hashCode); hashCode = HashCodeHelper.GetHashCode(Regexp, hashCode); + hashCode = HashCodeHelper.GetHashCode(MinLength, hashCode); + hashCode = HashCodeHelper.GetHashCode(MaxLength, hashCode); + hashCode = HashCodeHelper.GetHashCode(IsRequired, hashCode); + hashCode = HashCodeHelper.GetHashCode(IsFilter, hashCode); + hashCode = HashCodeHelper.GetHashCode(Searchable, hashCode); + hashCode = HashCodeHelper.GetHashCode(Multiple, hashCode); + hashCode = HashCodeHelper.GetHashCode(DefaultValue, hashCode); + hashCode = HashCodeHelper.GetHashCode(Visible, hashCode); hashCode = HashCodeHelper.GetHashCode(PossibleValues, hashCode); - hashCode = HashCodeHelper.GetHashCode(Roles, hashCode); hashCode = HashCodeHelper.GetHashCode(Trackers, hashCode); + hashCode = HashCodeHelper.GetHashCode(Roles, hashCode); return hashCode; } } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(CustomField left, CustomField right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(CustomField left, CustomField right) + { + return !Equals(left, right); + } #endregion private string DebuggerDisplay => diff --git a/src/redmine-net-api/Types/CustomFieldPossibleValue.cs b/src/redmine-net-api/Types/CustomFieldPossibleValue.cs index 0934e711..8c39c325 100644 --- a/src/redmine-net-api/Types/CustomFieldPossibleValue.cs +++ b/src/redmine-net-api/Types/CustomFieldPossibleValue.cs @@ -139,7 +139,8 @@ public void WriteJson(JsonWriter writer) { } public bool Equals(CustomFieldPossibleValue other) { if (other == null) return false; - return Value == other.Value; + return string.Equals(Value, other.Value, StringComparison.Ordinal) + && string.Equals(Label, other.Label, StringComparison.Ordinal); } /// @@ -163,11 +164,34 @@ public override int GetHashCode() { unchecked { - var hashCode = 13; + var hashCode = 17; hashCode = HashCodeHelper.GetHashCode(Value, hashCode); + hashCode = HashCodeHelper.GetHashCode(Label, hashCode); return hashCode; } } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(CustomFieldPossibleValue left, CustomFieldPossibleValue right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(CustomFieldPossibleValue left, CustomFieldPossibleValue right) + { + return !Equals(left, right); + } #endregion /// diff --git a/src/redmine-net-api/Types/CustomFieldValue.cs b/src/redmine-net-api/Types/CustomFieldValue.cs index a345acdf..5cc91ae5 100644 --- a/src/redmine-net-api/Types/CustomFieldValue.cs +++ b/src/redmine-net-api/Types/CustomFieldValue.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -30,7 +30,10 @@ namespace Redmine.Net.Api.Types /// [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.VALUE)] - public class CustomFieldValue : IXmlSerializable, IJsonSerializable, IEquatable, ICloneable + public class CustomFieldValue : + IXmlSerializable + ,IJsonSerializable + ,IEquatable { /// /// @@ -166,12 +169,34 @@ public override int GetHashCode() { unchecked { - var hashCode = 13; + var hashCode = 17; hashCode = HashCodeHelper.GetHashCode(Info, hashCode); return hashCode; } } + /// + /// + /// + /// + /// + /// + public static bool operator ==(CustomFieldValue left, CustomFieldValue right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(CustomFieldValue left, CustomFieldValue right) + { + return !Equals(left, right); + } + #endregion #region Implementation of IClonable diff --git a/src/redmine-net-api/Types/Detail.cs b/src/redmine-net-api/Types/Detail.cs index 9d74336a..35e3fcc4 100644 --- a/src/redmine-net-api/Types/Detail.cs +++ b/src/redmine-net-api/Types/Detail.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -30,7 +30,10 @@ namespace Redmine.Net.Api.Types /// [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.DETAIL)] - public sealed class Detail : IXmlSerializable, IJsonSerializable, IEquatable + public sealed class Detail : + IXmlSerializable + ,IJsonSerializable + ,IEquatable { /// /// @@ -200,7 +203,7 @@ public override int GetHashCode() { unchecked { - var hashCode = 13; + var hashCode = 17; hashCode = HashCodeHelper.GetHashCode(Property, hashCode); hashCode = HashCodeHelper.GetHashCode(Name, hashCode); hashCode = HashCodeHelper.GetHashCode(OldValue, hashCode); @@ -209,6 +212,28 @@ public override int GetHashCode() return hashCode; } } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(Detail left, Detail right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(Detail left, Detail right) + { + return !Equals(left, right); + } #endregion /// diff --git a/src/redmine-net-api/Types/DocumentCategory.cs b/src/redmine-net-api/Types/DocumentCategory.cs index 490bfa13..1028a41d 100644 --- a/src/redmine-net-api/Types/DocumentCategory.cs +++ b/src/redmine-net-api/Types/DocumentCategory.cs @@ -121,7 +121,7 @@ public override void ReadJson(JsonReader reader) } #endregion - #region Implementation of IEquatable + #region Implementation of IEquatable /// /// @@ -132,7 +132,10 @@ public bool Equals(DocumentCategory other) { if (other == null) return false; - return Id == other.Id && Name == other.Name && IsDefault == other.IsDefault && IsActive == other.IsActive; + return Id == other.Id + && Name == other.Name + && IsDefault == other.IsDefault + && IsActive == other.IsActive; } /// @@ -156,22 +159,42 @@ public override int GetHashCode() { unchecked { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); - hashCode = HashCodeHelper.GetHashCode(Name, hashCode); + var hashCode = base.GetHashCode(); hashCode = HashCodeHelper.GetHashCode(IsDefault, hashCode); hashCode = HashCodeHelper.GetHashCode(IsActive, hashCode); return hashCode; } } + /// + /// + /// + /// + /// + /// + public static bool operator ==(DocumentCategory left, DocumentCategory right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(DocumentCategory left, DocumentCategory right) + { + return !Equals(left, right); + } + #endregion /// /// /// /// - private string DebuggerDisplay => $"[{nameof(TimeEntryActivity)}:{ToString()}, IsDefault={IsDefault.ToString(CultureInfo.InvariantCulture)}, IsActive={IsActive.ToString(CultureInfo.InvariantCulture)}]"; + private string DebuggerDisplay => $"[{nameof(DocumentCategory)}, IsDefault={IsDefault.ToString(CultureInfo.InvariantCulture)}, IsActive={IsActive.ToString(CultureInfo.InvariantCulture)}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Error.cs b/src/redmine-net-api/Types/Error.cs index 96621f51..96cd20bd 100644 --- a/src/redmine-net-api/Types/Error.cs +++ b/src/redmine-net-api/Types/Error.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -143,11 +143,33 @@ public override int GetHashCode() { unchecked { - var hashCode = 13; + var hashCode = 17; hashCode = HashCodeHelper.GetHashCode(Info, hashCode); return hashCode; } } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(Error left, Error right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(Error left, Error right) + { + return !Equals(left, right); + } #endregion /// diff --git a/src/redmine-net-api/Types/File.cs b/src/redmine-net-api/Types/File.cs index 2afc20e1..4630f5f8 100644 --- a/src/redmine-net-api/Types/File.cs +++ b/src/redmine-net-api/Types/File.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -209,20 +209,33 @@ public override void WriteJson(JsonWriter writer) public override bool Equals(File other) { if (other == null) return false; - return Id == other.Id - && Filename == other.Filename - && FileSize == other.FileSize - && Description == other.Description - && ContentType == other.ContentType - && ContentUrl == other.ContentUrl - && Author == other.Author - && CreatedOn == other.CreatedOn - && Version == other.Version - && Digest == other.Digest - && Downloads == other.Downloads - && Token == other.Token; + return base.Equals(other) + && string.Equals(Filename, other.Filename, StringComparison.OrdinalIgnoreCase) + && string.Equals(ContentType, other.ContentType, StringComparison.OrdinalIgnoreCase) + && string.Equals(Description, other.Description, StringComparison.OrdinalIgnoreCase) + && string.Equals(ContentUrl, other.ContentUrl, StringComparison.OrdinalIgnoreCase) + && string.Equals(Digest, other.Digest, StringComparison.OrdinalIgnoreCase) + && Equals(Author, other.Author) + && FileSize == other.FileSize + && CreatedOn == other.CreatedOn + && Version == other.Version + && Downloads == other.Downloads; + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as File); } + /// /// /// @@ -230,22 +243,40 @@ public override bool Equals(File other) public override int GetHashCode() { var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(Filename, hashCode); hashCode = HashCodeHelper.GetHashCode(FileSize, hashCode); hashCode = HashCodeHelper.GetHashCode(ContentType, hashCode); hashCode = HashCodeHelper.GetHashCode(Description, hashCode); - hashCode = HashCodeHelper.GetHashCode(Author, hashCode); hashCode = HashCodeHelper.GetHashCode(ContentUrl, hashCode); - hashCode = HashCodeHelper.GetHashCode(Author, hashCode); hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); hashCode = HashCodeHelper.GetHashCode(Version, hashCode); hashCode = HashCodeHelper.GetHashCode(Digest, hashCode); hashCode = HashCodeHelper.GetHashCode(Downloads, hashCode); - return hashCode; } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(File left, File right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(File left, File right) + { + return !Equals(left, right); + } #endregion /// diff --git a/src/redmine-net-api/Types/Group.cs b/src/redmine-net-api/Types/Group.cs index 9db8382a..45001031 100644 --- a/src/redmine-net-api/Types/Group.cs +++ b/src/redmine-net-api/Types/Group.cs @@ -164,11 +164,10 @@ public override void WriteJson(JsonWriter writer) public bool Equals(Group other) { if (other == null) return false; - return Id == other.Id - && Name == other.Name - && (Users != null ? Users.Equals(other.Users) : other.Users == null) - && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null) - && (Memberships != null ? Memberships.Equals(other.Memberships) : other.Memberships == null); + return base.Equals(other) + && Equals(Users, other.Users) + && Equals(CustomFields, other.CustomFields) + && Equals(Memberships, other.Memberships); } /// @@ -192,15 +191,35 @@ public override int GetHashCode() { unchecked { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); - hashCode = HashCodeHelper.GetHashCode(Name, hashCode); + var hashCode = base.GetHashCode(); hashCode = HashCodeHelper.GetHashCode(Users, hashCode); hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); hashCode = HashCodeHelper.GetHashCode(Memberships, hashCode); return hashCode; } } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(Group left, Group right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(Group left, Group right) + { + return !Equals(left, right); + } #endregion diff --git a/src/redmine-net-api/Types/Identifiable.cs b/src/redmine-net-api/Types/Identifiable.cs index 799355b8..24124e0c 100644 --- a/src/redmine-net-api/Types/Identifiable.cs +++ b/src/redmine-net-api/Types/Identifiable.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,6 +23,7 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Internals; using Redmine.Net.Api.Serialization; +using NotImplementedException = System.NotImplementedException; namespace Redmine.Net.Api.Types { @@ -31,7 +32,8 @@ namespace Redmine.Net.Api.Types /// /// [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] - public abstract class Identifiable : IXmlSerializable, IJsonSerializable, IEquatable, IEquatable> where T : Identifiable + public abstract class Identifiable : IXmlSerializable, IJsonSerializable, IEquatable + where T : Identifiable { #region Properties /// @@ -121,7 +123,7 @@ public override int GetHashCode() { unchecked { - var hashCode = 13; + var hashCode = 17; hashCode = HashCodeHelper.GetHashCode(Id, hashCode); return hashCode; } diff --git a/src/redmine-net-api/Types/IdentifiableName.cs b/src/redmine-net-api/Types/IdentifiableName.cs index 277e6929..d9deafde 100644 --- a/src/redmine-net-api/Types/IdentifiableName.cs +++ b/src/redmine-net-api/Types/IdentifiableName.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -169,6 +169,19 @@ public override bool Equals(IdentifiableName other) return Id == other.Id && string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase); } + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as IdentifiableName); + } + /// /// /// @@ -182,6 +195,28 @@ public override int GetHashCode() return hashCode; } } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(IdentifiableName left, IdentifiableName right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(IdentifiableName left, IdentifiableName right) + { + return !Equals(left, right); + } #endregion /// diff --git a/src/redmine-net-api/Types/Issue.cs b/src/redmine-net-api/Types/Issue.cs index 7864a2c5..8559b3ce 100644 --- a/src/redmine-net-api/Types/Issue.cs +++ b/src/redmine-net-api/Types/Issue.cs @@ -38,7 +38,8 @@ namespace Redmine.Net.Api.Types /// [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.ISSUE)] - public sealed class Issue : Identifiable, ICloneable + public sealed class Issue : + Identifiable { #region Properties /// @@ -488,21 +489,34 @@ public override bool Equals(Issue other) && DueDate == other.DueDate && DoneRatio == other.DoneRatio && EstimatedHours == other.EstimatedHours - && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null) + && SpentHours == other.SpentHours && CreatedOn == other.CreatedOn && UpdatedOn == other.UpdatedOn && AssignedTo == other.AssignedTo && FixedVersion == other.FixedVersion && Notes == other.Notes - && (Watchers != null ? Watchers.Equals(other.Watchers) : other.Watchers == null) && ClosedOn == other.ClosedOn - && SpentHours == other.SpentHours && PrivateNotes == other.PrivateNotes - && (Attachments != null ? Attachments.Equals(other.Attachments) : other.Attachments == null) - && (ChangeSets != null ? ChangeSets.Equals(other.ChangeSets) : other.ChangeSets == null) - && (Children != null ? Children.Equals(other.Children) : other.Children == null) - && (Journals != null ? Journals.Equals(other.Journals) : other.Journals == null) - && (Relations != null ? Relations.Equals(other.Relations) : other.Relations == null); + && Attachments.Equals(other.Attachments) + && CustomFields.Equals(other.CustomFields) + && ChangeSets.Equals(other.ChangeSets) + && Children.Equals(other.Children) + && Journals.Equals(other.Journals) + && Relations.Equals(other.Relations) + && Watchers.Equals(other.Watchers); + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as Issue); } /// @@ -550,6 +564,28 @@ public override int GetHashCode() return hashCode; } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(Issue left, Issue right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(Issue left, Issue right) + { + return !Equals(left, right); + } #endregion #region Implementation of IClonable @@ -564,7 +600,7 @@ public object Clone() AssignedTo = AssignedTo, Author = Author, Category = Category, - CustomFields = CustomFields.Clone(), + CustomFields = CustomFields, Description = Description, DoneRatio = DoneRatio, DueDate = DueDate, @@ -578,7 +614,7 @@ public object Clone() Project = Project, FixedVersion = FixedVersion, Notes = Notes, - Watchers = Watchers.Clone() + Watchers = Watchers }; return issue; } diff --git a/src/redmine-net-api/Types/IssueAllowedStatus.cs b/src/redmine-net-api/Types/IssueAllowedStatus.cs index 35a1478c..88013681 100644 --- a/src/redmine-net-api/Types/IssueAllowedStatus.cs +++ b/src/redmine-net-api/Types/IssueAllowedStatus.cs @@ -19,6 +19,7 @@ limitations under the License. using System.Xml.Serialization; using Newtonsoft.Json; using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Internals; namespace Redmine.Net.Api.Types { @@ -66,6 +67,68 @@ public override void ReadJson(JsonReader reader) } } + /// + /// + /// + /// + /// + public bool Equals(IssueAllowedStatus other) + { + if (other == null) return false; + return Id == other.Id + && Name == other.Name + && IsClosed == other.IsClosed; + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as IssueAllowedStatus); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + unchecked + { + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(IsClosed, hashCode); + return hashCode; + } + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(IssueAllowedStatus left, IssueAllowedStatus right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(IssueAllowedStatus left, IssueAllowedStatus right) + { + return !Equals(left, right); + } + private string DebuggerDisplay => $"[{nameof(IssueAllowedStatus)}: {ToString()}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/IssueCategory.cs b/src/redmine-net-api/Types/IssueCategory.cs index c7fb203e..abde781e 100644 --- a/src/redmine-net-api/Types/IssueCategory.cs +++ b/src/redmine-net-api/Types/IssueCategory.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -153,6 +153,19 @@ public override bool Equals(IssueCategory other) return Id == other.Id && Project == other.Project && AssignTo == other.AssignTo && Name == other.Name; } + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as IssueCategory); + } + /// /// /// @@ -161,14 +174,35 @@ public override int GetHashCode() { unchecked { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); + var hashCode = base.GetHashCode(); hashCode = HashCodeHelper.GetHashCode(Project, hashCode); hashCode = HashCodeHelper.GetHashCode(AssignTo, hashCode); hashCode = HashCodeHelper.GetHashCode(Name, hashCode); return hashCode; } } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(IssueCategory left, IssueCategory right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(IssueCategory left, IssueCategory right) + { + return !Equals(left, right); + } #endregion /// diff --git a/src/redmine-net-api/Types/IssueChild.cs b/src/redmine-net-api/Types/IssueChild.cs index f0821625..317aa80f 100644 --- a/src/redmine-net-api/Types/IssueChild.cs +++ b/src/redmine-net-api/Types/IssueChild.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -30,7 +30,7 @@ namespace Redmine.Net.Api.Types /// [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.ISSUE)] - public sealed class IssueChild : Identifiable, ICloneable + public sealed class IssueChild : Identifiable { #region Properties /// @@ -113,9 +113,23 @@ public override void ReadJson(JsonReader reader) public override bool Equals(IssueChild other) { if (other == null) return false; - return Id == other.Id && Tracker == other.Tracker && Subject == other.Subject; + return base.Equals(other) + && Tracker == other.Tracker && Subject == other.Subject; } + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as IssueChild); + } + /// /// /// @@ -124,26 +138,51 @@ public override int GetHashCode() { unchecked { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); + var hashCode = base.GetHashCode(); hashCode = HashCodeHelper.GetHashCode(Tracker, hashCode); hashCode = HashCodeHelper.GetHashCode(Subject, hashCode); return hashCode; } } - #endregion + + /// + /// + /// + /// + /// + /// + public static bool operator ==(IssueChild left, IssueChild right) + { + return Equals(left, right); + } - #region Implementation of IClonable + /// + /// + /// + /// + /// + /// + public static bool operator !=(IssueChild left, IssueChild right) + { + return !Equals(left, right); + } + #endregion + #region Implementation of IClonable /// /// /// /// - public object Clone() + public new IssueChild Clone() { - var issueChild = new IssueChild { Subject = Subject, Tracker = Tracker }; - return issueChild; + return new IssueChild + { + Id = Id, + Tracker = Tracker, + Subject = Subject + }; } + #endregion /// @@ -151,6 +190,5 @@ public object Clone() /// /// private string DebuggerDisplay => $"[{nameof(IssueChild)}: {ToString()}, Tracker={Tracker}, Subject={Subject}]"; - } } diff --git a/src/redmine-net-api/Types/IssueCustomField.cs b/src/redmine-net-api/Types/IssueCustomField.cs index 65ad1049..5c570862 100644 --- a/src/redmine-net-api/Types/IssueCustomField.cs +++ b/src/redmine-net-api/Types/IssueCustomField.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -31,7 +31,9 @@ namespace Redmine.Net.Api.Types /// [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.CUSTOM_FIELD)] - public sealed class IssueCustomField : IdentifiableName, IEquatable, ICloneable, IValue + public sealed class IssueCustomField : + IdentifiableName + ,IEquatable { #region Properties /// @@ -214,9 +216,22 @@ public bool Equals(IssueCustomField other) return Id == other.Id && Name == other.Name && Multiple == other.Multiple - && (Values != null ? Values.Equals(other.Values) : other.Values == null); + && Values.Equals(other.Values); } + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as IssueCustomField); + } + /// /// /// @@ -225,14 +240,34 @@ public override int GetHashCode() { unchecked { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); - hashCode = HashCodeHelper.GetHashCode(Name, hashCode); + var hashCode = base.GetHashCode(); hashCode = HashCodeHelper.GetHashCode(Values, hashCode); hashCode = HashCodeHelper.GetHashCode(Multiple, hashCode); return hashCode; } } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(IssueCustomField left, IssueCustomField right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(IssueCustomField left, IssueCustomField right) + { + return !Equals(left, right); + } #endregion #region Implementation of IClonable @@ -242,7 +277,7 @@ public override int GetHashCode() /// public object Clone() { - var issueCustomField = new IssueCustomField { Multiple = Multiple, Values = Values.Clone() }; + var issueCustomField = new IssueCustomField { Multiple = Multiple, Values = Values }; return issueCustomField; } #endregion @@ -270,15 +305,5 @@ public static string GetValue(object item) /// /// private string DebuggerDisplay => $"[{nameof(IssueCustomField)}: {ToString()} Values={Values.Dump()}, Multiple={Multiple.ToString(CultureInfo.InvariantCulture)}]"; - - /// - /// - /// - /// - /// - public override bool Equals(object obj) - { - return Equals(obj as IssueCustomField); - } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/IssuePriority.cs b/src/redmine-net-api/Types/IssuePriority.cs index 081e41c4..305cf04f 100644 --- a/src/redmine-net-api/Types/IssuePriority.cs +++ b/src/redmine-net-api/Types/IssuePriority.cs @@ -30,7 +30,9 @@ namespace Redmine.Net.Api.Types /// [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.ISSUE_PRIORITY)] - public sealed class IssuePriority : IdentifiableName, IEquatable + public sealed class IssuePriority : + IdentifiableName + ,IEquatable { #region Properties /// @@ -114,7 +116,9 @@ public bool Equals(IssuePriority other) { if (other == null) return false; - return Id == other.Id && Name == other.Name && IsDefault == other.IsDefault && IsActive == other.IsActive; + return Id == other.Id && Name == other.Name + && IsDefault == other.IsDefault + && IsActive == other.IsActive; } /// @@ -138,14 +142,34 @@ public override int GetHashCode() { unchecked { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); - hashCode = HashCodeHelper.GetHashCode(Name, hashCode); + var hashCode = base.GetHashCode(); hashCode = HashCodeHelper.GetHashCode(IsDefault, hashCode); hashCode = HashCodeHelper.GetHashCode(IsActive, hashCode); return hashCode; } } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(IssuePriority left, IssuePriority right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(IssuePriority left, IssuePriority right) + { + return !Equals(left, right); + } #endregion /// diff --git a/src/redmine-net-api/Types/IssueRelation.cs b/src/redmine-net-api/Types/IssueRelation.cs index 7f7e8381..84c9c501 100644 --- a/src/redmine-net-api/Types/IssueRelation.cs +++ b/src/redmine-net-api/Types/IssueRelation.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -32,7 +32,8 @@ namespace Redmine.Net.Api.Types /// [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.RELATION)] - public sealed class IssueRelation : Identifiable + public sealed class IssueRelation : + Identifiable { #region Properties /// @@ -219,6 +220,19 @@ public override bool Equals(IssueRelation other) if (other == null) return false; return Id == other.Id && IssueId == other.IssueId && IssueToId == other.IssueToId && Type == other.Type && Delay == other.Delay; } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as IssueRelation); + } /// /// @@ -228,8 +242,7 @@ public override int GetHashCode() { unchecked { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); + var hashCode = base.GetHashCode(); hashCode = HashCodeHelper.GetHashCode(IssueId, hashCode); hashCode = HashCodeHelper.GetHashCode(IssueToId, hashCode); hashCode = HashCodeHelper.GetHashCode(Type, hashCode); @@ -237,6 +250,28 @@ public override int GetHashCode() return hashCode; } } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(IssueRelation left, IssueRelation right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(IssueRelation left, IssueRelation right) + { + return !Equals(left, right); + } #endregion /// diff --git a/src/redmine-net-api/Types/IssueRelationType.cs b/src/redmine-net-api/Types/IssueRelationType.cs index 35a34519..e2564de4 100644 --- a/src/redmine-net-api/Types/IssueRelationType.cs +++ b/src/redmine-net-api/Types/IssueRelationType.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,11 +14,7 @@ You may obtain a copy of the License at limitations under the License. */ -using System; using System.Xml.Serialization; -using Newtonsoft.Json; -using Newtonsoft.Json.Converters; -using System.Runtime.Serialization; namespace Redmine.Net.Api.Types { @@ -81,78 +77,4 @@ public enum IssueRelationType [XmlEnum("copied_from")] CopiedFrom } - - // /// - // public class IssueRelationTypeConverter : JsonConverter - // { - // /// - // public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - // { - // IssueRelationType messageTransportResponseStatus = (IssueRelationType) value; - // - // switch (messageTransportResponseStatus) - // { - // case IssueRelationType.Undefined: - // break; - // case IssueRelationType.Relates: - // writer.WriteValue("relates"); - // break; - // case IssueRelationType.Duplicates: - // writer.WriteValue("duplicates"); - // break; - // case IssueRelationType.Duplicated: - // writer.WriteValue("duplicated"); - // break; - // case IssueRelationType.Blocks: - // writer.WriteValue("blocks"); - // break; - // case IssueRelationType.Blocked: - // writer.WriteValue("blocked"); - // break; - // case IssueRelationType.Precedes: - // writer.WriteValue("precedes"); - // break; - // case IssueRelationType.Follows: - // writer.WriteValue("follows"); - // break; - // case IssueRelationType.CopiedTo: - // writer.WriteValue("copied_to"); - // break; - // case IssueRelationType.CopiedFrom: - // writer.WriteValue("copied_from"); - // break; - // default: - // throw new ArgumentOutOfRangeException(); - // } - // } - // - // /// - // public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - // { - // var enumString = (string) reader.Value; - // switch (enumString) - // { - // case "relates": - // case "duplicates": - // case "duplicated": - // case "blocks": - // case "blocked": - // case "precedes": - // case "follows": - // return Enum.Parse(typeof(IssueRelationType), enumString, true); - // case "copied_to": - // return IssueRelationType.CopiedTo; - // case "copied_from": - // return IssueRelationType.CopiedFrom; - // default: - // throw new ArgumentOutOfRangeException(); - // } - // } - // - // /// - // public override bool CanConvert(Type objectType) - // { - // return objectType == typeof(string); - // } - // } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/IssueStatus.cs b/src/redmine-net-api/Types/IssueStatus.cs index 80164ece..9f4657c0 100644 --- a/src/redmine-net-api/Types/IssueStatus.cs +++ b/src/redmine-net-api/Types/IssueStatus.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -140,14 +140,34 @@ public override int GetHashCode() { unchecked { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); - hashCode = HashCodeHelper.GetHashCode(Name, hashCode); + var hashCode = base.GetHashCode(); hashCode = HashCodeHelper.GetHashCode(IsClosed, hashCode); hashCode = HashCodeHelper.GetHashCode(IsDefault, hashCode); return hashCode; } } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(IssueStatus left, IssueStatus right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(IssueStatus left, IssueStatus right) + { + return !Equals(left, right); + } #endregion /// diff --git a/src/redmine-net-api/Types/Journal.cs b/src/redmine-net-api/Types/Journal.cs index 5ed9b608..6f776b24 100644 --- a/src/redmine-net-api/Types/Journal.cs +++ b/src/redmine-net-api/Types/Journal.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -31,7 +31,8 @@ namespace Redmine.Net.Api.Types /// [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.JOURNAL)] - public sealed class Journal : Identifiable + public sealed class Journal : + Identifiable { #region Properties /// @@ -180,13 +181,29 @@ public override void WriteJson(JsonWriter writer) public override bool Equals(Journal other) { if (other == null) return false; - return Id == other.Id - && User == other.User - && Notes == other.Notes - && CreatedOn == other.CreatedOn - && (Details != null ? Details.Equals(other.Details) : other.Details == null); + return base.Equals(other) + && Equals(User, other.User) + && Equals(Details, other.Details) + && string.Equals(Notes, other.Notes, StringComparison.OrdinalIgnoreCase) + && CreatedOn == other.CreatedOn + && UpdatedOn == other.UpdatedOn + && Equals(UpdatedBy, other.UpdatedBy) + && PrivateNotes == other.PrivateNotes; } + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as Journal); + } + /// /// /// @@ -195,15 +212,39 @@ public override int GetHashCode() { unchecked { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); + var hashCode = base.GetHashCode(); hashCode = HashCodeHelper.GetHashCode(User, hashCode); hashCode = HashCodeHelper.GetHashCode(Notes, hashCode); hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); hashCode = HashCodeHelper.GetHashCode(Details, hashCode); + hashCode = HashCodeHelper.GetHashCode(PrivateNotes, hashCode); + hashCode = HashCodeHelper.GetHashCode(UpdatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(UpdatedBy, hashCode); return hashCode; } } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(Journal left, Journal right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(Journal left, Journal right) + { + return !Equals(left, right); + } #endregion /// diff --git a/src/redmine-net-api/Types/Membership.cs b/src/redmine-net-api/Types/Membership.cs index d74e22c7..4243030e 100644 --- a/src/redmine-net-api/Types/Membership.cs +++ b/src/redmine-net-api/Types/Membership.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -128,6 +128,19 @@ public override bool Equals(Membership other) && Project != null ? Project.Equals(other.Project) : other.Project == null && Roles != null ? Roles.Equals(other.Roles) : other.Roles == null; } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as Membership); + } /// /// @@ -137,8 +150,7 @@ public override int GetHashCode() { unchecked { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); + var hashCode = base.GetHashCode(); hashCode = HashCodeHelper.GetHashCode(Group, hashCode); hashCode = HashCodeHelper.GetHashCode(Project, hashCode); hashCode = HashCodeHelper.GetHashCode(User, hashCode); @@ -146,6 +158,28 @@ public override int GetHashCode() return hashCode; } } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(Membership left, Membership right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(Membership left, Membership right) + { + return !Equals(left, right); + } #endregion /// diff --git a/src/redmine-net-api/Types/MembershipRole.cs b/src/redmine-net-api/Types/MembershipRole.cs index 25af2590..b1069206 100644 --- a/src/redmine-net-api/Types/MembershipRole.cs +++ b/src/redmine-net-api/Types/MembershipRole.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -114,7 +114,8 @@ public override void WriteJson(JsonWriter writer) public bool Equals(MembershipRole other) { if (other == null) return false; - return Id == other.Id && Name == other.Name && Inherited == other.Inherited; + return base.Equals(other) + && Inherited == other.Inherited; } /// @@ -138,13 +139,33 @@ public override int GetHashCode() { unchecked { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); - hashCode = HashCodeHelper.GetHashCode(Name, hashCode); + var hashCode = base.GetHashCode(); hashCode = HashCodeHelper.GetHashCode(Inherited, hashCode); return hashCode; } } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(MembershipRole left, MembershipRole right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(MembershipRole left, MembershipRole right) + { + return !Equals(left, right); + } #endregion #region Implementation of IClonable diff --git a/src/redmine-net-api/Types/MyAccount.cs b/src/redmine-net-api/Types/MyAccount.cs index 3138b036..fc86e19a 100644 --- a/src/redmine-net-api/Types/MyAccount.cs +++ b/src/redmine-net-api/Types/MyAccount.cs @@ -190,10 +190,24 @@ public override bool Equals(MyAccount other) && string.Equals(FirstName, other.FirstName, StringComparison.OrdinalIgnoreCase) && string.Equals(LastName, other.LastName, StringComparison.OrdinalIgnoreCase) && string.Equals(ApiKey, other.ApiKey, StringComparison.OrdinalIgnoreCase) + && Email.Equals(other.Email, StringComparison.OrdinalIgnoreCase) && IsAdmin == other.IsAdmin && CreatedOn == other.CreatedOn && LastLoginOn == other.LastLoginOn - && (CustomFields?.Equals(other.CustomFields) ?? other.CustomFields == null); + && CustomFields.Equals(other.CustomFields); + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as MyAccount); } /// @@ -214,6 +228,28 @@ public override int GetHashCode() return hashCode; } } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(MyAccount left, MyAccount right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(MyAccount left, MyAccount right) + { + return !Equals(left, right); + } private string DebuggerDisplay => $@"[ {nameof(MyAccount)}: Id={Id.ToString(CultureInfo.InvariantCulture)}, diff --git a/src/redmine-net-api/Types/MyAccountCustomField.cs b/src/redmine-net-api/Types/MyAccountCustomField.cs index a09a6bbe..f01ab673 100644 --- a/src/redmine-net-api/Types/MyAccountCustomField.cs +++ b/src/redmine-net-api/Types/MyAccountCustomField.cs @@ -108,12 +108,28 @@ public override void WriteJson(JsonWriter writer) { } - /// - public override bool Equals(IdentifiableName other) + /// + /// + /// + /// + /// + public override bool Equals(object obj) { - var result = base.Equals(other); - - return result && string.Equals(Value,((MyAccountCustomField)other)?.Value, StringComparison.OrdinalIgnoreCase); + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as MyAccountCustomField); + } + + /// + /// + /// + /// + /// + public bool Equals(MyAccountCustomField other) + { + return base.Equals(other) + && string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); } /// @@ -126,6 +142,28 @@ public override int GetHashCode() return hashCode; } } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(MyAccountCustomField left, MyAccountCustomField right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(MyAccountCustomField left, MyAccountCustomField right) + { + return !Equals(left, right); + } /// /// diff --git a/src/redmine-net-api/Types/News.cs b/src/redmine-net-api/Types/News.cs index e5fbb3b4..03305ab0 100644 --- a/src/redmine-net-api/Types/News.cs +++ b/src/redmine-net-api/Types/News.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -202,16 +202,32 @@ public override void WriteJson(JsonWriter writer) /// /// /// - public override bool Equals(News other) + public new bool Equals(News other) { - if (other == null) return false; - return Id == other.Id - && Project == other.Project - && Author == other.Author - && string.Equals(Title,other.Title,StringComparison.OrdinalIgnoreCase) - && string.Equals(Summary, other.Summary, StringComparison.OrdinalIgnoreCase) - && string.Equals(Description, other.Description, StringComparison.OrdinalIgnoreCase) - && CreatedOn == other.CreatedOn; + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + + return base.Equals(other) + && Equals(Project, other.Project) + && Equals(Author, other.Author) + && string.Equals(Title, other.Title, StringComparison.Ordinal) + && string.Equals(Summary, other.Summary, StringComparison.Ordinal) + && string.Equals(Description, other.Description, StringComparison.Ordinal) + && CreatedOn.Equals(other.CreatedOn) + && Equals(Comments, other.Comments); + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as News); } /// @@ -229,9 +245,32 @@ public override int GetHashCode() hashCode = HashCodeHelper.GetHashCode(Summary, hashCode); hashCode = HashCodeHelper.GetHashCode(Description, hashCode); hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(Comments, hashCode); return hashCode; } } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(News left, News right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(News left, News right) + { + return !Equals(left, right); + } #endregion /// diff --git a/src/redmine-net-api/Types/NewsComment.cs b/src/redmine-net-api/Types/NewsComment.cs index 48496829..92057ba9 100644 --- a/src/redmine-net-api/Types/NewsComment.cs +++ b/src/redmine-net-api/Types/NewsComment.cs @@ -104,6 +104,19 @@ public override bool Equals(NewsComment other) if (other == null) return false; return Id == other.Id && Author == other.Author && Content == other.Content; } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as NewsComment); + } /// public override int GetHashCode() @@ -115,6 +128,28 @@ public override int GetHashCode() return hashCode; } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(NewsComment left, NewsComment right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(NewsComment left, NewsComment right) + { + return !Equals(left, right); + } private string DebuggerDisplay => $@"[{nameof(IssueAllowedStatus)}: {ToString()}, {nameof(NewsComment)}: {ToString()}, diff --git a/src/redmine-net-api/Types/Permission.cs b/src/redmine-net-api/Types/Permission.cs index fcfcbbf1..d83e887f 100644 --- a/src/redmine-net-api/Types/Permission.cs +++ b/src/redmine-net-api/Types/Permission.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -122,11 +122,33 @@ public override int GetHashCode() { unchecked { - var hashCode = 13; + var hashCode = 17; hashCode = HashCodeHelper.GetHashCode(Info, hashCode); return hashCode; } } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(Permission left, Permission right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(Permission left, Permission right) + { + return !Equals(left, right); + } #endregion /// diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs index 44dd0c0d..2af4593f 100644 --- a/src/redmine-net-api/Types/Project.cs +++ b/src/redmine-net-api/Types/Project.cs @@ -307,23 +307,37 @@ public bool Equals(Project other) return false; } - return Id == other.Id - && string.Equals(Identifier, other.Identifier, StringComparison.OrdinalIgnoreCase) - && string.Equals(Description, other.Description, StringComparison.OrdinalIgnoreCase) - && (Parent != null ? Parent.Equals(other.Parent) : other.Parent == null) - && string.Equals(HomePage, other.HomePage, StringComparison.OrdinalIgnoreCase) - && CreatedOn == other.CreatedOn - && UpdatedOn == other.UpdatedOn - && Status == other.Status - && IsPublic == other.IsPublic - && InheritMembers == other.InheritMembers - && (Trackers != null ? Trackers.Equals(other.Trackers) : other.Trackers == null) - && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null) - && (IssueCategories != null ? IssueCategories.Equals(other.IssueCategories) : other.IssueCategories == null) - && (EnabledModules != null ? EnabledModules.Equals(other.EnabledModules) : other.EnabledModules == null) - && (TimeEntryActivities != null ? TimeEntryActivities.Equals(other.TimeEntryActivities) : other.TimeEntryActivities == null) - && (DefaultAssignee != null ? DefaultAssignee.Equals(other.DefaultAssignee) : other.DefaultAssignee == null) - && (DefaultVersion != null ? DefaultVersion.Equals(other.DefaultVersion) : other.DefaultVersion == null); + return base.Equals(other) + && string.Equals(Identifier, other.Identifier, StringComparison.OrdinalIgnoreCase) + && string.Equals(Description, other.Description, StringComparison.OrdinalIgnoreCase) + && string.Equals(HomePage, other.HomePage, StringComparison.OrdinalIgnoreCase) + && string.Equals(Identifier, other.Identifier, StringComparison.OrdinalIgnoreCase) + && CreatedOn == other.CreatedOn + && UpdatedOn == other.UpdatedOn + && Status == other.Status + && IsPublic == other.IsPublic + && InheritMembers == other.InheritMembers + && Equals(DefaultAssignee, other.DefaultAssignee) + && Equals(DefaultVersion, other.DefaultVersion) + && Equals(Parent, other.Parent) + && Equals(Trackers, other.Trackers) + && Equals(CustomFields, other.CustomFields) + && Equals(IssueCategories, other.IssueCategories) + && Equals(EnabledModules, other.EnabledModules) + && Equals(TimeEntryActivities, other.TimeEntryActivities); + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as Project); } /// @@ -355,6 +369,28 @@ public override int GetHashCode() return hashCode; } } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(Project left, Project right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(Project left, Project right) + { + return !Equals(left, right); + } #endregion /// @@ -379,15 +415,5 @@ public override int GetHashCode() IssueCategories={IssueCategories.Dump()}, EnabledModules={EnabledModules.Dump()}, TimeEntryActivities = {TimeEntryActivities.Dump()}]"; - - /// - /// - /// - /// - /// - public override bool Equals(object obj) - { - return Equals(obj as Project); - } } } diff --git a/src/redmine-net-api/Types/ProjectMembership.cs b/src/redmine-net-api/Types/ProjectMembership.cs index 8ed99f79..8a5fd6ce 100644 --- a/src/redmine-net-api/Types/ProjectMembership.cs +++ b/src/redmine-net-api/Types/ProjectMembership.cs @@ -162,12 +162,25 @@ public override bool Equals(ProjectMembership other) { if (other == null) return false; return Id == other.Id - && Project.Equals(other.Project) - && Roles.Equals(other.Roles) - && (User != null ? User.Equals(other.User) : other.User == null) - && (Group != null ? Group.Equals(other.Group) : other.Group == null); + && Equals(Project, other.Project) + && Equals(Roles, other.Roles) + && Equals(User, other.User) + && Equals(Group, other.Group); } + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as ProjectMembership); + } + /// /// /// @@ -184,6 +197,28 @@ public override int GetHashCode() return hashCode; } } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(ProjectMembership left, ProjectMembership right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(ProjectMembership left, ProjectMembership right) + { + return !Equals(left, right); + } #endregion /// diff --git a/src/redmine-net-api/Types/Query.cs b/src/redmine-net-api/Types/Query.cs index 3c68cf8e..f5ef00c8 100644 --- a/src/redmine-net-api/Types/Query.cs +++ b/src/redmine-net-api/Types/Query.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -116,9 +116,24 @@ public bool Equals(Query other) { if (other == null) return false; - return other.Id == Id && other.Name == Name && other.IsPublic == IsPublic && other.ProjectId == ProjectId; + return base.Equals(other) + && IsPublic == other.IsPublic + && ProjectId == other.ProjectId; } - + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as Query); + } + /// /// /// @@ -127,30 +142,40 @@ public override int GetHashCode() { unchecked { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); - hashCode = HashCodeHelper.GetHashCode(Name, hashCode); + var hashCode = base.GetHashCode(); hashCode = HashCodeHelper.GetHashCode(IsPublic, hashCode); hashCode = HashCodeHelper.GetHashCode(ProjectId, hashCode); return hashCode; } } - #endregion - + /// /// /// + /// + /// /// - private string DebuggerDisplay => $"[{nameof(Query)}: {ToString()}, IsPublic={IsPublic.ToString(CultureInfo.InvariantCulture)}, ProjectId={ProjectId?.ToString(CultureInfo.InvariantCulture)}]"; + public static bool operator ==(Query left, Query right) + { + return Equals(left, right); + } /// /// /// - /// + /// + /// /// - public override bool Equals(object obj) + public static bool operator !=(Query left, Query right) { - return Equals(obj as Query); + return !Equals(left, right); } + #endregion + + /// + /// + /// + /// + private string DebuggerDisplay => $"[{nameof(Query)}: {ToString()}, IsPublic={IsPublic.ToString(CultureInfo.InvariantCulture)}, ProjectId={ProjectId?.ToString(CultureInfo.InvariantCulture)}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Role.cs b/src/redmine-net-api/Types/Role.cs index e2b6384a..4998f2a4 100644 --- a/src/redmine-net-api/Types/Role.cs +++ b/src/redmine-net-api/Types/Role.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -168,9 +168,7 @@ public override int GetHashCode() { unchecked { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); - hashCode = HashCodeHelper.GetHashCode(Name, hashCode); + var hashCode = base.GetHashCode(); hashCode = HashCodeHelper.GetHashCode(IsAssignable, hashCode); hashCode = HashCodeHelper.GetHashCode(IssuesVisibility, hashCode); hashCode = HashCodeHelper.GetHashCode(TimeEntriesVisibility, hashCode); @@ -179,6 +177,28 @@ public override int GetHashCode() return hashCode; } } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(Role left, Role right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(Role left, Role right) + { + return !Equals(left, right); + } #endregion /// diff --git a/src/redmine-net-api/Types/Search.cs b/src/redmine-net-api/Types/Search.cs index 9f35e29a..72aa88d7 100644 --- a/src/redmine-net-api/Types/Search.cs +++ b/src/redmine-net-api/Types/Search.cs @@ -123,16 +123,24 @@ public void ReadJson(JsonReader reader) public bool Equals(Search other) { if (other == null) return false; - return Id == other.Id && string.Equals(Title, other.Title, StringComparison.OrdinalIgnoreCase) - && string.Equals(Description, other.Description, StringComparison.OrdinalIgnoreCase) - && string.Equals(Url, other.Url, StringComparison.OrdinalIgnoreCase) - && string.Equals(Type, other.Type, StringComparison.OrdinalIgnoreCase) - && DateTime == other.DateTime; + return Id == other.Id + && string.Equals(Title, other.Title, StringComparison.OrdinalIgnoreCase) + && string.Equals(Description, other.Description, StringComparison.OrdinalIgnoreCase) + && string.Equals(Url, other.Url, StringComparison.OrdinalIgnoreCase) + && string.Equals(Type, other.Type, StringComparison.OrdinalIgnoreCase) + && DateTime == other.DateTime; } - - /// + + /// + /// + /// + /// + /// public override bool Equals(object obj) { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; return Equals(obj as Search); } @@ -151,6 +159,28 @@ public override int GetHashCode() return hashCode; } } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(Search left, Search right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(Search left, Search right) + { + return !Equals(left, right); + } private string DebuggerDisplay => $@"[{nameof(Search)}:Id={Id.ToString(CultureInfo.InvariantCulture)},Title={Title},Type={Type},Url={Url},Description={Description}, DateTime={DateTime?.ToString("u", CultureInfo.InvariantCulture)}]"; } diff --git a/src/redmine-net-api/Types/TimeEntry.cs b/src/redmine-net-api/Types/TimeEntry.cs index 1a088178..a6691bdb 100644 --- a/src/redmine-net-api/Types/TimeEntry.cs +++ b/src/redmine-net-api/Types/TimeEntry.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -33,7 +33,7 @@ namespace Redmine.Net.Api.Types /// [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.TIME_ENTRY)] - public sealed class TimeEntry : Identifiable, ICloneable + public sealed class TimeEntry : Identifiable { #region Properties private string comments; @@ -236,9 +236,22 @@ public override bool Equals(TimeEntry other) && User == other.User && CreatedOn == other.CreatedOn && UpdatedOn == other.UpdatedOn - && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null); + && Equals(CustomFields, other.CustomFields); } + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as TimeEntry); + } + /// /// /// @@ -261,6 +274,28 @@ public override int GetHashCode() return hashCode; } } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(TimeEntry left, TimeEntry right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(TimeEntry left, TimeEntry right) + { + return !Equals(left, right); + } #endregion #region Implementation of ICloneable diff --git a/src/redmine-net-api/Types/TimeEntryActivity.cs b/src/redmine-net-api/Types/TimeEntryActivity.cs index 762cc5e8..dbc6afe3 100644 --- a/src/redmine-net-api/Types/TimeEntryActivity.cs +++ b/src/redmine-net-api/Types/TimeEntryActivity.cs @@ -156,15 +156,34 @@ public override int GetHashCode() { unchecked { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); - hashCode = HashCodeHelper.GetHashCode(Name, hashCode); + var hashCode = base.GetHashCode(); hashCode = HashCodeHelper.GetHashCode(IsDefault, hashCode); hashCode = HashCodeHelper.GetHashCode(IsActive, hashCode); return hashCode; } } + /// + /// + /// + /// + /// + /// + public static bool operator ==(TimeEntryActivity left, TimeEntryActivity right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(TimeEntryActivity left, TimeEntryActivity right) + { + return !Equals(left, right); + } #endregion /// diff --git a/src/redmine-net-api/Types/Tracker.cs b/src/redmine-net-api/Types/Tracker.cs index d203e6c2..891ae4d0 100644 --- a/src/redmine-net-api/Types/Tracker.cs +++ b/src/redmine-net-api/Types/Tracker.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -145,12 +145,35 @@ public override int GetHashCode() { unchecked { - var hashCode = 13; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); - hashCode = HashCodeHelper.GetHashCode(Name, hashCode); + int hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(DefaultStatus, hashCode); + hashCode = HashCodeHelper.GetHashCode(Description, hashCode); + hashCode = HashCodeHelper.GetHashCode(EnabledStandardFields, hashCode); return hashCode; } } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(Tracker left, Tracker right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(Tracker left, Tracker right) + { + return !Equals(left, right); + } #endregion /// @@ -158,6 +181,5 @@ public override int GetHashCode() /// /// private string DebuggerDisplay => $"[{nameof(Tracker)}: {base.ToString()}]"; - } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/TrackerCoreField.cs b/src/redmine-net-api/Types/TrackerCoreField.cs index 5f8edf54..f5e7f6d2 100644 --- a/src/redmine-net-api/Types/TrackerCoreField.cs +++ b/src/redmine-net-api/Types/TrackerCoreField.cs @@ -118,10 +118,32 @@ public override int GetHashCode() { unchecked { - var hashCode = 13; + var hashCode = 17; hashCode = HashCodeHelper.GetHashCode(Name, hashCode); return hashCode; } } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(TrackerCoreField left, TrackerCoreField right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(TrackerCoreField left, TrackerCoreField right) + { + return !Equals(left, right); + } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Upload.cs b/src/redmine-net-api/Types/Upload.cs index 073690c8..00beb32f 100644 --- a/src/redmine-net-api/Types/Upload.cs +++ b/src/redmine-net-api/Types/Upload.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -196,7 +196,7 @@ public override int GetHashCode() { unchecked { - var hashCode = 13; + var hashCode = 17; hashCode = HashCodeHelper.GetHashCode(Token, hashCode); hashCode = HashCodeHelper.GetHashCode(FileName, hashCode); hashCode = HashCodeHelper.GetHashCode(Description, hashCode); @@ -204,6 +204,28 @@ public override int GetHashCode() return hashCode; } } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(Upload left, Upload right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(Upload left, Upload right) + { + return !Equals(left, right); + } #endregion /// diff --git a/src/redmine-net-api/Types/User.cs b/src/redmine-net-api/Types/User.cs index 64225abb..7633af2a 100644 --- a/src/redmine-net-api/Types/User.cs +++ b/src/redmine-net-api/Types/User.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -343,14 +343,26 @@ public override bool Equals(User other) && LastLoginOn == other.LastLoginOn && Status == other.Status && MustChangePassword == other.MustChangePassword - && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null) - && (Memberships != null ? Memberships.Equals(other.Memberships) : other.Memberships == null) - && (Groups != null ? Groups.Equals(other.Groups) : other.Groups == null) + && Equals(CustomFields, other.CustomFields) + && Equals(Memberships, other.Memberships) + && Equals(Groups, other.Groups) && string.Equals(TwoFactorAuthenticationScheme,other.TwoFactorAuthenticationScheme, StringComparison.OrdinalIgnoreCase) && IsAdmin == other.IsAdmin && PasswordChangedOn == other.PasswordChangedOn - && UpdatedOn == other.UpdatedOn - ; + && UpdatedOn == other.UpdatedOn; + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as User); } /// @@ -385,6 +397,28 @@ public override int GetHashCode() return hashCode; } } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(User left, User right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(User left, User right) + { + return !Equals(left, right); + } #endregion /// diff --git a/src/redmine-net-api/Types/Version.cs b/src/redmine-net-api/Types/Version.cs index d3c52e6e..b03d52bb 100644 --- a/src/redmine-net-api/Types/Version.cs +++ b/src/redmine-net-api/Types/Version.cs @@ -238,12 +238,25 @@ public override bool Equals(Version other) && Sharing == other.Sharing && CreatedOn == other.CreatedOn && UpdatedOn == other.UpdatedOn - && (CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null) + && Equals(CustomFields, other.CustomFields) && string.Equals(WikiPageTitle,other.WikiPageTitle, StringComparison.OrdinalIgnoreCase) && EstimatedHours == other.EstimatedHours - && SpentHours == other.SpentHours - ; + && SpentHours == other.SpentHours; } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as Version); + } + /// /// /// @@ -267,6 +280,28 @@ public override int GetHashCode() return hashCode; } } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(Version left, Version right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(Version left, Version right) + { + return !Equals(left, right); + } #endregion /// diff --git a/src/redmine-net-api/Types/Watcher.cs b/src/redmine-net-api/Types/Watcher.cs index 7dbc2b8d..c3949e37 100644 --- a/src/redmine-net-api/Types/Watcher.cs +++ b/src/redmine-net-api/Types/Watcher.cs @@ -26,7 +26,8 @@ namespace Redmine.Net.Api.Types /// [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.USER)] - public sealed class Watcher : IdentifiableName, IValue, ICloneable + public sealed class Watcher : Identifiable + ,IValue { #region Implementation of IValue /// @@ -43,7 +44,7 @@ public sealed class Watcher : IdentifiableName, IValue, ICloneable /// public object Clone() { - var watcher = new Watcher { Id = Id, Name = Name }; + var watcher = new Watcher { Id = Id }; return watcher; } #endregion From ba344ae9bc9fb71501d07576248ce763b46f85bb Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 30 Mar 2025 21:12:04 +0300 Subject: [PATCH 424/549] [Clone] Add Clone (#377) --- .../Extensions/CollectionExtensions.cs | 6 ++- src/redmine-net-api/ICloneableOfT.cs | 14 ++++++ src/redmine-net-api/Types/Attachment.cs | 35 +++++++++++++ src/redmine-net-api/Types/ChangeSet.cs | 18 +++++++ src/redmine-net-api/Types/CustomFieldValue.cs | 6 +-- src/redmine-net-api/Types/Detail.cs | 11 +++++ src/redmine-net-api/Types/Identifiable.cs | 9 ++++ src/redmine-net-api/Types/IdentifiableName.cs | 14 ++++++ src/redmine-net-api/Types/Issue.cs | 49 ++++++++++++------- src/redmine-net-api/Types/IssueChild.cs | 5 +- src/redmine-net-api/Types/IssueCustomField.cs | 30 ++++++++++-- src/redmine-net-api/Types/IssueRelation.cs | 26 ++++++++++ src/redmine-net-api/Types/Journal.cs | 28 +++++++++++ src/redmine-net-api/Types/TimeEntry.cs | 3 +- src/redmine-net-api/Types/Upload.cs | 15 ++++++ src/redmine-net-api/Types/Watcher.cs | 17 +++++-- 16 files changed, 252 insertions(+), 34 deletions(-) create mode 100644 src/redmine-net-api/ICloneableOfT.cs diff --git a/src/redmine-net-api/Extensions/CollectionExtensions.cs b/src/redmine-net-api/Extensions/CollectionExtensions.cs index 5a14bfe3..60703635 100755 --- a/src/redmine-net-api/Extensions/CollectionExtensions.cs +++ b/src/redmine-net-api/Extensions/CollectionExtensions.cs @@ -17,6 +17,7 @@ limitations under the License. using System; using System.Collections.Generic; using System.Text; +using Redmine.Net.Api.Types; namespace Redmine.Net.Api.Extensions { @@ -30,8 +31,9 @@ public static class CollectionExtensions /// /// /// The list to clone. + /// /// - public static IList Clone(this IList listToClone) where T : ICloneable + public static IList Clone(this IList listToClone, bool resetId) where T : ICloneable { if (listToClone == null) { @@ -43,7 +45,7 @@ public static IList Clone(this IList listToClone) where T : ICloneable for (var index = 0; index < listToClone.Count; index++) { var item = listToClone[index]; - clonedList.Add((T) item.Clone()); + clonedList.Add(item.Clone(resetId)); } return clonedList; diff --git a/src/redmine-net-api/ICloneableOfT.cs b/src/redmine-net-api/ICloneableOfT.cs new file mode 100644 index 00000000..dd58fde2 --- /dev/null +++ b/src/redmine-net-api/ICloneableOfT.cs @@ -0,0 +1,14 @@ +namespace Redmine.Net.Api; + +/// +/// +/// +/// +public interface ICloneable +{ + /// + /// + /// + /// + internal T Clone(bool resetId); +} \ No newline at end of file diff --git a/src/redmine-net-api/Types/Attachment.cs b/src/redmine-net-api/Types/Attachment.cs index f831d647..bc394f77 100644 --- a/src/redmine-net-api/Types/Attachment.cs +++ b/src/redmine-net-api/Types/Attachment.cs @@ -33,6 +33,7 @@ namespace Redmine.Net.Api.Types [XmlRoot(RedmineKeys.ATTACHMENT)] public sealed class Attachment : Identifiable + , ICloneable { #region Properties /// @@ -266,5 +267,39 @@ public override int GetHashCode() Author={Author}, CreatedOn={CreatedOn?.ToString("u", CultureInfo.InvariantCulture)}]"; + /// + /// + /// + /// + public new Attachment Clone(bool resetId) + { + if (resetId) + { + return new Attachment + { + FileName = FileName, + FileSize = FileSize, + ContentType = ContentType, + Description = Description, + ContentUrl = ContentUrl, + ThumbnailUrl = ThumbnailUrl, + Author = Author?.Clone(false), + CreatedOn = CreatedOn + }; + } + + return new Attachment + { + Id = Id, + FileName = FileName, + FileSize = FileSize, + ContentType = ContentType, + Description = Description, + ContentUrl = ContentUrl, + ThumbnailUrl = ThumbnailUrl, + Author = Author?.Clone(true), + CreatedOn = CreatedOn + }; + } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/ChangeSet.cs b/src/redmine-net-api/Types/ChangeSet.cs index bf07bc83..dae77667 100644 --- a/src/redmine-net-api/Types/ChangeSet.cs +++ b/src/redmine-net-api/Types/ChangeSet.cs @@ -33,6 +33,7 @@ namespace Redmine.Net.Api.Types [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.CHANGE_SET)] public sealed class ChangeSet : IXmlSerializable, IJsonSerializable, IEquatable + ,ICloneable { #region Properties /// @@ -157,6 +158,23 @@ public bool Equals(ChangeSet other) && CommittedOn == other.CommittedOn; } + /// + /// + /// + /// + /// + /// + public ChangeSet Clone(bool resetId) + { + return new ChangeSet() + { + User = User, + Comments = Comments, + Revision = Revision, + CommittedOn = CommittedOn, + }; + } + /// /// /// diff --git a/src/redmine-net-api/Types/CustomFieldValue.cs b/src/redmine-net-api/Types/CustomFieldValue.cs index 5cc91ae5..592bb4f4 100644 --- a/src/redmine-net-api/Types/CustomFieldValue.cs +++ b/src/redmine-net-api/Types/CustomFieldValue.cs @@ -34,6 +34,7 @@ public class CustomFieldValue : IXmlSerializable ,IJsonSerializable ,IEquatable + ,ICloneable { /// /// @@ -205,10 +206,9 @@ public override int GetHashCode() /// /// /// - public object Clone() + public CustomFieldValue Clone(bool resetId) { - var customFieldValue = new CustomFieldValue {Info = Info}; - return customFieldValue; + return new CustomFieldValue { Info = Info }; } #endregion diff --git a/src/redmine-net-api/Types/Detail.cs b/src/redmine-net-api/Types/Detail.cs index 35e3fcc4..a17a0d9a 100644 --- a/src/redmine-net-api/Types/Detail.cs +++ b/src/redmine-net-api/Types/Detail.cs @@ -34,6 +34,7 @@ public sealed class Detail : IXmlSerializable ,IJsonSerializable ,IEquatable + ,ICloneable { /// /// @@ -182,6 +183,16 @@ public bool Equals(Detail other) && string.Equals(NewValue, other.NewValue, StringComparison.OrdinalIgnoreCase); } + /// + /// + /// + /// + /// + public Detail Clone(bool resetId) + { + return new Detail(Name, Property, OldValue, NewValue); + } + /// /// /// diff --git a/src/redmine-net-api/Types/Identifiable.cs b/src/redmine-net-api/Types/Identifiable.cs index 24124e0c..e5123673 100644 --- a/src/redmine-net-api/Types/Identifiable.cs +++ b/src/redmine-net-api/Types/Identifiable.cs @@ -33,6 +33,7 @@ namespace Redmine.Net.Api.Types /// [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] public abstract class Identifiable : IXmlSerializable, IJsonSerializable, IEquatable + , ICloneable> where T : Identifiable { #region Properties @@ -158,5 +159,13 @@ public override int GetHashCode() /// private string DebuggerDisplay => $"Id={Id.ToString(CultureInfo.InvariantCulture)}"; + /// + /// + /// + /// + public virtual Identifiable Clone(bool resetId) + { + throw new NotImplementedException(); + } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/IdentifiableName.cs b/src/redmine-net-api/Types/IdentifiableName.cs index d9deafde..b4717d4e 100644 --- a/src/redmine-net-api/Types/IdentifiableName.cs +++ b/src/redmine-net-api/Types/IdentifiableName.cs @@ -29,6 +29,7 @@ namespace Redmine.Net.Api.Types /// [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] public class IdentifiableName : Identifiable + , ICloneable { /// /// @@ -224,5 +225,18 @@ public override int GetHashCode() /// /// private string DebuggerDisplay => $"[{nameof(IdentifiableName)}: {base.ToString()}, Name={Name}]"; + + /// + /// + /// + /// + public new IdentifiableName Clone(bool resetId) + { + return new IdentifiableName + { + Id = Id, + Name = Name + }; + } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Issue.cs b/src/redmine-net-api/Types/Issue.cs index 8559b3ce..9e97f373 100644 --- a/src/redmine-net-api/Types/Issue.cs +++ b/src/redmine-net-api/Types/Issue.cs @@ -40,6 +40,7 @@ namespace Redmine.Net.Api.Types [XmlRoot(RedmineKeys.ISSUE)] public sealed class Issue : Identifiable + ,ICloneable { #region Properties /// @@ -588,36 +589,51 @@ public override int GetHashCode() } #endregion - #region Implementation of IClonable + #region Implementation of IClonable /// /// /// /// - public object Clone() + public new Issue Clone(bool resetId) { var issue = new Issue { - AssignedTo = AssignedTo, - Author = Author, - Category = Category, - CustomFields = CustomFields, + Project = Project?.Clone(false), + Tracker = Tracker?.Clone(false), + Status = Status?.Clone(false), + Priority = Priority?.Clone(false), + Author = Author?.Clone(false), + Category = Category?.Clone(false), + Subject = Subject, Description = Description, - DoneRatio = DoneRatio, + StartDate = StartDate, DueDate = DueDate, - SpentHours = SpentHours, + DoneRatio = DoneRatio, + IsPrivate = IsPrivate, EstimatedHours = EstimatedHours, - Priority = Priority, - StartDate = StartDate, - Status = Status, - Subject = Subject, - Tracker = Tracker, - Project = Project, - FixedVersion = FixedVersion, + TotalEstimatedHours = TotalEstimatedHours, + SpentHours = SpentHours, + TotalSpentHours = TotalSpentHours, + AssignedTo = AssignedTo?.Clone(false), + FixedVersion = FixedVersion?.Clone(false), Notes = Notes, - Watchers = Watchers + PrivateNotes = PrivateNotes, + CreatedOn = CreatedOn, + UpdatedOn = UpdatedOn, + ClosedOn = ClosedOn, + ParentIssue = ParentIssue?.Clone(false), + CustomFields = CustomFields?.Clone(false), + Journals = Journals?.Clone(false), + Attachments = Attachments?.Clone(false), + Relations = Relations?.Clone(false), + Children = Children?.Clone(false), + Watchers = Watchers?.Clone(false), + Uploads = Uploads?.Clone(false), }; + return issue; } + #endregion /// @@ -659,6 +675,5 @@ public IdentifiableName AsParent() Children={Children.Dump()}, Uploads={Uploads.Dump()}, Watchers={Watchers.Dump()}]"; - } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/IssueChild.cs b/src/redmine-net-api/Types/IssueChild.cs index 317aa80f..f92e3fd4 100644 --- a/src/redmine-net-api/Types/IssueChild.cs +++ b/src/redmine-net-api/Types/IssueChild.cs @@ -31,6 +31,7 @@ namespace Redmine.Net.Api.Types [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.ISSUE)] public sealed class IssueChild : Identifiable + ,ICloneable { #region Properties /// @@ -173,12 +174,12 @@ public override int GetHashCode() /// /// /// - public new IssueChild Clone() + public new IssueChild Clone(bool resetId) { return new IssueChild { Id = Id, - Tracker = Tracker, + Tracker = Tracker?.Clone(false), Subject = Subject }; } diff --git a/src/redmine-net-api/Types/IssueCustomField.cs b/src/redmine-net-api/Types/IssueCustomField.cs index 5c570862..31d012cc 100644 --- a/src/redmine-net-api/Types/IssueCustomField.cs +++ b/src/redmine-net-api/Types/IssueCustomField.cs @@ -34,6 +34,7 @@ namespace Redmine.Net.Api.Types public sealed class IssueCustomField : IdentifiableName ,IEquatable + ,ICloneable, IValue { #region Properties /// @@ -270,16 +271,37 @@ public override int GetHashCode() } #endregion - #region Implementation of IClonable + #region Implementation of IClonable /// /// /// /// - public object Clone() + public new IssueCustomField Clone(bool resetId) { - var issueCustomField = new IssueCustomField { Multiple = Multiple, Values = Values }; - return issueCustomField; + IssueCustomField clone; + if (resetId) + { + clone = new IssueCustomField(); + } + else + { + clone = new IssueCustomField + { + Id = Id, + }; + } + + clone.Name = Name; + clone.Multiple = Multiple; + + if (Values != null) + { + clone.Values = new List(Values); + } + + return clone; } + #endregion #region Implementation of IValue diff --git a/src/redmine-net-api/Types/IssueRelation.cs b/src/redmine-net-api/Types/IssueRelation.cs index 84c9c501..16571f7e 100644 --- a/src/redmine-net-api/Types/IssueRelation.cs +++ b/src/redmine-net-api/Types/IssueRelation.cs @@ -34,6 +34,7 @@ namespace Redmine.Net.Api.Types [XmlRoot(RedmineKeys.RELATION)] public sealed class IssueRelation : Identifiable + ,ICloneable { #region Properties /// @@ -284,5 +285,30 @@ public override int GetHashCode() Type={Type:G}, Delay={Delay?.ToString(CultureInfo.InvariantCulture)}]"; + /// + /// + /// + /// + public new IssueRelation Clone(bool resetId) + { + if (resetId) + { + return new IssueRelation + { + IssueId = IssueId, + IssueToId = IssueToId, + Type = Type, + Delay = Delay + }; + } + return new IssueRelation + { + Id = Id, + IssueId = IssueId, + IssueToId = IssueToId, + Type = Type, + Delay = Delay + }; + } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Journal.cs b/src/redmine-net-api/Types/Journal.cs index 6f776b24..325eedbd 100644 --- a/src/redmine-net-api/Types/Journal.cs +++ b/src/redmine-net-api/Types/Journal.cs @@ -33,6 +33,7 @@ namespace Redmine.Net.Api.Types [XmlRoot(RedmineKeys.JOURNAL)] public sealed class Journal : Identifiable + ,ICloneable { #region Properties /// @@ -253,5 +254,32 @@ public override int GetHashCode() /// private string DebuggerDisplay => $"[{nameof(Journal)}: {ToString()}, User={User}, Notes={Notes}, CreatedOn={CreatedOn?.ToString("u", CultureInfo.InvariantCulture)}, Details={Details.Dump()}]"; + /// + /// + /// + /// + public new Journal Clone(bool resetId) + { + if (resetId) + { + return new Journal + { + User = User?.Clone(false), + Notes = Notes, + CreatedOn = CreatedOn, + PrivateNotes = PrivateNotes, + Details = Details?.Clone(false) + }; + } + return new Journal + { + Id = Id, + User = User?.Clone(false), + Notes = Notes, + CreatedOn = CreatedOn, + PrivateNotes = PrivateNotes, + Details = Details?.Clone(false) + }; + } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/TimeEntry.cs b/src/redmine-net-api/Types/TimeEntry.cs index a6691bdb..afb8d110 100644 --- a/src/redmine-net-api/Types/TimeEntry.cs +++ b/src/redmine-net-api/Types/TimeEntry.cs @@ -34,6 +34,7 @@ namespace Redmine.Net.Api.Types [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.TIME_ENTRY)] public sealed class TimeEntry : Identifiable + , ICloneable { #region Properties private string comments; @@ -303,7 +304,7 @@ public override int GetHashCode() /// /// /// - public object Clone() + public new TimeEntry Clone(bool resetId) { var timeEntry = new TimeEntry { diff --git a/src/redmine-net-api/Types/Upload.cs b/src/redmine-net-api/Types/Upload.cs index 00beb32f..ca2ac4ee 100644 --- a/src/redmine-net-api/Types/Upload.cs +++ b/src/redmine-net-api/Types/Upload.cs @@ -32,6 +32,7 @@ namespace Redmine.Net.Api.Types [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.UPLOAD)] public sealed class Upload : IXmlSerializable, IJsonSerializable, IEquatable + , ICloneable { #region Properties /// @@ -234,5 +235,19 @@ public override int GetHashCode() /// private string DebuggerDisplay => $"[Upload: Token={Token}, FileName={FileName}, ContentType={ContentType}, Description={Description}]"; + /// + /// + /// + /// + public Upload Clone(bool resetId) + { + return new Upload + { + Token = Token, + FileName = FileName, + ContentType = ContentType, + Description = Description + }; + } } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Watcher.cs b/src/redmine-net-api/Types/Watcher.cs index c3949e37..d413cab1 100644 --- a/src/redmine-net-api/Types/Watcher.cs +++ b/src/redmine-net-api/Types/Watcher.cs @@ -27,6 +27,7 @@ namespace Redmine.Net.Api.Types [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.USER)] public sealed class Watcher : Identifiable + ,ICloneable ,IValue { #region Implementation of IValue @@ -37,16 +38,23 @@ public sealed class Watcher : Identifiable #endregion - #region Implementation of ICloneable + #region Implementation of ICloneable /// /// /// /// - public object Clone() + public new Watcher Clone(bool resetId) { - var watcher = new Watcher { Id = Id }; - return watcher; + if (resetId) + { + return new Watcher(); + } + return new Watcher + { + Id = Id + }; } + #endregion /// @@ -54,6 +62,5 @@ public object Clone() /// /// private string DebuggerDisplay => $"[{nameof(Watcher)}: {ToString()}]"; - } } \ No newline at end of file From b59799643b4e723230e61aeb2abe9a72848a87c8 Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 30 Mar 2025 22:50:50 +0300 Subject: [PATCH 425/549] Add tests (#378) * [Equality] Improvements * [Packages] Bump up * [Tests] Fix namespace * [Tests] Host * [Tests] RedmineFixture * [Tests] XmlSerializerFixture * [Tests] JsonSerializerFixture * [Tests] Xml deserializer tests * [Tests] Json deserializer tests * [Tests ] Add clonable tests * [#229] Add tests * [Tests] RedmineApiUrls * [#371] Add tests * [Tests] Equality --- .../Internals/HashCodeHelper.cs | 25 + src/redmine-net-api/Types/Attachment.cs | 12 +- src/redmine-net-api/Types/ChangeSet.cs | 2 +- src/redmine-net-api/Types/CustomField.cs | 33 +- .../Types/CustomFieldPossibleValue.cs | 16 +- src/redmine-net-api/Types/CustomFieldValue.cs | 2 +- src/redmine-net-api/Types/Detail.cs | 8 +- src/redmine-net-api/Types/DocumentCategory.cs | 3 +- src/redmine-net-api/Types/Error.cs | 2 +- src/redmine-net-api/Types/File.cs | 12 +- src/redmine-net-api/Types/Group.cs | 6 +- src/redmine-net-api/Types/Identifiable.cs | 4 +- src/redmine-net-api/Types/IdentifiableName.cs | 2 +- src/redmine-net-api/Types/Issue.cs | 24 +- src/redmine-net-api/Types/IssueChild.cs | 3 +- src/redmine-net-api/Types/IssueCustomField.cs | 5 +- src/redmine-net-api/Types/IssuePriority.cs | 2 +- src/redmine-net-api/Types/IssueRelation.cs | 6 +- src/redmine-net-api/Types/IssueStatus.cs | 4 +- src/redmine-net-api/Types/Journal.cs | 19 +- src/redmine-net-api/Types/Membership.cs | 2 +- src/redmine-net-api/Types/MyAccount.cs | 17 +- .../Types/MyAccountCustomField.cs | 9 +- src/redmine-net-api/Types/News.cs | 29 +- src/redmine-net-api/Types/NewsComment.cs | 18 +- src/redmine-net-api/Types/Permission.cs | 2 +- src/redmine-net-api/Types/Project.cs | 24 +- .../Types/ProjectMembership.cs | 8 +- src/redmine-net-api/Types/Role.cs | 14 +- src/redmine-net-api/Types/Search.cs | 8 +- src/redmine-net-api/Types/TimeEntry.cs | 4 +- .../Types/TimeEntryActivity.cs | 4 +- src/redmine-net-api/Types/Tracker.cs | 5 +- src/redmine-net-api/Types/TrackerCoreField.cs | 13 +- src/redmine-net-api/Types/Upload.cs | 8 +- src/redmine-net-api/Types/User.cs | 38 +- src/redmine-net-api/Types/Version.cs | 18 +- src/redmine-net-api/Types/WikiPage.cs | 9 +- .../Bugs/RedmineApi-229.cs | 125 ++++ .../Bugs/RedmineApi-371.cs | 22 +- .../Clone/AttachmentCloneTests.cs | 82 +++ .../Clone/IssueCloneTests.cs | 129 ++++ .../Clone/JournalCloneTests.cs | 60 ++ .../Equality/AttachmentEqualityTests.cs | 66 ++ .../Equality/BaseEqualityTests.cs | 66 ++ .../Equality/CustomFieldPossibleValueTests.cs | 24 + .../Equality/CustomFieldRoleTests.cs | 24 + .../Equality/CustomFieldTests.cs | 34 ++ .../Equality/DetailTests.cs | 28 + .../Equality/ErrorTests.cs | 16 + .../Equality/GroupTests.cs | 26 + .../Equality/GroupUserTests.cs | 24 + .../Equality/IssueCategoryTests.cs | 28 + .../Equality/IssueEqualityTests.cs | 114 ++++ .../Equality/IssueStatusTests.cs | 28 + .../Equality/JournalEqualityTests.cs | 72 +++ .../Equality/MembershipTests.cs | 28 + .../Equality/MyAccountCustomFieldTests.cs | 26 + .../Equality/MyAccountTests.cs | 40 ++ .../Equality/NewsTests.cs | 37 ++ .../Equality/PermissionTests.cs | 22 + .../Equality/ProjectMembershipTests.cs | 28 + .../Equality/ProjectTests.cs | 91 +++ .../Equality/QueryTests.cs | 28 + .../Equality/RoleTests.cs | 35 ++ .../Equality/SearchTests.cs | 33 + .../Equality/TimeEntryActivityTests.cs | 28 + .../Equality/TimeEntryTests.cs | 53 ++ .../Equality/TrackerCoreFieldTests.cs | 16 + .../Equality/TrackerCustomFieldTests.cs | 24 + .../Equality/UploadTests.cs | 28 + .../Equality/UserGroupTests.cs | 25 + .../Equality/UserTests.cs | 65 ++ .../Equality/VersionTests.cs | 47 ++ .../Equality/WatcherTests.cs | 22 + .../Equality/WikiPageTests.cs | 47 ++ .../JsonRedmineSerializerCollection.cs | 7 + .../Collections/RedmineCollection.cs | 10 + .../XmlRedmineSerializerCollection.cs | 7 + .../Infrastructure/Constants.cs | 8 + .../Fixtures/JsonSerializerFixture.cs | 9 + .../Fixtures/RedmineApiUrlsFixture.cs | 31 + .../{ => Fixtures}/RedmineFixture.cs | 11 +- .../Fixtures/XmlSerializerFixture.cs | 8 + .../Infrastructure/Order/CaseOrder.cs | 2 +- .../Infrastructure/Order/CollectionOrderer.cs | 2 +- .../Infrastructure/Order/OrderAttribute.cs | 2 +- .../Infrastructure/RedmineCollection.cs | 9 - .../Serialization/Json/MyAccount.cs | 56 ++ .../Serialization/Json/RoleTests.cs | 45 ++ .../Serialization/Xml/AttachmentTests.cs | 43 ++ .../Serialization/Xml/CustomFieldTests.cs | 65 ++ .../Serialization/Xml/EnumerationTests.cs | 117 ++++ .../Serialization/Xml/ErrorTests.cs | 32 + .../Serialization/Xml/FileTests.cs | 87 +++ .../Serialization/Xml/GroupTests.cs | 67 ++ .../Serialization/Xml/IssueCategoryTests.cs | 73 +++ .../Serialization/Xml/IssueStatusTests.cs | 47 ++ .../Serialization/Xml/IssueTests.cs | 204 +++++++ .../Serialization/Xml/MembershipTests.cs | 95 +++ .../Serialization/Xml/MyAccountTests.cs | 79 +++ .../Serialization/Xml/NewsTests.cs | 67 ++ .../Serialization/Xml/ProjectTests.cs | 87 +++ .../Serialization/Xml/QueryTests.cs | 51 ++ .../Serialization/Xml/RelationTests.cs | 81 +++ .../Serialization/Xml/RoleTests.cs | 95 +++ .../Serialization/Xml/SearchTests.cs | 59 ++ .../Serialization/Xml/TrackerTests.cs | 134 ++++ .../Serialization/Xml/UploadTests.cs | 62 ++ .../Serialization/Xml/UserTests.cs | 159 +++++ .../Serialization/Xml/VersionTests.cs | 111 ++++ .../Serialization/Xml/WikiTests.cs | 86 +++ .../{HostValidationTests.cs => HostTests.cs} | 12 +- .../Tests/RedmineApiUrlsTests.cs | 573 ++++++++++++++++++ .../redmine-net-api.Tests.csproj | 16 +- 115 files changed, 4594 insertions(+), 226 deletions(-) create mode 100644 tests/redmine-net-api.Tests/Bugs/RedmineApi-229.cs create mode 100644 tests/redmine-net-api.Tests/Clone/AttachmentCloneTests.cs create mode 100644 tests/redmine-net-api.Tests/Clone/IssueCloneTests.cs create mode 100644 tests/redmine-net-api.Tests/Clone/JournalCloneTests.cs create mode 100644 tests/redmine-net-api.Tests/Equality/AttachmentEqualityTests.cs create mode 100644 tests/redmine-net-api.Tests/Equality/BaseEqualityTests.cs create mode 100644 tests/redmine-net-api.Tests/Equality/CustomFieldPossibleValueTests.cs create mode 100644 tests/redmine-net-api.Tests/Equality/CustomFieldRoleTests.cs create mode 100644 tests/redmine-net-api.Tests/Equality/CustomFieldTests.cs create mode 100644 tests/redmine-net-api.Tests/Equality/DetailTests.cs create mode 100644 tests/redmine-net-api.Tests/Equality/ErrorTests.cs create mode 100644 tests/redmine-net-api.Tests/Equality/GroupTests.cs create mode 100644 tests/redmine-net-api.Tests/Equality/GroupUserTests.cs create mode 100644 tests/redmine-net-api.Tests/Equality/IssueCategoryTests.cs create mode 100644 tests/redmine-net-api.Tests/Equality/IssueEqualityTests.cs create mode 100644 tests/redmine-net-api.Tests/Equality/IssueStatusTests.cs create mode 100644 tests/redmine-net-api.Tests/Equality/JournalEqualityTests.cs create mode 100644 tests/redmine-net-api.Tests/Equality/MembershipTests.cs create mode 100644 tests/redmine-net-api.Tests/Equality/MyAccountCustomFieldTests.cs create mode 100644 tests/redmine-net-api.Tests/Equality/MyAccountTests.cs create mode 100644 tests/redmine-net-api.Tests/Equality/NewsTests.cs create mode 100644 tests/redmine-net-api.Tests/Equality/PermissionTests.cs create mode 100644 tests/redmine-net-api.Tests/Equality/ProjectMembershipTests.cs create mode 100644 tests/redmine-net-api.Tests/Equality/ProjectTests.cs create mode 100644 tests/redmine-net-api.Tests/Equality/QueryTests.cs create mode 100644 tests/redmine-net-api.Tests/Equality/RoleTests.cs create mode 100644 tests/redmine-net-api.Tests/Equality/SearchTests.cs create mode 100644 tests/redmine-net-api.Tests/Equality/TimeEntryActivityTests.cs create mode 100644 tests/redmine-net-api.Tests/Equality/TimeEntryTests.cs create mode 100644 tests/redmine-net-api.Tests/Equality/TrackerCoreFieldTests.cs create mode 100644 tests/redmine-net-api.Tests/Equality/TrackerCustomFieldTests.cs create mode 100644 tests/redmine-net-api.Tests/Equality/UploadTests.cs create mode 100644 tests/redmine-net-api.Tests/Equality/UserGroupTests.cs create mode 100644 tests/redmine-net-api.Tests/Equality/UserTests.cs create mode 100644 tests/redmine-net-api.Tests/Equality/VersionTests.cs create mode 100644 tests/redmine-net-api.Tests/Equality/WatcherTests.cs create mode 100644 tests/redmine-net-api.Tests/Equality/WikiPageTests.cs create mode 100644 tests/redmine-net-api.Tests/Infrastructure/Collections/JsonRedmineSerializerCollection.cs create mode 100644 tests/redmine-net-api.Tests/Infrastructure/Collections/RedmineCollection.cs create mode 100644 tests/redmine-net-api.Tests/Infrastructure/Collections/XmlRedmineSerializerCollection.cs create mode 100644 tests/redmine-net-api.Tests/Infrastructure/Constants.cs create mode 100644 tests/redmine-net-api.Tests/Infrastructure/Fixtures/JsonSerializerFixture.cs create mode 100644 tests/redmine-net-api.Tests/Infrastructure/Fixtures/RedmineApiUrlsFixture.cs rename tests/redmine-net-api.Tests/Infrastructure/{ => Fixtures}/RedmineFixture.cs (71%) create mode 100644 tests/redmine-net-api.Tests/Infrastructure/Fixtures/XmlSerializerFixture.cs delete mode 100644 tests/redmine-net-api.Tests/Infrastructure/RedmineCollection.cs create mode 100644 tests/redmine-net-api.Tests/Serialization/Json/MyAccount.cs create mode 100644 tests/redmine-net-api.Tests/Serialization/Json/RoleTests.cs create mode 100644 tests/redmine-net-api.Tests/Serialization/Xml/AttachmentTests.cs create mode 100644 tests/redmine-net-api.Tests/Serialization/Xml/CustomFieldTests.cs create mode 100644 tests/redmine-net-api.Tests/Serialization/Xml/EnumerationTests.cs create mode 100644 tests/redmine-net-api.Tests/Serialization/Xml/ErrorTests.cs create mode 100644 tests/redmine-net-api.Tests/Serialization/Xml/FileTests.cs create mode 100644 tests/redmine-net-api.Tests/Serialization/Xml/GroupTests.cs create mode 100644 tests/redmine-net-api.Tests/Serialization/Xml/IssueCategoryTests.cs create mode 100644 tests/redmine-net-api.Tests/Serialization/Xml/IssueStatusTests.cs create mode 100644 tests/redmine-net-api.Tests/Serialization/Xml/IssueTests.cs create mode 100644 tests/redmine-net-api.Tests/Serialization/Xml/MembershipTests.cs create mode 100644 tests/redmine-net-api.Tests/Serialization/Xml/MyAccountTests.cs create mode 100644 tests/redmine-net-api.Tests/Serialization/Xml/NewsTests.cs create mode 100644 tests/redmine-net-api.Tests/Serialization/Xml/ProjectTests.cs create mode 100644 tests/redmine-net-api.Tests/Serialization/Xml/QueryTests.cs create mode 100644 tests/redmine-net-api.Tests/Serialization/Xml/RelationTests.cs create mode 100644 tests/redmine-net-api.Tests/Serialization/Xml/RoleTests.cs create mode 100644 tests/redmine-net-api.Tests/Serialization/Xml/SearchTests.cs create mode 100644 tests/redmine-net-api.Tests/Serialization/Xml/TrackerTests.cs create mode 100644 tests/redmine-net-api.Tests/Serialization/Xml/UploadTests.cs create mode 100644 tests/redmine-net-api.Tests/Serialization/Xml/UserTests.cs create mode 100644 tests/redmine-net-api.Tests/Serialization/Xml/VersionTests.cs create mode 100644 tests/redmine-net-api.Tests/Serialization/Xml/WikiTests.cs rename tests/redmine-net-api.Tests/Tests/{HostValidationTests.cs => HostTests.cs} (96%) create mode 100644 tests/redmine-net-api.Tests/Tests/RedmineApiUrlsTests.cs diff --git a/src/redmine-net-api/Internals/HashCodeHelper.cs b/src/redmine-net-api/Internals/HashCodeHelper.cs index f2610116..4d2ac468 100755 --- a/src/redmine-net-api/Internals/HashCodeHelper.cs +++ b/src/redmine-net-api/Internals/HashCodeHelper.cs @@ -57,6 +57,31 @@ public static int GetHashCode(IList list, int hash) where T : class return hashCode; } } + + public static int GetHashCode(List list, int hash) where T : class + { + unchecked + { + var hashCode = hash; + if (list == null) + { + return hashCode; + } + + hashCode = (hashCode * 17) + list.Count; + + foreach (var t in list) + { + hashCode *= 17; + if (t != null) + { + hashCode += t.GetHashCode(); + } + } + + return hashCode; + } + } /// /// Returns a hash code for this instance. diff --git a/src/redmine-net-api/Types/Attachment.cs b/src/redmine-net-api/Types/Attachment.cs index bc394f77..da35047f 100644 --- a/src/redmine-net-api/Types/Attachment.cs +++ b/src/redmine-net-api/Types/Attachment.cs @@ -189,12 +189,12 @@ public override bool Equals(Attachment other) { if (other == null) return false; return base.Equals(other) - && string.Equals(FileName, other.FileName, StringComparison.OrdinalIgnoreCase) - && string.Equals(ContentType, other.ContentType, StringComparison.OrdinalIgnoreCase) - && string.Equals(Description, other.Description, StringComparison.OrdinalIgnoreCase) - && string.Equals(ContentUrl, other.ContentUrl, StringComparison.OrdinalIgnoreCase) - && string.Equals(ThumbnailUrl, other.ThumbnailUrl, StringComparison.OrdinalIgnoreCase) - && Equals(Author, other.Author) + && string.Equals(FileName, other.FileName, StringComparison.Ordinal) + && string.Equals(ContentType, other.ContentType, StringComparison.Ordinal) + && string.Equals(Description, other.Description, StringComparison.Ordinal) + && string.Equals(ContentUrl, other.ContentUrl, StringComparison.Ordinal) + && string.Equals(ThumbnailUrl, other.ThumbnailUrl, StringComparison.Ordinal) + && Author == other.Author && FileSize == other.FileSize && CreatedOn == other.CreatedOn; } diff --git a/src/redmine-net-api/Types/ChangeSet.cs b/src/redmine-net-api/Types/ChangeSet.cs index dae77667..3e9937af 100644 --- a/src/redmine-net-api/Types/ChangeSet.cs +++ b/src/redmine-net-api/Types/ChangeSet.cs @@ -154,7 +154,7 @@ public bool Equals(ChangeSet other) return Revision == other.Revision && User == other.User - && Comments == other.Comments + && string.Equals(Comments, other.Comments, StringComparison.Ordinal) && CommittedOn == other.CommittedOn; } diff --git a/src/redmine-net-api/Types/CustomField.cs b/src/redmine-net-api/Types/CustomField.cs index 486e492f..f10a4162 100644 --- a/src/redmine-net-api/Types/CustomField.cs +++ b/src/redmine-net-api/Types/CustomField.cs @@ -207,22 +207,23 @@ public bool Equals(CustomField other) { if (other == null) return false; - return base.Equals(other) - && string.Equals(CustomizedType, other.CustomizedType, StringComparison.OrdinalIgnoreCase) - && string.Equals(Description, other.Description, StringComparison.OrdinalIgnoreCase) - && string.Equals(FieldFormat, other.FieldFormat, StringComparison.OrdinalIgnoreCase) - && string.Equals(Regexp, other.Regexp, StringComparison.OrdinalIgnoreCase) - && string.Equals(DefaultValue, other.DefaultValue, StringComparison.Ordinal) - && MinLength == other.MinLength - && MaxLength == other.MaxLength - && IsRequired == other.IsRequired - && IsFilter == other.IsFilter - && Searchable == other.Searchable - && Multiple == other.Multiple - && Visible == other.Visible - && Equals(PossibleValues, other.PossibleValues) - && Equals(Trackers, other.Trackers) - && Equals(Roles, other.Roles); + var result = base.Equals(other) + && string.Equals(CustomizedType, other.CustomizedType, StringComparison.Ordinal) + && string.Equals(Description, other.Description, StringComparison.Ordinal) + && string.Equals(FieldFormat, other.FieldFormat, StringComparison.Ordinal) + && string.Equals(Regexp, other.Regexp, StringComparison.Ordinal) + && string.Equals(DefaultValue, other.DefaultValue, StringComparison.Ordinal) + && MinLength == other.MinLength + && MaxLength == other.MaxLength + && IsRequired == other.IsRequired + && IsFilter == other.IsFilter + && Searchable == other.Searchable + && Multiple == other.Multiple + && Visible == other.Visible + && (PossibleValues?.Equals(other.PossibleValues) ?? other.PossibleValues == null) + && (Trackers?.Equals(other.Trackers) ?? other.Trackers == null) + && (Roles?.Equals(other.Roles) ?? other.Roles == null); + return result; } /// diff --git a/src/redmine-net-api/Types/CustomFieldPossibleValue.cs b/src/redmine-net-api/Types/CustomFieldPossibleValue.cs index 8c39c325..2d3d0029 100644 --- a/src/redmine-net-api/Types/CustomFieldPossibleValue.cs +++ b/src/redmine-net-api/Types/CustomFieldPossibleValue.cs @@ -73,9 +73,7 @@ public void ReadXml(XmlReader reader) switch (reader.Name) { case RedmineKeys.LABEL: Label = reader.ReadElementContentAsString(); break; - case RedmineKeys.VALUE: Value = reader.ReadElementContentAsString(); break; - default: reader.Read(); break; } } @@ -111,14 +109,9 @@ public void ReadJson(JsonReader reader) switch (reader.Value) { - case RedmineKeys.LABEL: - Label = reader.ReadAsString(); break; - - case RedmineKeys.VALUE: - - Value = reader.ReadAsString(); break; - default: - reader.Read(); break; + case RedmineKeys.LABEL: Label = reader.ReadAsString(); break; + case RedmineKeys.VALUE: Value = reader.ReadAsString(); break; + default: reader.Read(); break; } } } @@ -139,8 +132,9 @@ public void WriteJson(JsonWriter writer) { } public bool Equals(CustomFieldPossibleValue other) { if (other == null) return false; - return string.Equals(Value, other.Value, StringComparison.Ordinal) + var result = string.Equals(Value, other.Value, StringComparison.Ordinal) && string.Equals(Label, other.Label, StringComparison.Ordinal); + return result; } /// diff --git a/src/redmine-net-api/Types/CustomFieldValue.cs b/src/redmine-net-api/Types/CustomFieldValue.cs index 592bb4f4..19569f1e 100644 --- a/src/redmine-net-api/Types/CustomFieldValue.cs +++ b/src/redmine-net-api/Types/CustomFieldValue.cs @@ -146,7 +146,7 @@ public void WriteJson(JsonWriter writer) public bool Equals(CustomFieldValue other) { if (other == null) return false; - return string.Equals(Info, other.Info, StringComparison.OrdinalIgnoreCase); + return string.Equals(Info, other.Info, StringComparison.Ordinal); } /// diff --git a/src/redmine-net-api/Types/Detail.cs b/src/redmine-net-api/Types/Detail.cs index a17a0d9a..53a0b9a1 100644 --- a/src/redmine-net-api/Types/Detail.cs +++ b/src/redmine-net-api/Types/Detail.cs @@ -177,10 +177,10 @@ public void ReadJson(JsonReader reader) public bool Equals(Detail other) { if (other == null) return false; - return string.Equals(Property, other.Property, StringComparison.OrdinalIgnoreCase) - && string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase) - && string.Equals(OldValue, other.OldValue, StringComparison.OrdinalIgnoreCase) - && string.Equals(NewValue, other.NewValue, StringComparison.OrdinalIgnoreCase); + return string.Equals(Property, other.Property, StringComparison.Ordinal) + && string.Equals(Name, other.Name, StringComparison.Ordinal) + && string.Equals(OldValue, other.OldValue, StringComparison.Ordinal) + && string.Equals(NewValue, other.NewValue, StringComparison.Ordinal); } /// diff --git a/src/redmine-net-api/Types/DocumentCategory.cs b/src/redmine-net-api/Types/DocumentCategory.cs index 1028a41d..4040fade 100644 --- a/src/redmine-net-api/Types/DocumentCategory.cs +++ b/src/redmine-net-api/Types/DocumentCategory.cs @@ -132,8 +132,7 @@ public bool Equals(DocumentCategory other) { if (other == null) return false; - return Id == other.Id - && Name == other.Name + return base.Equals(other) && IsDefault == other.IsDefault && IsActive == other.IsActive; } diff --git a/src/redmine-net-api/Types/Error.cs b/src/redmine-net-api/Types/Error.cs index 96cd20bd..a3712f4b 100644 --- a/src/redmine-net-api/Types/Error.cs +++ b/src/redmine-net-api/Types/Error.cs @@ -119,7 +119,7 @@ public bool Equals(Error other) { if (other == null) return false; - return string.Equals(Info,other.Info, StringComparison.OrdinalIgnoreCase); + return string.Equals(Info, other.Info, StringComparison.Ordinal); } /// diff --git a/src/redmine-net-api/Types/File.cs b/src/redmine-net-api/Types/File.cs index 4630f5f8..eb936dd1 100644 --- a/src/redmine-net-api/Types/File.cs +++ b/src/redmine-net-api/Types/File.cs @@ -210,12 +210,12 @@ public override bool Equals(File other) { if (other == null) return false; return base.Equals(other) - && string.Equals(Filename, other.Filename, StringComparison.OrdinalIgnoreCase) - && string.Equals(ContentType, other.ContentType, StringComparison.OrdinalIgnoreCase) - && string.Equals(Description, other.Description, StringComparison.OrdinalIgnoreCase) - && string.Equals(ContentUrl, other.ContentUrl, StringComparison.OrdinalIgnoreCase) - && string.Equals(Digest, other.Digest, StringComparison.OrdinalIgnoreCase) - && Equals(Author, other.Author) + && string.Equals(Filename, other.Filename, StringComparison.Ordinal) + && string.Equals(ContentType, other.ContentType, StringComparison.Ordinal) + && string.Equals(Description, other.Description, StringComparison.Ordinal) + && string.Equals(ContentUrl, other.ContentUrl, StringComparison.Ordinal) + && string.Equals(Digest, other.Digest, StringComparison.Ordinal) + && Author == other.Author && FileSize == other.FileSize && CreatedOn == other.CreatedOn && Version == other.Version diff --git a/src/redmine-net-api/Types/Group.cs b/src/redmine-net-api/Types/Group.cs index 45001031..71e92025 100644 --- a/src/redmine-net-api/Types/Group.cs +++ b/src/redmine-net-api/Types/Group.cs @@ -165,9 +165,9 @@ public bool Equals(Group other) { if (other == null) return false; return base.Equals(other) - && Equals(Users, other.Users) - && Equals(CustomFields, other.CustomFields) - && Equals(Memberships, other.Memberships); + && Users != null ? Users.Equals(other.Users) : other.Users == null + && CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null + && Memberships != null ? Memberships.Equals(other.Memberships) : other.Memberships == null; } /// diff --git a/src/redmine-net-api/Types/Identifiable.cs b/src/redmine-net-api/Types/Identifiable.cs index e5123673..b0f81444 100644 --- a/src/redmine-net-api/Types/Identifiable.cs +++ b/src/redmine-net-api/Types/Identifiable.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2023 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -41,7 +41,7 @@ public abstract class Identifiable : IXmlSerializable, IJsonSerializable, IEq /// Gets the id. /// /// The id. - public int Id { get; protected set; } + public int Id { get; protected internal set; } #endregion #region Implementation of IXmlSerialization diff --git a/src/redmine-net-api/Types/IdentifiableName.cs b/src/redmine-net-api/Types/IdentifiableName.cs index b4717d4e..02fb5c6d 100644 --- a/src/redmine-net-api/Types/IdentifiableName.cs +++ b/src/redmine-net-api/Types/IdentifiableName.cs @@ -167,7 +167,7 @@ public override void WriteJson(JsonWriter writer) public override bool Equals(IdentifiableName other) { if (other == null) return false; - return Id == other.Id && string.Equals(Name, other.Name, StringComparison.OrdinalIgnoreCase); + return Id == other.Id && string.Equals(Name, other.Name, StringComparison.Ordinal); } /// diff --git a/src/redmine-net-api/Types/Issue.cs b/src/redmine-net-api/Types/Issue.cs index 9e97f373..7d9b488e 100644 --- a/src/redmine-net-api/Types/Issue.cs +++ b/src/redmine-net-api/Types/Issue.cs @@ -484,8 +484,8 @@ public override bool Equals(Issue other) && Priority == other.Priority && Author == other.Author && Category == other.Category - && Subject == other.Subject - && Description == other.Description + && string.Equals(Subject, other.Subject, StringComparison.Ordinal) + && string.Equals(Description, other.Description, StringComparison.Ordinal) && StartDate == other.StartDate && DueDate == other.DueDate && DoneRatio == other.DoneRatio @@ -495,16 +495,16 @@ public override bool Equals(Issue other) && UpdatedOn == other.UpdatedOn && AssignedTo == other.AssignedTo && FixedVersion == other.FixedVersion - && Notes == other.Notes + && string.Equals(Notes, other.Notes, StringComparison.Ordinal) && ClosedOn == other.ClosedOn && PrivateNotes == other.PrivateNotes - && Attachments.Equals(other.Attachments) - && CustomFields.Equals(other.CustomFields) - && ChangeSets.Equals(other.ChangeSets) - && Children.Equals(other.Children) - && Journals.Equals(other.Journals) - && Relations.Equals(other.Relations) - && Watchers.Equals(other.Watchers); + && (Attachments?.Equals(other.Attachments) ?? other.Attachments == null) + && (CustomFields?.Equals(other.CustomFields) ?? other.CustomFields == null) + && (ChangeSets?.Equals(other.ChangeSets) ?? other.ChangeSets == null) + && (Children?.Equals(other.Children) ?? other.Children == null) + && (Journals?.Equals(other.Journals) ?? other.Journals == null) + && (Relations?.Equals(other.Relations) ?? other.Relations == null) + && (Watchers?.Equals(other.Watchers) ?? other.Watchers == null); } /// @@ -529,19 +529,19 @@ public override int GetHashCode() var hashCode = base.GetHashCode(); hashCode = HashCodeHelper.GetHashCode(Project, hashCode); + hashCode = HashCodeHelper.GetHashCode(Tracker, hashCode); hashCode = HashCodeHelper.GetHashCode(Status, hashCode); hashCode = HashCodeHelper.GetHashCode(Priority, hashCode); hashCode = HashCodeHelper.GetHashCode(Author, hashCode); hashCode = HashCodeHelper.GetHashCode(Category, hashCode); - + hashCode = HashCodeHelper.GetHashCode(Subject, hashCode); hashCode = HashCodeHelper.GetHashCode(Description, hashCode); hashCode = HashCodeHelper.GetHashCode(StartDate, hashCode); hashCode = HashCodeHelper.GetHashCode(Project, hashCode); hashCode = HashCodeHelper.GetHashCode(DueDate, hashCode); hashCode = HashCodeHelper.GetHashCode(DoneRatio, hashCode); - hashCode = HashCodeHelper.GetHashCode(PrivateNotes, hashCode); hashCode = HashCodeHelper.GetHashCode(EstimatedHours, hashCode); hashCode = HashCodeHelper.GetHashCode(SpentHours, hashCode); diff --git a/src/redmine-net-api/Types/IssueChild.cs b/src/redmine-net-api/Types/IssueChild.cs index f92e3fd4..6418aa4f 100644 --- a/src/redmine-net-api/Types/IssueChild.cs +++ b/src/redmine-net-api/Types/IssueChild.cs @@ -115,7 +115,8 @@ public override bool Equals(IssueChild other) { if (other == null) return false; return base.Equals(other) - && Tracker == other.Tracker && Subject == other.Subject; + && Tracker == other.Tracker + && string.Equals(Subject, other.Subject, StringComparison.Ordinal); } /// diff --git a/src/redmine-net-api/Types/IssueCustomField.cs b/src/redmine-net-api/Types/IssueCustomField.cs index 31d012cc..1cb47bf4 100644 --- a/src/redmine-net-api/Types/IssueCustomField.cs +++ b/src/redmine-net-api/Types/IssueCustomField.cs @@ -214,10 +214,9 @@ public override void ReadJson(JsonReader reader) public bool Equals(IssueCustomField other) { if (other == null) return false; - return Id == other.Id - && Name == other.Name + return base.Equals(other) && Multiple == other.Multiple - && Values.Equals(other.Values); + && (Values?.Equals(other.Values) ?? other.Values == null); } /// diff --git a/src/redmine-net-api/Types/IssuePriority.cs b/src/redmine-net-api/Types/IssuePriority.cs index 305cf04f..be107464 100644 --- a/src/redmine-net-api/Types/IssuePriority.cs +++ b/src/redmine-net-api/Types/IssuePriority.cs @@ -116,7 +116,7 @@ public bool Equals(IssuePriority other) { if (other == null) return false; - return Id == other.Id && Name == other.Name + return base.Equals(other) && IsDefault == other.IsDefault && IsActive == other.IsActive; } diff --git a/src/redmine-net-api/Types/IssueRelation.cs b/src/redmine-net-api/Types/IssueRelation.cs index 16571f7e..88ae9eb2 100644 --- a/src/redmine-net-api/Types/IssueRelation.cs +++ b/src/redmine-net-api/Types/IssueRelation.cs @@ -219,7 +219,11 @@ private static IssueRelationType ReadIssueRelationType(string value) public override bool Equals(IssueRelation other) { if (other == null) return false; - return Id == other.Id && IssueId == other.IssueId && IssueToId == other.IssueToId && Type == other.Type && Delay == other.Delay; + return Id == other.Id + && IssueId == other.IssueId + && IssueToId == other.IssueToId + && Type == other.Type + && Delay == other.Delay; } /// diff --git a/src/redmine-net-api/Types/IssueStatus.cs b/src/redmine-net-api/Types/IssueStatus.cs index 9f4657c0..1e842ac0 100644 --- a/src/redmine-net-api/Types/IssueStatus.cs +++ b/src/redmine-net-api/Types/IssueStatus.cs @@ -116,7 +116,9 @@ public override void ReadJson(JsonReader reader) public bool Equals(IssueStatus other) { if (other == null) return false; - return Id == other.Id && Name == other.Name && IsClosed == other.IsClosed && IsDefault == other.IsDefault; + return base.Equals(other) + && IsClosed == other.IsClosed + && IsDefault == other.IsDefault; } /// diff --git a/src/redmine-net-api/Types/Journal.cs b/src/redmine-net-api/Types/Journal.cs index 325eedbd..75cd78a3 100644 --- a/src/redmine-net-api/Types/Journal.cs +++ b/src/redmine-net-api/Types/Journal.cs @@ -130,8 +130,6 @@ public override void WriteXml(XmlWriter writer) #endregion #region Implementation of IJsonSerialization - - /// /// /// @@ -182,14 +180,15 @@ public override void WriteJson(JsonWriter writer) public override bool Equals(Journal other) { if (other == null) return false; - return base.Equals(other) - && Equals(User, other.User) - && Equals(Details, other.Details) - && string.Equals(Notes, other.Notes, StringComparison.OrdinalIgnoreCase) - && CreatedOn == other.CreatedOn - && UpdatedOn == other.UpdatedOn - && Equals(UpdatedBy, other.UpdatedBy) - && PrivateNotes == other.PrivateNotes; + var result = base.Equals(other); + result = result && User == other.User; + result = result && UpdatedBy == other.UpdatedBy; + result = result && (Details?.Equals(other.Details) ?? other.Details == null); + result = result && string.Equals(Notes, other.Notes, StringComparison.Ordinal); + result = result && CreatedOn == other.CreatedOn; + result = result && UpdatedOn == other.UpdatedOn; + result = result && PrivateNotes == other.PrivateNotes; + return result; } /// diff --git a/src/redmine-net-api/Types/Membership.cs b/src/redmine-net-api/Types/Membership.cs index 4243030e..f9f92ff1 100644 --- a/src/redmine-net-api/Types/Membership.cs +++ b/src/redmine-net-api/Types/Membership.cs @@ -125,7 +125,7 @@ public override bool Equals(Membership other) { if (other == null) return false; return Id == other.Id - && Project != null ? Project.Equals(other.Project) : other.Project == null + && Project == other.Project && Roles != null ? Roles.Equals(other.Roles) : other.Roles == null; } diff --git a/src/redmine-net-api/Types/MyAccount.cs b/src/redmine-net-api/Types/MyAccount.cs index fc86e19a..0bcf9831 100644 --- a/src/redmine-net-api/Types/MyAccount.cs +++ b/src/redmine-net-api/Types/MyAccount.cs @@ -186,15 +186,15 @@ public override bool Equals(MyAccount other) { if (other == null) return false; return Id == other.Id - && string.Equals(Login, other.Login, StringComparison.OrdinalIgnoreCase) - && string.Equals(FirstName, other.FirstName, StringComparison.OrdinalIgnoreCase) - && string.Equals(LastName, other.LastName, StringComparison.OrdinalIgnoreCase) - && string.Equals(ApiKey, other.ApiKey, StringComparison.OrdinalIgnoreCase) - && Email.Equals(other.Email, StringComparison.OrdinalIgnoreCase) + && string.Equals(Login, other.Login, StringComparison.Ordinal) + && string.Equals(FirstName, other.FirstName, StringComparison.Ordinal) + && string.Equals(LastName, other.LastName, StringComparison.Ordinal) + && string.Equals(ApiKey, other.ApiKey, StringComparison.Ordinal) + && string.Equals(Email, other.Email, StringComparison.Ordinal) && IsAdmin == other.IsAdmin && CreatedOn == other.CreatedOn && LastLoginOn == other.LastLoginOn - && CustomFields.Equals(other.CustomFields); + && (CustomFields?.Equals(other.CustomFields) ?? other.CustomFields == null); } /// @@ -215,15 +215,16 @@ public override int GetHashCode() { unchecked { - var hashCode = base.GetHashCode(); + var hashCode = 17; + hashCode = HashCodeHelper.GetHashCode(Id, hashCode); hashCode = HashCodeHelper.GetHashCode(Login, hashCode); hashCode = HashCodeHelper.GetHashCode(FirstName, hashCode); hashCode = HashCodeHelper.GetHashCode(LastName, hashCode); + hashCode = HashCodeHelper.GetHashCode(ApiKey, hashCode); hashCode = HashCodeHelper.GetHashCode(Email, hashCode); hashCode = HashCodeHelper.GetHashCode(IsAdmin, hashCode); hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); hashCode = HashCodeHelper.GetHashCode(LastLoginOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(ApiKey, hashCode); hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); return hashCode; } diff --git a/src/redmine-net-api/Types/MyAccountCustomField.cs b/src/redmine-net-api/Types/MyAccountCustomField.cs index f01ab673..8350527d 100644 --- a/src/redmine-net-api/Types/MyAccountCustomField.cs +++ b/src/redmine-net-api/Types/MyAccountCustomField.cs @@ -29,14 +29,13 @@ namespace Redmine.Net.Api.Types /// [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.CUSTOM_FIELD)] - public sealed class MyAccountCustomField : IdentifiableName + public sealed class MyAccountCustomField : IdentifiableName, IEquatable { /// /// Initializes a new instance of the class. /// /// Serialization public MyAccountCustomField() { } - /// /// @@ -117,8 +116,7 @@ public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != GetType()) return false; - return Equals(obj as MyAccountCustomField); + return obj is MyAccountCustomField other && Equals(other); } /// @@ -128,8 +126,9 @@ public override bool Equals(object obj) /// public bool Equals(MyAccountCustomField other) { + if (other == null) return false; return base.Equals(other) - && string.Equals(Value, other.Value, StringComparison.OrdinalIgnoreCase); + && string.Equals(Value, other.Value, StringComparison.Ordinal); } /// diff --git a/src/redmine-net-api/Types/News.cs b/src/redmine-net-api/Types/News.cs index 03305ab0..e4796eef 100644 --- a/src/redmine-net-api/Types/News.cs +++ b/src/redmine-net-api/Types/News.cs @@ -202,19 +202,21 @@ public override void WriteJson(JsonWriter writer) /// /// /// - public new bool Equals(News other) + public override bool Equals(News other) { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; + if(other == null) return false; - return base.Equals(other) - && Equals(Project, other.Project) - && Equals(Author, other.Author) - && string.Equals(Title, other.Title, StringComparison.Ordinal) - && string.Equals(Summary, other.Summary, StringComparison.Ordinal) - && string.Equals(Description, other.Description, StringComparison.Ordinal) - && CreatedOn.Equals(other.CreatedOn) - && Equals(Comments, other.Comments); + var result = base.Equals(other); + result = result && Project == other.Project; + result = result && Author == other.Author; + result = result && string.Equals(Title, other.Title, StringComparison.Ordinal); + result = result && string.Equals(Summary, other.Summary, StringComparison.Ordinal); + result = result && string.Equals(Description, other.Description, StringComparison.Ordinal); + result = result && CreatedOn == other.CreatedOn; + result = result && (Attachments?.Equals(other.Attachments) ?? other.Attachments == null); + result = result && (Comments?.Equals(other.Comments) ?? other.Comments == null); + result = result && (Uploads?.Equals(other.Uploads) ?? other.Uploads == null); + return result; } /// @@ -238,7 +240,8 @@ public override int GetHashCode() { unchecked { - var hashCode = base.GetHashCode(); + var hashCode = 17; + hashCode = HashCodeHelper.GetHashCode(Id, hashCode); hashCode = HashCodeHelper.GetHashCode(Project, hashCode); hashCode = HashCodeHelper.GetHashCode(Author, hashCode); hashCode = HashCodeHelper.GetHashCode(Title, hashCode); @@ -246,6 +249,8 @@ public override int GetHashCode() hashCode = HashCodeHelper.GetHashCode(Description, hashCode); hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); hashCode = HashCodeHelper.GetHashCode(Comments, hashCode); + hashCode = HashCodeHelper.GetHashCode(Attachments, hashCode); + hashCode = HashCodeHelper.GetHashCode(Uploads, hashCode); return hashCode; } } diff --git a/src/redmine-net-api/Types/NewsComment.cs b/src/redmine-net-api/Types/NewsComment.cs index 92057ba9..3766204f 100644 --- a/src/redmine-net-api/Types/NewsComment.cs +++ b/src/redmine-net-api/Types/NewsComment.cs @@ -14,6 +14,7 @@ You may obtain a copy of the License at limitations under the License. */ +using System; using System.Diagnostics; using System.Xml; using System.Xml.Serialization; @@ -102,7 +103,9 @@ public override void WriteJson(JsonWriter writer) public override bool Equals(NewsComment other) { if (other == null) return false; - return Id == other.Id && Author == other.Author && Content == other.Content; + return Id == other.Id + && Author == other.Author + && string.Equals(Content, other.Content, StringComparison.Ordinal); } /// @@ -121,12 +124,15 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - var hashCode = base.GetHashCode(); - - hashCode = HashCodeHelper.GetHashCode(Author, hashCode); - hashCode = HashCodeHelper.GetHashCode(Content, hashCode); + unchecked + { + var hashCode = 17; + hashCode = HashCodeHelper.GetHashCode(Id, hashCode); + hashCode = HashCodeHelper.GetHashCode(Author, hashCode); + hashCode = HashCodeHelper.GetHashCode(Content, hashCode); - return hashCode; + return hashCode; + } } /// diff --git a/src/redmine-net-api/Types/Permission.cs b/src/redmine-net-api/Types/Permission.cs index d83e887f..fba871af 100644 --- a/src/redmine-net-api/Types/Permission.cs +++ b/src/redmine-net-api/Types/Permission.cs @@ -98,7 +98,7 @@ public void WriteJson(JsonWriter writer) { } /// public bool Equals(Permission other) { - return other != null && Info == other.Info; + return other != null && string.Equals(Info, other.Info, StringComparison.Ordinal); } /// diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs index 2af4593f..55af2209 100644 --- a/src/redmine-net-api/Types/Project.cs +++ b/src/redmine-net-api/Types/Project.cs @@ -308,23 +308,23 @@ public bool Equals(Project other) } return base.Equals(other) - && string.Equals(Identifier, other.Identifier, StringComparison.OrdinalIgnoreCase) - && string.Equals(Description, other.Description, StringComparison.OrdinalIgnoreCase) - && string.Equals(HomePage, other.HomePage, StringComparison.OrdinalIgnoreCase) - && string.Equals(Identifier, other.Identifier, StringComparison.OrdinalIgnoreCase) + && string.Equals(Identifier, other.Identifier, StringComparison.Ordinal) + && string.Equals(Description, other.Description, StringComparison.Ordinal) + && string.Equals(HomePage, other.HomePage, StringComparison.Ordinal) + && string.Equals(Identifier, other.Identifier, StringComparison.Ordinal) && CreatedOn == other.CreatedOn && UpdatedOn == other.UpdatedOn && Status == other.Status && IsPublic == other.IsPublic && InheritMembers == other.InheritMembers - && Equals(DefaultAssignee, other.DefaultAssignee) - && Equals(DefaultVersion, other.DefaultVersion) - && Equals(Parent, other.Parent) - && Equals(Trackers, other.Trackers) - && Equals(CustomFields, other.CustomFields) - && Equals(IssueCategories, other.IssueCategories) - && Equals(EnabledModules, other.EnabledModules) - && Equals(TimeEntryActivities, other.TimeEntryActivities); + && DefaultAssignee == other.DefaultAssignee + && DefaultVersion == other.DefaultVersion + && Parent == other.Parent + && (Trackers?.Equals(other.Trackers) ?? other.Trackers == null) + && (CustomFields?.Equals(other.CustomFields) ?? other.CustomFields == null) + && (IssueCategories?.Equals(other.IssueCategories) ?? other.IssueCategories == null) + && (EnabledModules?.Equals(other.EnabledModules) ?? other.EnabledModules == null) + && (TimeEntryActivities?.Equals(other.TimeEntryActivities) ?? other.TimeEntryActivities == null); } /// diff --git a/src/redmine-net-api/Types/ProjectMembership.cs b/src/redmine-net-api/Types/ProjectMembership.cs index 8a5fd6ce..8dd3642a 100644 --- a/src/redmine-net-api/Types/ProjectMembership.cs +++ b/src/redmine-net-api/Types/ProjectMembership.cs @@ -162,10 +162,10 @@ public override bool Equals(ProjectMembership other) { if (other == null) return false; return Id == other.Id - && Equals(Project, other.Project) - && Equals(Roles, other.Roles) - && Equals(User, other.User) - && Equals(Group, other.Group); + && Project == other.Project + && User == other.User + && Group == other.Group + && Roles != null ? Roles.Equals(other.Roles) : other.Roles == null; } /// diff --git a/src/redmine-net-api/Types/Role.cs b/src/redmine-net-api/Types/Role.cs index 4998f2a4..10a2420b 100644 --- a/src/redmine-net-api/Types/Role.cs +++ b/src/redmine-net-api/Types/Role.cs @@ -137,13 +137,13 @@ public override void ReadJson(JsonReader reader) public bool Equals(Role other) { if (other == null) return false; - return EqualityComparer.Default.Equals(Id, other.Id) && - EqualityComparer.Default.Equals(Name, other.Name) && - IsAssignable == other.IsAssignable && - EqualityComparer.Default.Equals(IssuesVisibility, other.IssuesVisibility) && - EqualityComparer.Default.Equals(TimeEntriesVisibility, other.TimeEntriesVisibility) && - EqualityComparer.Default.Equals(UsersVisibility, other.UsersVisibility) && - EqualityComparer>.Default.Equals(Permissions, other.Permissions); + return Id == other.Id + && string.Equals(Name, other.Name, StringComparison.Ordinal) + && IsAssignable == other.IsAssignable + && IssuesVisibility == other.IssuesVisibility + && TimeEntriesVisibility == other.TimeEntriesVisibility + && UsersVisibility == other.UsersVisibility + && Permissions != null ? Permissions.Equals(other.Permissions) : other.Permissions == null; } diff --git a/src/redmine-net-api/Types/Search.cs b/src/redmine-net-api/Types/Search.cs index 72aa88d7..71ba4885 100644 --- a/src/redmine-net-api/Types/Search.cs +++ b/src/redmine-net-api/Types/Search.cs @@ -124,10 +124,10 @@ public bool Equals(Search other) { if (other == null) return false; return Id == other.Id - && string.Equals(Title, other.Title, StringComparison.OrdinalIgnoreCase) - && string.Equals(Description, other.Description, StringComparison.OrdinalIgnoreCase) - && string.Equals(Url, other.Url, StringComparison.OrdinalIgnoreCase) - && string.Equals(Type, other.Type, StringComparison.OrdinalIgnoreCase) + && string.Equals(Title, other.Title, StringComparison.Ordinal) + && string.Equals(Description, other.Description, StringComparison.Ordinal) + && string.Equals(Url, other.Url, StringComparison.Ordinal) + && string.Equals(Type, other.Type, StringComparison.Ordinal) && DateTime == other.DateTime; } diff --git a/src/redmine-net-api/Types/TimeEntry.cs b/src/redmine-net-api/Types/TimeEntry.cs index afb8d110..79ad91bd 100644 --- a/src/redmine-net-api/Types/TimeEntry.cs +++ b/src/redmine-net-api/Types/TimeEntry.cs @@ -233,11 +233,11 @@ public override bool Equals(TimeEntry other) && SpentOn == other.SpentOn && Hours == other.Hours && Activity == other.Activity - && Comments == other.Comments + && string.Equals(Comments, other.Comments, StringComparison.Ordinal) && User == other.User && CreatedOn == other.CreatedOn && UpdatedOn == other.UpdatedOn - && Equals(CustomFields, other.CustomFields); + && (CustomFields?.Equals(other.CustomFields) ?? other.CustomFields == null); } /// diff --git a/src/redmine-net-api/Types/TimeEntryActivity.cs b/src/redmine-net-api/Types/TimeEntryActivity.cs index dbc6afe3..6e0ef6c7 100644 --- a/src/redmine-net-api/Types/TimeEntryActivity.cs +++ b/src/redmine-net-api/Types/TimeEntryActivity.cs @@ -132,7 +132,9 @@ public bool Equals(TimeEntryActivity other) { if (other == null) return false; - return Id == other.Id && Name == other.Name && IsDefault == other.IsDefault && IsActive == other.IsActive; + return base.Equals(other) + && IsDefault == other.IsDefault + && IsActive == other.IsActive; } /// diff --git a/src/redmine-net-api/Types/Tracker.cs b/src/redmine-net-api/Types/Tracker.cs index 891ae4d0..13d26b31 100644 --- a/src/redmine-net-api/Types/Tracker.cs +++ b/src/redmine-net-api/Types/Tracker.cs @@ -121,7 +121,10 @@ public bool Equals(Tracker other) { if (other == null) return false; - return Id == other.Id && Name == other.Name; + return base.Equals(other) + && DefaultStatus == other.DefaultStatus + && string.Equals(Description, other.Description, StringComparison.Ordinal) + && EnabledStandardFields != null ? EnabledStandardFields.Equals(other.EnabledStandardFields) : other.EnabledStandardFields != null; } /// diff --git a/src/redmine-net-api/Types/TrackerCoreField.cs b/src/redmine-net-api/Types/TrackerCoreField.cs index f5e7f6d2..54b5f1be 100644 --- a/src/redmine-net-api/Types/TrackerCoreField.cs +++ b/src/redmine-net-api/Types/TrackerCoreField.cs @@ -16,6 +16,17 @@ namespace Redmine.Net.Api.Types [XmlRoot(RedmineKeys.FIELD)] public sealed class TrackerCoreField: IXmlSerializable, IJsonSerializable, IEquatable { + /// + /// + /// + public TrackerCoreField() + { + } + + internal TrackerCoreField(string name) + { + Name = name; + } /// /// /// @@ -94,7 +105,7 @@ public void ReadJson(JsonReader reader) /// public bool Equals(TrackerCoreField other) { - return other != null && Name == other.Name; + return other != null && string.Equals(Name, other.Name, StringComparison.Ordinal); } /// diff --git a/src/redmine-net-api/Types/Upload.cs b/src/redmine-net-api/Types/Upload.cs index ca2ac4ee..be702c1f 100644 --- a/src/redmine-net-api/Types/Upload.cs +++ b/src/redmine-net-api/Types/Upload.cs @@ -170,10 +170,10 @@ public void WriteJson(JsonWriter writer) public bool Equals(Upload other) { return other != null - && string.Equals(Token, other.Token, StringComparison.OrdinalIgnoreCase) - && string.Equals(FileName, other.FileName, StringComparison.OrdinalIgnoreCase) - && string.Equals(Description, other.Description, StringComparison.OrdinalIgnoreCase) - && string.Equals(ContentType, other.ContentType, StringComparison.OrdinalIgnoreCase); + && string.Equals(Token, other.Token, StringComparison.Ordinal) + && string.Equals(FileName, other.FileName, StringComparison.Ordinal) + && string.Equals(Description, other.Description, StringComparison.Ordinal) + && string.Equals(ContentType, other.ContentType, StringComparison.Ordinal); } /// diff --git a/src/redmine-net-api/Types/User.cs b/src/redmine-net-api/Types/User.cs index 7633af2a..e47ae005 100644 --- a/src/redmine-net-api/Types/User.cs +++ b/src/redmine-net-api/Types/User.cs @@ -331,25 +331,25 @@ public override bool Equals(User other) { if (other == null) return false; return Id == other.Id - && string.Equals(AvatarUrl,other.AvatarUrl, StringComparison.OrdinalIgnoreCase) - && string.Equals(Login,other.Login, StringComparison.OrdinalIgnoreCase) - && string.Equals(FirstName,other.FirstName, StringComparison.OrdinalIgnoreCase) - && string.Equals(LastName,other.LastName, StringComparison.OrdinalIgnoreCase) - && string.Equals(Email,other.Email, StringComparison.OrdinalIgnoreCase) - && string.Equals(MailNotification,other.MailNotification, StringComparison.OrdinalIgnoreCase) - && string.Equals(ApiKey,other.ApiKey, StringComparison.OrdinalIgnoreCase) + && string.Equals(AvatarUrl,other.AvatarUrl, StringComparison.Ordinal) + && string.Equals(Login,other.Login, StringComparison.Ordinal) + && string.Equals(FirstName,other.FirstName, StringComparison.Ordinal) + && string.Equals(LastName,other.LastName, StringComparison.Ordinal) + && string.Equals(Email,other.Email, StringComparison.Ordinal) + && string.Equals(MailNotification,other.MailNotification, StringComparison.Ordinal) + && string.Equals(ApiKey,other.ApiKey, StringComparison.Ordinal) + && string.Equals(TwoFactorAuthenticationScheme,other.TwoFactorAuthenticationScheme, StringComparison.Ordinal) && AuthenticationModeId == other.AuthenticationModeId && CreatedOn == other.CreatedOn && LastLoginOn == other.LastLoginOn && Status == other.Status && MustChangePassword == other.MustChangePassword - && Equals(CustomFields, other.CustomFields) - && Equals(Memberships, other.Memberships) - && Equals(Groups, other.Groups) - && string.Equals(TwoFactorAuthenticationScheme,other.TwoFactorAuthenticationScheme, StringComparison.OrdinalIgnoreCase) && IsAdmin == other.IsAdmin && PasswordChangedOn == other.PasswordChangedOn - && UpdatedOn == other.UpdatedOn; + && UpdatedOn == other.UpdatedOn + && CustomFields != null ? CustomFields.Equals(other.CustomFields) : other.CustomFields == null + && Memberships != null ? Memberships.Equals(other.Memberships) : other.Memberships == null + && Groups != null ? Groups.Equals(other.Groups) : other.Groups == null; } /// @@ -373,27 +373,27 @@ public override int GetHashCode() { unchecked { - var hashCode = base.GetHashCode(); + var hashCode = 17; + hashCode = HashCodeHelper.GetHashCode(Id, hashCode); hashCode = HashCodeHelper.GetHashCode(AvatarUrl, hashCode); hashCode = HashCodeHelper.GetHashCode(Login, hashCode); - hashCode = HashCodeHelper.GetHashCode(Password, hashCode); hashCode = HashCodeHelper.GetHashCode(FirstName, hashCode); hashCode = HashCodeHelper.GetHashCode(LastName, hashCode); hashCode = HashCodeHelper.GetHashCode(Email, hashCode); hashCode = HashCodeHelper.GetHashCode(MailNotification, hashCode); + hashCode = HashCodeHelper.GetHashCode(ApiKey, hashCode); + hashCode = HashCodeHelper.GetHashCode(TwoFactorAuthenticationScheme, hashCode); hashCode = HashCodeHelper.GetHashCode(AuthenticationModeId, hashCode); hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); hashCode = HashCodeHelper.GetHashCode(LastLoginOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(ApiKey, hashCode); hashCode = HashCodeHelper.GetHashCode(Status, hashCode); hashCode = HashCodeHelper.GetHashCode(MustChangePassword, hashCode); - hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); - hashCode = HashCodeHelper.GetHashCode(Memberships, hashCode); - hashCode = HashCodeHelper.GetHashCode(Groups, hashCode); - hashCode = HashCodeHelper.GetHashCode(TwoFactorAuthenticationScheme, hashCode); hashCode = HashCodeHelper.GetHashCode(IsAdmin, hashCode); hashCode = HashCodeHelper.GetHashCode(PasswordChangedOn, hashCode); hashCode = HashCodeHelper.GetHashCode(UpdatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); + hashCode = HashCodeHelper.GetHashCode(Memberships, hashCode); + hashCode = HashCodeHelper.GetHashCode(Groups, hashCode); return hashCode; } } diff --git a/src/redmine-net-api/Types/Version.cs b/src/redmine-net-api/Types/Version.cs index b03d52bb..2861d4b3 100644 --- a/src/redmine-net-api/Types/Version.cs +++ b/src/redmine-net-api/Types/Version.cs @@ -230,16 +230,16 @@ public override void WriteJson(JsonWriter writer) public override bool Equals(Version other) { if (other == null) return false; - return Id == other.Id && Name == other.Name + return base.Equals(other) && Project == other.Project - && Description == other.Description - && Status == other.Status - && DueDate == other.DueDate - && Sharing == other.Sharing - && CreatedOn == other.CreatedOn - && UpdatedOn == other.UpdatedOn - && Equals(CustomFields, other.CustomFields) - && string.Equals(WikiPageTitle,other.WikiPageTitle, StringComparison.OrdinalIgnoreCase) + && string.Equals(Description, other.Description, StringComparison.Ordinal) + && Status == other.Status + && DueDate == other.DueDate + && Sharing == other.Sharing + && CreatedOn == other.CreatedOn + && UpdatedOn == other.UpdatedOn + && (CustomFields?.Equals(other.CustomFields) ?? other.CustomFields == null) + && string.Equals(WikiPageTitle,other.WikiPageTitle, StringComparison.Ordinal) && EstimatedHours == other.EstimatedHours && SpentHours == other.SpentHours; } diff --git a/src/redmine-net-api/Types/WikiPage.cs b/src/redmine-net-api/Types/WikiPage.cs index e5acd51b..d62aeb63 100644 --- a/src/redmine-net-api/Types/WikiPage.cs +++ b/src/redmine-net-api/Types/WikiPage.cs @@ -219,11 +219,12 @@ public override bool Equals(WikiPage other) && string.Equals(Title, other.Title, StringComparison.Ordinal) && string.Equals(Text, other.Text, StringComparison.Ordinal) && string.Equals(Comments, other.Comments, StringComparison.Ordinal) + && string.Equals(ParentTitle, other.ParentTitle, StringComparison.Ordinal) && Version == other.Version - && Equals(Author, other.Author) - && CreatedOn.Equals(other.CreatedOn) - && UpdatedOn.Equals(other.UpdatedOn) - && Equals(Attachments, other.Attachments); + && Author == other.Author + && CreatedOn == other.CreatedOn + && UpdatedOn == other.UpdatedOn + && (Attachments?.Equals(other.Attachments) ?? other.Attachments == null); } /// diff --git a/tests/redmine-net-api.Tests/Bugs/RedmineApi-229.cs b/tests/redmine-net-api.Tests/Bugs/RedmineApi-229.cs new file mode 100644 index 00000000..64ae736a --- /dev/null +++ b/tests/redmine-net-api.Tests/Bugs/RedmineApi-229.cs @@ -0,0 +1,125 @@ +using System; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Bugs; + +public sealed class RedmineApi229 +{ + [Fact] + public void Equals_ShouldReturnTrue_WhenComparingWithSelf() + { + // Arrange + var timeEntry = CreateSampleTimeEntry(); + + // Act & Assert + Assert.True(timeEntry.Equals(timeEntry), "TimeEntry should equal itself (reference equality)"); + Assert.True(timeEntry == timeEntry, "TimeEntry should equal itself using == operator"); + Assert.True(timeEntry.Equals((object)timeEntry), "TimeEntry should equal itself when cast to object"); + Assert.Equal(timeEntry.GetHashCode(), timeEntry.GetHashCode()); + } + + [Fact] + public void Equals_ShouldReturnTrue_WhenComparingIdenticalInstances() + { + // Arrange + var timeEntry1 = CreateSampleTimeEntry(); + var timeEntry2 = CreateSampleTimeEntry(); + + // Act & Assert + Assert.True(timeEntry1.Equals(timeEntry2), "Identical TimeEntry instances should be equal"); + Assert.True(timeEntry2.Equals(timeEntry1), "Equality should be symmetric"); + Assert.Equal(timeEntry1.GetHashCode(), timeEntry2.GetHashCode()); + } + + [Fact] + public void Equals_ShouldReturnFalse_WhenComparingWithNull() + { + // Arrange + var timeEntry = CreateSampleTimeEntry(); + + // Act & Assert + Assert.False(timeEntry.Equals(null)); + Assert.False(timeEntry.Equals((object)null)); + } + + [Fact] + public void Equals_ShouldReturnFalse_WhenComparingDifferentTypes() + { + // Arrange + var timeEntry = CreateSampleTimeEntry(); + var differentObject = new object(); + + // Act & Assert + Assert.False(timeEntry.Equals(differentObject)); + } + + [Theory] + [MemberData(nameof(GetDifferentTimeEntries))] + public void Equals_ShouldReturnFalse_WhenPropertiesDiffer(TimeEntry different, string propertyName) + { + // Arrange + var baseline = CreateSampleTimeEntry(); + + // Act & Assert + Assert.False(baseline.Equals(different), $"TimeEntries should not be equal when {propertyName} differs"); + } + + private static TimeEntry CreateSampleTimeEntry() => new() + { + Id = 1, + Project = new IdentifiableName { Id = 1, Name = "Project" }, + Issue = new IdentifiableName { Id = 1, Name = "Issue" }, + User = new IdentifiableName { Id = 1, Name = "User" }, + Activity = new IdentifiableName { Id = 1, Name = "Activity" }, + Hours = (decimal)8.0, + Comments = "Test comment", + SpentOn = new DateTime(2023, 1, 1), + CreatedOn = new DateTime(2023, 1, 1), + UpdatedOn = new DateTime(2023, 1, 1), + CustomFields = + [ + new() { Id = 1, Name = "Field1"} + ] + }; + + public static TheoryData GetDifferentTimeEntries() + { + var data = new TheoryData(); + + // Different ID + var differentId = CreateSampleTimeEntry(); + differentId.Id = 2; + data.Add(differentId, "Id"); + + // Different Project + var differentProject = CreateSampleTimeEntry(); + differentProject.Project = new IdentifiableName { Id = 2, Name = "Different Project" }; + data.Add(differentProject, "Project"); + + // Different Issue + var differentIssue = CreateSampleTimeEntry(); + differentIssue.Issue = new IdentifiableName { Id = 2, Name = "Different Issue" }; + data.Add(differentIssue, "Issue"); + + // Different Hours + var differentHours = CreateSampleTimeEntry(); + differentHours.Hours = (decimal)4.0; + data.Add(differentHours, "Hours"); + + // Different CustomFields + var differentCustomFields = CreateSampleTimeEntry(); + differentCustomFields.CustomFields = + [ + new() { Id = 2, Name = "Field2" } + ]; + data.Add(differentCustomFields, "CustomFields"); + + // Different SpentOn + var differentSpentOn = CreateSampleTimeEntry(); + differentSpentOn.SpentOn = new DateTime(2023, 1, 2); + data.Add(differentSpentOn, "SpentOn"); + + return data; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Bugs/RedmineApi-371.cs b/tests/redmine-net-api.Tests/Bugs/RedmineApi-371.cs index c77ac1c1..08b3049d 100644 --- a/tests/redmine-net-api.Tests/Bugs/RedmineApi-371.cs +++ b/tests/redmine-net-api.Tests/Bugs/RedmineApi-371.cs @@ -1,5 +1,5 @@ using System.Collections.Specialized; -using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Tests; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Net; using Redmine.Net.Api.Types; @@ -7,24 +7,24 @@ namespace Padi.DotNet.RedmineAPI.Tests.Bugs; -public sealed class RedmineApi371 : IClassFixture +public sealed class RedmineApi371 : IClassFixture { - private readonly RedmineFixture _fixture; + private readonly RedmineApiUrlsFixture _fixture; - public RedmineApi371(RedmineFixture fixture) + public RedmineApi371(RedmineApiUrlsFixture fixture) { _fixture = fixture; } [Fact] - public void Should_Return_IssueCategories() + public void Should_Return_IssueCategories_For_Project_Url() { - var result = _fixture.RedmineManager.Get(new RequestOptions() - { - QueryString = new NameValueCollection() + var result = _fixture.Sut.GetListFragment( + new RequestOptions { - { "project_id", 1.ToInvariantString() } - } - }); + QueryString = new NameValueCollection{ { "project_id", 1.ToInvariantString() } } + }); + + Assert.Equal($"projects/1/issue_categories.{_fixture.Format}", result); } } \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Clone/AttachmentCloneTests.cs b/tests/redmine-net-api.Tests/Clone/AttachmentCloneTests.cs new file mode 100644 index 00000000..dc8a2830 --- /dev/null +++ b/tests/redmine-net-api.Tests/Clone/AttachmentCloneTests.cs @@ -0,0 +1,82 @@ +using System; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Clone; + +public sealed class AttachmentCloneTests +{ + [Fact] + public void Clone_WithPopulatedProperties_ReturnsDeepCopy() + { + // Arrange + var attachment = new Attachment + { + Id = 1, + FileName = "test.txt", + FileSize = 1024, + ContentType = "text/plain", + Description = "Test file", + ContentUrl = "http://example.com/test.txt", + ThumbnailUrl = "http://example.com/thumb.txt", + Author = new IdentifiableName(1, "John Doe"), + CreatedOn = DateTime.Now + }; + + // Act + var clone = attachment.Clone(false); + + // Assert + Assert.NotNull(clone); + Assert.NotSame(attachment, clone); + Assert.Equal(attachment.Id, clone.Id); + Assert.Equal(attachment.FileName, clone.FileName); + Assert.Equal(attachment.FileSize, clone.FileSize); + Assert.Equal(attachment.ContentType, clone.ContentType); + Assert.Equal(attachment.Description, clone.Description); + Assert.Equal(attachment.ContentUrl, clone.ContentUrl); + Assert.Equal(attachment.ThumbnailUrl, clone.ThumbnailUrl); + Assert.Equal(attachment.CreatedOn, clone.CreatedOn); + + Assert.NotSame(attachment.Author, clone.Author); + Assert.Equal(attachment.Author.Id, clone.Author.Id); + Assert.Equal(attachment.Author.Name, clone.Author.Name); + } + + [Fact] + public void Clone_With_ResetId_True_Should_Return_A_Copy_With_Id_Set_Zero() + { + // Arrange + var attachment = new Attachment + { + Id = 1, + FileName = "test.txt", + FileSize = 1024, + ContentType = "text/plain", + Description = "Test file", + ContentUrl = "http://example.com/test.txt", + ThumbnailUrl = "http://example.com/thumb.txt", + Author = new IdentifiableName(1, "John Doe"), + CreatedOn = DateTime.Now + }; + + // Act + var clone = attachment.Clone(true); + + // Assert + Assert.NotNull(clone); + Assert.NotSame(attachment, clone); + Assert.NotEqual(attachment.Id, clone.Id); + Assert.Equal(attachment.FileName, clone.FileName); + Assert.Equal(attachment.FileSize, clone.FileSize); + Assert.Equal(attachment.ContentType, clone.ContentType); + Assert.Equal(attachment.Description, clone.Description); + Assert.Equal(attachment.ContentUrl, clone.ContentUrl); + Assert.Equal(attachment.ThumbnailUrl, clone.ThumbnailUrl); + Assert.Equal(attachment.CreatedOn, clone.CreatedOn); + + Assert.NotSame(attachment.Author, clone.Author); + Assert.Equal(attachment.Author.Id, clone.Author.Id); + Assert.Equal(attachment.Author.Name, clone.Author.Name); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Clone/IssueCloneTests.cs b/tests/redmine-net-api.Tests/Clone/IssueCloneTests.cs new file mode 100644 index 00000000..3aac1e6c --- /dev/null +++ b/tests/redmine-net-api.Tests/Clone/IssueCloneTests.cs @@ -0,0 +1,129 @@ +using System; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Clone; + +public sealed class IssueCloneTests +{ + [Fact] + public void Clone_WithNullProperties_ReturnsNewInstanceWithNullProperties() + { + // Arrange + var issue = new Issue(); + + // Act + var clone = issue.Clone(true); + + // Assert + Assert.NotNull(clone); + Assert.NotSame(issue, clone); + Assert.Equal(issue.Id, clone.Id); + Assert.Null(clone.Project); + Assert.Null(clone.Tracker); + Assert.Null(clone.Status); + } + + [Fact] + public void Clone_WithPopulatedProperties_ReturnsDeepCopy() + { + // Arrange + var issue = CreateSampleIssue(); + + // Act + var clone = issue.Clone(true); + + // Assert + Assert.NotNull(clone); + Assert.NotSame(issue, clone); + + Assert.NotEqual(issue.Id, clone.Id); + Assert.Equal(issue.Subject, clone.Subject); + Assert.Equal(issue.Description, clone.Description); + Assert.Equal(issue.DoneRatio, clone.DoneRatio); + Assert.Equal(issue.IsPrivate, clone.IsPrivate); + Assert.Equal(issue.EstimatedHours, clone.EstimatedHours); + Assert.Equal(issue.CreatedOn, clone.CreatedOn); + Assert.Equal(issue.UpdatedOn, clone.UpdatedOn); + Assert.Equal(issue.ClosedOn, clone.ClosedOn); + + Assert.NotSame(issue.Project, clone.Project); + Assert.Equal(issue.Project.Id, clone.Project.Id); + Assert.Equal(issue.Project.Name, clone.Project.Name); + + Assert.NotSame(issue.Tracker, clone.Tracker); + Assert.Equal(issue.Tracker.Id, clone.Tracker.Id); + Assert.Equal(issue.Tracker.Name, clone.Tracker.Name); + + Assert.NotSame(issue.CustomFields, clone.CustomFields); + Assert.Equal(issue.CustomFields.Count, clone.CustomFields.Count); + for (var i = 0; i < issue.CustomFields.Count; i++) + { + Assert.NotSame(issue.CustomFields[i], clone.CustomFields[i]); + Assert.Equal(issue.CustomFields[i].Id, clone.CustomFields[i].Id); + Assert.Equal(issue.CustomFields[i].Name, clone.CustomFields[i].Name); + } + + Assert.NotNull(clone.Attachments); + Assert.Equal(issue.Attachments.Count, clone.Attachments.Count); + Assert.All(clone.Attachments, Assert.NotNull); + } + + [Fact] + public void Clone_ModifyingClone_DoesNotAffectOriginal() + { + // Arrange + var issue = CreateSampleIssue(); + var clone = issue.Clone(true); + + // Act + clone.Subject = "Modified Subject"; + clone.Project.Name = "Modified Project"; + clone.CustomFields[0].Values = [new CustomFieldValue("Modified Value")]; + + // Assert + Assert.NotEqual(issue.Subject, clone.Subject); + Assert.NotEqual(issue.Project.Name, clone.Project.Name); + Assert.NotEqual(issue.CustomFields[0].Values, clone.CustomFields[0].Values); + } + + private static Issue CreateSampleIssue() + { + return new Issue + { + Id = 1, + Project = new IdentifiableName(100, "Test Project"), + Tracker = new IdentifiableName(200, "Bug"), + Status = new IdentifiableName(300, "New"), + Priority = new IdentifiableName(400, "Normal"), + Author = new IdentifiableName(500, "John Doe"), + Subject = "Test Issue", + Description = "Test Description", + StartDate = DateTime.Today, + DueDate = DateTime.Today.AddDays(7), + DoneRatio = 50, + IsPrivate = false, + EstimatedHours = 8.5f, + CreatedOn = DateTime.Now.AddDays(-1), + UpdatedOn = DateTime.Now, + CustomFields = + [ + new IssueCustomField + { + Id = 1, + Name = "Custom Field 1", + } + ], + Attachments = + [ + new Attachment + { + Id = 1, + FileName = "test.txt", + FileSize = 1024, + Author = new IdentifiableName(1, "Author") + } + ] + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Clone/JournalCloneTests.cs b/tests/redmine-net-api.Tests/Clone/JournalCloneTests.cs new file mode 100644 index 00000000..37440f6e --- /dev/null +++ b/tests/redmine-net-api.Tests/Clone/JournalCloneTests.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Clone; + +public sealed class JournalCloneTests +{ + [Fact] + public void Clone_WithPopulatedProperties_ReturnsDeepCopy() + { + // Arrange + var journal = new Journal + { + Id = 1, + User = new IdentifiableName(1, "John Doe"), + Notes = "Test notes", + CreatedOn = DateTime.Now, + PrivateNotes = true, + Details = (List) + [ + new Detail + { + Property = "status_id", + Name = "Status", + OldValue = "1", + NewValue = "2" + } + ] + }; + + // Act + var clone = journal.Clone(false); + + // Assert + Assert.NotNull(clone); + Assert.NotSame(journal, clone); + Assert.Equal(journal.Id, clone.Id); + Assert.Equal(journal.Notes, clone.Notes); + Assert.Equal(journal.CreatedOn, clone.CreatedOn); + Assert.Equal(journal.PrivateNotes, clone.PrivateNotes); + + Assert.NotSame(journal.User, clone.User); + Assert.Equal(journal.User.Id, clone.User.Id); + Assert.Equal(journal.User.Name, clone.User.Name); + + Assert.NotNull(clone.Details); + Assert.NotSame(journal.Details, clone.Details); + Assert.Equal(journal.Details.Count, clone.Details.Count); + + var originalDetail = journal.Details[0]; + var clonedDetail = clone.Details[0]; + Assert.NotSame(originalDetail, clonedDetail); + Assert.Equal(originalDetail.Property, clonedDetail.Property); + Assert.Equal(originalDetail.Name, clonedDetail.Name); + Assert.Equal(originalDetail.OldValue, clonedDetail.OldValue); + Assert.Equal(originalDetail.NewValue, clonedDetail.NewValue); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/AttachmentEqualityTests.cs b/tests/redmine-net-api.Tests/Equality/AttachmentEqualityTests.cs new file mode 100644 index 00000000..6cc72dd1 --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/AttachmentEqualityTests.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class AttachmentEqualityTests +{ + [Fact] + public void Equals_SameReference_ReturnsTrue() + { + var attachment = CreateSampleAttachment(); + Assert.True(attachment.Equals(attachment)); + } + + [Fact] + public void Equals_Null_ReturnsFalse() + { + var attachment = CreateSampleAttachment(); + Assert.False(attachment.Equals(null)); + } + + [Theory] + [MemberData(nameof(GetDifferentAttachments))] + public void Equals_DifferentProperties_ReturnsFalse(Attachment attachment1, Attachment attachment2, string propertyName) + { + Assert.False(attachment1.Equals(attachment2), $"Attachments should not be equal when {propertyName} is different"); + } + + public static IEnumerable GetDifferentAttachments() + { + var baseAttachment = CreateSampleAttachment(); + + // Different FileName + var differentFileName = CreateSampleAttachment(); + differentFileName.FileName = "different.txt"; + yield return [baseAttachment, differentFileName, "FileName"]; + + // Different FileSize + var differentFileSize = CreateSampleAttachment(); + differentFileSize.FileSize = 2048; + yield return [baseAttachment, differentFileSize, "FileSize"]; + + // Different Author + var differentAuthor = CreateSampleAttachment(); + differentAuthor.Author = new IdentifiableName { Id = 999, Name = "Different Author" }; + yield return [baseAttachment, differentAuthor, "Author"]; + } + + private static Attachment CreateSampleAttachment() + { + return new Attachment + { + Id = 1, + FileName = "test.txt", + FileSize = 1024, + ContentType = "text/plain", + Description = "Test file", + ContentUrl = "https://example.com/test.txt", + ThumbnailUrl = "https://example.com/thumb.txt", + Author = new IdentifiableName { Id = 1, Name = "John Doe" }, + CreatedOn = DateTime.Now + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/BaseEqualityTests.cs b/tests/redmine-net-api.Tests/Equality/BaseEqualityTests.cs new file mode 100644 index 00000000..5ff00d44 --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/BaseEqualityTests.cs @@ -0,0 +1,66 @@ +using System; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public abstract class BaseEqualityTests where T : class, IEquatable + { + protected abstract T CreateSampleInstance(); + protected abstract T CreateDifferentInstance(); + + [Fact] + public void Equals_SameReference_ReturnsTrue() + { + var instance = CreateSampleInstance(); + Assert.True(instance.Equals(instance)); + } + + [Fact] + public void Equals_Null_ReturnsFalse() + { + var instance = CreateSampleInstance(); + Assert.False(instance.Equals(null)); + } + + [Fact] + public void Equals_DifferentType_ReturnsFalse() + { + var instance = CreateSampleInstance(); + var differentObject = new object(); + Assert.False(instance.Equals(differentObject)); + } + + [Fact] + public void Equals_IdenticalProperties_ReturnsTrue() + { + var instance1 = CreateSampleInstance(); + var instance2 = CreateSampleInstance(); + Assert.True(instance1.Equals(instance2)); + Assert.True(instance2.Equals(instance1)); + } + + [Fact] + public void Equals_DifferentProperties_ReturnsFalse() + { + var instance1 = CreateSampleInstance(); + var instance2 = CreateDifferentInstance(); + Assert.False(instance1.Equals(instance2)); + Assert.False(instance2.Equals(instance1)); + } + + [Fact] + public void GetHashCode_SameProperties_ReturnsSameValue() + { + var instance1 = CreateSampleInstance(); + var instance2 = CreateSampleInstance(); + Assert.Equal(instance1.GetHashCode(), instance2.GetHashCode()); + } + + [Fact] + public void GetHashCode_DifferentProperties_ReturnsDifferentValues() + { + var instance1 = CreateSampleInstance(); + var instance2 = CreateDifferentInstance(); + Assert.NotEqual(instance1.GetHashCode(), instance2.GetHashCode()); + } + } \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/CustomFieldPossibleValueTests.cs b/tests/redmine-net-api.Tests/Equality/CustomFieldPossibleValueTests.cs new file mode 100644 index 00000000..c6d0fdfc --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/CustomFieldPossibleValueTests.cs @@ -0,0 +1,24 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class CustomFieldPossibleValueTests : BaseEqualityTests +{ + protected override CustomFieldPossibleValue CreateSampleInstance() + { + return new CustomFieldPossibleValue + { + Value = "test-value", + Label = "Test Label" + }; + } + + protected override CustomFieldPossibleValue CreateDifferentInstance() + { + return new CustomFieldPossibleValue + { + Value = "different-value", + Label = "Different Label" + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/CustomFieldRoleTests.cs b/tests/redmine-net-api.Tests/Equality/CustomFieldRoleTests.cs new file mode 100644 index 00000000..7026ff0c --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/CustomFieldRoleTests.cs @@ -0,0 +1,24 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class CustomFieldRoleTests : BaseEqualityTests +{ + protected override IdentifiableName CreateSampleInstance() + { + return new CustomFieldRole + { + Id = 1, + Name = "Test Role" + }; + } + + protected override IdentifiableName CreateDifferentInstance() + { + return new CustomFieldRole + { + Id = 2, + Name = "Different Role" + }; + } +} diff --git a/tests/redmine-net-api.Tests/Equality/CustomFieldTests.cs b/tests/redmine-net-api.Tests/Equality/CustomFieldTests.cs new file mode 100644 index 00000000..4ae461b5 --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/CustomFieldTests.cs @@ -0,0 +1,34 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class CustomFieldTests : BaseEqualityTests +{ + protected override CustomField CreateSampleInstance() + { + return new CustomField + { + Id = 1, + Name = "Test Field", + CustomizedType = "issue", + FieldFormat = "string", + Regexp = "", + MinLength = 0, + MaxLength = 100, + IsRequired = false, + IsFilter = true, + Searchable = true, + Multiple = false, + DefaultValue = "default", + Visible = true, + PossibleValues = [new CustomFieldPossibleValue { Value = "value1", Label = "Label 1" }] + }; + } + + protected override CustomField CreateDifferentInstance() + { + var field = CreateSampleInstance(); + field.Name = "Different Field"; + return field; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/DetailTests.cs b/tests/redmine-net-api.Tests/Equality/DetailTests.cs new file mode 100644 index 00000000..296a21d3 --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/DetailTests.cs @@ -0,0 +1,28 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class DetailTests : BaseEqualityTests +{ + protected override Detail CreateSampleInstance() + { + return new Detail + { + Property = "status", + Name = "Status", + OldValue = "1", + NewValue = "2" + }; + } + + protected override Detail CreateDifferentInstance() + { + return new Detail + { + Property = "priority", + Name = "Priority", + OldValue = "3", + NewValue = "4" + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/ErrorTests.cs b/tests/redmine-net-api.Tests/Equality/ErrorTests.cs new file mode 100644 index 00000000..46f52c3b --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/ErrorTests.cs @@ -0,0 +1,16 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class ErrorTests : BaseEqualityTests +{ + protected override Error CreateSampleInstance() + { + return new Error( "Test error" ); + } + + protected override Error CreateDifferentInstance() + { + return new Error("Different error"); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/GroupTests.cs b/tests/redmine-net-api.Tests/Equality/GroupTests.cs new file mode 100644 index 00000000..ee272db1 --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/GroupTests.cs @@ -0,0 +1,26 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class GroupTests : BaseEqualityTests +{ + protected override Group CreateSampleInstance() + { + return new Group + { + Id = 1, + Name = "Test Group", + Users = [new GroupUser { Id = 1, Name = "User 1" }], + CustomFields = [new IssueCustomField { Id = 1, Name = "Field 1" }], + Memberships = [new Membership { Id = 1, Project = new IdentifiableName { Id = 1, Name = "Project 1" } }] + }; + } + + protected override Group CreateDifferentInstance() + { + var group = CreateSampleInstance(); + group.Name = "Different Group"; + group.Users = [new GroupUser { Id = 2, Name = "User 2" }]; + return group; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/GroupUserTests.cs b/tests/redmine-net-api.Tests/Equality/GroupUserTests.cs new file mode 100644 index 00000000..f177b4eb --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/GroupUserTests.cs @@ -0,0 +1,24 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class GroupUserTests : BaseEqualityTests +{ + protected override IdentifiableName CreateSampleInstance() + { + return new GroupUser + { + Id = 1, + Name = "Test User" + }; + } + + protected override IdentifiableName CreateDifferentInstance() + { + return new GroupUser + { + Id = 2, + Name = "Different User" + }; + } +} diff --git a/tests/redmine-net-api.Tests/Equality/IssueCategoryTests.cs b/tests/redmine-net-api.Tests/Equality/IssueCategoryTests.cs new file mode 100644 index 00000000..1a00e021 --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/IssueCategoryTests.cs @@ -0,0 +1,28 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class IssueCategoryTests : BaseEqualityTests +{ + protected override IssueCategory CreateSampleInstance() + { + return new IssueCategory + { + Id = 1, + Name = "Test Category", + Project = new IdentifiableName { Id = 1, Name = "Project 1" }, + AssignTo = new IdentifiableName { Id = 1, Name = "User 1" } + }; + } + + protected override IssueCategory CreateDifferentInstance() + { + return new IssueCategory + { + Id = 2, + Name = "Different Category", + Project = new IdentifiableName { Id = 2, Name = "Project 2" }, + AssignTo = new IdentifiableName { Id = 2, Name = "User 2" } + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/IssueEqualityTests.cs b/tests/redmine-net-api.Tests/Equality/IssueEqualityTests.cs new file mode 100644 index 00000000..90dcc08a --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/IssueEqualityTests.cs @@ -0,0 +1,114 @@ +using System; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class IssueEqualityTests +{ + [Fact] + public void Equals_SameReference_ReturnsTrue() + { + var issue = CreateSampleIssue(); + Assert.True(issue.Equals(issue)); + } + + [Fact] + public void Equals_Null_ReturnsFalse() + { + var issue = CreateSampleIssue(); + Assert.False(issue.Equals(null)); + } + + [Fact] + public void Equals_DifferentType_ReturnsFalse() + { + var issue = CreateSampleIssue(); + var differentObject = new object(); + Assert.False(issue.Equals(differentObject)); + } + + [Fact] + public void Equals_IdenticalProperties_ReturnsTrue() + { + var issue1 = CreateSampleIssue(); + var issue2 = CreateSampleIssue(); + Assert.True(issue1.Equals(issue2)); + Assert.True(issue2.Equals(issue1)); + } + + [Fact] + public void GetHashCode_SameProperties_ReturnsSameValue() + { + var issue1 = CreateSampleIssue(); + var issue2 = CreateSampleIssue(); + Assert.Equal(issue1.GetHashCode(), issue2.GetHashCode()); + } + + [Fact] + public void OperatorEquals_SameObjects_ReturnsTrue() + { + var issue1 = CreateSampleIssue(); + var issue2 = CreateSampleIssue(); + Assert.True(issue1 == issue2); + } + + [Fact] + public void OperatorNotEquals_DifferentObjects_ReturnsTrue() + { + var issue1 = CreateSampleIssue(); + var issue2 = CreateSampleIssue(); + issue2.Subject = "Different Subject"; + Assert.True(issue1 != issue2); + } + + [Fact] + public void Equals_NullCollections_ReturnsTrue() + { + var issue1 = CreateSampleIssue(); + var issue2 = CreateSampleIssue(); + issue1.CustomFields = null; + issue2.CustomFields = null; + Assert.True(issue1.Equals(issue2)); + } + + [Fact] + public void Equals_DifferentCollectionSizes_ReturnsFalse() + { + var issue1 = CreateSampleIssue(); + var issue2 = CreateSampleIssue(); + issue2.CustomFields.Add(new IssueCustomField { Id = 2, Name = "Additional Field" }); + Assert.False(issue1.Equals(issue2)); + } + + private static Issue CreateSampleIssue() + { + return new Issue + { + Id = 1, + Project = new IdentifiableName { Id = 100, Name = "Test Project" }, + Tracker = new IdentifiableName { Id = 1, Name = "Bug" }, + Status = new IdentifiableName { Id = 1, Name = "New" }, + Priority = new IdentifiableName { Id = 1, Name = "Normal" }, + Author = new IdentifiableName { Id = 1, Name = "John Doe" }, + Subject = "Test Issue", + Description = "Test Description", + StartDate = new DateTime(2025, 02,02,10,10,10).Date, + DueDate = new DateTime(2025, 02,02,10,10,10).Date.AddDays(7), + DoneRatio = 0, + IsPrivate = false, + EstimatedHours = 8.5f, + CreatedOn = new DateTime(2025, 02,02,10,10,10), + UpdatedOn = new DateTime(2025, 02,04,15,10,5), + CustomFields = + [ + new IssueCustomField + { + Id = 1, + Name = "Custom Field 1", + Values = [new CustomFieldValue("Value 1")] + } + ] + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/IssueStatusTests.cs b/tests/redmine-net-api.Tests/Equality/IssueStatusTests.cs new file mode 100644 index 00000000..793f7e5c --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/IssueStatusTests.cs @@ -0,0 +1,28 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class IssueStatusTests : BaseEqualityTests +{ + protected override IssueStatus CreateSampleInstance() + { + return new IssueStatus + { + Id = 1, + Name = "New", + IsDefault = true, + IsClosed = false + }; + } + + protected override IssueStatus CreateDifferentInstance() + { + return new IssueStatus + { + Id = 2, + Name = "Closed", + IsDefault = false, + IsClosed = true + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/JournalEqualityTests.cs b/tests/redmine-net-api.Tests/Equality/JournalEqualityTests.cs new file mode 100644 index 00000000..16f2a073 --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/JournalEqualityTests.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class JournalEqualityTests +{ + [Fact] + public void Equals_SameReference_ReturnsTrue() + { + var journal = CreateSampleJournal(); + Assert.True(journal.Equals(journal)); + } + + [Fact] + public void Equals_Null_ReturnsFalse() + { + var journal = CreateSampleJournal(); + Assert.False(journal.Equals(null)); + } + + [Theory] + [MemberData(nameof(GetDifferentJournals))] + public void Equals_DifferentProperties_ReturnsFalse(Journal journal1, Journal journal2, string propertyName) + { + Assert.False(journal1.Equals(journal2), $"Journals should not be equal when {propertyName} is different"); + } + + public static IEnumerable GetDifferentJournals() + { + var baseJournal = CreateSampleJournal(); + + // Different Notes + var differentNotes = CreateSampleJournal(); + differentNotes.Notes = "Different notes"; + yield return [baseJournal, differentNotes, "Notes"]; + + // Different User + var differentUser = CreateSampleJournal(); + differentUser.User = new IdentifiableName { Id = 999, Name = "Different User" }; + yield return [baseJournal, differentUser, "User"]; + + // Different Details + var differentDetails = CreateSampleJournal(); + differentDetails.Details[0].NewValue = "Different value"; + yield return [baseJournal, differentDetails, "Details"]; + } + + private static Journal CreateSampleJournal() + { + return new Journal + { + Id = 1, + User = new IdentifiableName { Id = 1, Name = "John Doe" }, + Notes = "Test notes", + CreatedOn = new DateTime(2025,02,14,14,04,00), + PrivateNotes = true, + Details = + [ + new Detail + { + Property = "status_id", + Name = "Status", + OldValue = "1", + NewValue = "2" + } + ] + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/MembershipTests.cs b/tests/redmine-net-api.Tests/Equality/MembershipTests.cs new file mode 100644 index 00000000..41f15546 --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/MembershipTests.cs @@ -0,0 +1,28 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class MembershipTests : BaseEqualityTests +{ + protected override Membership CreateSampleInstance() + { + return new Membership + { + Id = 1, + Project = new IdentifiableName { Id = 1, Name = "Project 1" }, + User = new IdentifiableName { Id = 1, Name = "User 1" }, + Roles = [new MembershipRole { Id = 1, Name = "Developer", Inherited = false }] + }; + } + + protected override Membership CreateDifferentInstance() + { + return new Membership + { + Id = 2, + Project = new IdentifiableName { Id = 2, Name = "Project 2" }, + User = new IdentifiableName { Id = 2, Name = "User 2" }, + Roles = [new MembershipRole { Id = 2, Name = "Manager", Inherited = true }] + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/MyAccountCustomFieldTests.cs b/tests/redmine-net-api.Tests/Equality/MyAccountCustomFieldTests.cs new file mode 100644 index 00000000..aa2036d6 --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/MyAccountCustomFieldTests.cs @@ -0,0 +1,26 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public class MyAccountCustomFieldTests : BaseEqualityTests +{ + protected override MyAccountCustomField CreateSampleInstance() + { + return new MyAccountCustomField + { + Id = 1, + Name = "Test Field", + Value = "Test Value", + }; + } + + protected override MyAccountCustomField CreateDifferentInstance() + { + return new MyAccountCustomField + { + Id = 2, + Name = "Different Field", + Value = "Different Value", + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/MyAccountTests.cs b/tests/redmine-net-api.Tests/Equality/MyAccountTests.cs new file mode 100644 index 00000000..6c0fad96 --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/MyAccountTests.cs @@ -0,0 +1,40 @@ +using System; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class MyAccountTests : BaseEqualityTests +{ + protected override MyAccount CreateSampleInstance() + { + return new MyAccount + { + Id = 1, + Login = "testaccount", + FirstName = "Test", + LastName = "Account", + Email = "test@example.com", + CreatedOn = new DateTime(2023, 1, 1).Date, + LastLoginOn = new DateTime(2023, 1, 1).Date, + ApiKey = "abc123", + CustomFields = [ + new MyAccountCustomField() { Value = "Value 1" } + ] + }; + } + + protected override MyAccount CreateDifferentInstance() + { + return new MyAccount + { + Id = 2, + Login = "differentaccount", + FirstName = "Different", + LastName = "Account", + Email = "different@example.com", + CreatedOn = new DateTime(2023, 1, 2).Date, + LastLoginOn = new DateTime(2023, 1, 2).Date, + ApiKey = "xyz789" + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/NewsTests.cs b/tests/redmine-net-api.Tests/Equality/NewsTests.cs new file mode 100644 index 00000000..953775ea --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/NewsTests.cs @@ -0,0 +1,37 @@ +using System; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class NewsTests : BaseEqualityTests +{ + protected override News CreateSampleInstance() + { + return new News + { + Id = 1, + Project = new IdentifiableName { Id = 1, Name = "Project 1" }, + Author = new IdentifiableName { Id = 1, Name = "Author 1" }, + Title = "Test News", + Summary = "Test Summary", + Description = "Test Description", + CreatedOn = new DateTime(2023, 1, 1, 0, 0, 0).Date, + Comments = [new NewsComment { Id = 1, Content = "Test Comment" }] + }; + } + + protected override News CreateDifferentInstance() + { + return new News + { + Id = 2, + Project = new IdentifiableName { Id = 2, Name = "Project 2" }, + Author = new IdentifiableName { Id = 2, Name = "Author 2" }, + Title = "Different News", + Summary = "Different Summary", + Description = "Different Description", + CreatedOn = new DateTime(2023, 1, 2).Date, + Comments = [new NewsComment { Id = 2, Content = "Different Comment" }] + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/PermissionTests.cs b/tests/redmine-net-api.Tests/Equality/PermissionTests.cs new file mode 100644 index 00000000..cc35641b --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/PermissionTests.cs @@ -0,0 +1,22 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class PermissionTests : BaseEqualityTests +{ + protected override Permission CreateSampleInstance() + { + return new Permission + { + Info = "add_issues" + }; + } + + protected override Permission CreateDifferentInstance() + { + return new Permission + { + Info = "edit_issues" + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/ProjectMembershipTests.cs b/tests/redmine-net-api.Tests/Equality/ProjectMembershipTests.cs new file mode 100644 index 00000000..6ed4ea0d --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/ProjectMembershipTests.cs @@ -0,0 +1,28 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class ProjectMembershipTests : BaseEqualityTests +{ + protected override ProjectMembership CreateSampleInstance() + { + return new ProjectMembership + { + Id = 1, + Project = new IdentifiableName { Id = 1, Name = "Project 1" }, + User = new IdentifiableName { Id = 1, Name = "User 1" }, + Roles = [new MembershipRole { Id = 1, Name = "Developer" }] + }; + } + + protected override ProjectMembership CreateDifferentInstance() + { + return new ProjectMembership + { + Id = 2, + Project = new IdentifiableName { Id = 2, Name = "Project 2" }, + User = new IdentifiableName { Id = 2, Name = "User 2" }, + Roles = [new MembershipRole { Id = 2, Name = "Manager" }] + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/ProjectTests.cs b/tests/redmine-net-api.Tests/Equality/ProjectTests.cs new file mode 100644 index 00000000..2f2ce5a6 --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/ProjectTests.cs @@ -0,0 +1,91 @@ +using System; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class ProjectTests : BaseEqualityTests +{ + protected override Project CreateSampleInstance() + { + return new Project + { + Id = 1, + Name = "Test Project", + Identifier = "test-project", + Description = "Test Description", + HomePage = "https://test.com/", + Status = ProjectStatus.Active, + IsPublic = true, + InheritMembers = true, + DefaultAssignee = new IdentifiableName(5, "DefaultAssignee"), + DefaultVersion = new IdentifiableName(5, "DefaultVersion"), + Parent = new IdentifiableName { Id = 1, Name = "Parent Project" }, + CreatedOn = new DateTime(2023, 1, 1).Date, + UpdatedOn = new DateTime(2023, 1, 1).Date, + Trackers = + [ + new() { Id = 1, Name = "Bug" }, + new() { Id = 2, Name = "Feature" } + ], + + CustomFields = + [ + new() { Id = 1, Name = "Field1"}, + new() { Id = 2, Name = "Field2"} + ], + + IssueCategories = + [ + new() { Id = 1, Name = "Category1" }, + new() { Id = 2, Name = "Category2" } + ], + EnabledModules = + [ + new() { Id = 1, Name = "Module1" }, + new() { Id = 2, Name = "Module2" } + ], + TimeEntryActivities = + [ + new() { Id = 1, Name = "Activity1" }, + new() { Id = 2, Name = "Activity2" } + ] + }; + } + + protected override Project CreateDifferentInstance() + { + return new Project + { + Id = 2, + Name = "Different Project", + Identifier = "different-project", + Description = "Different Description", + HomePage = "https://different.com/", + Status = ProjectStatus.Archived, + IsPublic = false, + Parent = new IdentifiableName { Id = 2, Name = "Different Parent" }, + CreatedOn = new DateTime(2023, 1, 2).Date, + UpdatedOn = new DateTime(2023, 1, 2).Date, + Trackers = + [ + new() { Id = 3, Name = "Different Bug" } + ], + CustomFields = + [ + new() { Id = 3, Name = "DifferentField"} + ], + IssueCategories = + [ + new() { Id = 3, Name = "DifferentCategory" } + ], + EnabledModules = + [ + new() { Id = 3, Name = "DifferentModule" } + ], + TimeEntryActivities = + [ + new() { Id = 3, Name = "DifferentActivity" } + ] + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/QueryTests.cs b/tests/redmine-net-api.Tests/Equality/QueryTests.cs new file mode 100644 index 00000000..74b8beee --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/QueryTests.cs @@ -0,0 +1,28 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class QueryTests : BaseEqualityTests +{ + protected override Query CreateSampleInstance() + { + return new Query + { + Id = 1, + Name = "Test Query", + IsPublic = true, + ProjectId = 1 + }; + } + + protected override Query CreateDifferentInstance() + { + return new Query + { + Id = 2, + Name = "Different Query", + IsPublic = false, + ProjectId = 2 + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/RoleTests.cs b/tests/redmine-net-api.Tests/Equality/RoleTests.cs new file mode 100644 index 00000000..8879c4ba --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/RoleTests.cs @@ -0,0 +1,35 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class RoleTests : BaseEqualityTests +{ + protected override Role CreateSampleInstance() + { + return new Role + { + Id = 1, + Name = "Developer", + Permissions = + [ + new Permission { Info = "add_issues" }, + new Permission { Info = "edit_issues" } + ], + IsAssignable = true + }; + } + + protected override Role CreateDifferentInstance() + { + return new Role + { + Id = 2, + Name = "Manager", + Permissions = + [ + new Permission { Info = "manage_project" } + ], + IsAssignable = false + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/SearchTests.cs b/tests/redmine-net-api.Tests/Equality/SearchTests.cs new file mode 100644 index 00000000..4962b131 --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/SearchTests.cs @@ -0,0 +1,33 @@ +using System; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class SearchTests : BaseEqualityTests +{ + protected override Search CreateSampleInstance() + { + return new Search + { + Id = 1, + Title = "Test Search", + Type = "issue", + Url = "http://example.com/search", + Description = "Test Description", + DateTime = new DateTime(2023, 1, 1).Date + }; + } + + protected override Search CreateDifferentInstance() + { + return new Search + { + Id = 2, + Title = "Different Search", + Type = "wiki", + Url = "http://example.com/different", + Description = "Different Description", + DateTime = new DateTime(2023, 1, 2).Date + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/TimeEntryActivityTests.cs b/tests/redmine-net-api.Tests/Equality/TimeEntryActivityTests.cs new file mode 100644 index 00000000..a1554384 --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/TimeEntryActivityTests.cs @@ -0,0 +1,28 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class TimeEntryActivityTests : BaseEqualityTests +{ + protected override TimeEntryActivity CreateSampleInstance() + { + return new TimeEntryActivity + { + Id = 1, + Name = "Development", + IsDefault = true, + IsActive = true + }; + } + + protected override TimeEntryActivity CreateDifferentInstance() + { + return new TimeEntryActivity + { + Id = 2, + Name = "Testing", + IsDefault = false, + IsActive = false + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/TimeEntryTests.cs b/tests/redmine-net-api.Tests/Equality/TimeEntryTests.cs new file mode 100644 index 00000000..6e1c3426 --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/TimeEntryTests.cs @@ -0,0 +1,53 @@ +using System; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class TimeEntryTests : BaseEqualityTests +{ + protected override TimeEntry CreateSampleInstance() + { + return new TimeEntry + { + Id = 1, + Project = new IdentifiableName { Id = 1, Name = "Project 1" }, + Issue = new IdentifiableName { Id = 1, Name = "Issue 1" }, + User = new IdentifiableName { Id = 1, Name = "User 1" }, + Activity = new IdentifiableName { Id = 1, Name = "Development" }, + Hours = (decimal)8.0, + Comments = "Work done", + SpentOn = new DateTime(2023, 1, 1).Date, + CreatedOn = new DateTime(2023, 1, 1).Date, + UpdatedOn = new DateTime(2023, 1, 1).Date, + CustomFields = + [ + new IssueCustomField + { + Id = 1, + Name = "Field 1", + Values = + [ + new CustomFieldValue("value") + ] + } + ] + }; + } + + protected override TimeEntry CreateDifferentInstance() + { + return new TimeEntry + { + Id = 2, + Project = new IdentifiableName { Id = 2, Name = "Project 2" }, + Issue = new IdentifiableName { Id = 2, Name = "Issue 2" }, + User = new IdentifiableName { Id = 2, Name = "User 2" }, + Activity = new IdentifiableName { Id = 2, Name = "Testing" }, + Hours = (decimal)4.0, + Comments = "Different work", + SpentOn = new DateTime(2023, 1, 2).Date, + CreatedOn = new DateTime(2023, 1, 2).Date, + UpdatedOn = new DateTime(2023, 1, 2).Date + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/TrackerCoreFieldTests.cs b/tests/redmine-net-api.Tests/Equality/TrackerCoreFieldTests.cs new file mode 100644 index 00000000..24f6cecb --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/TrackerCoreFieldTests.cs @@ -0,0 +1,16 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class TrackerCoreFieldTests : BaseEqualityTests +{ + protected override TrackerCoreField CreateSampleInstance() + { + return new TrackerCoreField("Developer"); + } + + protected override TrackerCoreField CreateDifferentInstance() + { + return new TrackerCoreField("Admin"); + } +} diff --git a/tests/redmine-net-api.Tests/Equality/TrackerCustomFieldTests.cs b/tests/redmine-net-api.Tests/Equality/TrackerCustomFieldTests.cs new file mode 100644 index 00000000..e06fd6ea --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/TrackerCustomFieldTests.cs @@ -0,0 +1,24 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class TrackerCustomFieldTests : BaseEqualityTests +{ + protected override IdentifiableName CreateSampleInstance() + { + return new TrackerCustomField + { + Id = 1, + Name = "Test Field" + }; + } + + protected override IdentifiableName CreateDifferentInstance() + { + return new TrackerCustomField + { + Id = 2, + Name = "Different Field" + }; + } +} diff --git a/tests/redmine-net-api.Tests/Equality/UploadTests.cs b/tests/redmine-net-api.Tests/Equality/UploadTests.cs new file mode 100644 index 00000000..48ded660 --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/UploadTests.cs @@ -0,0 +1,28 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class UploadTests : BaseEqualityTests +{ + protected override Upload CreateSampleInstance() + { + return new Upload + { + Token = "abc123", + FileName = "test.pdf", + ContentType = "application/pdf", + Description = "Test Upload" + }; + } + + protected override Upload CreateDifferentInstance() + { + return new Upload + { + Token = "xyz789", + FileName = "different.pdf", + ContentType = "application/pdf", + Description = "Different Upload" + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/UserGroupTests.cs b/tests/redmine-net-api.Tests/Equality/UserGroupTests.cs new file mode 100644 index 00000000..005809cb --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/UserGroupTests.cs @@ -0,0 +1,25 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class UserGroupTests : BaseEqualityTests +{ + protected override IdentifiableName CreateSampleInstance() + { + return new UserGroup + { + Id = 1, + Name = "Test Group" + }; + } + + protected override IdentifiableName CreateDifferentInstance() + { + return new UserGroup + { + Id = 2, + Name = "Different Group" + }; + } +} + diff --git a/tests/redmine-net-api.Tests/Equality/UserTests.cs b/tests/redmine-net-api.Tests/Equality/UserTests.cs new file mode 100644 index 00000000..ffc52415 --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/UserTests.cs @@ -0,0 +1,65 @@ +using System; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class UserTests : BaseEqualityTests +{ + protected override User CreateSampleInstance() + { + return new User + { + Id = 1, + Login = "testuser", + FirstName = "Test", + LastName = "User", + Email = "test@example.com", + CreatedOn = new DateTime(2023, 1, 1).Date, + LastLoginOn = new DateTime(2023, 1, 1).Date, + ApiKey = "abc123", + Status = UserStatus.StatusActive, + IsAdmin = false, + CustomFields = + [ + new IssueCustomField + { + Id = 1, + Name = "Field 1", + Values = + [ + new CustomFieldValue("Value 1") + ] + } + ], + Memberships = + [ + new Membership + { + Id = 1, + Project = new IdentifiableName { Id = 1, Name = "Project 1" } + } + ], + Groups = + [ + new UserGroup { Id = 1, Name = "Group 1" } + ] + }; + } + + protected override User CreateDifferentInstance() + { + return new User + { + Id = 2, + Login = "differentuser", + FirstName = "Different", + LastName = "User", + Email = "different@example.com", + CreatedOn = new DateTime(2023, 1, 2).Date, + LastLoginOn = new DateTime(2023, 1, 2).Date, + ApiKey = "xyz789", + Status = UserStatus.StatusLocked, + IsAdmin = true + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/VersionTests.cs b/tests/redmine-net-api.Tests/Equality/VersionTests.cs new file mode 100644 index 00000000..5d3e06cc --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/VersionTests.cs @@ -0,0 +1,47 @@ +using System; +using Redmine.Net.Api.Types; +using Version = Redmine.Net.Api.Types.Version; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class VersionTests : BaseEqualityTests +{ + protected override Version CreateSampleInstance() + { + return new Version + { + Id = 1, + Project = new IdentifiableName { Id = 1, Name = "Project 1" }, + Name = "1.0.0", + Description = "First Release", + Status = VersionStatus.Open, + DueDate = new DateTime(2023, 12, 31).Date, + CreatedOn = new DateTime(2023, 1, 1).Date, + UpdatedOn = new DateTime(2023, 1, 1).Date, + Sharing = VersionSharing.None, + CustomFields = + [ + new IssueCustomField + { + Id = 1, Name = "Field 1", Values = [new CustomFieldValue("Value 1")] + } + ] + }; + } + + protected override Version CreateDifferentInstance() + { + return new Version + { + Id = 2, + Project = new IdentifiableName { Id = 2, Name = "Project 2" }, + Name = "2.0.0", + Description = "Second Release", + Status = VersionStatus.Closed, + DueDate = new DateTime(2024, 12, 31).Date, + CreatedOn = new DateTime(2023, 1, 2).Date, + UpdatedOn = new DateTime(2023, 1, 2).Date, + Sharing = VersionSharing.System + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/WatcherTests.cs b/tests/redmine-net-api.Tests/Equality/WatcherTests.cs new file mode 100644 index 00000000..300ece6f --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/WatcherTests.cs @@ -0,0 +1,22 @@ +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class WatcherTests : BaseEqualityTests +{ + protected override Watcher CreateSampleInstance() + { + return new Watcher + { + Id = 1, + }; + } + + protected override Watcher CreateDifferentInstance() + { + return new Watcher + { + Id = 2, + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Equality/WikiPageTests.cs b/tests/redmine-net-api.Tests/Equality/WikiPageTests.cs new file mode 100644 index 00000000..6462f7e6 --- /dev/null +++ b/tests/redmine-net-api.Tests/Equality/WikiPageTests.cs @@ -0,0 +1,47 @@ +using System; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Tests.Equality; + +public sealed class WikiPageTests : BaseEqualityTests +{ + protected override WikiPage CreateSampleInstance() + { + return new WikiPage + { + Id = 1, + Title = "Home Page", + Text = "Welcome to the wiki", + Version = 1, + Author = new IdentifiableName { Id = 1, Name = "Author 1" }, + Comments = "Initial version", + CreatedOn = new DateTime(2023, 1, 1), + UpdatedOn = new DateTime(2023, 1, 1), + Attachments = + [ + new Attachment + { + Id = 1, + FileName = "doc.pdf", + FileSize = 1024, + Author = new IdentifiableName { Id = 1, Name = "Author 1" } + } + ] + }; + } + + protected override WikiPage CreateDifferentInstance() + { + return new WikiPage + { + Id = 2, + Title = "Different Page", + Text = "Different content", + Version = 2, + Author = new IdentifiableName { Id = 2, Name = "Author 2" }, + Comments = "Updated version", + CreatedOn = new DateTime(2023, 1, 2), + UpdatedOn = new DateTime(2023, 1, 2) + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Infrastructure/Collections/JsonRedmineSerializerCollection.cs b/tests/redmine-net-api.Tests/Infrastructure/Collections/JsonRedmineSerializerCollection.cs new file mode 100644 index 00000000..45b0fe86 --- /dev/null +++ b/tests/redmine-net-api.Tests/Infrastructure/Collections/JsonRedmineSerializerCollection.cs @@ -0,0 +1,7 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure.Collections; + +[CollectionDefinition(Constants.JsonRedmineSerializerCollection)] +public sealed class JsonRedmineSerializerCollection : ICollectionFixture { } \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Infrastructure/Collections/RedmineCollection.cs b/tests/redmine-net-api.Tests/Infrastructure/Collections/RedmineCollection.cs new file mode 100644 index 00000000..8a30da0c --- /dev/null +++ b/tests/redmine-net-api.Tests/Infrastructure/Collections/RedmineCollection.cs @@ -0,0 +1,10 @@ +#if !(NET20 || NET40) +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure.Collections +{ + [CollectionDefinition(Constants.RedmineCollection)] + public sealed class RedmineCollection : ICollectionFixture { } +} +#endif \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Infrastructure/Collections/XmlRedmineSerializerCollection.cs b/tests/redmine-net-api.Tests/Infrastructure/Collections/XmlRedmineSerializerCollection.cs new file mode 100644 index 00000000..02ca7492 --- /dev/null +++ b/tests/redmine-net-api.Tests/Infrastructure/Collections/XmlRedmineSerializerCollection.cs @@ -0,0 +1,7 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure.Collections; + +[CollectionDefinition(Constants.XmlRedmineSerializerCollection)] +public sealed class XmlRedmineSerializerCollection : ICollectionFixture { } \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Infrastructure/Constants.cs b/tests/redmine-net-api.Tests/Infrastructure/Constants.cs new file mode 100644 index 00000000..f680e785 --- /dev/null +++ b/tests/redmine-net-api.Tests/Infrastructure/Constants.cs @@ -0,0 +1,8 @@ +namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure; + +public static class Constants +{ + public const string XmlRedmineSerializerCollection = "XmlRedmineSerializerCollection"; + public const string JsonRedmineSerializerCollection = "JsonRedmineSerializerCollection"; + public const string RedmineCollection = "RedmineCollection"; +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Infrastructure/Fixtures/JsonSerializerFixture.cs b/tests/redmine-net-api.Tests/Infrastructure/Fixtures/JsonSerializerFixture.cs new file mode 100644 index 00000000..d787dd62 --- /dev/null +++ b/tests/redmine-net-api.Tests/Infrastructure/Fixtures/JsonSerializerFixture.cs @@ -0,0 +1,9 @@ +using Redmine.Net.Api.Serialization; + +namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; + +public sealed class JsonSerializerFixture +{ + internal IRedmineSerializer Serializer { get; private set; } = new JsonRedmineSerializer(); + +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Infrastructure/Fixtures/RedmineApiUrlsFixture.cs b/tests/redmine-net-api.Tests/Infrastructure/Fixtures/RedmineApiUrlsFixture.cs new file mode 100644 index 00000000..7a914dae --- /dev/null +++ b/tests/redmine-net-api.Tests/Infrastructure/Fixtures/RedmineApiUrlsFixture.cs @@ -0,0 +1,31 @@ +using System.Diagnostics; +using Redmine.Net.Api.Net; + +namespace Padi.DotNet.RedmineAPI.Tests.Tests; + +public sealed class RedmineApiUrlsFixture +{ + internal string Format { get; private set; } + + public RedmineApiUrlsFixture() + { + SetMimeTypeJson(); + SetMimeTypeXml(); + + Sut = new RedmineApiUrls(Format); + } + + internal RedmineApiUrls Sut { get; } + + [Conditional("DEBUG_JSON")] + private void SetMimeTypeJson() + { + Format = "json"; + } + + [Conditional("DEBUG_XML")] + private void SetMimeTypeXml() + { + Format = "json"; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Infrastructure/RedmineFixture.cs b/tests/redmine-net-api.Tests/Infrastructure/Fixtures/RedmineFixture.cs similarity index 71% rename from tests/redmine-net-api.Tests/Infrastructure/RedmineFixture.cs rename to tests/redmine-net-api.Tests/Infrastructure/Fixtures/RedmineFixture.cs index cbcfbc63..6865c398 100644 --- a/tests/redmine-net-api.Tests/Infrastructure/RedmineFixture.cs +++ b/tests/redmine-net-api.Tests/Infrastructure/Fixtures/RedmineFixture.cs @@ -1,9 +1,8 @@ using System.Diagnostics; using Redmine.Net.Api; -using Redmine.Net.Api.Authentication; using Redmine.Net.Api.Serialization; -namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure +namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures { public sealed class RedmineFixture { @@ -17,23 +16,25 @@ public RedmineFixture () Credentials = TestHelper.GetApplicationConfiguration(); _redmineManagerOptionsBuilder = new RedmineManagerOptionsBuilder() - .WithHost(Credentials.Uri) + .WithHost(Credentials.Uri ?? "localhost") .WithApiKeyAuthentication(Credentials.ApiKey); SetMimeTypeXml(); SetMimeTypeJson(); + + RedmineManager = new RedmineManager(_redmineManagerOptionsBuilder); } [Conditional("DEBUG_JSON")] private void SetMimeTypeJson() { - RedmineManager = new RedmineManager(_redmineManagerOptionsBuilder.WithSerializationType(SerializationType.Json)); + _redmineManagerOptionsBuilder.WithSerializationType(SerializationType.Json); } [Conditional("DEBUG_XML")] private void SetMimeTypeXml() { - RedmineManager = new RedmineManager(_redmineManagerOptionsBuilder.WithSerializationType(SerializationType.Xml)); + _redmineManagerOptionsBuilder.WithSerializationType(SerializationType.Xml); } } } \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Infrastructure/Fixtures/XmlSerializerFixture.cs b/tests/redmine-net-api.Tests/Infrastructure/Fixtures/XmlSerializerFixture.cs new file mode 100644 index 00000000..700329e1 --- /dev/null +++ b/tests/redmine-net-api.Tests/Infrastructure/Fixtures/XmlSerializerFixture.cs @@ -0,0 +1,8 @@ +using Redmine.Net.Api.Serialization; + +namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; + +public sealed class XmlSerializerFixture +{ + internal IRedmineSerializer Serializer { get; private set; } = new XmlRedmineSerializer(); +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Infrastructure/Order/CaseOrder.cs b/tests/redmine-net-api.Tests/Infrastructure/Order/CaseOrder.cs index 97c849af..97ddb56a 100644 --- a/tests/redmine-net-api.Tests/Infrastructure/Order/CaseOrder.cs +++ b/tests/redmine-net-api.Tests/Infrastructure/Order/CaseOrder.cs @@ -6,7 +6,7 @@ using Xunit.Abstractions; using Xunit.Sdk; -namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure +namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure.Order { /// /// Custom xUnit test case orderer that uses the OrderAttribute diff --git a/tests/redmine-net-api.Tests/Infrastructure/Order/CollectionOrderer.cs b/tests/redmine-net-api.Tests/Infrastructure/Order/CollectionOrderer.cs index abe9cd91..ae7b01da 100644 --- a/tests/redmine-net-api.Tests/Infrastructure/Order/CollectionOrderer.cs +++ b/tests/redmine-net-api.Tests/Infrastructure/Order/CollectionOrderer.cs @@ -7,7 +7,7 @@ using Xunit; using Xunit.Abstractions; -namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure +namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure.Order { /// /// Custom xUnit test collection orderer that uses the OrderAttribute diff --git a/tests/redmine-net-api.Tests/Infrastructure/Order/OrderAttribute.cs b/tests/redmine-net-api.Tests/Infrastructure/Order/OrderAttribute.cs index bc837971..c8f07627 100644 --- a/tests/redmine-net-api.Tests/Infrastructure/Order/OrderAttribute.cs +++ b/tests/redmine-net-api.Tests/Infrastructure/Order/OrderAttribute.cs @@ -1,6 +1,6 @@ using System; -namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure +namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure.Order { public sealed class OrderAttribute : Attribute { diff --git a/tests/redmine-net-api.Tests/Infrastructure/RedmineCollection.cs b/tests/redmine-net-api.Tests/Infrastructure/RedmineCollection.cs deleted file mode 100644 index fa2bcbd4..00000000 --- a/tests/redmine-net-api.Tests/Infrastructure/RedmineCollection.cs +++ /dev/null @@ -1,9 +0,0 @@ -#if !(NET20 || NET40) -using Xunit; - -namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure -{ - [CollectionDefinition("RedmineCollection")] - public sealed class RedmineCollection : ICollectionFixture { } -} -#endif \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Json/MyAccount.cs b/tests/redmine-net-api.Tests/Serialization/Json/MyAccount.cs new file mode 100644 index 00000000..9aa09515 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Json/MyAccount.cs @@ -0,0 +1,56 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Json; + +[Collection(Constants.JsonRedmineSerializerCollection)] +public class MyAccount(JsonSerializerFixture fixture) +{ + [Fact] + public void Test_Xml_Serialization() + { + const string input = """ + { + "user": { + "id": 3, + "login": "dlopper", + "admin": false, + "firstname": "Dave", + "lastname": "Lopper", + "mail": "dlopper@somenet.foo", + "created_on": "2006-07-19T17:33:19Z", + "last_login_on": "2020-06-14T13:03:34Z", + "api_key": "c308a59c9dea95920b13522fb3e0fb7fae4f292d", + "custom_fields": [ + { + "id": 4, + "name": "Phone number", + "value": null + }, + { + "id": 5, + "name": "Money", + "value": null + } + ] + } + } + """; + + var output = fixture.Serializer.Deserialize(input); + Assert.Equal(3, output.Id); + Assert.Equal("dlopper", output.Login); + Assert.False(output.IsAdmin); + Assert.Equal("Dave", output.FirstName); + Assert.Equal("Lopper", output.LastName); + Assert.Equal("dlopper@somenet.foo", output.Email); + Assert.Equal("c308a59c9dea95920b13522fb3e0fb7fae4f292d", output.ApiKey); + Assert.NotNull(output.CustomFields); + Assert.Equal(2, output.CustomFields.Count); + Assert.Equal("Phone number", output.CustomFields[0].Name); + Assert.Equal(4, output.CustomFields[0].Id); + Assert.Equal("Money", output.CustomFields[1].Name); + Assert.Equal(5, output.CustomFields[1].Id); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Json/RoleTests.cs b/tests/redmine-net-api.Tests/Serialization/Json/RoleTests.cs new file mode 100644 index 00000000..2434ea18 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Json/RoleTests.cs @@ -0,0 +1,45 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Json; + +[Collection(Constants.JsonRedmineSerializerCollection)] +public sealed class RoleTests(JsonSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Role_And_Permissions() + { + const string input = """ + { + "role": { + "id": 5, + "name": "Reporter", + "assignable": true, + "issues_visibility": "default", + "time_entries_visibility": "all", + "users_visibility": "all", + "permissions": [ + "view_issues", + "add_issues", + "add_issue_notes", + ] + } + } + """; + + var role = fixture.Serializer.Deserialize(input); + + Assert.Equal(5, role.Id); + Assert.Equal("Reporter", role.Name); + Assert.True(role.IsAssignable); + Assert.Equal("default", role.IssuesVisibility); + Assert.Equal("all", role.TimeEntriesVisibility); + Assert.Equal("all", role.UsersVisibility); + Assert.Equal(3, role.Permissions.Count); + Assert.Equal("view_issues", role.Permissions[0].Info); + Assert.Equal("add_issues", role.Permissions[1].Info); + Assert.Equal("add_issue_notes", role.Permissions[2].Info); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/AttachmentTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/AttachmentTests.cs new file mode 100644 index 00000000..85caa2ae --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/AttachmentTests.cs @@ -0,0 +1,43 @@ +using System; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class AttachmentTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Attachment() + { + const string input = """ + + + 6243 + test.txt + 124 + text/plain + This is an attachment + http://localhost:3000/attachments/download/6243/test.txt + + 2011-07-18T22:58:40+02:00 + + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + Assert.Equal(6243, output.Id); + Assert.Equal("test.txt", output.FileName); + Assert.Equal(124, output.FileSize); + Assert.Equal("text/plain", output.ContentType); + Assert.Equal("This is an attachment", output.Description); + Assert.Equal("http://localhost:3000/attachments/download/6243/test.txt", output.ContentUrl); + Assert.Equal("Jean-Philippe Lang", output.Author.Name); + Assert.Equal(1, output.Author.Id); + Assert.Equal(new DateTime(2011, 7, 18, 20, 58, 40, DateTimeKind.Utc).ToLocalTime(), output.CreatedOn); + + } +} + diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/CustomFieldTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/CustomFieldTests.cs new file mode 100644 index 00000000..0cb541a9 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/CustomFieldTests.cs @@ -0,0 +1,65 @@ +using System.Linq; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public sealed class CustomFieldTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_CustomFields() + { + const string input = """ + + + + 1 + Affected version + issue + list + + + + true + true + true + true + + false + + + 0.5.x + + + 0.6.x + + + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(1, output.TotalItems); + + var customFields = output.Items.ToList(); + Assert.Equal(1, customFields[0].Id); + Assert.Equal("Affected version", customFields[0].Name); + Assert.Equal("issue", customFields[0].CustomizedType); + Assert.Equal("list", customFields[0].FieldFormat); + Assert.True(customFields[0].IsRequired); + Assert.True(customFields[0].IsFilter); + Assert.True(customFields[0].Searchable); + Assert.True(customFields[0].Multiple); + Assert.False(customFields[0].Visible); + + var possibleValues = customFields[0].PossibleValues.ToList(); + Assert.Equal(2, possibleValues.Count); + Assert.Equal("0.5.x", possibleValues[0].Value); + Assert.Equal("0.6.x", possibleValues[1].Value); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/EnumerationTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/EnumerationTests.cs new file mode 100644 index 00000000..2d870e8c --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/EnumerationTests.cs @@ -0,0 +1,117 @@ +using System.Linq; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class EnumerationTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Issue_Priorities() + { + const string input = """ + + + + 3 + Low + false + + + 4 + Normal + true + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(2, output.TotalItems); + + var issuePriorities = output.Items.ToList(); + Assert.Equal(2, issuePriorities.Count); + + Assert.Equal(3, issuePriorities[0].Id); + Assert.Equal("Low", issuePriorities[0].Name); + Assert.False(issuePriorities[0].IsDefault); + + Assert.Equal(4, issuePriorities[1].Id); + Assert.Equal("Normal", issuePriorities[1].Name); + Assert.True(issuePriorities[1].IsDefault); + } + + [Fact] + public void Should_Deserialize_TimeEntry_Activities() + { + const string input = """ + + + + 8 + Design + false + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Single(output.Items); + + var timeEntryActivities = output.Items.ToList(); + Assert.Equal(8, timeEntryActivities[0].Id); + Assert.Equal("Design", timeEntryActivities[0].Name); + Assert.False(timeEntryActivities[0].IsDefault); + } + + [Fact] + public void Should_Deserialize_Document_Categories() + { + const string input = """ + + + + 1 + Uncategorized + false + + + 2 + User documentation + false + + + 3 + Technical documentation + false + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(3, output.TotalItems); + + var documentCategories = output.Items.ToList(); + Assert.Equal(3, documentCategories.Count); + + Assert.Equal(1, documentCategories[0].Id); + Assert.Equal("Uncategorized", documentCategories[0].Name); + Assert.False(documentCategories[0].IsDefault); + + Assert.Equal(2, documentCategories[1].Id); + Assert.Equal("User documentation", documentCategories[1].Name); + Assert.False(documentCategories[1].IsDefault); + + Assert.Equal(3, documentCategories[2].Id); + Assert.Equal("Technical documentation", documentCategories[2].Name); + Assert.False(documentCategories[2].IsDefault); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/ErrorTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/ErrorTests.cs new file mode 100644 index 00000000..c5e447a5 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/ErrorTests.cs @@ -0,0 +1,32 @@ +using System.Linq; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public sealed class ErrorTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Errors() + { + const string input = """ + + First name can't be blank + Email is invalid + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(2, output.TotalItems); + + var errors = output.Items.ToList(); + Assert.Equal("First name can't be blank", errors[0].Info); + Assert.Equal("Email is invalid", errors[1].Info); + + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/FileTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/FileTests.cs new file mode 100644 index 00000000..72fbd208 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/FileTests.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class FileTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_File() + { + const string input = """ + + + 12 + foo-1.0-setup.exe + 74753799 + application/octet-stream + Foo App for Windows + http://localhost:3000/attachments/download/12/foo-1.0-setup.exe + + 2017-01-04T09:12:32Z + + 1276481102f218c981e0324180bafd9f + 12 + + """; + + var output = fixture.Serializer.Deserialize(input); + Assert.NotNull(output); + Assert.Equal(12, output.Id); + Assert.Equal("foo-1.0-setup.exe", output.Filename); + Assert.Equal("application/octet-stream", output.ContentType); + Assert.Equal("Foo App for Windows", output.Description); + Assert.Equal("http://localhost:3000/attachments/download/12/foo-1.0-setup.exe", output.ContentUrl); + Assert.Equal(1, output.Author.Id); + Assert.Equal("Redmine Admin", output.Author.Name); + Assert.Equal(new DateTimeOffset(new DateTime(2017,01,04,09,12,32, DateTimeKind.Utc)), new DateTimeOffset(output.CreatedOn!.Value)); + Assert.Equal(2, output.Version.Id); + Assert.Equal("1.0", output.Version.Name); + Assert.Equal("1276481102f218c981e0324180bafd9f", output.Digest); + Assert.Equal(12, output.Downloads); + } + + [Fact] + public void Should_Deserialize_Files() + { + const string input = """ + + + + 12 + foo-1.0-setup.exe + 74753799 + application/octet-stream + Foo App for Windows + http://localhost:3000/attachments/download/12/foo-1.0-setup.exe + + 2017-01-04T09:12:32Z + + 1276481102f218c981e0324180bafd9f + 12 + + + 11 + foo-1.0.dmg + 6886287 + application/x-octet-stream + Foo App for macOS + http://localhost:3000/attachments/download/11/foo-1.0.dmg + + 2017-01-04T09:12:07Z + + 14758f1afd44c09b7992073ccf00b43d + 5 + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + Assert.NotNull(output); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/GroupTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/GroupTests.cs new file mode 100644 index 00000000..3e057eac --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/GroupTests.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using System.Linq; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class GroupTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Group() + { + const string input = """ + + 20 + Developers + + + + + + """; + + var output = fixture.Serializer.Deserialize(input); + Assert.NotNull(output); + Assert.Equal(20, output.Id); + Assert.Equal("Developers", output.Name); + Assert.NotNull(output.Users); + Assert.Equal(2, output.Users.Count); + Assert.Equal("John Smith", output.Users[0].Name); + Assert.Equal("Dave Loper", output.Users[1].Name); + Assert.Equal(5, output.Users[0].Id); + Assert.Equal(8, output.Users[1].Id); + } + + [Fact] + public void Should_Deserialize_Groups() + { + const string input = """ + + + + 53 + Managers + + + 55 + Developers + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + Assert.NotNull(output); + Assert.Equal(2, output.TotalItems); + + var groups = output.Items.ToList(); + Assert.Equal(53, groups[0].Id); + Assert.Equal("Managers", groups[0].Name); + + Assert.Equal(55, groups[1].Id); + Assert.Equal("Developers", groups[1].Name); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/IssueCategoryTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/IssueCategoryTests.cs new file mode 100644 index 00000000..9bf24bdb --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/IssueCategoryTests.cs @@ -0,0 +1,73 @@ +using System.Collections.Generic; +using System.Linq; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class IssueCategoryTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Issue_Category() + { + const string input = """ + + + 2 + + UI + + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + Assert.Equal(2, output.Id); + Assert.Equal("Redmine", output.Project.Name); + Assert.Equal(1, output.Project.Id); + Assert.Equal("UI", output.Name); + } + + [Fact] + public void Should_Deserialize_Issue_Categories() + { + const string input = """ + + + + 57 + + UI + + + + 58 + + Test + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(2, output.TotalItems); + + var issueCategories = output.Items.ToList(); + Assert.Equal(2, issueCategories.Count); + + Assert.Equal(57, issueCategories[0].Id); + Assert.Equal("Foo", issueCategories[0].Project.Name); + Assert.Equal(17, issueCategories[0].Project.Id); + Assert.Equal("UI", issueCategories[0].Name); + Assert.Equal("John Smith", issueCategories[0].AssignTo.Name); + Assert.Equal(22, issueCategories[0].AssignTo.Id); + + Assert.Equal(58, issueCategories[1].Id); + Assert.Equal("Foo", issueCategories[1].Project.Name); + Assert.Equal(17, issueCategories[1].Project.Id); + Assert.Equal("Test", issueCategories[1].Name); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/IssueStatusTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/IssueStatusTests.cs new file mode 100644 index 00000000..8e699d0b --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/IssueStatusTests.cs @@ -0,0 +1,47 @@ +using System.Linq; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class IssueStatusTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Issue_Statuses() + { + const string input = """ + + + + 1 + New + false + + + 2 + Closed + true + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(2, output.TotalItems); + + var issueStatuses = output.Items.ToList(); + Assert.Equal(2, issueStatuses.Count); + + Assert.Equal(1, issueStatuses[0].Id); + Assert.Equal("New", issueStatuses[0].Name); + Assert.False(issueStatuses[0].IsClosed); + + Assert.Equal(2, issueStatuses[1].Id); + Assert.Equal("Closed", issueStatuses[1].Name); + Assert.True(issueStatuses[1].IsClosed); + } +} + diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/IssueTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/IssueTests.cs new file mode 100644 index 00000000..e326f8db --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/IssueTests.cs @@ -0,0 +1,204 @@ +using System; +using System.Linq; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class IssueTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Issues() + { + const string input = """ + + + + 4326 + + + + + + + Aggregate Multiple Issue Changes for Email Notifications + + This is not to be confused with another useful proposed feature that + would do digest emails for notifications. + + 2009-12-03 + + 0 + + Thu Dec 03 15:02:12 +0100 2009 + Sun Jan 03 12:08:41 +0100 2010 + + + 4325 + + + + + + + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(1640, output.TotalItems); + + var issues = output.Items.ToList(); + Assert.Equal(4326, issues[0].Id); + Assert.Equal("Redmine", issues[0].Project.Name); + Assert.Equal(1, issues[0].Project.Id); + Assert.Equal("Feature", issues[0].Tracker.Name); + Assert.Equal(2, issues[0].Tracker.Id); + Assert.Equal("New", issues[0].Status.Name); + Assert.Equal(1, issues[0].Status.Id); + Assert.Equal("Normal", issues[0].Priority.Name); + Assert.Equal(4, issues[0].Priority.Id); + Assert.Equal("John Smith", issues[0].Author.Name); + Assert.Equal(10106, issues[0].Author.Id); + Assert.Equal("Email notifications", issues[0].Category.Name); + Assert.Equal(9, issues[0].Category.Id); + Assert.Equal("Aggregate Multiple Issue Changes for Email Notifications", issues[0].Subject); + Assert.Contains("This is not to be confused with another useful proposed feature", issues[0].Description); + Assert.Equal(new DateTime(2009, 12, 3), issues[0].StartDate); + Assert.Null(issues[0].DueDate); + Assert.Equal(0, issues[0].DoneRatio); + Assert.Null(issues[0].EstimatedHours); + // Assert.Equal(new DateTime(2009, 12, 3, 14, 2, 12, DateTimeKind.Utc).ToLocalTime(), issues[0].CreatedOn); + // Assert.Equal(new DateTime(2010, 1, 3, 11, 8, 41, DateTimeKind.Utc).ToLocalTime(), issues[0].UpdatedOn); + + Assert.Equal(4325, issues[1].Id); + Assert.Null(issues[1].Journals); + Assert.Null(issues[1].ChangeSets); + Assert.Null(issues[1].CustomFields); + } + + [Fact] + public void Should_Deserialize_Issues_With_CustomFields() + { + const string input = """ + + + + 4326 + + + + + + + + Aggregate Multiple Issue Changes for Email Notifications + + + This is not to be confused with another useful proposed feature that + would do digest emails for notifications. + + 2009-12-03 + + 0 + + + Duplicate + Test + 1 + 2010-01-12 + + Thu Dec 03 15:02:12 +0100 2009 + Sun Jan 03 12:08:41 +0100 2010 + + + 4325 + + + + + + + 1.0.1 + + + Fixed + + + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + } + + [Fact] + public void Should_Deserialize_Issue_With_Journals() + { + const string input = """ + + 1 + + + + + + Fixed in Revision 128 + 2007-01-01T05:21:00+01:00 +
+ + + + + 2009-08-13T11:33:17+02:00 +
+ + 5 + 8 + +
+
+ + + + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + Assert.Equal(1, output.Id); + Assert.Equal("Redmine", output.Project.Name); + Assert.Equal(1, output.Project.Id); + Assert.Equal("Defect", output.Tracker.Name); + Assert.Equal(1, output.Tracker.Id); + + var journals = output.Journals.ToList(); + Assert.Equal(2, journals.Count); + + Assert.Equal(1, journals[0].Id); + Assert.Equal("Jean-Philippe Lang", journals[0].User.Name); + Assert.Equal(1, journals[0].User.Id); + Assert.Equal("Fixed in Revision 128", journals[0].Notes); + Assert.Equal(new DateTime(2007, 1, 1, 4, 21, 0, DateTimeKind.Utc).ToLocalTime(), journals[0].CreatedOn); + Assert.Null(journals[0].Details); + + Assert.Equal(10531, journals[1].Id); + Assert.Equal("efgh efgh", journals[1].User.Name); + Assert.Equal(7384, journals[1].User.Id); + Assert.Null(journals[1].Notes); + Assert.Equal(new DateTime(2009, 8, 13, 9, 33, 17, DateTimeKind.Utc).ToLocalTime(), journals[1].CreatedOn); + + var details = journals[1].Details.ToList(); + Assert.Single(details); + Assert.Equal("attr", details[0].Property); + Assert.Equal("status_id", details[0].Name); + Assert.Equal("5", details[0].OldValue); + Assert.Equal("8", details[0].NewValue); + + } +} + diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/MembershipTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/MembershipTests.cs new file mode 100644 index 00000000..9bede534 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/MembershipTests.cs @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public sealed class MembershipTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Memberships() + { + const string input = """ + + + + 1 + + + + + + + + 3 + + + + + + + + 4 + + + + + + + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + } + + [Fact] + public void Should_Deserialize_Membership() + { + const string input = """ + + + 1 + + + + """; + + var output = fixture.Serializer.Deserialize(input); + Assert.Equal(1, output.Id); + Assert.Equal("Redmine", output.Project.Name); + Assert.Equal(1, output.Project.Id); + Assert.Equal("David Robert", output.User.Name); + Assert.Equal(17, output.User.Id); + } + + [Fact] + public void Should_Deserialize_Membership_With_Roles() + { + const string input = """ + + + 1 + + + + + + + """; + + var output = fixture.Serializer.Deserialize(input); + Assert.Equal(1, output.Id); + Assert.Equal("Redmine", output.Project.Name); + Assert.Equal(1, output.Project.Id); + Assert.Equal("David Robert", output.User.Name); + Assert.Equal(17, output.User.Id); + Assert.NotNull(output.Roles); + Assert.Single(output.Roles); + Assert.Equal("Manager", output.Roles[0].Name); + Assert.Equal(1, output.Roles[0].Id); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/MyAccountTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/MyAccountTests.cs new file mode 100644 index 00000000..ae1b70e2 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/MyAccountTests.cs @@ -0,0 +1,79 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class MyAccountTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_MyAccount() + { + const string input = """ + + + 3 + dlopper + false + Dave + Lopper + dlopper@somenet.foo + 2006-07-19T17:33:19Z + 2020-06-14T13:03:34Z + c308a59c9dea95920b13522fb3e0fb7fae4f292d + + """; + + var output = fixture.Serializer.Deserialize(input); + Assert.Equal(3, output.Id); + Assert.Equal("dlopper", output.Login); + Assert.False(output.IsAdmin); + Assert.Equal("Dave", output.FirstName); + Assert.Equal("Lopper", output.LastName); + Assert.Equal("dlopper@somenet.foo", output.Email); + Assert.Equal("c308a59c9dea95920b13522fb3e0fb7fae4f292d", output.ApiKey); + } + + [Fact] + public void Should_Deserialize_MyAccount_With_CustomFields() + { + const string input = """ + + + 3 + dlopper + false + Dave + Lopper + dlopper@somenet.foo + 2006-07-19T17:33:19Z + 2020-06-14T13:03:34Z + c308a59c9dea95920b13522fb3e0fb7fae4f292d + + + + + + + + + + """; + + var output = fixture.Serializer.Deserialize(input); + Assert.Equal(3, output.Id); + Assert.Equal("dlopper", output.Login); + Assert.False(output.IsAdmin); + Assert.Equal("Dave", output.FirstName); + Assert.Equal("Lopper", output.LastName); + Assert.Equal("dlopper@somenet.foo", output.Email); + Assert.Equal("c308a59c9dea95920b13522fb3e0fb7fae4f292d", output.ApiKey); + Assert.NotNull(output.CustomFields); + Assert.Equal(2, output.CustomFields.Count); + Assert.Equal("Phone number", output.CustomFields[0].Name); + Assert.Equal(4, output.CustomFields[0].Id); + Assert.Equal("Money", output.CustomFields[1].Name); + Assert.Equal(5, output.CustomFields[1].Id); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/NewsTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/NewsTests.cs new file mode 100644 index 00000000..0b29de40 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/NewsTests.cs @@ -0,0 +1,67 @@ +using System; +using System.Linq; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class NewsTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_News() + { + const string input = """ + + + + 54 + + + Redmine 1.1.3 released + + Redmine 1.1.3 has been released + 2011-04-29T14:00:25+02:00 + + + 53 + + + Redmine 1.1.2 bug/security fix released + + Redmine 1.1.2 has been released + 2011-03-07T21:07:03+01:00 + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(2, output.TotalItems); + + var newsItems = output.Items.ToList(); + Assert.Equal(2, newsItems.Count); + + Assert.Equal(54, newsItems[0].Id); + Assert.Equal("Redmine", newsItems[0].Project.Name); + Assert.Equal(1, newsItems[0].Project.Id); + Assert.Equal("Jean-Philippe Lang", newsItems[0].Author.Name); + Assert.Equal(1, newsItems[0].Author.Id); + Assert.Equal("Redmine 1.1.3 released", newsItems[0].Title); + Assert.Equal("Redmine 1.1.3 has been released", newsItems[0].Description); + Assert.Equal(new DateTime(2011, 4, 29, 12, 0, 25, DateTimeKind.Utc).ToLocalTime(), newsItems[0].CreatedOn); + + Assert.Equal(53, newsItems[1].Id); + Assert.Equal("Redmine", newsItems[1].Project.Name); + Assert.Equal(1, newsItems[1].Project.Id); + Assert.Equal("Jean-Philippe Lang", newsItems[1].Author.Name); + Assert.Equal(1, newsItems[1].Author.Id); + Assert.Equal("Redmine 1.1.2 bug/security fix released", newsItems[1].Title); + Assert.Equal("Redmine 1.1.2 has been released", newsItems[1].Description); + Assert.Equal(new DateTime(2011, 3, 7, 20, 7, 3, DateTimeKind.Utc).ToLocalTime(), newsItems[1].CreatedOn); + + } +} + diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/ProjectTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/ProjectTests.cs new file mode 100644 index 00000000..fd0072c7 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/ProjectTests.cs @@ -0,0 +1,87 @@ +using System; +using System.Linq; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class ProjectTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Project() + { + const string input = """ + + + 1 + Redmine + redmine + + Redmine is a flexible project management web application written using Ruby on Rails framework. + + + 1 + + + + 2007-09-29T12:03:04+02:00 + 2009-03-15T12:35:11+01:00 + true + + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + + Assert.Equal(1, output.Id); + Assert.Equal("Redmine", output.Name); + Assert.Equal("redmine", output.Identifier); + Assert.Contains("Redmine is a flexible project management web application", output.Description); + Assert.Equal(new DateTime(2007, 9, 29, 10, 3, 4, DateTimeKind.Utc).ToLocalTime(), output.CreatedOn); + Assert.Equal(new DateTime(2009, 3, 15, 11, 35, 11, DateTimeKind.Utc).ToLocalTime(), output.UpdatedOn); + Assert.True(output.IsPublic); + } + + [Fact] + public void Should_Deserialize_Projects() + { + const string input = """ + + + + 1 + Redmine + redmine + + Redmine is a flexible project management web application written using Ruby on Rails framework. + + 2007-09-29T12:03:04+02:00 + 2009-03-15T12:35:11+01:00 + true + + + 2 + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(2, output.TotalItems); + + var projects = output.Items.ToList(); + Assert.Equal(1, projects[0].Id); + Assert.Equal("Redmine", projects[0].Name); + Assert.Equal("redmine", projects[0].Identifier); + Assert.Contains("Redmine is a flexible project management web application", projects[0].Description); + Assert.Equal(new DateTime(2007, 9, 29, 10, 3, 4, DateTimeKind.Utc).ToLocalTime(), projects[0].CreatedOn); + Assert.Equal(new DateTime(2009, 3, 15, 11, 35, 11, DateTimeKind.Utc).ToLocalTime(), projects[0].UpdatedOn); + Assert.True(projects[0].IsPublic); + + Assert.Equal(2, projects[1].Id); + } +} diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/QueryTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/QueryTests.cs new file mode 100644 index 00000000..5927739a --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/QueryTests.cs @@ -0,0 +1,51 @@ +using System.Linq; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class QueryTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Version() + { + const string input = """ + + + + 84 + Documentation issues + true + 1 + + + 1 + Open defects + true + 1 + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(5, output.TotalItems); + + var queries = output.Items.ToList(); + Assert.Equal(2, queries.Count); + + Assert.Equal(84, queries[0].Id); + Assert.Equal("Documentation issues", queries[0].Name); + Assert.True(queries[0].IsPublic); + Assert.Equal(1, queries[0].ProjectId); + + Assert.Equal(1, queries[1].Id); + Assert.Equal("Open defects", queries[1].Name); + Assert.True(queries[1].IsPublic); + Assert.Equal(1, queries[1].ProjectId); + } +} + diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/RelationTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/RelationTests.cs new file mode 100644 index 00000000..f06be8ac --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/RelationTests.cs @@ -0,0 +1,81 @@ +using System.Linq; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class RelationTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Relation() + { + const string input = """ + + + 1819 + 8470 + 8469 + relates + + + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + Assert.Equal(1819, output.Id); + Assert.Equal(8470, output.IssueId); + Assert.Equal(8469, output.IssueToId); + Assert.Equal(IssueRelationType.Relates, output.Type); + Assert.Null(output.Delay); + } + + [Fact] + public void Should_Deserialize_Relations() + { + const string input = """ + + + + 1819 + 8470 + 8469 + relates + + + + 1820 + 8470 + 8467 + relates + + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(2, output.TotalItems); + + var relations = output.Items.ToList(); + Assert.Equal(2, relations.Count); + + Assert.Equal(1819, relations[0].Id); + Assert.Equal(8470, relations[0].IssueId); + Assert.Equal(8469, relations[0].IssueToId); + Assert.Equal(IssueRelationType.Relates, relations[0].Type); + Assert.Null(relations[0].Delay); + + Assert.Equal(1820, relations[1].Id); + Assert.Equal(8470, relations[1].IssueId); + Assert.Equal(8467, relations[1].IssueToId); + Assert.Equal(IssueRelationType.Relates, relations[1].Type); + Assert.Null(relations[1].Delay); + } +} + + diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/RoleTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/RoleTests.cs new file mode 100644 index 00000000..7d695ef5 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/RoleTests.cs @@ -0,0 +1,95 @@ +using System.Linq; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public sealed class RoleTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Role() + { + const string input = """ + + 5 + Reporter + true + default + all + all + + """; + var role = fixture.Serializer.Deserialize(input); + + Assert.Equal(5, role.Id); + Assert.Equal("Reporter", role.Name); + Assert.True(role.IsAssignable); + Assert.Equal("default", role.IssuesVisibility); + Assert.Equal("all", role.TimeEntriesVisibility); + Assert.Equal("all", role.UsersVisibility); + } + + [Fact] + public void Should_Deserialize_Role_And_Permissions() + { + const string input = """ + + 5 + Reporter + true + default + all + all + + view_issues + add_issues + add_issue_notes + + + """; + var role = fixture.Serializer.Deserialize(input); + + Assert.Equal(5, role.Id); + Assert.Equal("Reporter", role.Name); + Assert.True(role.IsAssignable); + Assert.Equal("default", role.IssuesVisibility); + Assert.Equal("all", role.TimeEntriesVisibility); + Assert.Equal("all", role.UsersVisibility); + Assert.Equal(3, role.Permissions.Count); + Assert.Equal("view_issues", role.Permissions[0].Info); + Assert.Equal("add_issues", role.Permissions[1].Info); + Assert.Equal("add_issue_notes", role.Permissions[2].Info); + } + + [Fact] + public void Should_Deserialize_Roles() + { + const string input = """ + + + + 1 + Manager + + + 2 + Developer + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + Assert.NotNull(output); + Assert.Equal(2, output.TotalItems); + + var roles = output.Items.ToList(); + Assert.Equal(1, roles[0].Id); + Assert.Equal("Manager", roles[0].Name); + + Assert.Equal(2, roles[1].Id); + Assert.Equal("Developer", roles[1].Name); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/SearchTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/SearchTests.cs new file mode 100644 index 00000000..5c60001f --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/SearchTests.cs @@ -0,0 +1,59 @@ +using System; +using System.Linq; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public sealed class SearchTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Search_Result() + { + const string input = """ + + + 5 + Wiki: Wiki_Page_Name + wiki-page + http://www.redmine.org/projects/new_crm_dev/wiki/Wiki_Page_Name + h1. Wiki Page Name wiki_keyword + 2016-03-25T05:23:35Z + + + 10 + Issue #10 (Closed): Issue_Title + issue closed + http://www.redmin.org/issues/10 + issue_keyword + 2016-03-24T05:18:59Z + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(2, output.TotalItems); + Assert.Equal(25, output.PageSize); + + var results = output.Items.ToList(); + Assert.Equal(5, results[0].Id); + Assert.Equal("Wiki: Wiki_Page_Name", results[0].Title); + Assert.Equal("wiki-page", results[0].Type); + Assert.Equal("http://www.redmine.org/projects/new_crm_dev/wiki/Wiki_Page_Name", results[0].Url); + Assert.Equal("h1. Wiki Page Name wiki_keyword", results[0].Description); + Assert.Equal(new DateTime(2016, 3, 25, 5, 23, 35, DateTimeKind.Utc).ToLocalTime(), results[0].DateTime); + + Assert.Equal(10, results[1].Id); + Assert.Equal("Issue #10 (Closed): Issue_Title", results[1].Title); + Assert.Equal("issue closed", results[1].Type); + Assert.Equal("http://www.redmin.org/issues/10", results[1].Url); + Assert.Equal("issue_keyword", results[1].Description); + Assert.Equal(new DateTime(2016, 3, 24, 5, 18, 59, DateTimeKind.Utc).ToLocalTime(), results[1].DateTime); + + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/TrackerTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/TrackerTests.cs new file mode 100644 index 00000000..a61410cb --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/TrackerTests.cs @@ -0,0 +1,134 @@ +using System.Linq; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class TrackerTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Tracker() + { + const string input = """ + + + 1 + Defect + + Description for Bug tracker + + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + + Assert.Equal(1, output.Id); + Assert.Equal("Defect", output.Name); + Assert.Equal("New", output.DefaultStatus.Name); + Assert.Equal("Description for Bug tracker", output.Description); + } + + [Fact] + public void Should_Deserialize_Tracker_With_Enumerations() + { + const string input = """ + + + 1 + Defect + + Description for Bug tracker + + assigned_to_id + category_id + fixed_version_id + parent_issue_id + start_date + due_date + estimated_hours + done_ratio + description + + + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + + Assert.Equal(1, output.Id); + Assert.Equal("Defect", output.Name); + Assert.Equal("New", output.DefaultStatus.Name); + Assert.Equal("Description for Bug tracker", output.Description); + Assert.Equal(9, output.EnabledStandardFields.Count); + } + + [Fact] + public void Should_Deserialize_Trackers() + { + const string input = """ + + + + 1 + Defect + + Description for Bug tracker + + assigned_to_id + category_id + fixed_version_id + parent_issue_id + start_date + due_date + estimated_hours + done_ratio + description + + + + 2 + Feature + + Description for Feature request tracker + + assigned_to_id + category_id + fixed_version_id + parent_issue_id + start_date + due_date + estimated_hours + done_ratio + description + + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(2, output.TotalItems); + + var trackers = output.Items.ToList(); + Assert.Equal(2, trackers.Count); + + Assert.Equal(1, trackers[0].Id); + Assert.Equal("Defect", trackers[0].Name); + Assert.Equal("New", trackers[0].DefaultStatus.Name); + Assert.Equal("Description for Bug tracker", trackers[0].Description); + Assert.Equal(9, trackers[0].EnabledStandardFields.Count); + + Assert.Equal(2, trackers[1].Id); + Assert.Equal("Feature", trackers[1].Name); + Assert.Equal("New", trackers[1].DefaultStatus.Name); + Assert.Equal("Description for Feature request tracker", trackers[1].Description); + Assert.Equal(9, trackers[1].EnabledStandardFields.Count); + + } +} diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/UploadTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/UploadTests.cs new file mode 100644 index 00000000..ff191c89 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/UploadTests.cs @@ -0,0 +1,62 @@ +using System.Linq; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class UploadTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Upload() + { + const string input = """ + + + #{token1} + test1.txt + + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + Assert.Equal("#{token1}", output.Token); + Assert.Equal("test1.txt", output.FileName); + } + + [Fact] + public void Should_Deserialize_Uploads() + { + const string input = """ + + + + #{token1} + test1.txt + + + #{token2} + test1.txt + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(2, output.TotalItems); + + var uploads = output.Items.ToList(); + Assert.Equal(2, uploads.Count); + + Assert.Equal("#{token1}", uploads[0].Token); + Assert.Equal("test1.txt", uploads[0].FileName); + + Assert.Equal("#{token2}", uploads[1].Token); + Assert.Equal("test1.txt", uploads[1].FileName); + + } +} + diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/UserTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/UserTests.cs new file mode 100644 index 00000000..e24b0a57 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/UserTests.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections; +using System.Linq; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class UserTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_User() + { + const string input = """ + + + 3 + jplang + Jean-Philippe + Lang + jp_lang@yahoo.fr + 2007-09-28T00:16:04+02:00 + 2010-08-01T18:05:45+02:00 + 2011-08-01T18:05:45+02:00 + 2011-08-01T18:05:45+02:00 + ebc3f6b781a6fb3f2b0a83ce0ebb80e0d585189d + + 1 + + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + Assert.Equal(3, output.Id); + Assert.Equal("jplang", output.Login); + Assert.Equal("Jean-Philippe", output.FirstName); + Assert.Equal("Lang", output.LastName); + Assert.Equal("jp_lang@yahoo.fr", output.Email); + Assert.Equal(new DateTime(2007, 9, 28, 0, 16, 4, DateTimeKind.Local).AddHours(1), output.CreatedOn); + Assert.Equal(new DateTime(2010, 8, 1, 18, 5, 45, DateTimeKind.Local).AddHours(1), output.UpdatedOn); + Assert.Equal(new DateTime(2011, 8, 1, 18, 5, 45, DateTimeKind.Local).AddHours(1), output.LastLoginOn); + Assert.Equal(new DateTime(2011, 8, 1, 18, 5, 45, DateTimeKind.Local).AddHours(1), output.PasswordChangedOn); + Assert.Equal("ebc3f6b781a6fb3f2b0a83ce0ebb80e0d585189d", output.ApiKey); + Assert.Empty(output.AvatarUrl); + Assert.Equal(UserStatus.StatusActive, output.Status); + } + + [Fact] + public void Should_Deserialize_User_With_Memberships() + { + const string input = """ + + + 3 + jplang + Jean-Philippe + Lang + jp_lang@yahoo.fr + 2007-09-28T00:16:04+02:00 + 2010-08-01T18:05:45+02:00 + 2011-08-01T18:05:45+02:00 + 2011-08-01T18:05:45+02:00 + ebc3f6b781a6fb3f2b0a83ce0ebb80e0d585189d + + 1 + + + + + + + + + + + + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + Assert.Equal(3, output.Id); + Assert.Equal("jplang", output.Login); + Assert.Equal("Jean-Philippe", output.FirstName); + Assert.Equal("Lang", output.LastName); + Assert.Equal("jp_lang@yahoo.fr", output.Email); + Assert.Equal(new DateTime(2007, 9, 28, 0, 16, 4, DateTimeKind.Local).AddHours(1), output.CreatedOn); + Assert.Equal(new DateTime(2010, 8, 1, 18, 5, 45, DateTimeKind.Local).AddHours(1), output.UpdatedOn); + Assert.Equal(new DateTime(2011, 8, 1, 18, 5, 45, DateTimeKind.Local).AddHours(1), output.LastLoginOn); + Assert.Equal(new DateTime(2011, 8, 1, 18, 5, 45, DateTimeKind.Local).AddHours(1), output.PasswordChangedOn); + Assert.Equal("ebc3f6b781a6fb3f2b0a83ce0ebb80e0d585189d", output.ApiKey); + Assert.Empty(output.AvatarUrl); + Assert.Equal(UserStatus.StatusActive, output.Status); + + var memberships = output.Memberships.ToList(); + Assert.Single(memberships); + Assert.Equal("Redmine", memberships[0].Project.Name); + Assert.Equal(1, memberships[0].Project.Id); + + var roles = memberships[0].Roles.ToList(); + Assert.Equal(2, roles.Count); + Assert.Equal("Administrator", roles[0].Name); + Assert.Equal(3, roles[0].Id); + Assert.Equal("Contributor", roles[1].Name); + Assert.Equal(4, roles[1].Id); + } + + [Fact] + public void Should_Deserialize_User_With_Groups() + { + const string input = """ + + + 3 + jplang + Jean-Philippe + Lang + jp_lang@yahoo.fr + 2007-09-28T00:16:04+02:00 + 2010-08-01T18:05:45+02:00 + 2011-08-01T18:05:45+02:00 + 2011-08-01T18:05:45+02:00 + ebc3f6b781a6fb3f2b0a83ce0ebb80e0d585189d + + 1 + + + + + + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + Assert.Equal(3, output.Id); + Assert.Equal("jplang", output.Login); + Assert.Equal("Jean-Philippe", output.FirstName); + Assert.Equal("Lang", output.LastName); + Assert.Equal("jp_lang@yahoo.fr", output.Email); + Assert.Equal(new DateTime(2007, 9, 28, 0, 16, 4, DateTimeKind.Local).AddHours(1), output.CreatedOn); + Assert.Equal(new DateTime(2010, 8, 1, 18, 5, 45, DateTimeKind.Local).AddHours(1), output.UpdatedOn); + Assert.Equal(new DateTime(2011, 8, 1, 18, 5, 45, DateTimeKind.Local).AddHours(1), output.LastLoginOn); + Assert.Equal(new DateTime(2011, 8, 1, 18, 5, 45, DateTimeKind.Local).AddHours(1), output.PasswordChangedOn); + Assert.Equal("ebc3f6b781a6fb3f2b0a83ce0ebb80e0d585189d", output.ApiKey); + Assert.Empty(output.AvatarUrl); + Assert.Equal(UserStatus.StatusActive, output.Status); + + var groups = output.Groups.ToList(); + Assert.Single(groups); + Assert.Equal("Developers", groups[0].Name); + Assert.Equal(20, groups[0].Id); + } +} + diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/VersionTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/VersionTests.cs new file mode 100644 index 00000000..860e1a93 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/VersionTests.cs @@ -0,0 +1,111 @@ +using System; +using System.Linq; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class VersionTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Version() + { + const string input = """ + + + 2 + + 0.8 + + closed + 2008-12-30 + 0.0 + 0.0 + 2008-03-09T12:52:12+01:00 + 2009-11-15T12:22:12+01:00 + + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + Assert.Equal(2, output.Id); + Assert.Equal("Redmine", output.Project.Name); + Assert.Equal(1, output.Project.Id); + Assert.Equal("0.8", output.Name); + Assert.Equal(VersionStatus.Closed, output.Status); + Assert.Equal(new DateTime(2008, 12, 30), output.DueDate); + Assert.Equal(0.0f, output.EstimatedHours); + Assert.Equal(0.0f, output.SpentHours); + Assert.Equal(new DateTime(2008, 3, 9, 12, 52, 12, DateTimeKind.Local).AddHours(1), output.CreatedOn); + Assert.Equal(new DateTime(2009, 11, 15, 12, 22, 12, DateTimeKind.Local).AddHours(1), output.UpdatedOn); + + } + + [Fact] + public void Should_Deserialize_Versions() + { + const string input = """ + + + + 1 + + 0.7 + + closed + 2008-04-28 + none + 2008-03-09T12:52:06+01:00 + 2009-11-15T12:22:12+01:00 + FooBarWikiPage + + + 2 + + 0.8 + + closed + 2008-12-30 + none + FooBarWikiPage + 2008-03-09T12:52:12+01:00 + 2009-11-15T12:22:12+01:00 + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(34, output.TotalItems); + + var versions = output.Items.ToList(); + Assert.Equal(1, versions[0].Id); + Assert.Equal("Redmine", versions[0].Project.Name); + Assert.Equal(1, versions[0].Project.Id); + Assert.Equal("0.7", versions[0].Name); + Assert.Equal(VersionStatus.Closed, versions[0].Status); + Assert.Equal(new DateTime(2008, 4, 28), versions[0].DueDate); + Assert.Equal(VersionSharing.None, versions[0].Sharing); + Assert.Equal("FooBarWikiPage", versions[0].WikiPageTitle); + Assert.Equal(new DateTime(2008, 3, 9, 12, 52, 6, DateTimeKind.Local).AddHours(1), versions[0].CreatedOn); + Assert.Equal(new DateTime(2009, 11, 15, 12, 22, 12, DateTimeKind.Local).AddHours(1), versions[0].UpdatedOn); + + Assert.Equal(2, versions[1].Id); + Assert.Equal("Redmine", versions[1].Project.Name); + Assert.Equal(1, versions[1].Project.Id); + Assert.Equal("0.8", versions[1].Name); + Assert.Equal(VersionStatus.Closed, versions[1].Status); + Assert.Equal(new DateTime(2008, 12, 30), versions[1].DueDate); + Assert.Equal(VersionSharing.None, versions[1].Sharing); + Assert.Equal(new DateTime(2008, 3, 9, 12, 52, 12, DateTimeKind.Local).AddHours(1), versions[1].CreatedOn); + Assert.Equal(new DateTime(2009, 11, 15, 12, 22, 12, DateTimeKind.Local).AddHours(1), versions[1].UpdatedOn); + + } +} + + \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/WikiTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/WikiTests.cs new file mode 100644 index 00000000..768ffde0 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Xml/WikiTests.cs @@ -0,0 +1,86 @@ +using System; +using System.Linq; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; + +[Collection(Constants.XmlRedmineSerializerCollection)] +public class WikiTests(XmlSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Wiki_Page() + { + const string input = """ + + + UsersGuide + + h1. Users Guide + ... + ... + 22 + + Typo + 2009-05-18T20:11:52Z + 2012-10-02T11:38:18Z + + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + Assert.Equal("UsersGuide", output.Title); + Assert.NotNull(output.ParentTitle); + Assert.Equal("Installation_Guide", output.ParentTitle); + + Assert.NotNull(output.Text); + Assert.False(string.IsNullOrWhiteSpace(output.Text), "Text should not be empty"); + + var lines = output.Text!.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); + var firstLine = lines[0].Trim(); + + Assert.Equal("h1. Users Guide", firstLine); + + Assert.Equal(22, output.Version); + Assert.NotNull(output.Author); + Assert.Equal(11, output.Author.Id); + Assert.Equal("John Smith", output.Author.Name); + Assert.Equal("Typo", output.Comments); + Assert.Equal(new DateTime(2009, 5, 18, 20, 11, 52, DateTimeKind.Utc).ToLocalTime(), output.CreatedOn); + Assert.Equal(new DateTime(2012, 10, 2, 11, 38, 18, DateTimeKind.Utc).ToLocalTime(), output.UpdatedOn); + + } + + [Fact] + public void Should_Deserialize_Wiki_Pages() + { + const string input = """ + + + + UsersGuide + 2 + 2008-03-09T12:07:08Z + 2008-03-09T23:41:33+01:00 + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(1, output.TotalItems); + + var wikiPages = output.Items.ToList(); + Assert.Equal("UsersGuide", wikiPages[0].Title); + Assert.Equal(2, wikiPages[0].Version); + Assert.Equal(new DateTime(2008, 3, 9, 12, 7, 8, DateTimeKind.Utc).ToLocalTime(), wikiPages[0].CreatedOn); + Assert.Equal(new DateTime(2008, 3, 9, 22, 41, 33, DateTimeKind.Utc).ToLocalTime(), wikiPages[0].UpdatedOn); + } +} + + + + \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Tests/HostValidationTests.cs b/tests/redmine-net-api.Tests/Tests/HostTests.cs similarity index 96% rename from tests/redmine-net-api.Tests/Tests/HostValidationTests.cs rename to tests/redmine-net-api.Tests/Tests/HostTests.cs index 32448c72..89943a7c 100644 --- a/tests/redmine-net-api.Tests/Tests/HostValidationTests.cs +++ b/tests/redmine-net-api.Tests/Tests/HostTests.cs @@ -1,4 +1,4 @@ -using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Order; using Redmine.Net.Api; using Redmine.Net.Api.Exceptions; using Xunit; @@ -7,7 +7,7 @@ namespace Padi.DotNet.RedmineAPI.Tests.Tests { [Trait("Redmine-api", "Host")] [Order(1)] - public sealed class HostValidationTests + public sealed class HostTests { [Theory] [InlineData(null)] @@ -54,15 +54,15 @@ public void Should_Throw_Redmine_Exception_When_Host_Is_Invalid(string host) [InlineData("www.domain.com:3000", "https://www.domain.com:3000/")] [InlineData("https://www.google.com/", "https://www.google.com/")] [InlineData("http://example.com:8080/", "http://example.com:8080/")] - [InlineData("http://example.com/path", "http://example.com/")] + [InlineData("http://example.com/path", "http://example.com/path")] [InlineData("http://example.com/?param=value", "http://example.com/")] [InlineData("http://example.com/#fragment", "http://example.com/")] [InlineData("http://example.com/", "http://example.com/")] [InlineData("http://example.com/?param=value", "http://example.com/")] [InlineData("http://example.com/#fragment", "http://example.com/")] - [InlineData("http://example.com/path/page", "http://example.com/")] - [InlineData("http://example.com/path/page?param=value", "http://example.com/")] - [InlineData("http://example.com/path/page#fragment","http://example.com/")] + [InlineData("http://example.com/path/page", "http://example.com/path/page")] + [InlineData("http://example.com/path/page?param=value", "http://example.com/path/page")] + [InlineData("http://example.com/path/page#fragment","http://example.com/path/page")] [InlineData("http://[::1]:8080/", "http://[::1]/")] [InlineData("http://www.domain.com/title/index.htm", "http://www.domain.com/")] [InlineData("http://www.localhost.com/", "http://www.localhost.com/")] diff --git a/tests/redmine-net-api.Tests/Tests/RedmineApiUrlsTests.cs b/tests/redmine-net-api.Tests/Tests/RedmineApiUrlsTests.cs new file mode 100644 index 00000000..8b0121bb --- /dev/null +++ b/tests/redmine-net-api.Tests/Tests/RedmineApiUrlsTests.cs @@ -0,0 +1,573 @@ +using System; +using System.Collections.Specialized; +using Redmine.Net.Api; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Net; +using Redmine.Net.Api.Types; +using Xunit; +using Version = Redmine.Net.Api.Types.Version; + + +namespace Padi.DotNet.RedmineAPI.Tests.Tests; + +public class RedmineApiUrlsTests(RedmineApiUrlsFixture fixture) : IClassFixture +{ + [Fact] + public void MyAccount_ReturnsCorrectUrl() + { + var result = fixture.Sut.MyAccount(); + Assert.Equal("my/account.json", result); + } + + [Theory] + [MemberData(nameof(ProjectOperationsData))] + public void ProjectOperations_ReturnsCorrectUrl(string projectId, Func operation, string expected) + { + var result = operation(projectId); + Assert.Equal(expected, result); + } + + [Theory] + [MemberData(nameof(WikiOperationsData))] + public void WikiOperations_ReturnsCorrectUrl(string projectId, string pageName, Func operation, string expected) + { + var result = operation(projectId, pageName); + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("123", "456", "issues/123/watchers/456.json")] + public void IssueWatcherRemove_WithValidIds_ReturnsCorrectUrl(string issueId, string userId, string expected) + { + var result = fixture.Sut.IssueWatcherRemove(issueId, userId); + Assert.Equal(expected, result); + } + + [Theory] + [InlineData(null, "456")] + [InlineData("123", null)] + [InlineData("", "456")] + [InlineData("123", "")] + public void IssueWatcherRemove_WithInvalidIds_ThrowsRedmineException(string issueId, string userId) + { + Assert.Throws(() => fixture.Sut.IssueWatcherRemove(issueId, userId)); + } + + [Theory] + [MemberData(nameof(AttachmentOperationsData))] + public void AttachmentOperations_WithValidInput_ReturnsCorrectUrl(string input, Func operation, string expected) + { + var result = operation(input); + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("test.txt", "uploads.json?filename=test.txt")] + [InlineData("file with spaces.pdf", "uploads.json?filename=file%20with%20spaces.pdf")] + public void UploadFragment_WithFileName_ReturnsCorrectlyEncodedUrl(string fileName, string expected) + { + var result = fixture.Sut.UploadFragment(fileName); + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("project1", "versions")] + [InlineData("project1", "issue_categories")] + public void ProjectParentFragment_ForDifferentTypes_ReturnsCorrectUrl(string projectId, string fragment) + { + var expected = $"projects/{projectId}/{fragment}.json"; + var result = fixture.Sut.ProjectParentFragment(projectId, fragment); + Assert.Equal(expected, result); + } + + [Theory] + [InlineData("issue1", "relations")] + [InlineData("issue1", "watchers")] + public void IssueParentFragment_ForDifferentTypes_ReturnsCorrectUrl(string issueId, string fragment) + { + var expected = $"issues/{issueId}/{fragment}.json"; + var result = fixture.Sut.IssueParentFragment(issueId, fragment); + Assert.Equal(expected, result); + } + + [Theory] + [MemberData(nameof(GetFragmentTestData))] + public void GetFragment_ForAllTypes_ReturnsCorrectUrl(Type type, string id, string expected) + { + var result = fixture.Sut.GetFragment(type, id); + Assert.Equal(expected, result); + } + + [Theory] + [MemberData(nameof(CreateEntityTestData))] + public void CreateEntity_ForAllTypes_ReturnsCorrectUrl(Type type, string ownerId, string expected) + { + var result = fixture.Sut.CreateEntityFragment(type, ownerId); + Assert.Equal(expected, result); + } + + [Theory] + [MemberData(nameof(GetListTestData))] + public void GetList_ForAllTypes_ReturnsCorrectUrl(Type type, string ownerId, string expected) + { + var result = fixture.Sut.GetListFragment(type, ownerId); + Assert.Equal(expected, result); + } + + [Theory] + [MemberData(nameof(InvalidTypeTestData))] + public void GetList_WithInvalidType_ThrowRedmineException(Type invalidType) + { + var exception = Assert.Throws(() => fixture.Sut.GetListFragment(invalidType)); + Assert.Contains("There is no uri fragment defined for type", exception.Message); + } + + [Theory] + [MemberData(nameof(GetListWithIssueIdTestData))] + public void GetListFragment_WithIssueIdInRequestOptions_ReturnsCorrectUrl(Type type, string issueId, string expected) + { + var requestOptions = new RequestOptions + { + QueryString = new NameValueCollection + { + { RedmineKeys.ISSUE_ID, issueId } + } + }; + + var result = fixture.Sut.GetListFragment(type, requestOptions); + Assert.Equal(expected, result); + } + + [Theory] + [MemberData(nameof(GetListWithProjectIdTestData))] + public void GetListFragment_WithProjectIdInRequestOptions_ReturnsCorrectUrl(Type type, string projectId, string expected) + { + var requestOptions = new RequestOptions + { + QueryString = new NameValueCollection + { + { RedmineKeys.PROJECT_ID, projectId } + } + }; + + var result = fixture.Sut.GetListFragment(type, requestOptions); + Assert.Equal(expected, result); + } + + [Theory] + [MemberData(nameof(GetListWithBothIdsTestData))] + public void GetListFragment_WithBothIds_PrioritizesProjectId(Type type, string projectId, string issueId, string expected) + { + var requestOptions = new RequestOptions + { + QueryString = new NameValueCollection + { + { RedmineKeys.PROJECT_ID, projectId }, + { RedmineKeys.ISSUE_ID, issueId } + } + }; + + var result = fixture.Sut.GetListFragment(type, requestOptions); + Assert.Equal(expected, result); + } + + [Theory] + [MemberData(nameof(GetListWithNoIdsTestData))] + public void GetListFragment_WithNoIds_ReturnsDefaultUrl(Type type, string expected) + { + var result = fixture.Sut.GetListFragment(type, new RequestOptions()); + Assert.Equal(expected, result); + } + + [Theory] + [ClassData(typeof(RedmineTypeTestData))] + public void GetListFragment_ForAllTypes_ReturnsCorrectUrl(Type type, string parentId, string expected) + { + var result = fixture.Sut.GetListFragment(type, parentId); + Assert.Equal(expected, result); + } + + [Theory] + [MemberData(nameof(GetListEntityRequestOptionTestData))] + public void GetListFragment_WithEmptyOptions_ReturnsCorrectUrl(Type type, RequestOptions requestOptions, string expected) + { + var result = fixture.Sut.GetListFragment(type, requestOptions); + Assert.Equal(expected, result); + } + + [Theory] + [ClassData(typeof(RedmineTypeTestData))] + public void GetListFragment_WithNullOptions_ReturnsCorrectUrl(Type type, string parentId, string expected) + { + var result = fixture.Sut.GetListFragment(type, parentId); + Assert.Equal(expected, result); + } + + [Theory] + [MemberData(nameof(GetListWithNullRequestOptionsTestData))] + public void GetListFragment_WithNullRequestOptions_ReturnsDefaultUrl(Type type, string expected) + { + var result = fixture.Sut.GetListFragment(type, (RequestOptions)null); + Assert.Equal(expected, result); + } + + [Theory] + [MemberData(nameof(GetListWithEmptyQueryStringTestData))] + public void GetListFragment_WithEmptyQueryString_ReturnsDefaultUrl(Type type, string expected) + { + var requestOptions = new RequestOptions + { + QueryString = null + }; + + var result = fixture.Sut.GetListFragment(type, requestOptions); + Assert.Equal(expected, result); + } + + [Fact] + public void GetListFragment_WithCustomQueryParameters_DoesNotAffectUrl() + { + var requestOptions = new RequestOptions + { + QueryString = new NameValueCollection + { + { "status_id", "1" }, + { "assigned_to_id", "me" }, + { "sort", "priority:desc" } + } + }; + + var result = fixture.Sut.GetListFragment(requestOptions); + Assert.Equal("issues.json", result); + } + + [Theory] + [MemberData(nameof(GetListWithInvalidTypeTestData))] + public void GetListFragment_WithInvalidType_ThrowsRedmineException(Type invalidType) + { + var exception = Assert.Throws(() => fixture.Sut.GetListFragment(invalidType)); + + Assert.Contains("There is no uri fragment defined for type", exception.Message); + } + + public static TheoryData GetListWithBothIdsTestData() + { + return new TheoryData + { + { + typeof(Version), + "project1", + "issue1", + "projects/project1/versions.json" + }, + { + typeof(IssueCategory), + "project2", + "issue2", + "projects/project2/issue_categories.json" + } + }; + } + + public class RedmineTypeTestData : TheoryData + { + public RedmineTypeTestData() + { + Add(null, "issues.json"); + Add(null,"projects.json"); + Add(null,"users.json"); + Add(null,"time_entries.json"); + Add(null,"custom_fields.json"); + Add(null,"groups.json"); + Add(null,"news.json"); + Add(null,"queries.json"); + Add(null,"roles.json"); + Add(null,"issue_statuses.json"); + Add(null,"trackers.json"); + Add(null,"enumerations/issue_priorities.json"); + Add(null,"enumerations/time_entry_activities.json"); + Add("1","projects/1/versions.json"); + Add("1","projects/1/issue_categories.json"); + Add("1","projects/1/memberships.json"); + Add("1","issues/1/relations.json"); + Add(null,"attachments.json"); + Add(null,"custom_fields.json"); + Add(null,"journals.json"); + Add(null,"search.json"); + Add(null,"watchers.json"); + } + + private void Add(string parentId, string expected) where T : class, new() + { + AddRow(typeof(T), parentId, expected); + } + } + + public static TheoryData GetFragmentTestData() + { + return new TheoryData + { + { typeof(Attachment), "1", "attachments/1.json" }, + { typeof(CustomField), "2", "custom_fields/2.json" }, + { typeof(Group), "3", "groups/3.json" }, + { typeof(Issue), "4", "issues/4.json" }, + { typeof(IssueCategory), "5", "issue_categories/5.json" }, + { typeof(IssueCustomField), "6", "custom_fields/6.json" }, + { typeof(IssuePriority), "7", "enumerations/issue_priorities/7.json" }, + { typeof(IssueRelation), "8", "relations/8.json" }, + { typeof(IssueStatus), "9", "issue_statuses/9.json" }, + { typeof(Journal), "10", "journals/10.json" }, + { typeof(News), "11", "news/11.json" }, + { typeof(Project), "12", "projects/12.json" }, + { typeof(ProjectMembership), "13", "memberships/13.json" }, + { typeof(Query), "14", "queries/14.json" }, + { typeof(Role), "15", "roles/15.json" }, + { typeof(Search), "16", "search/16.json" }, + { typeof(TimeEntry), "17", "time_entries/17.json" }, + { typeof(TimeEntryActivity), "18", "enumerations/time_entry_activities/18.json" }, + { typeof(Tracker), "19", "trackers/19.json" }, + { typeof(User), "20", "users/20.json" }, + { typeof(Version), "21", "versions/21.json" }, + { typeof(Watcher), "22", "watchers/22.json" } + }; + } + + public static TheoryData CreateEntityTestData() + { + return new TheoryData + { + { typeof(Version), "project1", "projects/project1/versions.json" }, + { typeof(IssueCategory), "project1", "projects/project1/issue_categories.json" }, + { typeof(ProjectMembership), "project1", "projects/project1/memberships.json" }, + + { typeof(IssueRelation), "issue1", "issues/issue1/relations.json" }, + + { typeof(File), "project1", "projects/project1/files.json" }, + { typeof(Upload), null, "uploads.json" }, + { typeof(Attachment), "issue1", "/attachments/issues/issue1.json" }, + + { typeof(Issue), null, "issues.json" }, + { typeof(Project), null, "projects.json" }, + { typeof(User), null, "users.json" }, + { typeof(TimeEntry), null, "time_entries.json" }, + { typeof(News), null, "news.json" }, + { typeof(Query), null, "queries.json" }, + { typeof(Role), null, "roles.json" }, + { typeof(Group), null, "groups.json" }, + { typeof(CustomField), null, "custom_fields.json" }, + { typeof(IssueStatus), null, "issue_statuses.json" }, + { typeof(Tracker), null, "trackers.json" }, + { typeof(IssuePriority), null, "enumerations/issue_priorities.json" }, + { typeof(TimeEntryActivity), null, "enumerations/time_entry_activities.json" } + }; + } + + public static TheoryData GetListEntityRequestOptionTestData() + { + var rqWithProjectId = new RequestOptions() + { + QueryString = new NameValueCollection() + { + {RedmineKeys.PROJECT_ID, "project1"} + } + }; + var rqWithPIssueId = new RequestOptions() + { + QueryString = new NameValueCollection() + { + {RedmineKeys.ISSUE_ID, "issue1"} + } + }; + return new TheoryData + { + { typeof(Version), rqWithProjectId, "projects/project1/versions.json" }, + { typeof(IssueCategory), rqWithProjectId, "projects/project1/issue_categories.json" }, + { typeof(ProjectMembership), rqWithProjectId, "projects/project1/memberships.json" }, + + { typeof(IssueRelation), rqWithPIssueId, "issues/issue1/relations.json" }, + + { typeof(File), rqWithProjectId, "projects/project1/files.json" }, + { typeof(Attachment), rqWithPIssueId, "attachments.json" }, + + { typeof(Issue), null, "issues.json" }, + { typeof(Project), null, "projects.json" }, + { typeof(User), null, "users.json" }, + { typeof(TimeEntry), null, "time_entries.json" }, + { typeof(News), null, "news.json" }, + { typeof(Query), null, "queries.json" }, + { typeof(Role), null, "roles.json" }, + { typeof(Group), null, "groups.json" }, + { typeof(CustomField), null, "custom_fields.json" }, + { typeof(IssueStatus), null, "issue_statuses.json" }, + { typeof(Tracker), null, "trackers.json" }, + { typeof(IssuePriority), null, "enumerations/issue_priorities.json" }, + { typeof(TimeEntryActivity), null, "enumerations/time_entry_activities.json" } + }; + } + + public static TheoryData GetListTestData() + { + return new TheoryData + { + { typeof(Version), "project1", "projects/project1/versions.json" }, + { typeof(IssueCategory), "project1", "projects/project1/issue_categories.json" }, + { typeof(ProjectMembership), "project1", "projects/project1/memberships.json" }, + + { typeof(IssueRelation), "issue1", "issues/issue1/relations.json" }, + + { typeof(File), "project1", "projects/project1/files.json" }, + + { typeof(Issue), null, "issues.json" }, + { typeof(Project), null, "projects.json" }, + { typeof(User), null, "users.json" }, + { typeof(TimeEntry), null, "time_entries.json" }, + { typeof(News), null, "news.json" }, + { typeof(Query), null, "queries.json" }, + { typeof(Role), null, "roles.json" }, + { typeof(Group), null, "groups.json" }, + { typeof(CustomField), null, "custom_fields.json" }, + { typeof(IssueStatus), null, "issue_statuses.json" }, + { typeof(Tracker), null, "trackers.json" }, + { typeof(IssuePriority), null, "enumerations/issue_priorities.json" }, + { typeof(TimeEntryActivity), null, "enumerations/time_entry_activities.json" } + }; + } + + public static TheoryData GetListWithIssueIdTestData() + { + return new TheoryData + { + { typeof(IssueRelation), "issue1", "issues/issue1/relations.json" }, + }; + } + + public static TheoryData GetListWithProjectIdTestData() + { + return new TheoryData + { + { typeof(Version), "1", "projects/1/versions.json" }, + { typeof(IssueCategory), "1", "projects/1/issue_categories.json" }, + { typeof(ProjectMembership), "1", "projects/1/memberships.json" }, + { typeof(File), "1", "projects/1/files.json" }, + }; + } + + public static TheoryData GetListWithNullRequestOptionsTestData() + { + return new TheoryData + { + { typeof(Issue), "issues.json" }, + { typeof(Project), "projects.json" }, + { typeof(User), "users.json" } + }; + } + + public static TheoryData GetListWithEmptyQueryStringTestData() + { + return new TheoryData + { + { typeof(Issue), "issues.json" }, + { typeof(Project), "projects.json" }, + { typeof(User), "users.json" } + }; + } + + public static TheoryData GetListWithInvalidTypeTestData() + { + return + [ + typeof(string), + typeof(int), + typeof(DateTime), + typeof(object) + ]; + } + + public static TheoryData GetListWithNoIdsTestData() + { + return new TheoryData + { + { typeof(Issue), "issues.json" }, + { typeof(Project), "projects.json" }, + { typeof(User), "users.json" }, + { typeof(TimeEntry), "time_entries.json" }, + { typeof(CustomField), "custom_fields.json" } + }; + } + + public static TheoryData InvalidTypeTestData() + { + return + [ + typeof(object), + typeof(int) + ]; + } + + public static TheoryData, string> AttachmentOperationsData() + { + var fixture = new RedmineApiUrlsFixture(); + return new TheoryData, string> + { + { + "123", + id => fixture.Sut.AttachmentUpdate(id), + "attachments/issues/123.json" + }, + { + "456", + id => fixture.Sut.IssueWatcherAdd(id), + "issues/456/watchers.json" + } + }; + } + + public static TheoryData, string> ProjectOperationsData() + { + var fixture = new RedmineApiUrlsFixture(); + return new TheoryData, string> + { + { + "test-project", + id => fixture.Sut.ProjectClose(id), + "projects/test-project/close.json" + }, + { + "test-project", + id => fixture.Sut.ProjectReopen(id), + "projects/test-project/reopen.json" + }, + { + "test-project", + id => fixture.Sut.ProjectArchive(id), + "projects/test-project/archive.json" + }, + { + "test-project", + id => fixture.Sut.ProjectUnarchive(id), + "projects/test-project/unarchive.json" + } + }; + } + + public static TheoryData, string> WikiOperationsData() + { + var fixture = new RedmineApiUrlsFixture(); + return new TheoryData, string> + { + { + "project1", + "page1", + (id, page) => fixture.Sut.ProjectWikiPage(id, page), + "projects/project1/wiki/page1.json" + }, + { + "project1", + "page1", + (id, page) => fixture.Sut.ProjectWikiPageCreate(id, page), + "projects/project1/wiki/page1.json" + } + }; + } + +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj index 90f40281..87deb601 100644 --- a/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj +++ b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj @@ -54,17 +54,17 @@ - - - - - - - + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + runtime; build; native; contentfiles; analyzers; buildtransitive From d5027106487d8fd327c30c0ccbf2e3fe3495a01a Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 30 Mar 2025 23:35:15 +0300 Subject: [PATCH 426/549] [StringExtensions] Add ReplaceEndings (#379) --- .../Extensions/StringExtensions.cs | 20 +++++++++++++++++++ src/redmine-net-api/RedmineManager.cs | 2 ++ src/redmine-net-api/RedmineManagerAsync.cs | 5 ++--- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/redmine-net-api/Extensions/StringExtensions.cs b/src/redmine-net-api/Extensions/StringExtensions.cs index c02836e7..607c8547 100644 --- a/src/redmine-net-api/Extensions/StringExtensions.cs +++ b/src/redmine-net-api/Extensions/StringExtensions.cs @@ -18,6 +18,7 @@ limitations under the License. using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Security; +using System.Text.RegularExpressions; namespace Redmine.Net.Api.Extensions { @@ -156,5 +157,24 @@ internal static string ToInvariantString(this T value) where T : struct _ => value.ToString(), }; } + + private const string CRLR = "\r\n"; + private const string CR = "\r"; + private const string LR = "\n"; + + internal static string ReplaceEndings(this string input, string replacement = CRLR) + { + if (input.IsNullOrWhiteSpace()) + { + return input; + } + + #if NET6_0_OR_GREATER + input = input.ReplaceLineEndings(CRLR); + #else + input = Regex.Replace(input, $"{CRLR}|{CR}|{LR}", CRLR); + #endif + return input; + } } } \ No newline at end of file diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index 49cf2ee0..0043919e 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -175,6 +175,8 @@ public void Update(string id, T entity, string projectId = null, RequestOptio var payload = Serializer.Serialize(entity); + payload = payload.ReplaceEndings(); + ApiClient.Update(url, payload, requestOptions); } diff --git a/src/redmine-net-api/RedmineManagerAsync.cs b/src/redmine-net-api/RedmineManagerAsync.cs index 4039f028..fb1bba7d 100644 --- a/src/redmine-net-api/RedmineManagerAsync.cs +++ b/src/redmine-net-api/RedmineManagerAsync.cs @@ -18,7 +18,6 @@ limitations under the License. using System; using System.Collections.Generic; using System.Collections.Specialized; -using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Redmine.Net.Api.Extensions; @@ -31,7 +30,7 @@ namespace Redmine.Net.Api; public partial class RedmineManager: IRedmineManagerAsync { - private const string CRLR = "\r\n"; + /// public async Task CountAsync(RequestOptions requestOptions, CancellationToken cancellationToken = default) @@ -208,7 +207,7 @@ public async Task UpdateAsync(string id, T entity, RequestOptions requestOpti var payload = Serializer.Serialize(entity); - payload = Regex.Replace(payload, "\r\n|\r|\n",CRLR); + payload = payload.ReplaceEndings(); await ApiClient.UpdateAsync(url, payload, requestOptions, cancellationToken: cancellationToken).ConfigureAwait(false); } From 280cca74b8643b53b22bea1abe0bee6bb7df8dd5 Mon Sep 17 00:00:00 2001 From: Padi Date: Tue, 15 Apr 2025 11:19:11 +0300 Subject: [PATCH 427/549] Update copyright --- src/redmine-net-api/Authentication/IRedmineAuthentication.cs | 2 +- .../Authentication/RedmineApiKeyAuthentication.cs | 2 +- .../Authentication/RedmineBasicAuthentication.cs | 2 +- src/redmine-net-api/Authentication/RedmineNoAuthentication.cs | 2 +- src/redmine-net-api/Exceptions/ConflictException.cs | 2 +- src/redmine-net-api/Exceptions/ForbiddenException.cs | 2 +- src/redmine-net-api/Exceptions/InternalServerErrorException.cs | 2 +- .../Exceptions/NameResolutionFailureException.cs | 2 +- src/redmine-net-api/Exceptions/NotAcceptableException.cs | 2 +- src/redmine-net-api/Exceptions/NotFoundException.cs | 2 +- src/redmine-net-api/Exceptions/RedmineException.cs | 2 +- src/redmine-net-api/Exceptions/RedmineTimeoutException.cs | 2 +- src/redmine-net-api/Exceptions/UnauthorizedException.cs | 2 +- src/redmine-net-api/Extensions/CollectionExtensions.cs | 2 +- src/redmine-net-api/Extensions/IntExtensions.cs | 2 +- .../Extensions/RedmineManagerAsyncExtensions.Obsolete.cs | 2 +- src/redmine-net-api/Extensions/RedmineManagerExtensions.cs | 2 +- src/redmine-net-api/Extensions/SemaphoreSlimExtensions.cs | 2 +- src/redmine-net-api/Extensions/StringExtensions.cs | 2 +- src/redmine-net-api/Extensions/TaskExtensions.cs | 2 +- src/redmine-net-api/IRedmineManager.Obsolete.cs | 2 +- src/redmine-net-api/IRedmineManager.cs | 2 +- src/redmine-net-api/IRedmineManagerAsync.cs | 2 +- src/redmine-net-api/Internals/HashCodeHelper.cs | 2 +- src/redmine-net-api/Net/ApiRequestMessage.cs | 2 +- src/redmine-net-api/Net/ApiRequestMessageContent.cs | 2 +- src/redmine-net-api/Net/ApiResponseMessage.cs | 2 +- src/redmine-net-api/Net/ApiResponseMessageExtensions.cs | 2 +- src/redmine-net-api/Net/HttpVerbs.cs | 2 +- src/redmine-net-api/Net/IRedmineApiClient.cs | 2 +- src/redmine-net-api/Net/IRedmineApiClientOptions.cs | 2 +- src/redmine-net-api/Net/RedirectType.cs | 2 +- src/redmine-net-api/Net/RedmineApiUrls.cs | 2 +- src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs | 2 +- src/redmine-net-api/Net/RequestOptions.cs | 2 +- .../Net/WebClient/Extensions/NameValueCollectionExtensions.cs | 2 +- src/redmine-net-api/Net/WebClient/Extensions/WebExtensions.cs | 2 +- src/redmine-net-api/Net/WebClient/IRedmineWebClientObsolete.cs | 2 +- .../Net/WebClient/InternalRedmineApiWebClient.cs | 2 +- src/redmine-net-api/Net/WebClient/InternalWebClient.cs | 2 +- .../MessageContent/ByteArrayApiRequestMessageContent.cs | 2 +- .../WebClient/MessageContent/StreamApiRequestMessageContent.cs | 2 +- .../WebClient/MessageContent/StringApiRequestMessageContent.cs | 2 +- src/redmine-net-api/Net/WebClient/RedmineWebClient.Obsolete.cs | 2 +- src/redmine-net-api/Net/WebClient/RedmineWebClientOptions.cs | 2 +- src/redmine-net-api/RedmineConstants.cs | 2 +- src/redmine-net-api/RedmineKeys.cs | 2 +- src/redmine-net-api/RedmineManager.Obsolete.cs | 2 +- src/redmine-net-api/RedmineManager.cs | 2 +- src/redmine-net-api/RedmineManagerAsync.cs | 2 +- src/redmine-net-api/RedmineManagerOptions.cs | 2 +- src/redmine-net-api/RedmineManagerOptionsBuilder.cs | 2 +- src/redmine-net-api/SearchFilterBuilder.cs | 2 +- src/redmine-net-api/Serialization/IRedmineSerializer.cs | 2 +- .../Serialization/Json/Extensions/JsonReaderExtensions.cs | 2 +- .../Serialization/Json/Extensions/JsonWriterExtensions.cs | 2 +- src/redmine-net-api/Serialization/Json/IJsonSerializable.cs | 2 +- src/redmine-net-api/Serialization/Json/JsonObject.cs | 2 +- src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs | 2 +- src/redmine-net-api/Serialization/MimeFormatObsolete.cs | 2 +- src/redmine-net-api/Serialization/RedmineSerializerFactory.cs | 2 +- src/redmine-net-api/Serialization/SerializationHelper.cs | 2 +- src/redmine-net-api/Serialization/SerializationType.cs | 2 +- src/redmine-net-api/Serialization/Xml/CacheKeyFactory.cs | 2 +- .../Serialization/Xml/Extensions/XmlReaderExtensions.cs | 2 +- .../Serialization/Xml/Extensions/XmlWriterExtensions.cs | 2 +- src/redmine-net-api/Serialization/Xml/IXmlSerializerCache.cs | 2 +- src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs | 2 +- src/redmine-net-api/Serialization/Xml/XmlSerializerCache.cs | 2 +- src/redmine-net-api/Serialization/Xml/XmlTextReaderBuilder.cs | 2 +- src/redmine-net-api/Types/Attachment.cs | 2 +- src/redmine-net-api/Types/Attachments.cs | 2 +- src/redmine-net-api/Types/ChangeSet.cs | 2 +- src/redmine-net-api/Types/CustomField.cs | 2 +- src/redmine-net-api/Types/CustomFieldPossibleValue.cs | 2 +- src/redmine-net-api/Types/CustomFieldRole.cs | 2 +- src/redmine-net-api/Types/CustomFieldValue.cs | 2 +- src/redmine-net-api/Types/Detail.cs | 2 +- src/redmine-net-api/Types/DocumentCategory.cs | 2 +- src/redmine-net-api/Types/Error.cs | 2 +- src/redmine-net-api/Types/File.cs | 2 +- src/redmine-net-api/Types/Group.cs | 2 +- src/redmine-net-api/Types/GroupUser.cs | 2 +- src/redmine-net-api/Types/IValue.cs | 2 +- src/redmine-net-api/Types/Identifiable.cs | 2 +- src/redmine-net-api/Types/IdentifiableName.cs | 2 +- src/redmine-net-api/Types/Issue.cs | 2 +- src/redmine-net-api/Types/IssueAllowedStatus.cs | 2 +- src/redmine-net-api/Types/IssueCategory.cs | 2 +- src/redmine-net-api/Types/IssueChild.cs | 2 +- src/redmine-net-api/Types/IssueCustomField.cs | 2 +- src/redmine-net-api/Types/IssuePriority.cs | 2 +- src/redmine-net-api/Types/IssueRelation.cs | 2 +- src/redmine-net-api/Types/IssueRelationType.cs | 2 +- src/redmine-net-api/Types/IssueStatus.cs | 2 +- src/redmine-net-api/Types/Journal.cs | 2 +- src/redmine-net-api/Types/Membership.cs | 2 +- src/redmine-net-api/Types/MembershipRole.cs | 2 +- src/redmine-net-api/Types/MyAccount.cs | 2 +- src/redmine-net-api/Types/MyAccountCustomField.cs | 2 +- src/redmine-net-api/Types/News.cs | 2 +- src/redmine-net-api/Types/NewsComment.cs | 2 +- src/redmine-net-api/Types/PagedResults.cs | 2 +- src/redmine-net-api/Types/Permission.cs | 2 +- src/redmine-net-api/Types/Project.cs | 2 +- src/redmine-net-api/Types/ProjectEnabledModule.cs | 2 +- src/redmine-net-api/Types/ProjectIssueCategory.cs | 2 +- src/redmine-net-api/Types/ProjectMembership.cs | 2 +- src/redmine-net-api/Types/ProjectStatus.cs | 2 +- src/redmine-net-api/Types/ProjectTimeEntryActivity.cs | 2 +- src/redmine-net-api/Types/ProjectTracker.cs | 2 +- src/redmine-net-api/Types/Query.cs | 2 +- src/redmine-net-api/Types/Role.cs | 2 +- src/redmine-net-api/Types/Search.cs | 2 +- src/redmine-net-api/Types/TimeEntry.cs | 2 +- src/redmine-net-api/Types/TimeEntryActivity.cs | 2 +- src/redmine-net-api/Types/Tracker.cs | 2 +- src/redmine-net-api/Types/TrackerCustomField.cs | 2 +- src/redmine-net-api/Types/Upload.cs | 2 +- src/redmine-net-api/Types/User.cs | 2 +- src/redmine-net-api/Types/UserGroup.cs | 2 +- src/redmine-net-api/Types/UserStatus.cs | 2 +- src/redmine-net-api/Types/Version.cs | 2 +- src/redmine-net-api/Types/VersionSharing.cs | 2 +- src/redmine-net-api/Types/VersionStatus.cs | 2 +- src/redmine-net-api/Types/Watcher.cs | 2 +- src/redmine-net-api/Types/WikiPage.cs | 2 +- src/redmine-net-api/_net20/ExtensionAttribute.cs | 2 +- src/redmine-net-api/_net20/Func.cs | 2 +- src/redmine-net-api/_net20/RedmineManagerAsyncObsolete.cs | 2 +- 130 files changed, 130 insertions(+), 130 deletions(-) diff --git a/src/redmine-net-api/Authentication/IRedmineAuthentication.cs b/src/redmine-net-api/Authentication/IRedmineAuthentication.cs index 6823243a..d14d6fcf 100644 --- a/src/redmine-net-api/Authentication/IRedmineAuthentication.cs +++ b/src/redmine-net-api/Authentication/IRedmineAuthentication.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Authentication/RedmineApiKeyAuthentication.cs b/src/redmine-net-api/Authentication/RedmineApiKeyAuthentication.cs index c0a34580..a037a60c 100644 --- a/src/redmine-net-api/Authentication/RedmineApiKeyAuthentication.cs +++ b/src/redmine-net-api/Authentication/RedmineApiKeyAuthentication.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Authentication/RedmineBasicAuthentication.cs b/src/redmine-net-api/Authentication/RedmineBasicAuthentication.cs index 2e8da6cb..e78aa653 100644 --- a/src/redmine-net-api/Authentication/RedmineBasicAuthentication.cs +++ b/src/redmine-net-api/Authentication/RedmineBasicAuthentication.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Authentication/RedmineNoAuthentication.cs b/src/redmine-net-api/Authentication/RedmineNoAuthentication.cs index 6fb7fe8a..4f2ed673 100644 --- a/src/redmine-net-api/Authentication/RedmineNoAuthentication.cs +++ b/src/redmine-net-api/Authentication/RedmineNoAuthentication.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/ConflictException.cs b/src/redmine-net-api/Exceptions/ConflictException.cs index bc098687..183baf56 100644 --- a/src/redmine-net-api/Exceptions/ConflictException.cs +++ b/src/redmine-net-api/Exceptions/ConflictException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/ForbiddenException.cs b/src/redmine-net-api/Exceptions/ForbiddenException.cs index 75e6192b..e5e4d8ca 100644 --- a/src/redmine-net-api/Exceptions/ForbiddenException.cs +++ b/src/redmine-net-api/Exceptions/ForbiddenException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/InternalServerErrorException.cs b/src/redmine-net-api/Exceptions/InternalServerErrorException.cs index ccf3d5aa..5d12673c 100644 --- a/src/redmine-net-api/Exceptions/InternalServerErrorException.cs +++ b/src/redmine-net-api/Exceptions/InternalServerErrorException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/NameResolutionFailureException.cs b/src/redmine-net-api/Exceptions/NameResolutionFailureException.cs index 81da3053..da0350d0 100644 --- a/src/redmine-net-api/Exceptions/NameResolutionFailureException.cs +++ b/src/redmine-net-api/Exceptions/NameResolutionFailureException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/NotAcceptableException.cs b/src/redmine-net-api/Exceptions/NotAcceptableException.cs index 0c865fbc..bbae9b9c 100644 --- a/src/redmine-net-api/Exceptions/NotAcceptableException.cs +++ b/src/redmine-net-api/Exceptions/NotAcceptableException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/NotFoundException.cs b/src/redmine-net-api/Exceptions/NotFoundException.cs index cde236b1..d6c593dc 100644 --- a/src/redmine-net-api/Exceptions/NotFoundException.cs +++ b/src/redmine-net-api/Exceptions/NotFoundException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/RedmineException.cs b/src/redmine-net-api/Exceptions/RedmineException.cs index ccb10313..db791454 100644 --- a/src/redmine-net-api/Exceptions/RedmineException.cs +++ b/src/redmine-net-api/Exceptions/RedmineException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/RedmineTimeoutException.cs b/src/redmine-net-api/Exceptions/RedmineTimeoutException.cs index a919fd96..31ec968f 100644 --- a/src/redmine-net-api/Exceptions/RedmineTimeoutException.cs +++ b/src/redmine-net-api/Exceptions/RedmineTimeoutException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Exceptions/UnauthorizedException.cs b/src/redmine-net-api/Exceptions/UnauthorizedException.cs index c77c37f8..214f7b84 100644 --- a/src/redmine-net-api/Exceptions/UnauthorizedException.cs +++ b/src/redmine-net-api/Exceptions/UnauthorizedException.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Extensions/CollectionExtensions.cs b/src/redmine-net-api/Extensions/CollectionExtensions.cs index 60703635..6b56e79c 100755 --- a/src/redmine-net-api/Extensions/CollectionExtensions.cs +++ b/src/redmine-net-api/Extensions/CollectionExtensions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Extensions/IntExtensions.cs b/src/redmine-net-api/Extensions/IntExtensions.cs index 5f838547..fa0e57aa 100644 --- a/src/redmine-net-api/Extensions/IntExtensions.cs +++ b/src/redmine-net-api/Extensions/IntExtensions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Extensions/RedmineManagerAsyncExtensions.Obsolete.cs b/src/redmine-net-api/Extensions/RedmineManagerAsyncExtensions.Obsolete.cs index 90f25fb8..d2b42862 100644 --- a/src/redmine-net-api/Extensions/RedmineManagerAsyncExtensions.Obsolete.cs +++ b/src/redmine-net-api/Extensions/RedmineManagerAsyncExtensions.Obsolete.cs @@ -1,5 +1,5 @@ /* -Copyright 2011 - 2023 Adrian Popescu +Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs index 254e4855..35c5e006 100644 --- a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs +++ b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Extensions/SemaphoreSlimExtensions.cs b/src/redmine-net-api/Extensions/SemaphoreSlimExtensions.cs index a47dbb9b..016dd51f 100644 --- a/src/redmine-net-api/Extensions/SemaphoreSlimExtensions.cs +++ b/src/redmine-net-api/Extensions/SemaphoreSlimExtensions.cs @@ -1,5 +1,5 @@ /* -Copyright 2011 - 2023 Adrian Popescu +Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Extensions/StringExtensions.cs b/src/redmine-net-api/Extensions/StringExtensions.cs index 607c8547..03fdc059 100644 --- a/src/redmine-net-api/Extensions/StringExtensions.cs +++ b/src/redmine-net-api/Extensions/StringExtensions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Extensions/TaskExtensions.cs b/src/redmine-net-api/Extensions/TaskExtensions.cs index c74b6d4d..dd182846 100644 --- a/src/redmine-net-api/Extensions/TaskExtensions.cs +++ b/src/redmine-net-api/Extensions/TaskExtensions.cs @@ -1,5 +1,5 @@ /* -Copyright 2011 - 2023 Adrian Popescu +Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/IRedmineManager.Obsolete.cs b/src/redmine-net-api/IRedmineManager.Obsolete.cs index 8704367c..028984a3 100644 --- a/src/redmine-net-api/IRedmineManager.Obsolete.cs +++ b/src/redmine-net-api/IRedmineManager.Obsolete.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/IRedmineManager.cs b/src/redmine-net-api/IRedmineManager.cs index e45ec6e7..5e4f5437 100644 --- a/src/redmine-net-api/IRedmineManager.cs +++ b/src/redmine-net-api/IRedmineManager.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/IRedmineManagerAsync.cs b/src/redmine-net-api/IRedmineManagerAsync.cs index adc9456b..4d2343d6 100644 --- a/src/redmine-net-api/IRedmineManagerAsync.cs +++ b/src/redmine-net-api/IRedmineManagerAsync.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Internals/HashCodeHelper.cs b/src/redmine-net-api/Internals/HashCodeHelper.cs index 4d2ac468..19f92d6a 100755 --- a/src/redmine-net-api/Internals/HashCodeHelper.cs +++ b/src/redmine-net-api/Internals/HashCodeHelper.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Net/ApiRequestMessage.cs b/src/redmine-net-api/Net/ApiRequestMessage.cs index c3bdb891..b0b7a2fb 100644 --- a/src/redmine-net-api/Net/ApiRequestMessage.cs +++ b/src/redmine-net-api/Net/ApiRequestMessage.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Net/ApiRequestMessageContent.cs b/src/redmine-net-api/Net/ApiRequestMessageContent.cs index e484c81a..94c5f5e9 100644 --- a/src/redmine-net-api/Net/ApiRequestMessageContent.cs +++ b/src/redmine-net-api/Net/ApiRequestMessageContent.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Net/ApiResponseMessage.cs b/src/redmine-net-api/Net/ApiResponseMessage.cs index 4cdf66c0..971aaabb 100644 --- a/src/redmine-net-api/Net/ApiResponseMessage.cs +++ b/src/redmine-net-api/Net/ApiResponseMessage.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Net/ApiResponseMessageExtensions.cs b/src/redmine-net-api/Net/ApiResponseMessageExtensions.cs index 36aeaf6e..f039a451 100644 --- a/src/redmine-net-api/Net/ApiResponseMessageExtensions.cs +++ b/src/redmine-net-api/Net/ApiResponseMessageExtensions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Net/HttpVerbs.cs b/src/redmine-net-api/Net/HttpVerbs.cs index bcd88271..e7851896 100644 --- a/src/redmine-net-api/Net/HttpVerbs.cs +++ b/src/redmine-net-api/Net/HttpVerbs.cs @@ -1,5 +1,5 @@ /* -Copyright 2011 - 2023 Adrian Popescu +Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Net/IRedmineApiClient.cs b/src/redmine-net-api/Net/IRedmineApiClient.cs index 586a001a..f9ffc4f8 100644 --- a/src/redmine-net-api/Net/IRedmineApiClient.cs +++ b/src/redmine-net-api/Net/IRedmineApiClient.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Net/IRedmineApiClientOptions.cs b/src/redmine-net-api/Net/IRedmineApiClientOptions.cs index 263c703a..3a11601f 100644 --- a/src/redmine-net-api/Net/IRedmineApiClientOptions.cs +++ b/src/redmine-net-api/Net/IRedmineApiClientOptions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Net/RedirectType.cs b/src/redmine-net-api/Net/RedirectType.cs index 7793e23c..ae9aedb5 100644 --- a/src/redmine-net-api/Net/RedirectType.cs +++ b/src/redmine-net-api/Net/RedirectType.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Net/RedmineApiUrls.cs b/src/redmine-net-api/Net/RedmineApiUrls.cs index 493bfa6a..f893c338 100644 --- a/src/redmine-net-api/Net/RedmineApiUrls.cs +++ b/src/redmine-net-api/Net/RedmineApiUrls.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs b/src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs index 763060aa..4312ba9e 100644 --- a/src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs +++ b/src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Net/RequestOptions.cs b/src/redmine-net-api/Net/RequestOptions.cs index 1b514c8a..10f7c77e 100644 --- a/src/redmine-net-api/Net/RequestOptions.cs +++ b/src/redmine-net-api/Net/RequestOptions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Net/WebClient/Extensions/NameValueCollectionExtensions.cs b/src/redmine-net-api/Net/WebClient/Extensions/NameValueCollectionExtensions.cs index 7e3420b0..6d573e17 100644 --- a/src/redmine-net-api/Net/WebClient/Extensions/NameValueCollectionExtensions.cs +++ b/src/redmine-net-api/Net/WebClient/Extensions/NameValueCollectionExtensions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Net/WebClient/Extensions/WebExtensions.cs b/src/redmine-net-api/Net/WebClient/Extensions/WebExtensions.cs index 9d454562..3a3b1a2a 100644 --- a/src/redmine-net-api/Net/WebClient/Extensions/WebExtensions.cs +++ b/src/redmine-net-api/Net/WebClient/Extensions/WebExtensions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Net/WebClient/IRedmineWebClientObsolete.cs b/src/redmine-net-api/Net/WebClient/IRedmineWebClientObsolete.cs index 1f6be22c..3b7c1bd9 100644 --- a/src/redmine-net-api/Net/WebClient/IRedmineWebClientObsolete.cs +++ b/src/redmine-net-api/Net/WebClient/IRedmineWebClientObsolete.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs index 51b1ad7c..df94c7fa 100644 --- a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs +++ b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Net/WebClient/InternalWebClient.cs b/src/redmine-net-api/Net/WebClient/InternalWebClient.cs index dbcdf193..2bec6d92 100644 --- a/src/redmine-net-api/Net/WebClient/InternalWebClient.cs +++ b/src/redmine-net-api/Net/WebClient/InternalWebClient.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Net/WebClient/MessageContent/ByteArrayApiRequestMessageContent.cs b/src/redmine-net-api/Net/WebClient/MessageContent/ByteArrayApiRequestMessageContent.cs index 4f72fc83..a1456ad2 100644 --- a/src/redmine-net-api/Net/WebClient/MessageContent/ByteArrayApiRequestMessageContent.cs +++ b/src/redmine-net-api/Net/WebClient/MessageContent/ByteArrayApiRequestMessageContent.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Net/WebClient/MessageContent/StreamApiRequestMessageContent.cs b/src/redmine-net-api/Net/WebClient/MessageContent/StreamApiRequestMessageContent.cs index e7527234..ed49becf 100644 --- a/src/redmine-net-api/Net/WebClient/MessageContent/StreamApiRequestMessageContent.cs +++ b/src/redmine-net-api/Net/WebClient/MessageContent/StreamApiRequestMessageContent.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Net/WebClient/MessageContent/StringApiRequestMessageContent.cs b/src/redmine-net-api/Net/WebClient/MessageContent/StringApiRequestMessageContent.cs index e69a5fee..3a1d7590 100644 --- a/src/redmine-net-api/Net/WebClient/MessageContent/StringApiRequestMessageContent.cs +++ b/src/redmine-net-api/Net/WebClient/MessageContent/StringApiRequestMessageContent.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Net/WebClient/RedmineWebClient.Obsolete.cs b/src/redmine-net-api/Net/WebClient/RedmineWebClient.Obsolete.cs index 688a499e..0276931c 100644 --- a/src/redmine-net-api/Net/WebClient/RedmineWebClient.Obsolete.cs +++ b/src/redmine-net-api/Net/WebClient/RedmineWebClient.Obsolete.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Net/WebClient/RedmineWebClientOptions.cs b/src/redmine-net-api/Net/WebClient/RedmineWebClientOptions.cs index cd76daf1..714df02d 100644 --- a/src/redmine-net-api/Net/WebClient/RedmineWebClientOptions.cs +++ b/src/redmine-net-api/Net/WebClient/RedmineWebClientOptions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/RedmineConstants.cs b/src/redmine-net-api/RedmineConstants.cs index f8fdad88..e9d00588 100644 --- a/src/redmine-net-api/RedmineConstants.cs +++ b/src/redmine-net-api/RedmineConstants.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/RedmineKeys.cs b/src/redmine-net-api/RedmineKeys.cs index 5ecae9fd..045533cb 100644 --- a/src/redmine-net-api/RedmineKeys.cs +++ b/src/redmine-net-api/RedmineKeys.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/RedmineManager.Obsolete.cs b/src/redmine-net-api/RedmineManager.Obsolete.cs index 2ac4a608..950ecafb 100644 --- a/src/redmine-net-api/RedmineManager.Obsolete.cs +++ b/src/redmine-net-api/RedmineManager.Obsolete.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index 0043919e..9ddbcdbe 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/RedmineManagerAsync.cs b/src/redmine-net-api/RedmineManagerAsync.cs index fb1bba7d..f7060e5a 100644 --- a/src/redmine-net-api/RedmineManagerAsync.cs +++ b/src/redmine-net-api/RedmineManagerAsync.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/RedmineManagerOptions.cs b/src/redmine-net-api/RedmineManagerOptions.cs index a0928c44..3801922b 100644 --- a/src/redmine-net-api/RedmineManagerOptions.cs +++ b/src/redmine-net-api/RedmineManagerOptions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/RedmineManagerOptionsBuilder.cs b/src/redmine-net-api/RedmineManagerOptionsBuilder.cs index 417bcdec..b54db5b3 100644 --- a/src/redmine-net-api/RedmineManagerOptionsBuilder.cs +++ b/src/redmine-net-api/RedmineManagerOptionsBuilder.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/SearchFilterBuilder.cs b/src/redmine-net-api/SearchFilterBuilder.cs index 65e8a7df..856fb1a7 100644 --- a/src/redmine-net-api/SearchFilterBuilder.cs +++ b/src/redmine-net-api/SearchFilterBuilder.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Serialization/IRedmineSerializer.cs b/src/redmine-net-api/Serialization/IRedmineSerializer.cs index 1c2e5eec..e6e064f4 100644 --- a/src/redmine-net-api/Serialization/IRedmineSerializer.cs +++ b/src/redmine-net-api/Serialization/IRedmineSerializer.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Serialization/Json/Extensions/JsonReaderExtensions.cs b/src/redmine-net-api/Serialization/Json/Extensions/JsonReaderExtensions.cs index 94776ceb..7d490315 100644 --- a/src/redmine-net-api/Serialization/Json/Extensions/JsonReaderExtensions.cs +++ b/src/redmine-net-api/Serialization/Json/Extensions/JsonReaderExtensions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Serialization/Json/Extensions/JsonWriterExtensions.cs b/src/redmine-net-api/Serialization/Json/Extensions/JsonWriterExtensions.cs index 0d5bd08e..c921d706 100644 --- a/src/redmine-net-api/Serialization/Json/Extensions/JsonWriterExtensions.cs +++ b/src/redmine-net-api/Serialization/Json/Extensions/JsonWriterExtensions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Serialization/Json/IJsonSerializable.cs b/src/redmine-net-api/Serialization/Json/IJsonSerializable.cs index c325545f..af82ab73 100644 --- a/src/redmine-net-api/Serialization/Json/IJsonSerializable.cs +++ b/src/redmine-net-api/Serialization/Json/IJsonSerializable.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Serialization/Json/JsonObject.cs b/src/redmine-net-api/Serialization/Json/JsonObject.cs index 452df4a4..612b2a10 100644 --- a/src/redmine-net-api/Serialization/Json/JsonObject.cs +++ b/src/redmine-net-api/Serialization/Json/JsonObject.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs b/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs index 42807e3f..41f66958 100644 --- a/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs +++ b/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Serialization/MimeFormatObsolete.cs b/src/redmine-net-api/Serialization/MimeFormatObsolete.cs index 16d54fd3..1bad6a4f 100755 --- a/src/redmine-net-api/Serialization/MimeFormatObsolete.cs +++ b/src/redmine-net-api/Serialization/MimeFormatObsolete.cs @@ -1,5 +1,5 @@ /* -Copyright 2011 - 2023 Adrian Popescu +Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Serialization/RedmineSerializerFactory.cs b/src/redmine-net-api/Serialization/RedmineSerializerFactory.cs index 240d1af8..a315ad44 100644 --- a/src/redmine-net-api/Serialization/RedmineSerializerFactory.cs +++ b/src/redmine-net-api/Serialization/RedmineSerializerFactory.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Serialization/SerializationHelper.cs b/src/redmine-net-api/Serialization/SerializationHelper.cs index 5ecd4b8f..225772aa 100644 --- a/src/redmine-net-api/Serialization/SerializationHelper.cs +++ b/src/redmine-net-api/Serialization/SerializationHelper.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Serialization/SerializationType.cs b/src/redmine-net-api/Serialization/SerializationType.cs index c46591f8..decd824c 100644 --- a/src/redmine-net-api/Serialization/SerializationType.cs +++ b/src/redmine-net-api/Serialization/SerializationType.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Serialization/Xml/CacheKeyFactory.cs b/src/redmine-net-api/Serialization/Xml/CacheKeyFactory.cs index f8a4fa72..45f17b12 100644 --- a/src/redmine-net-api/Serialization/Xml/CacheKeyFactory.cs +++ b/src/redmine-net-api/Serialization/Xml/CacheKeyFactory.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Serialization/Xml/Extensions/XmlReaderExtensions.cs b/src/redmine-net-api/Serialization/Xml/Extensions/XmlReaderExtensions.cs index 1c762b1a..8a57a1e6 100644 --- a/src/redmine-net-api/Serialization/Xml/Extensions/XmlReaderExtensions.cs +++ b/src/redmine-net-api/Serialization/Xml/Extensions/XmlReaderExtensions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Serialization/Xml/Extensions/XmlWriterExtensions.cs b/src/redmine-net-api/Serialization/Xml/Extensions/XmlWriterExtensions.cs index 8791c6b1..cf33cb1c 100644 --- a/src/redmine-net-api/Serialization/Xml/Extensions/XmlWriterExtensions.cs +++ b/src/redmine-net-api/Serialization/Xml/Extensions/XmlWriterExtensions.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Serialization/Xml/IXmlSerializerCache.cs b/src/redmine-net-api/Serialization/Xml/IXmlSerializerCache.cs index daa3afed..bfb5b416 100644 --- a/src/redmine-net-api/Serialization/Xml/IXmlSerializerCache.cs +++ b/src/redmine-net-api/Serialization/Xml/IXmlSerializerCache.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs b/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs index 74c26aea..f51e38ad 100644 --- a/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs +++ b/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Serialization/Xml/XmlSerializerCache.cs b/src/redmine-net-api/Serialization/Xml/XmlSerializerCache.cs index cdc73fe8..9a63c8d0 100644 --- a/src/redmine-net-api/Serialization/Xml/XmlSerializerCache.cs +++ b/src/redmine-net-api/Serialization/Xml/XmlSerializerCache.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Serialization/Xml/XmlTextReaderBuilder.cs b/src/redmine-net-api/Serialization/Xml/XmlTextReaderBuilder.cs index 2d24a598..dc83be34 100644 --- a/src/redmine-net-api/Serialization/Xml/XmlTextReaderBuilder.cs +++ b/src/redmine-net-api/Serialization/Xml/XmlTextReaderBuilder.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Attachment.cs b/src/redmine-net-api/Types/Attachment.cs index da35047f..0fa66eb9 100644 --- a/src/redmine-net-api/Types/Attachment.cs +++ b/src/redmine-net-api/Types/Attachment.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Attachments.cs b/src/redmine-net-api/Types/Attachments.cs index 15024d91..21655fec 100644 --- a/src/redmine-net-api/Types/Attachments.cs +++ b/src/redmine-net-api/Types/Attachments.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/ChangeSet.cs b/src/redmine-net-api/Types/ChangeSet.cs index 3e9937af..c7fddae5 100644 --- a/src/redmine-net-api/Types/ChangeSet.cs +++ b/src/redmine-net-api/Types/ChangeSet.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/CustomField.cs b/src/redmine-net-api/Types/CustomField.cs index f10a4162..b6761b32 100644 --- a/src/redmine-net-api/Types/CustomField.cs +++ b/src/redmine-net-api/Types/CustomField.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/CustomFieldPossibleValue.cs b/src/redmine-net-api/Types/CustomFieldPossibleValue.cs index 2d3d0029..6d972c4a 100644 --- a/src/redmine-net-api/Types/CustomFieldPossibleValue.cs +++ b/src/redmine-net-api/Types/CustomFieldPossibleValue.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/CustomFieldRole.cs b/src/redmine-net-api/Types/CustomFieldRole.cs index 8bbf192b..d870a5f9 100644 --- a/src/redmine-net-api/Types/CustomFieldRole.cs +++ b/src/redmine-net-api/Types/CustomFieldRole.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/CustomFieldValue.cs b/src/redmine-net-api/Types/CustomFieldValue.cs index 19569f1e..de3f4fdd 100644 --- a/src/redmine-net-api/Types/CustomFieldValue.cs +++ b/src/redmine-net-api/Types/CustomFieldValue.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Detail.cs b/src/redmine-net-api/Types/Detail.cs index 53a0b9a1..73468a6f 100644 --- a/src/redmine-net-api/Types/Detail.cs +++ b/src/redmine-net-api/Types/Detail.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/DocumentCategory.cs b/src/redmine-net-api/Types/DocumentCategory.cs index 4040fade..745f695c 100644 --- a/src/redmine-net-api/Types/DocumentCategory.cs +++ b/src/redmine-net-api/Types/DocumentCategory.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Error.cs b/src/redmine-net-api/Types/Error.cs index a3712f4b..06128f72 100644 --- a/src/redmine-net-api/Types/Error.cs +++ b/src/redmine-net-api/Types/Error.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/File.cs b/src/redmine-net-api/Types/File.cs index eb936dd1..874044b8 100644 --- a/src/redmine-net-api/Types/File.cs +++ b/src/redmine-net-api/Types/File.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Group.cs b/src/redmine-net-api/Types/Group.cs index 71e92025..ef097986 100644 --- a/src/redmine-net-api/Types/Group.cs +++ b/src/redmine-net-api/Types/Group.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/GroupUser.cs b/src/redmine-net-api/Types/GroupUser.cs index 90b77899..6b622f51 100644 --- a/src/redmine-net-api/Types/GroupUser.cs +++ b/src/redmine-net-api/Types/GroupUser.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IValue.cs b/src/redmine-net-api/Types/IValue.cs index ae430755..bbfe3a77 100755 --- a/src/redmine-net-api/Types/IValue.cs +++ b/src/redmine-net-api/Types/IValue.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Identifiable.cs b/src/redmine-net-api/Types/Identifiable.cs index b0f81444..0da0cb0d 100644 --- a/src/redmine-net-api/Types/Identifiable.cs +++ b/src/redmine-net-api/Types/Identifiable.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IdentifiableName.cs b/src/redmine-net-api/Types/IdentifiableName.cs index 02fb5c6d..f17348b0 100644 --- a/src/redmine-net-api/Types/IdentifiableName.cs +++ b/src/redmine-net-api/Types/IdentifiableName.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Issue.cs b/src/redmine-net-api/Types/Issue.cs index 7d9b488e..81cecc21 100644 --- a/src/redmine-net-api/Types/Issue.cs +++ b/src/redmine-net-api/Types/Issue.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IssueAllowedStatus.cs b/src/redmine-net-api/Types/IssueAllowedStatus.cs index 88013681..8cc7af61 100644 --- a/src/redmine-net-api/Types/IssueAllowedStatus.cs +++ b/src/redmine-net-api/Types/IssueAllowedStatus.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IssueCategory.cs b/src/redmine-net-api/Types/IssueCategory.cs index abde781e..2fc6bfe0 100644 --- a/src/redmine-net-api/Types/IssueCategory.cs +++ b/src/redmine-net-api/Types/IssueCategory.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IssueChild.cs b/src/redmine-net-api/Types/IssueChild.cs index 6418aa4f..7a9aeeb5 100644 --- a/src/redmine-net-api/Types/IssueChild.cs +++ b/src/redmine-net-api/Types/IssueChild.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IssueCustomField.cs b/src/redmine-net-api/Types/IssueCustomField.cs index 1cb47bf4..5a77e3e7 100644 --- a/src/redmine-net-api/Types/IssueCustomField.cs +++ b/src/redmine-net-api/Types/IssueCustomField.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IssuePriority.cs b/src/redmine-net-api/Types/IssuePriority.cs index be107464..877d3e31 100644 --- a/src/redmine-net-api/Types/IssuePriority.cs +++ b/src/redmine-net-api/Types/IssuePriority.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IssueRelation.cs b/src/redmine-net-api/Types/IssueRelation.cs index 88ae9eb2..5a80bd4c 100644 --- a/src/redmine-net-api/Types/IssueRelation.cs +++ b/src/redmine-net-api/Types/IssueRelation.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IssueRelationType.cs b/src/redmine-net-api/Types/IssueRelationType.cs index e2564de4..fff16392 100644 --- a/src/redmine-net-api/Types/IssueRelationType.cs +++ b/src/redmine-net-api/Types/IssueRelationType.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/IssueStatus.cs b/src/redmine-net-api/Types/IssueStatus.cs index 1e842ac0..1c9a12bd 100644 --- a/src/redmine-net-api/Types/IssueStatus.cs +++ b/src/redmine-net-api/Types/IssueStatus.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Journal.cs b/src/redmine-net-api/Types/Journal.cs index 75cd78a3..da2234a7 100644 --- a/src/redmine-net-api/Types/Journal.cs +++ b/src/redmine-net-api/Types/Journal.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Membership.cs b/src/redmine-net-api/Types/Membership.cs index f9f92ff1..84443fe7 100644 --- a/src/redmine-net-api/Types/Membership.cs +++ b/src/redmine-net-api/Types/Membership.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/MembershipRole.cs b/src/redmine-net-api/Types/MembershipRole.cs index b1069206..06770cb9 100644 --- a/src/redmine-net-api/Types/MembershipRole.cs +++ b/src/redmine-net-api/Types/MembershipRole.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/MyAccount.cs b/src/redmine-net-api/Types/MyAccount.cs index 0bcf9831..86c4bead 100644 --- a/src/redmine-net-api/Types/MyAccount.cs +++ b/src/redmine-net-api/Types/MyAccount.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/MyAccountCustomField.cs b/src/redmine-net-api/Types/MyAccountCustomField.cs index 8350527d..5e895a7c 100644 --- a/src/redmine-net-api/Types/MyAccountCustomField.cs +++ b/src/redmine-net-api/Types/MyAccountCustomField.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/News.cs b/src/redmine-net-api/Types/News.cs index e4796eef..e8a9866f 100644 --- a/src/redmine-net-api/Types/News.cs +++ b/src/redmine-net-api/Types/News.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/NewsComment.cs b/src/redmine-net-api/Types/NewsComment.cs index 3766204f..567c1b7f 100644 --- a/src/redmine-net-api/Types/NewsComment.cs +++ b/src/redmine-net-api/Types/NewsComment.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/PagedResults.cs b/src/redmine-net-api/Types/PagedResults.cs index b6b446e5..5721c2f5 100644 --- a/src/redmine-net-api/Types/PagedResults.cs +++ b/src/redmine-net-api/Types/PagedResults.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Permission.cs b/src/redmine-net-api/Types/Permission.cs index fba871af..9f2de487 100644 --- a/src/redmine-net-api/Types/Permission.cs +++ b/src/redmine-net-api/Types/Permission.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs index 55af2209..f3e327c0 100644 --- a/src/redmine-net-api/Types/Project.cs +++ b/src/redmine-net-api/Types/Project.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/ProjectEnabledModule.cs b/src/redmine-net-api/Types/ProjectEnabledModule.cs index 5128a5c0..e90971a8 100644 --- a/src/redmine-net-api/Types/ProjectEnabledModule.cs +++ b/src/redmine-net-api/Types/ProjectEnabledModule.cs @@ -1,5 +1,5 @@ /* -Copyright 2011 - 2023 Adrian Popescu +Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/ProjectIssueCategory.cs b/src/redmine-net-api/Types/ProjectIssueCategory.cs index d9181e17..76958c50 100644 --- a/src/redmine-net-api/Types/ProjectIssueCategory.cs +++ b/src/redmine-net-api/Types/ProjectIssueCategory.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/ProjectMembership.cs b/src/redmine-net-api/Types/ProjectMembership.cs index 8dd3642a..9979ab64 100644 --- a/src/redmine-net-api/Types/ProjectMembership.cs +++ b/src/redmine-net-api/Types/ProjectMembership.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/ProjectStatus.cs b/src/redmine-net-api/Types/ProjectStatus.cs index ecd504a2..1e98e1bb 100755 --- a/src/redmine-net-api/Types/ProjectStatus.cs +++ b/src/redmine-net-api/Types/ProjectStatus.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs b/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs index 44e23cd5..69daaca1 100644 --- a/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs +++ b/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/ProjectTracker.cs b/src/redmine-net-api/Types/ProjectTracker.cs index bf96390d..fa5a2533 100644 --- a/src/redmine-net-api/Types/ProjectTracker.cs +++ b/src/redmine-net-api/Types/ProjectTracker.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Query.cs b/src/redmine-net-api/Types/Query.cs index f5ef00c8..2506982c 100644 --- a/src/redmine-net-api/Types/Query.cs +++ b/src/redmine-net-api/Types/Query.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Role.cs b/src/redmine-net-api/Types/Role.cs index 10a2420b..a922fff5 100644 --- a/src/redmine-net-api/Types/Role.cs +++ b/src/redmine-net-api/Types/Role.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Search.cs b/src/redmine-net-api/Types/Search.cs index 71ba4885..c17f8367 100644 --- a/src/redmine-net-api/Types/Search.cs +++ b/src/redmine-net-api/Types/Search.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/TimeEntry.cs b/src/redmine-net-api/Types/TimeEntry.cs index 79ad91bd..5af98492 100644 --- a/src/redmine-net-api/Types/TimeEntry.cs +++ b/src/redmine-net-api/Types/TimeEntry.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/TimeEntryActivity.cs b/src/redmine-net-api/Types/TimeEntryActivity.cs index 6e0ef6c7..4fd873e3 100644 --- a/src/redmine-net-api/Types/TimeEntryActivity.cs +++ b/src/redmine-net-api/Types/TimeEntryActivity.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Tracker.cs b/src/redmine-net-api/Types/Tracker.cs index 13d26b31..ded9c388 100644 --- a/src/redmine-net-api/Types/Tracker.cs +++ b/src/redmine-net-api/Types/Tracker.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/TrackerCustomField.cs b/src/redmine-net-api/Types/TrackerCustomField.cs index 875d93b7..5372039b 100644 --- a/src/redmine-net-api/Types/TrackerCustomField.cs +++ b/src/redmine-net-api/Types/TrackerCustomField.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Upload.cs b/src/redmine-net-api/Types/Upload.cs index be702c1f..3948d5a5 100644 --- a/src/redmine-net-api/Types/Upload.cs +++ b/src/redmine-net-api/Types/Upload.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/User.cs b/src/redmine-net-api/Types/User.cs index e47ae005..481409bd 100644 --- a/src/redmine-net-api/Types/User.cs +++ b/src/redmine-net-api/Types/User.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/UserGroup.cs b/src/redmine-net-api/Types/UserGroup.cs index 88a173ca..57c7c0a8 100644 --- a/src/redmine-net-api/Types/UserGroup.cs +++ b/src/redmine-net-api/Types/UserGroup.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/UserStatus.cs b/src/redmine-net-api/Types/UserStatus.cs index ae00c930..86542357 100644 --- a/src/redmine-net-api/Types/UserStatus.cs +++ b/src/redmine-net-api/Types/UserStatus.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Version.cs b/src/redmine-net-api/Types/Version.cs index 2861d4b3..78dc24eb 100644 --- a/src/redmine-net-api/Types/Version.cs +++ b/src/redmine-net-api/Types/Version.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/VersionSharing.cs b/src/redmine-net-api/Types/VersionSharing.cs index 125f3f1c..d68e8236 100644 --- a/src/redmine-net-api/Types/VersionSharing.cs +++ b/src/redmine-net-api/Types/VersionSharing.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/VersionStatus.cs b/src/redmine-net-api/Types/VersionStatus.cs index 7e452f41..69c42ce9 100644 --- a/src/redmine-net-api/Types/VersionStatus.cs +++ b/src/redmine-net-api/Types/VersionStatus.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/Watcher.cs b/src/redmine-net-api/Types/Watcher.cs index d413cab1..7c0dc9d1 100644 --- a/src/redmine-net-api/Types/Watcher.cs +++ b/src/redmine-net-api/Types/Watcher.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/Types/WikiPage.cs b/src/redmine-net-api/Types/WikiPage.cs index d62aeb63..b2879278 100644 --- a/src/redmine-net-api/Types/WikiPage.cs +++ b/src/redmine-net-api/Types/WikiPage.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/_net20/ExtensionAttribute.cs b/src/redmine-net-api/_net20/ExtensionAttribute.cs index c6857d81..4c43dcfc 100755 --- a/src/redmine-net-api/_net20/ExtensionAttribute.cs +++ b/src/redmine-net-api/_net20/ExtensionAttribute.cs @@ -1,6 +1,6 @@ #if NET20 /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/_net20/Func.cs b/src/redmine-net-api/_net20/Func.cs index ce8db0e9..39095ef6 100644 --- a/src/redmine-net-api/_net20/Func.cs +++ b/src/redmine-net-api/_net20/Func.cs @@ -1,5 +1,5 @@ /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/redmine-net-api/_net20/RedmineManagerAsyncObsolete.cs b/src/redmine-net-api/_net20/RedmineManagerAsyncObsolete.cs index 5a8a2fcc..8344efba 100644 --- a/src/redmine-net-api/_net20/RedmineManagerAsyncObsolete.cs +++ b/src/redmine-net-api/_net20/RedmineManagerAsyncObsolete.cs @@ -1,7 +1,7 @@ #if NET20 /* - Copyright 2011 - 2023 Adrian Popescu + Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 83e216c87b1051cc99fd31613e8bc53226177909 Mon Sep 17 00:00:00 2001 From: Padi Date: Fri, 25 Apr 2025 08:42:31 +0300 Subject: [PATCH 428/549] Update README.md --- README.md | 154 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 102 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 11bb8f6c..cc860e6f 100755 --- a/README.md +++ b/README.md @@ -1,74 +1,124 @@ +# ![Redmine .NET API](https://raw.githubusercontent.com/zapadi/redmine-net-api/master/logo.png) redmine-net-api -![Redmine .NET Api](https://github.com/zapadi/redmine-net-api/workflows/CI%2FCD/badge.svg?branch=master) -![Appveyor last build status](https://ci.appveyor.com/api/projects/status/github/zapadi/redmine-net-api?branch=master&svg=true&passingText=master%20-%20OK&failingText=ups...) -[![NuGet package](https://img.shields.io/nuget/v/redmine-api.svg)](https://www.nuget.org/packages/redmine-api) -![Nuget](https://img.shields.io/nuget/dt/redmine-api) -Buy Me A Coffee +[![NuGet](https://img.shields.io/nuget/v/redmine-api.svg)](https://www.nuget.org/packages/redmine-api) +[![NuGet Downloads](https://img.shields.io/nuget/dt/redmine-api)](https://www.nuget.org/packages/redmine-api) +[![License](https://img.shields.io/github/license/zapadi/redmine-net-api)](LICENSE) +[![Contributors](https://img.shields.io/github/contributors/zapadi/redmine-net-api)](https://github.com/zapadi/redmine-net-api/graphs/contributors) -# redmine-net-api ![redmine-net-api logo](https://github.com/zapadi/redmine-net-api/blob/master/logo.png) +A modern and flexible .NET client library to interact with [Redmine](https://www.redmine.org)'s REST API. Supports XML and JSON formats with GZipped responses for improved performance. -redmine-net-api is a library for communicating with a Redmine project management application. -* Uses [Redmine's REST API.](http://www.redmine.org/projects/redmine/wiki/Rest_api/) -* Supports both XML and **JSON** formats. -* Supports GZipped responses from servers. -* This API provides access and basic CRUD operations (create, read, update, delete) for the resources described below: +## 🚀 Features -| Resource | Read | Create | Update | Delete | -|:--------------------|:-------:|:-------:|:-------:|:-------:| -| Attachments | ✓ | ✓ | ✗ | ✗ | -| Custom Fields | ✓ | ✗ | ✗ | ✗ | -| Enumerations | ✓ | ✗ | ✗ | ✗ | -| Files | ✓ | ✓ | ✗ | ✗ | -| Groups | ✓ | ✓ | ✓ | ✓ | -| Issues | ✓ | ✓ | ✓ | ✓ | -| Issue Categories | ✓ | ✓ | ✓ | ✓ | -| Issue Relations | ✓ | ✓ | ✓ | ✓ | -| Issue Statuses | ✓ | ✗ | ✗ | ✗ | -| My account | ✓ | ✗ | ✓ | ✗ | -| News | ✓ | ✓ | ✓ | ✓ | -| Projects | ✓ | ✓ | ✓ | ✓ | -| Project Memberships | ✓ | ✓ | ✓ | ✓ | -| Queries | ✓ | ✗ | ✗ | ✗ | -| Roles | ✓ | ✗ | ✗ | ✗ | -| Search | ✓ | | | | -| Time Entries | ✓ | ✓ | ✓ | ✓ | -| Trackers | ✓ | ✗ | ✗ | ✗ | -| Users | ✓ | ✓ | ✓ | ✓ | -| Versions | ✓ | ✓ | ✓ | ✓ | -| Wiki Pages | ✓ | ✓ | ✓ | ✓ | +- Full REST API support with CRUD operations +- Supports both XML and JSON data formats +- Handles GZipped server responses transparently +- Easy integration via NuGet package +- Actively maintained and community-driven -## WIKI +| Resource | Read | Create | Update | Delete | +|----------------------|:----:|:------:|:------:|:------:| +| Attachments | ✅ | ✅ | ❌ | ❌ | +| Custom Fields | ✅ | ❌ | ❌ | ❌ | +| Enumerations | ✅ | ❌ | ❌ | ❌ | +| Files | ✅ | ✅ | ❌ | ❌ | +| Groups | ✅ | ✅ | ✅ | ✅ | +| Issues | ✅ | ✅ | ✅ | ✅ | +| Issue Categories | ✅ | ✅ | ✅ | ✅ | +| Issue Relations | ✅ | ✅ | ✅ | ✅ | +| Issue Statuses | ✅ | ❌ | ❌ | ❌ | +| My Account | ✅ | ❌ | ✅ | ❌ | +| News | ✅ | ✅ | ✅ | ✅ | +| Projects | ✅ | ✅ | ✅ | ✅ | +| Project Memberships | ✅ | ✅ | ✅ | ✅ | +| Queries | ✅ | ❌ | ❌ | ❌ | +| Roles | ✅ | ❌ | ❌ | ❌ | +| Search | ✅ | | | | +| Time Entries | ✅ | ✅ | ✅ | ✅ | +| Trackers | ✅ | ❌ | ❌ | ❌ | +| Users | ✅ | ✅ | ✅ | ✅ | +| Versions | ✅ | ✅ | ✅ | ✅ | +| Wiki Pages | ✅ | ✅ | ✅ | ✅ | -Please review the ![wiki](https://github.com/zapadi/redmine-net-api/wiki) pages on how to use **redmine-net-api**. -## Contributing -Contributions are really appreciated! +## 📦 Installation -A good way to get started (flow): +Add the package via NuGet: -1. Fork the redmine-net-api repository. -2. Create a new branch in your current repos from the 'master' branch. -3. 'Check out' the code with *Git*, *GitHub Desktop*, *SourceTree*, *GitKraken*, *etc*. -4. Push commits and create a Pull Request (PR) to redmine-net-api. +```bash +dotnet add package Redmine.Net.Api +``` -## License -[![redmine-net-api](https://img.shields.io/hexpm/l/plug.svg)]() +Or via Package Manager Console: -The API is released under Apache 2 open-source license. You can use it for both personal and commercial purposes, build upon it and modify it. +```powershell +Install-Package Redmine.Net.Api +``` -## Contributors -Thanks to all the people who already contributed! + +## 🧑‍💻 Usage Example + +```csharp +using Redmine.Net.Api; +using Redmine.Net.Api.Types; +using System; +using System.Threading.Tasks; + +class Program +{ + static async Task Main() + { + var options = new RedmineManagerOptions + { + BaseAddress = "https://your-redmine-url/", + ApiKey = "your-api-key" + }; + + var manager = new RedmineManager(options); + + // Retrieve an issue asynchronously + var issue = await manager.GetObjectAsync(12345); + Console.WriteLine($"Issue subject: {issue.Subject}"); + } +} +``` +Explore more usage examples on the [Wiki](https://github.com/zapadi/redmine-net-api/wiki). + + +## 📚 Documentation + +Detailed API reference, guides, and tutorials are available in the [GitHub Wiki](https://github.com/zapadi/redmine-net-api/wiki). + + +## 🙌 Contributing + +We welcome contributions! Here's how you can help: + +1. Fork the repository +2. Create a new branch (`git checkout -b feature/my-feature`) +3. Make your changes and commit (`git commit -m 'Add some feature'`) +4. Push to your fork (`git push origin feature/my-feature`) +5. Open a Pull Request + +See the [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines. + + +## 🤝 Contributors + +Thanks to all contributors! -## Thanks -I would like to thank: +## 📝 License + +This project is licensed under the [Apache License 2.0](LICENSE). + + +## ☕ Support -* JetBrains for my Open Source ReSharper licence, +If you find this project useful, consider ![[buying me a coffee](https://cdn.buymeacoffee.com/buttons/lato-yellow.png)](https://www.buymeacoffee.com/vXCNnz9) to support development. -* AppVeyor for allowing free build CI services for Open Source projects From 9e2de5a754268146bcc910c0196bbf6a76bc17b8 Mon Sep 17 00:00:00 2001 From: Padi Date: Fri, 25 Apr 2025 08:45:27 +0300 Subject: [PATCH 429/549] Update PULL_REQUEST_TEMPLATE.md --- PULL_REQUEST_TEMPLATE.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index e69de29b..83503568 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,9 @@ +We welcome contributions! + +Here's how you can help: + +1. Fork the repository +2. Create a new branch (git checkout -b feature/my-feature) +3. Make your changes and commit (git commit -m 'Add some feature') +4. Push to your fork (git push origin feature/my-feature) +5. Open a Pull Request From fe31c316fa695989e2f96b61b343358cd3c62c8e Mon Sep 17 00:00:00 2001 From: Padi Date: Fri, 25 Apr 2025 08:46:14 +0300 Subject: [PATCH 430/549] Update README.md --- README.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/README.md b/README.md index cc860e6f..e55a5a0f 100755 --- a/README.md +++ b/README.md @@ -93,14 +93,6 @@ Detailed API reference, guides, and tutorials are available in the [GitHub Wiki] ## 🙌 Contributing -We welcome contributions! Here's how you can help: - -1. Fork the repository -2. Create a new branch (`git checkout -b feature/my-feature`) -3. Make your changes and commit (`git commit -m 'Add some feature'`) -4. Push to your fork (`git push origin feature/my-feature`) -5. Open a Pull Request - See the [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines. From c372cd2bdce278f8fc8e41aeed9b584e7f31cab8 Mon Sep 17 00:00:00 2001 From: Padi Date: Fri, 25 Apr 2025 15:30:55 +0300 Subject: [PATCH 431/549] Update README.md --- README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e55a5a0f..8bb1546c 100755 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![Contributors](https://img.shields.io/github/contributors/zapadi/redmine-net-api)](https://github.com/zapadi/redmine-net-api/graphs/contributors) -A modern and flexible .NET client library to interact with [Redmine](https://www.redmine.org)'s REST API. Supports XML and JSON formats with GZipped responses for improved performance. +A modern and flexible .NET client library to interact with [Redmine](https://www.redmine.org)'s REST API. ## 🚀 Features @@ -69,11 +69,9 @@ class Program { static async Task Main() { - var options = new RedmineManagerOptions - { - BaseAddress = "https://your-redmine-url/", - ApiKey = "your-api-key" - }; + var options = new RedmineManagerOptionsBuilder() + .WithHost("https://your-redmine-url/") + .WithApiKeyAuthentication("your-api-key"); var manager = new RedmineManager(options); From 5e4a65b3674e3520a5478b1e52e44cd2c1d5c90b Mon Sep 17 00:00:00 2001 From: Padi Date: Fri, 25 Apr 2025 15:32:52 +0300 Subject: [PATCH 432/549] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8bb1546c..4cf48cb0 100755 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ class Program var manager = new RedmineManager(options); // Retrieve an issue asynchronously - var issue = await manager.GetObjectAsync(12345); + var issue = await manager.GetAsync(12345); Console.WriteLine($"Issue subject: {issue.Subject}"); } } From 18209bbfd76cda9d40428de567fae9941cee37df Mon Sep 17 00:00:00 2001 From: Padi Date: Fri, 25 Apr 2025 16:46:31 +0300 Subject: [PATCH 433/549] Create FUNDING.yml --- .github/FUNDING.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..7b66a62f --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +buy_me_a_coffee: vXCNnz9 From ddacd569b951ca1646b70890cbcc0e5fa4985ffd Mon Sep 17 00:00:00 2001 From: Padi Date: Thu, 8 May 2025 11:24:36 +0300 Subject: [PATCH 434/549] Fix #371 & small refactoring (#384) --- .../Extensions/RedmineManagerExtensions.cs | 2 +- src/redmine-net-api/Net/RequestOptions.cs | 16 ++++ src/redmine-net-api/RedmineManager.cs | 6 +- src/redmine-net-api/RedmineManagerAsync.cs | 77 ++++++++----------- .../Serialization/Xml/XmlRedmineSerializer.cs | 2 +- .../Bugs/RedmineApi-371.cs | 6 +- 6 files changed, 56 insertions(+), 53 deletions(-) diff --git a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs index 35c5e006..18d0b5e6 100644 --- a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs +++ b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs @@ -449,7 +449,7 @@ public static PagedResults Search(this RedmineManager redmineManager, st { var parameters = CreateSearchParameters(q, limit, offset, searchFilter); - var response = redmineManager.GetPaginatedObjects(parameters); + var response = redmineManager.GetPaginated(new RequestOptions() {QueryString = parameters}); return response; } diff --git a/src/redmine-net-api/Net/RequestOptions.cs b/src/redmine-net-api/Net/RequestOptions.cs index 10f7c77e..1c31f7a6 100644 --- a/src/redmine-net-api/Net/RequestOptions.cs +++ b/src/redmine-net-api/Net/RequestOptions.cs @@ -43,4 +43,20 @@ public sealed class RequestOptions /// /// public string UserAgent { get; set; } + + /// + /// + /// + /// + public RequestOptions Clone() + { + return new RequestOptions + { + QueryString = QueryString != null ? new NameValueCollection(QueryString) : null, + ImpersonateUser = ImpersonateUser, + ContentType = ContentType, + Accept = Accept, + UserAgent = UserAgent + }; + } } \ No newline at end of file diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index 9ddbcdbe..6e10e7b4 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -140,7 +140,7 @@ public T Get(string id, RequestOptions requestOptions = null) public List Get(RequestOptions requestOptions = null) where T : class, new() { - var uri = RedmineApiUrls.GetListFragment(); + var uri = RedmineApiUrls.GetListFragment(requestOptions); return GetInternal(uri, requestOptions); } @@ -149,7 +149,7 @@ public List Get(RequestOptions requestOptions = null) public PagedResults GetPaginated(RequestOptions requestOptions = null) where T : class, new() { - var url = RedmineApiUrls.GetListFragment(); + var url = RedmineApiUrls.GetListFragment(requestOptions); return GetPaginatedInternal(url, requestOptions); } @@ -289,7 +289,7 @@ internal List GetInternal(string uri, RequestOptions requestOptions = null internal PagedResults GetPaginatedInternal(string uri = null, RequestOptions requestOptions = null) where T : class, new() { - uri = uri.IsNullOrWhiteSpace() ? RedmineApiUrls.GetListFragment() : uri; + uri = uri.IsNullOrWhiteSpace() ? RedmineApiUrls.GetListFragment(requestOptions) : uri; var response= ApiClient.Get(uri, requestOptions); diff --git a/src/redmine-net-api/RedmineManagerAsync.cs b/src/redmine-net-api/RedmineManagerAsync.cs index f7060e5a..72e2d6a6 100644 --- a/src/redmine-net-api/RedmineManagerAsync.cs +++ b/src/redmine-net-api/RedmineManagerAsync.cs @@ -30,8 +30,6 @@ namespace Redmine.Net.Api; public partial class RedmineManager: IRedmineManagerAsync { - - /// public async Task CountAsync(RequestOptions requestOptions, CancellationToken cancellationToken = default) where T : class, new() @@ -55,7 +53,7 @@ public async Task CountAsync(RequestOptions requestOptions, Cancellation public async Task> GetPagedAsync(RequestOptions requestOptions = null, CancellationToken cancellationToken = default) where T : class, new() { - var url = RedmineApiUrls.GetListFragment(); + var url = RedmineApiUrls.GetListFragment(requestOptions); var response = await ApiClient.GetAsync(url, requestOptions, cancellationToken).ConfigureAwait(false); @@ -71,67 +69,57 @@ public async Task> GetAsync(RequestOptions requestOptions = null, Can var isLimitSet = false; List resultList = null; - requestOptions ??= new RequestOptions(); - - if (requestOptions.QueryString == null) + var baseRequestOptions = requestOptions != null ? requestOptions.Clone() : new RequestOptions(); + if (baseRequestOptions.QueryString == null) { - requestOptions.QueryString = new NameValueCollection(); + baseRequestOptions.QueryString = new NameValueCollection(); } else { - isLimitSet = int.TryParse(requestOptions.QueryString[RedmineKeys.LIMIT], out pageSize); - int.TryParse(requestOptions.QueryString[RedmineKeys.OFFSET], out offset); + isLimitSet = int.TryParse(baseRequestOptions.QueryString[RedmineKeys.LIMIT], out pageSize); + int.TryParse(baseRequestOptions.QueryString[RedmineKeys.OFFSET], out offset); } if (pageSize == default) { - pageSize = PageSize > 0 - ? PageSize + pageSize = _redmineManagerOptions.PageSize > 0 + ? _redmineManagerOptions.PageSize : RedmineConstants.DEFAULT_PAGE_SIZE_VALUE; - requestOptions.QueryString.Set(RedmineKeys.LIMIT, pageSize.ToInvariantString()); + baseRequestOptions.QueryString.Set(RedmineKeys.LIMIT, pageSize.ToInvariantString()); } - - var hasOffset = RedmineManager.TypesWithOffset.ContainsKey(typeof(T)); + + var hasOffset = TypesWithOffset.ContainsKey(typeof(T)); if (hasOffset) { - requestOptions.QueryString.Set(RedmineKeys.OFFSET, offset.ToInvariantString()); + var firstPageOptions = baseRequestOptions.Clone(); + firstPageOptions.QueryString.Set(RedmineKeys.OFFSET, offset.ToInvariantString()); + var firstPage = await GetPagedAsync(firstPageOptions, cancellationToken).ConfigureAwait(false); - var tempResult = await GetPagedAsync(requestOptions, cancellationToken).ConfigureAwait(false); - - var totalCount = isLimitSet ? pageSize : tempResult.TotalItems; - - if (tempResult?.Items != null) + if (firstPage == null || firstPage.Items == null) { - resultList = new List(tempResult.Items); + return null; } - - var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); - - var remainingPages = totalPages - offset / pageSize; + + var totalCount = isLimitSet ? pageSize : firstPage.TotalItems; + resultList = new List(firstPage.Items); + var totalPages = (int)Math.Ceiling((double)totalCount / pageSize); + var remainingPages = totalPages - 1 - (offset / pageSize); if (remainingPages <= 0) { return resultList; } - + using (var semaphore = new SemaphoreSlim(MAX_CONCURRENT_TASKS)) { var pageFetchTasks = new List>>(); - - for (int page = 0; page < remainingPages; page++) + for (int page = 1; page <= remainingPages; page++) { await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - - var innerOffset = (page * pageSize) + offset; - - pageFetchTasks.Add(GetPagedInternalAsync(semaphore, new RequestOptions() - { - QueryString = new NameValueCollection() - { - {RedmineKeys.OFFSET, innerOffset.ToInvariantString()}, - {RedmineKeys.LIMIT, pageSize.ToInvariantString()} - } - }, cancellationToken)); + var pageOffset = (page * pageSize) + offset; + var pageRequestOptions = baseRequestOptions.Clone(); + pageRequestOptions.QueryString.Set(RedmineKeys.OFFSET, pageOffset.ToInvariantString()); + pageFetchTasks.Add(GetPagedInternalAsync(semaphore, pageRequestOptions, cancellationToken)); } var pageResults = await @@ -141,30 +129,27 @@ public async Task> GetAsync(RequestOptions requestOptions = null, Can TaskExtensions.WhenAll(pageFetchTasks) #endif .ConfigureAwait(false); - + foreach (var pageResult in pageResults) { if (pageResult?.Items == null) { continue; } - - resultList ??= new List(); - resultList.AddRange(pageResult.Items); } } } else { - var result = await GetPagedAsync(requestOptions, cancellationToken: cancellationToken) + var result = await GetPagedAsync(baseRequestOptions, cancellationToken: cancellationToken) .ConfigureAwait(false); if (result?.Items != null) { return new List(result.Items); } } - + return resultList; } @@ -245,7 +230,7 @@ private async Task> GetPagedInternalAsync(SemaphoreSlim semap { try { - var url = RedmineApiUrls.GetListFragment(); + var url = RedmineApiUrls.GetListFragment(requestOptions); var response = await ApiClient.GetAsync(url, requestOptions, cancellationToken).ConfigureAwait(false); diff --git a/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs b/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs index f51e38ad..253543c7 100644 --- a/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs +++ b/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs @@ -126,7 +126,7 @@ public string Serialize(T entity) where T : class var limit = xmlReader.ReadAttributeAsInt(RedmineKeys.LIMIT); var result = xmlReader.ReadElementContentAsCollection(); - if (totalItems == 0 && result.Count > 0) + if (totalItems == 0 && result?.Count > 0) { totalItems = result.Count; } diff --git a/tests/redmine-net-api.Tests/Bugs/RedmineApi-371.cs b/tests/redmine-net-api.Tests/Bugs/RedmineApi-371.cs index 08b3049d..d83e4db1 100644 --- a/tests/redmine-net-api.Tests/Bugs/RedmineApi-371.cs +++ b/tests/redmine-net-api.Tests/Bugs/RedmineApi-371.cs @@ -1,5 +1,6 @@ using System.Collections.Specialized; using Padi.DotNet.RedmineAPI.Tests.Tests; +using Redmine.Net.Api; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Net; using Redmine.Net.Api.Types; @@ -19,12 +20,13 @@ public RedmineApi371(RedmineApiUrlsFixture fixture) [Fact] public void Should_Return_IssueCategories_For_Project_Url() { + var projectIdAsString = 1.ToInvariantString(); var result = _fixture.Sut.GetListFragment( new RequestOptions { - QueryString = new NameValueCollection{ { "project_id", 1.ToInvariantString() } } + QueryString = new NameValueCollection{ { RedmineKeys.PROJECT_ID, projectIdAsString } } }); - Assert.Equal($"projects/1/issue_categories.{_fixture.Format}", result); + Assert.Equal($"projects/{projectIdAsString}/issue_categories.{_fixture.Format}", result); } } \ No newline at end of file From f605b2598162f43bd600d167cf46ea737b3048cf Mon Sep 17 00:00:00 2001 From: Padi Date: Thu, 8 May 2025 11:05:29 +0300 Subject: [PATCH 435/549] Fix IssueStatus xml deserialization --- src/redmine-net-api/Types/Issue.cs | 6 +- src/redmine-net-api/Types/IssueStatus.cs | 64 +++++++++- .../Clone/IssueCloneTests.cs | 2 +- .../Equality/IssueEqualityTests.cs | 2 +- .../Serialization/Json/IssuesTests.cs | 99 ++++++++++++++++ .../Serialization/Xml/IssueTests.cs | 110 +++++++++++++++++- 6 files changed, 273 insertions(+), 10 deletions(-) create mode 100644 tests/redmine-net-api.Tests/Serialization/Json/IssuesTests.cs diff --git a/src/redmine-net-api/Types/Issue.cs b/src/redmine-net-api/Types/Issue.cs index 81cecc21..1e17007d 100644 --- a/src/redmine-net-api/Types/Issue.cs +++ b/src/redmine-net-api/Types/Issue.cs @@ -59,7 +59,7 @@ public sealed class Issue : /// Gets or sets the status.Possible values: open, closed, * to get open and closed issues, status id /// /// The status. - public IdentifiableName Status { get; set; } + public IssueStatus Status { get; set; } /// /// Gets or sets the priority. @@ -307,7 +307,7 @@ public override void ReadXml(XmlReader reader) case RedmineKeys.RELATIONS: Relations = reader.ReadElementContentAsCollection(); break; case RedmineKeys.SPENT_HOURS: SpentHours = reader.ReadElementContentAsNullableFloat(); break; case RedmineKeys.START_DATE: StartDate = reader.ReadElementContentAsNullableDateTime(); break; - case RedmineKeys.STATUS: Status = new IdentifiableName(reader); break; + case RedmineKeys.STATUS: Status = new IssueStatus(reader); break; case RedmineKeys.SUBJECT: Subject = reader.ReadElementContentAsString(); break; case RedmineKeys.TOTAL_ESTIMATED_HOURS: TotalEstimatedHours = reader.ReadElementContentAsNullableFloat(); break; case RedmineKeys.TOTAL_SPENT_HOURS: TotalSpentHours = reader.ReadElementContentAsNullableFloat(); break; @@ -406,7 +406,7 @@ public override void ReadJson(JsonReader reader) case RedmineKeys.RELATIONS: Relations = reader.ReadAsCollection(); break; case RedmineKeys.SPENT_HOURS: SpentHours = (float?)reader.ReadAsDouble(); break; case RedmineKeys.START_DATE: StartDate = reader.ReadAsDateTime(); break; - case RedmineKeys.STATUS: Status = new IdentifiableName(reader); break; + case RedmineKeys.STATUS: Status = new IssueStatus(reader); break; case RedmineKeys.SUBJECT: Subject = reader.ReadAsString(); break; case RedmineKeys.TOTAL_ESTIMATED_HOURS: TotalEstimatedHours = (float?)reader.ReadAsDouble(); break; case RedmineKeys.TOTAL_SPENT_HOURS: TotalSpentHours = (float?)reader.ReadAsDouble(); break; diff --git a/src/redmine-net-api/Types/IssueStatus.cs b/src/redmine-net-api/Types/IssueStatus.cs index 1c9a12bd..632f48ef 100644 --- a/src/redmine-net-api/Types/IssueStatus.cs +++ b/src/redmine-net-api/Types/IssueStatus.cs @@ -30,8 +30,44 @@ namespace Redmine.Net.Api.Types /// [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.ISSUE_STATUS)] - public sealed class IssueStatus : IdentifiableName, IEquatable + public sealed class IssueStatus : IdentifiableName, IEquatable, ICloneable { + public IssueStatus() + { + + } + + /// + /// + /// + internal IssueStatus(int id, string name, bool isDefault = false, bool isClosed = false) + { + Id = id; + Name = name; + IsClosed = isClosed; + IsDefault = isDefault; + } + + internal IssueStatus(XmlReader reader) + { + Initialize(reader); + } + + internal IssueStatus(JsonReader reader) + { + Initialize(reader); + } + + private void Initialize(XmlReader reader) + { + ReadXml(reader); + } + + private void Initialize(JsonReader reader) + { + ReadJson(reader); + } + #region Properties /// /// Gets or sets a value indicating whether IssueStatus is default. @@ -55,6 +91,16 @@ public sealed class IssueStatus : IdentifiableName, IEquatable /// public override void ReadXml(XmlReader reader) { + if (reader.HasAttributes && reader.Name == "status") + { + Id = reader.ReadAttributeAsInt(RedmineKeys.ID); + IsClosed = reader.ReadAttributeAsBoolean(RedmineKeys.IS_CLOSED); + IsDefault = reader.ReadAttributeAsBoolean(RedmineKeys.IS_DEFAULT); + Name = reader.GetAttribute(RedmineKeys.NAME); + reader.Read(); + return; + } + reader.Read(); while (!reader.EOF) { @@ -121,6 +167,22 @@ public bool Equals(IssueStatus other) && IsDefault == other.IsDefault; } + /// + /// + /// + /// + /// + public new IssueStatus Clone(bool resetId) + { + return new IssueStatus + { + Id = Id, + Name = Name, + IsClosed = IsClosed, + IsDefault = IsDefault + }; + } + /// /// /// diff --git a/tests/redmine-net-api.Tests/Clone/IssueCloneTests.cs b/tests/redmine-net-api.Tests/Clone/IssueCloneTests.cs index 3aac1e6c..0d0b82fa 100644 --- a/tests/redmine-net-api.Tests/Clone/IssueCloneTests.cs +++ b/tests/redmine-net-api.Tests/Clone/IssueCloneTests.cs @@ -94,7 +94,7 @@ private static Issue CreateSampleIssue() Id = 1, Project = new IdentifiableName(100, "Test Project"), Tracker = new IdentifiableName(200, "Bug"), - Status = new IdentifiableName(300, "New"), + Status = new IssueStatus(300, "New"), Priority = new IdentifiableName(400, "Normal"), Author = new IdentifiableName(500, "John Doe"), Subject = "Test Issue", diff --git a/tests/redmine-net-api.Tests/Equality/IssueEqualityTests.cs b/tests/redmine-net-api.Tests/Equality/IssueEqualityTests.cs index 90dcc08a..5060aece 100644 --- a/tests/redmine-net-api.Tests/Equality/IssueEqualityTests.cs +++ b/tests/redmine-net-api.Tests/Equality/IssueEqualityTests.cs @@ -88,7 +88,7 @@ private static Issue CreateSampleIssue() Id = 1, Project = new IdentifiableName { Id = 100, Name = "Test Project" }, Tracker = new IdentifiableName { Id = 1, Name = "Bug" }, - Status = new IdentifiableName { Id = 1, Name = "New" }, + Status = new IssueStatus { Id = 1, Name = "New" }, Priority = new IdentifiableName { Id = 1, Name = "Normal" }, Author = new IdentifiableName { Id = 1, Name = "John Doe" }, Subject = "Test Issue", diff --git a/tests/redmine-net-api.Tests/Serialization/Json/IssuesTests.cs b/tests/redmine-net-api.Tests/Serialization/Json/IssuesTests.cs new file mode 100644 index 00000000..e1f9965c --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Json/IssuesTests.cs @@ -0,0 +1,99 @@ +using System; +using System.Linq; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Json; + +[Collection(Constants.JsonRedmineSerializerCollection)] +public class IssuesTests(JsonSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Issue_With_Watchers() + { + const string input = """ + { + "issue": { + "id": 5, + "project": { + "id": 1, + "name": "Project-Test" + }, + "tracker": { + "id": 1, + "name": "Bug" + }, + "status": { + "id": 1, + "name": "New", + "is_closed": true + }, + "priority": { + "id": 2, + "name": "Normal" + }, + "author": { + "id": 90, + "name": "Admin User" + }, + "fixed_version": { + "id": 2, + "name": "version2" + }, + "subject": "#380", + "description": "", + "start_date": "2025-04-28", + "due_date": null, + "done_ratio": 0, + "is_private": false, + "estimated_hours": null, + "total_estimated_hours": null, + "spent_hours": 0.0, + "total_spent_hours": 0.0, + "created_on": "2025-04-28T17:58:42Z", + "updated_on": "2025-04-28T17:58:42Z", + "closed_on": null, + "watchers": [ + { + "id": 91, + "name": "Normal User" + }, + { + "id": 90, + "name": "Admin User" + } + ] + } + } + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + Assert.Equal(5, output.Id); + Assert.Equal("Project-Test", output.Project.Name); + Assert.Equal(1, output.Project.Id); + Assert.Equal("Bug", output.Tracker.Name); + Assert.Equal(1, output.Tracker.Id); + Assert.Equal("New", output.Status.Name); + Assert.Equal(1, output.Status.Id); + Assert.True(output.Status.IsClosed); + Assert.False(output.Status.IsDefault); + Assert.Equal(2, output.FixedVersion.Id); + Assert.Equal("version2", output.FixedVersion.Name); + Assert.Equal(new DateTime(2025, 4, 28), output.StartDate); + Assert.Null(output.DueDate); + Assert.Equal(0, output.DoneRatio); + Assert.Null(output.EstimatedHours); + Assert.Null(output.TotalEstimatedHours); + + var watchers = output.Watchers.ToList(); + Assert.Equal(2, watchers.Count); + + Assert.Equal(91, watchers[0].Id); + Assert.Equal("Normal User", watchers[0].Name); + Assert.Equal(90, watchers[1].Id); + Assert.Equal("Admin User", watchers[1].Name); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/IssueTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/IssueTests.cs index e326f8db..6791587c 100644 --- a/tests/redmine-net-api.Tests/Serialization/Xml/IssueTests.cs +++ b/tests/redmine-net-api.Tests/Serialization/Xml/IssueTests.cs @@ -117,10 +117,8 @@ This is not to be confused with another useful proposed feature that 4325 - - - - + + 1.0.1 @@ -134,6 +132,48 @@ This is not to be confused with another useful proposed feature that """; var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(2, output.TotalItems); + + var issues = output.Items.ToList(); + Assert.Equal(4326, issues[0].Id); + Assert.Equal("Redmine", issues[0].Project.Name); + Assert.Equal(1, issues[0].Project.Id); + Assert.Equal("Feature", issues[0].Tracker.Name); + Assert.Equal(2, issues[0].Tracker.Id); + Assert.Equal("New", issues[0].Status.Name); + Assert.Equal(1, issues[0].Status.Id); + Assert.Equal("Normal", issues[0].Priority.Name); + Assert.Equal(4, issues[0].Priority.Id); + Assert.Equal("John Smith", issues[0].Author.Name); + Assert.Equal(10106, issues[0].Author.Id); + Assert.Equal("Email notifications", issues[0].Category.Name); + Assert.Equal(9, issues[0].Category.Id); + Assert.Contains("Aggregate Multiple Issue Changes for Email Notifications", issues[0].Subject); + Assert.Contains("This is not to be confused with another useful proposed feature", issues[0].Description); + Assert.Equal(new DateTime(2009, 12, 3), issues[0].StartDate); + Assert.Null(issues[0].DueDate); + Assert.Equal(0, issues[0].DoneRatio); + Assert.Null(issues[0].EstimatedHours); + + Assert.NotNull(issues[0].CustomFields); + var issueCustomFields = issues[0].CustomFields.ToList(); + Assert.Equal(4, issueCustomFields.Count); + + Assert.Equal(2,issueCustomFields[0].Id); + Assert.Equal("Duplicate",issueCustomFields[0].Values[0].Info); + Assert.False(issueCustomFields[0].Multiple); + Assert.Equal("Resolution",issueCustomFields[0].Name); + + Assert.NotNull(issues[1].CustomFields); + issueCustomFields = issues[1].CustomFields.ToList(); + Assert.Equal(2, issueCustomFields.Count); + + Assert.Equal(1,issueCustomFields[0].Id); + Assert.Equal("1.0.1",issueCustomFields[0].Values[0].Info); + Assert.False(issueCustomFields[0].Multiple); + Assert.Equal("Affected version",issueCustomFields[0].Name); } [Fact] @@ -200,5 +240,67 @@ public void Should_Deserialize_Issue_With_Journals() Assert.Equal("8", details[0].NewValue); } + + [Fact] + public void Should_Deserialize_Issue_With_Watchers() + { + const string input = """ + + + 5 + + + + + + + #380 + + 2025-04-28 + + 0 + false + + + 0.0 + 0.0 + 2025-04-28T17:58:42Z + 2025-04-28T17:58:42Z + + + + + + + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + Assert.Equal(5, output.Id); + Assert.Equal("Project-Test", output.Project.Name); + Assert.Equal(1, output.Project.Id); + Assert.Equal("Bug", output.Tracker.Name); + Assert.Equal(1, output.Tracker.Id); + Assert.Equal("New", output.Status.Name); + Assert.Equal(1, output.Status.Id); + Assert.True(output.Status.IsClosed); + Assert.False(output.Status.IsDefault); + Assert.Equal(2, output.FixedVersion.Id); + Assert.Equal("version2", output.FixedVersion.Name); + Assert.Equal(new DateTime(2025, 4, 28), output.StartDate); + Assert.Null(output.DueDate); + Assert.Equal(0, output.DoneRatio); + Assert.Null(output.EstimatedHours); + Assert.Null(output.TotalEstimatedHours); + + var watchers = output.Watchers.ToList(); + Assert.Equal(2, watchers.Count); + + Assert.Equal(91, watchers[0].Id); + Assert.Equal("Normal User", watchers[0].Name); + Assert.Equal(90, watchers[1].Id); + Assert.Equal("Admin User", watchers[1].Name); + } } From a246b6807fe4994d4855a097e1a3499f4c0d1555 Mon Sep 17 00:00:00 2001 From: Padi Date: Thu, 8 May 2025 11:35:07 +0300 Subject: [PATCH 436/549] [Watcher] Implement IEquatable --- src/redmine-net-api/Types/Watcher.cs | 76 ++++++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 4 deletions(-) diff --git a/src/redmine-net-api/Types/Watcher.cs b/src/redmine-net-api/Types/Watcher.cs index 7c0dc9d1..b990a464 100644 --- a/src/redmine-net-api/Types/Watcher.cs +++ b/src/redmine-net-api/Types/Watcher.cs @@ -1,4 +1,4 @@ -/* +/* Copyright 2011 - 2025 Adrian Popescu Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,6 +18,7 @@ limitations under the License. using System.Diagnostics; using System.Globalization; using System.Xml.Serialization; +using Redmine.Net.Api.Internals; namespace Redmine.Net.Api.Types { @@ -26,7 +27,8 @@ namespace Redmine.Net.Api.Types /// [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] [XmlRoot(RedmineKeys.USER)] - public sealed class Watcher : Identifiable + public sealed class Watcher : IdentifiableName + ,IEquatable ,ICloneable ,IValue { @@ -47,16 +49,82 @@ public sealed class Watcher : Identifiable { if (resetId) { - return new Watcher(); + return new Watcher() + { + Name = Name + }; } return new Watcher { - Id = Id + Id = Id, + Name = Name }; } #endregion + #region Implementation of IEquatable + /// + /// + /// + /// + /// + public bool Equals(Watcher other) + { + if (other == null) return false; + return Id == other.Id && string.Equals(Name, other.Name, StringComparison.Ordinal); + } + + /// + /// + /// + /// + /// + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals(obj as Watcher); + } + + /// + /// + /// + /// + public override int GetHashCode() + { + unchecked + { + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Name, hashCode); + return hashCode; + } + } + + /// + /// + /// + /// + /// + /// + public static bool operator ==(Watcher left, Watcher right) + { + return Equals(left, right); + } + + /// + /// + /// + /// + /// + /// + public static bool operator !=(Watcher left, Watcher right) + { + return !Equals(left, right); + } + #endregion + /// /// /// From 85bca83ce699af6614390344d8fbd21260658890 Mon Sep 17 00:00:00 2001 From: Padi Date: Thu, 8 May 2025 11:40:52 +0300 Subject: [PATCH 437/549] DebuggerDisplay improvements --- src/redmine-net-api/Types/Attachment.cs | 11 +------- src/redmine-net-api/Types/ChangeSet.cs | 7 +---- src/redmine-net-api/Types/CustomField.cs | 18 +------------ .../Types/CustomFieldPossibleValue.cs | 2 +- src/redmine-net-api/Types/CustomFieldRole.cs | 3 ++- src/redmine-net-api/Types/CustomFieldValue.cs | 2 +- src/redmine-net-api/Types/Detail.cs | 2 +- src/redmine-net-api/Types/DocumentCategory.cs | 2 +- src/redmine-net-api/Types/Error.cs | 2 +- src/redmine-net-api/Types/File.cs | 2 +- src/redmine-net-api/Types/Group.cs | 2 +- src/redmine-net-api/Types/GroupUser.cs | 3 ++- src/redmine-net-api/Types/Identifiable.cs | 3 ++- src/redmine-net-api/Types/IdentifiableName.cs | 2 +- src/redmine-net-api/Types/Issue.cs | 26 +------------------ .../Types/IssueAllowedStatus.cs | 2 +- src/redmine-net-api/Types/IssueCategory.cs | 2 +- src/redmine-net-api/Types/IssueChild.cs | 2 +- src/redmine-net-api/Types/IssueCustomField.cs | 2 +- src/redmine-net-api/Types/IssuePriority.cs | 2 +- src/redmine-net-api/Types/IssueRelation.cs | 6 +---- src/redmine-net-api/Types/IssueStatus.cs | 2 +- src/redmine-net-api/Types/Journal.cs | 2 +- src/redmine-net-api/Types/Membership.cs | 2 +- src/redmine-net-api/Types/MembershipRole.cs | 3 +-- src/redmine-net-api/Types/MyAccount.cs | 12 +-------- .../Types/MyAccountCustomField.cs | 2 +- src/redmine-net-api/Types/News.cs | 2 +- src/redmine-net-api/Types/NewsComment.cs | 6 ++--- src/redmine-net-api/Types/Permission.cs | 2 +- src/redmine-net-api/Types/Project.cs | 19 +------------- .../Types/ProjectEnabledModule.cs | 2 +- .../Types/ProjectIssueCategory.cs | 3 ++- .../Types/ProjectMembership.cs | 2 +- .../Types/ProjectTimeEntryActivity.cs | 3 ++- src/redmine-net-api/Types/ProjectTracker.cs | 3 ++- src/redmine-net-api/Types/Query.cs | 2 +- src/redmine-net-api/Types/Role.cs | 2 +- src/redmine-net-api/Types/Search.cs | 2 +- src/redmine-net-api/Types/TimeEntry.cs | 11 +------- .../Types/TimeEntryActivity.cs | 2 +- src/redmine-net-api/Types/Tracker.cs | 2 +- src/redmine-net-api/Types/TrackerCoreField.cs | 2 +- .../Types/TrackerCustomField.cs | 2 +- src/redmine-net-api/Types/Upload.cs | 2 +- src/redmine-net-api/Types/User.cs | 22 +--------------- src/redmine-net-api/Types/UserGroup.cs | 3 ++- src/redmine-net-api/Types/Version.cs | 13 +--------- src/redmine-net-api/Types/WikiPage.cs | 8 +----- 49 files changed, 57 insertions(+), 184 deletions(-) diff --git a/src/redmine-net-api/Types/Attachment.cs b/src/redmine-net-api/Types/Attachment.cs index 0fa66eb9..49b6476c 100644 --- a/src/redmine-net-api/Types/Attachment.cs +++ b/src/redmine-net-api/Types/Attachment.cs @@ -256,16 +256,7 @@ public override int GetHashCode() } #endregion - private string DebuggerDisplay => - $@"[{nameof(Attachment)}: -{ToString()}, -FileName={FileName}, -FileSize={FileSize.ToString(CultureInfo.InvariantCulture)}, -ContentType={ContentType}, -Description={Description}, -ContentUrl={ContentUrl}, -Author={Author}, -CreatedOn={CreatedOn?.ToString("u", CultureInfo.InvariantCulture)}]"; + private string DebuggerDisplay =>$"[Attachment: Id={Id.ToInvariantString()}, FileName={FileName}, FileSize={FileSize.ToInvariantString()}]"; /// /// diff --git a/src/redmine-net-api/Types/ChangeSet.cs b/src/redmine-net-api/Types/ChangeSet.cs index c7fddae5..be873273 100644 --- a/src/redmine-net-api/Types/ChangeSet.cs +++ b/src/redmine-net-api/Types/ChangeSet.cs @@ -232,12 +232,7 @@ public override int GetHashCode() /// /// /// - private string DebuggerDisplay => - $@"[{nameof(ChangeSet)}: -Revision={Revision.ToString(CultureInfo.InvariantCulture)}, -User='{User}', -CommittedOn={CommittedOn?.ToString("u", CultureInfo.InvariantCulture)}, -Comments='{Comments}']"; + private string DebuggerDisplay => $" ChangeSet: Revision={Revision}, CommittedOn={CommittedOn?.ToString("u", CultureInfo.InvariantCulture)}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/CustomField.cs b/src/redmine-net-api/Types/CustomField.cs index b6761b32..c99a1490 100644 --- a/src/redmine-net-api/Types/CustomField.cs +++ b/src/redmine-net-api/Types/CustomField.cs @@ -290,22 +290,6 @@ public override int GetHashCode() } #endregion - private string DebuggerDisplay => - $@"[{nameof(CustomField)}: {ToString()} -, CustomizedType={CustomizedType} -, Description={Description} -, FieldFormat={FieldFormat} -, Regexp={Regexp} -, MinLength={MinLength?.ToString(CultureInfo.InvariantCulture)} -, MaxLength={MaxLength?.ToString(CultureInfo.InvariantCulture)} -, IsRequired={IsRequired.ToString(CultureInfo.InvariantCulture)} -, IsFilter={IsFilter.ToString(CultureInfo.InvariantCulture)} -, Searchable={Searchable.ToString(CultureInfo.InvariantCulture)} -, Multiple={Multiple.ToString(CultureInfo.InvariantCulture)} -, DefaultValue={DefaultValue} -, Visible={Visible.ToString(CultureInfo.InvariantCulture)} -, PossibleValues={PossibleValues.Dump()} -, Trackers={Trackers.Dump()} -, Roles={Roles.Dump()}]"; + private string DebuggerDisplay => $"[CustomField: Id={Id.ToInvariantString()}, Name={Name}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/CustomFieldPossibleValue.cs b/src/redmine-net-api/Types/CustomFieldPossibleValue.cs index 6d972c4a..4a8854f7 100644 --- a/src/redmine-net-api/Types/CustomFieldPossibleValue.cs +++ b/src/redmine-net-api/Types/CustomFieldPossibleValue.cs @@ -192,7 +192,7 @@ public override int GetHashCode() /// ///
/// - private string DebuggerDisplay => $"[{nameof(CustomFieldPossibleValue)}: Label:{Label}, Value:{Value}]"; + private string DebuggerDisplay => $"[CustomFieldPossibleValue: Label:{Label}, Value:{Value}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/CustomFieldRole.cs b/src/redmine-net-api/Types/CustomFieldRole.cs index d870a5f9..bfffbc71 100644 --- a/src/redmine-net-api/Types/CustomFieldRole.cs +++ b/src/redmine-net-api/Types/CustomFieldRole.cs @@ -16,6 +16,7 @@ limitations under the License. using System.Diagnostics; using System.Xml.Serialization; +using Redmine.Net.Api.Extensions; namespace Redmine.Net.Api.Types { @@ -41,7 +42,7 @@ internal CustomFieldRole(int id, string name) /// ///
/// - private string DebuggerDisplay => $"[{nameof(CustomFieldRole)}: {ToString()}]"; + private string DebuggerDisplay => $"[CustomFieldRole: Id={Id.ToInvariantString()}, Name={Name}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/CustomFieldValue.cs b/src/redmine-net-api/Types/CustomFieldValue.cs index de3f4fdd..32a70959 100644 --- a/src/redmine-net-api/Types/CustomFieldValue.cs +++ b/src/redmine-net-api/Types/CustomFieldValue.cs @@ -217,6 +217,6 @@ public CustomFieldValue Clone(bool resetId) /// ///
/// - private string DebuggerDisplay => $"[{nameof(CustomFieldValue)}: {Info}]"; + private string DebuggerDisplay => $"[CustomFieldValue: {Info}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Detail.cs b/src/redmine-net-api/Types/Detail.cs index 73468a6f..b4738792 100644 --- a/src/redmine-net-api/Types/Detail.cs +++ b/src/redmine-net-api/Types/Detail.cs @@ -251,7 +251,7 @@ public override int GetHashCode() /// ///
/// - private string DebuggerDisplay => $"[{nameof(Detail)}: Property={Property}, Name={Name}, OldValue={OldValue}, NewValue={NewValue}]"; + private string DebuggerDisplay => $"[Detail: Property={Property}, Name={Name}, OldValue={OldValue}, NewValue={NewValue}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/DocumentCategory.cs b/src/redmine-net-api/Types/DocumentCategory.cs index 745f695c..4e01bc96 100644 --- a/src/redmine-net-api/Types/DocumentCategory.cs +++ b/src/redmine-net-api/Types/DocumentCategory.cs @@ -193,7 +193,7 @@ public override int GetHashCode() /// ///
/// - private string DebuggerDisplay => $"[{nameof(DocumentCategory)}, IsDefault={IsDefault.ToString(CultureInfo.InvariantCulture)}, IsActive={IsActive.ToString(CultureInfo.InvariantCulture)}]"; + private string DebuggerDisplay => $"[DocumentCategory: Id={Id.ToInvariantString()}, Name={Name}, IsDefault={IsDefault.ToInvariantString()}, IsActive={IsActive.ToInvariantString()}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Error.cs b/src/redmine-net-api/Types/Error.cs index 06128f72..f5285573 100644 --- a/src/redmine-net-api/Types/Error.cs +++ b/src/redmine-net-api/Types/Error.cs @@ -176,7 +176,7 @@ public override int GetHashCode() /// ///
/// - private string DebuggerDisplay => $"[{nameof(Error)}: {Info}]"; + private string DebuggerDisplay => $"[Error: {Info}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/File.cs b/src/redmine-net-api/Types/File.cs index 874044b8..f4f09e30 100644 --- a/src/redmine-net-api/Types/File.cs +++ b/src/redmine-net-api/Types/File.cs @@ -283,7 +283,7 @@ public override int GetHashCode() /// ///
/// - private string DebuggerDisplay => $"[{nameof(File)}: {ToString()}, Name={Filename}]"; + private string DebuggerDisplay => $"[File: {Id.ToInvariantString()}, Name={Filename}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Group.cs b/src/redmine-net-api/Types/Group.cs index ef097986..27bc3773 100644 --- a/src/redmine-net-api/Types/Group.cs +++ b/src/redmine-net-api/Types/Group.cs @@ -227,7 +227,7 @@ public override int GetHashCode() /// ///
/// - private string DebuggerDisplay => $"[{nameof(Group)}: {ToString()}, Users={Users.Dump()}, CustomFields={CustomFields.Dump()}, Memberships={Memberships.Dump()}]"; + private string DebuggerDisplay => $"[Group: Id={Id.ToInvariantString()}, Name={Name}]"; /// diff --git a/src/redmine-net-api/Types/GroupUser.cs b/src/redmine-net-api/Types/GroupUser.cs index 6b622f51..598e9462 100644 --- a/src/redmine-net-api/Types/GroupUser.cs +++ b/src/redmine-net-api/Types/GroupUser.cs @@ -17,6 +17,7 @@ limitations under the License. using System.Diagnostics; using System.Globalization; using System.Xml.Serialization; +using Redmine.Net.Api.Extensions; namespace Redmine.Net.Api.Types { @@ -38,7 +39,7 @@ public sealed class GroupUser : IdentifiableName, IValue /// /// /// - private string DebuggerDisplay => $"[{nameof(GroupUser)}: {ToString()}]"; + private string DebuggerDisplay => $"[GroupUser: Id={Id.ToInvariantString()}, Name={Name}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Identifiable.cs b/src/redmine-net-api/Types/Identifiable.cs index 0da0cb0d..02307d1d 100644 --- a/src/redmine-net-api/Types/Identifiable.cs +++ b/src/redmine-net-api/Types/Identifiable.cs @@ -21,6 +21,7 @@ limitations under the License. using System.Xml.Schema; using System.Xml.Serialization; using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; using Redmine.Net.Api.Serialization; using NotImplementedException = System.NotImplementedException; @@ -157,7 +158,7 @@ public override int GetHashCode() /// ///
/// - private string DebuggerDisplay => $"Id={Id.ToString(CultureInfo.InvariantCulture)}"; + private string DebuggerDisplay => $"Id={Id.ToInvariantString()}"; /// /// diff --git a/src/redmine-net-api/Types/IdentifiableName.cs b/src/redmine-net-api/Types/IdentifiableName.cs index f17348b0..2a985283 100644 --- a/src/redmine-net-api/Types/IdentifiableName.cs +++ b/src/redmine-net-api/Types/IdentifiableName.cs @@ -224,7 +224,7 @@ public override int GetHashCode() /// /// /// - private string DebuggerDisplay => $"[{nameof(IdentifiableName)}: {base.ToString()}, Name={Name}]"; + private string DebuggerDisplay => $"[IdentifiableName: Id={Id.ToInvariantString()}, Name={Name}]"; /// /// diff --git a/src/redmine-net-api/Types/Issue.cs b/src/redmine-net-api/Types/Issue.cs index 1e17007d..14f55a6f 100644 --- a/src/redmine-net-api/Types/Issue.cs +++ b/src/redmine-net-api/Types/Issue.cs @@ -650,30 +650,6 @@ public IdentifiableName AsParent() /// /// /// - private string DebuggerDisplay => - $@"[{nameof(Issue)}: {ToString()}, Project={Project}, Tracker={Tracker}, Status={Status}, -Priority={Priority}, Author={Author}, Category={Category}, Subject={Subject}, Description={Description}, -StartDate={StartDate?.ToString("u", CultureInfo.InvariantCulture)}, -DueDate={DueDate?.ToString("u", CultureInfo.InvariantCulture)}, -DoneRatio={DoneRatio?.ToString("F", CultureInfo.InvariantCulture)}, -PrivateNotes={PrivateNotes.ToString(CultureInfo.InvariantCulture)}, -EstimatedHours={EstimatedHours?.ToString("F", CultureInfo.InvariantCulture)}, -SpentHours={SpentHours?.ToString("F", CultureInfo.InvariantCulture)}, -CustomFields={CustomFields.Dump()}, -CreatedOn={CreatedOn?.ToString("u", CultureInfo.InvariantCulture)}, -UpdatedOn={UpdatedOn?.ToString("u", CultureInfo.InvariantCulture)}, -ClosedOn={ClosedOn?.ToString("u", CultureInfo.InvariantCulture)}, -Notes={Notes}, -AssignedTo={AssignedTo}, -ParentIssue={ParentIssue}, -FixedVersion={FixedVersion}, -IsPrivate={IsPrivate.ToString(CultureInfo.InvariantCulture)}, -Journals={Journals.Dump()}, -ChangeSets={ChangeSets.Dump()}, -Attachments={Attachments.Dump()}, -Relations={Relations.Dump()}, -Children={Children.Dump()}, -Uploads={Uploads.Dump()}, -Watchers={Watchers.Dump()}]"; + private string DebuggerDisplay => $"[Issue:Id={Id.ToInvariantString()}, Status={Status?.Name}, Priority={Priority?.Name}, DoneRatio={DoneRatio?.ToString("F", CultureInfo.InvariantCulture)},IsPrivate={IsPrivate.ToInvariantString()}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/IssueAllowedStatus.cs b/src/redmine-net-api/Types/IssueAllowedStatus.cs index 8cc7af61..9a47811a 100644 --- a/src/redmine-net-api/Types/IssueAllowedStatus.cs +++ b/src/redmine-net-api/Types/IssueAllowedStatus.cs @@ -129,6 +129,6 @@ public override int GetHashCode() return !Equals(left, right); } - private string DebuggerDisplay => $"[{nameof(IssueAllowedStatus)}: {ToString()}]"; + private string DebuggerDisplay => $"[IssueAllowedStatus: Id={Id.ToInvariantString()}, Name={Name}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/IssueCategory.cs b/src/redmine-net-api/Types/IssueCategory.cs index 2fc6bfe0..39c5781c 100644 --- a/src/redmine-net-api/Types/IssueCategory.cs +++ b/src/redmine-net-api/Types/IssueCategory.cs @@ -209,7 +209,7 @@ public override int GetHashCode() /// ///
/// - private string DebuggerDisplay => $"[{nameof(IssueCategory)}: {ToString()}, Project={Project}, AssignTo={AssignTo}, Name={Name}]"; + private string DebuggerDisplay => $"[IssueCategory: Id={Id.ToInvariantString()}, Name={Name}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/IssueChild.cs b/src/redmine-net-api/Types/IssueChild.cs index 7a9aeeb5..b6d3fcf1 100644 --- a/src/redmine-net-api/Types/IssueChild.cs +++ b/src/redmine-net-api/Types/IssueChild.cs @@ -191,6 +191,6 @@ public override int GetHashCode() /// ///
/// - private string DebuggerDisplay => $"[{nameof(IssueChild)}: {ToString()}, Tracker={Tracker}, Subject={Subject}]"; + private string DebuggerDisplay => $"[IssueChild: Id={Id.ToInvariantString()}]"; } } diff --git a/src/redmine-net-api/Types/IssueCustomField.cs b/src/redmine-net-api/Types/IssueCustomField.cs index 5a77e3e7..da8935c9 100644 --- a/src/redmine-net-api/Types/IssueCustomField.cs +++ b/src/redmine-net-api/Types/IssueCustomField.cs @@ -325,6 +325,6 @@ public static string GetValue(object item) /// ///
/// - private string DebuggerDisplay => $"[{nameof(IssueCustomField)}: {ToString()} Values={Values.Dump()}, Multiple={Multiple.ToString(CultureInfo.InvariantCulture)}]"; + private string DebuggerDisplay => $"[IssueCustomField: Id={Id.ToInvariantString()}, Name={Name}, Multiple={Multiple.ToInvariantString()}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/IssuePriority.cs b/src/redmine-net-api/Types/IssuePriority.cs index 877d3e31..82522462 100644 --- a/src/redmine-net-api/Types/IssuePriority.cs +++ b/src/redmine-net-api/Types/IssuePriority.cs @@ -176,7 +176,7 @@ public override int GetHashCode() /// ///
/// - private string DebuggerDisplay => $"[IssuePriority: {ToString()}, IsDefault={IsDefault.ToString(CultureInfo.InvariantCulture)}, IsActive={IsActive.ToString(CultureInfo.InvariantCulture)}]"; + private string DebuggerDisplay => $"[IssuePriority: Id={Id.ToInvariantString()},Name={Name}, IsDefault={IsDefault.ToInvariantString()},IsActive={IsActive.ToInvariantString()}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/IssueRelation.cs b/src/redmine-net-api/Types/IssueRelation.cs index 5a80bd4c..f5b254c6 100644 --- a/src/redmine-net-api/Types/IssueRelation.cs +++ b/src/redmine-net-api/Types/IssueRelation.cs @@ -283,11 +283,7 @@ public override int GetHashCode() /// ///
/// - private string DebuggerDisplay => $@"[{nameof(IssueRelation)}: {ToString()}, -IssueId={IssueId.ToString(CultureInfo.InvariantCulture)}, -IssueToId={IssueToId.ToString(CultureInfo.InvariantCulture)}, -Type={Type:G}, -Delay={Delay?.ToString(CultureInfo.InvariantCulture)}]"; + private string DebuggerDisplay => $"[IssueRelation: Id={Id.ToInvariantString()}, IssueId={IssueId.ToInvariantString()}, Type={Type:G}, Delay={Delay?.ToInvariantString()}]"; /// /// diff --git a/src/redmine-net-api/Types/IssueStatus.cs b/src/redmine-net-api/Types/IssueStatus.cs index 632f48ef..25d2a30a 100644 --- a/src/redmine-net-api/Types/IssueStatus.cs +++ b/src/redmine-net-api/Types/IssueStatus.cs @@ -238,7 +238,7 @@ public override int GetHashCode() /// /// /// - private string DebuggerDisplay => $"[{nameof(IssueStatus)}: {ToString()}, IsDefault={IsDefault.ToString(CultureInfo.InvariantCulture)}, IsClosed={IsClosed.ToString(CultureInfo.InvariantCulture)}]"; + private string DebuggerDisplay => $"[IssueStatus: Id={Id.ToInvariantString()}, Name={Name}, IsDefault={IsDefault.ToInvariantString()}, IsClosed={IsClosed.ToInvariantString()}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Journal.cs b/src/redmine-net-api/Types/Journal.cs index da2234a7..bf016b1b 100644 --- a/src/redmine-net-api/Types/Journal.cs +++ b/src/redmine-net-api/Types/Journal.cs @@ -251,7 +251,7 @@ public override int GetHashCode() /// ///
/// - private string DebuggerDisplay => $"[{nameof(Journal)}: {ToString()}, User={User}, Notes={Notes}, CreatedOn={CreatedOn?.ToString("u", CultureInfo.InvariantCulture)}, Details={Details.Dump()}]"; + private string DebuggerDisplay => $"[Journal: Id={Id.ToInvariantString()}, CreatedOn={CreatedOn?.ToString("u", CultureInfo.InvariantCulture)}]"; /// /// diff --git a/src/redmine-net-api/Types/Membership.cs b/src/redmine-net-api/Types/Membership.cs index 84443fe7..4b563614 100644 --- a/src/redmine-net-api/Types/Membership.cs +++ b/src/redmine-net-api/Types/Membership.cs @@ -186,6 +186,6 @@ public override int GetHashCode() /// /// /// - private string DebuggerDisplay => $"[{nameof(Membership)}: {ToString()}, Group={Group}, Project={Project}, User={User}, Roles={Roles.Dump()}]"; + private string DebuggerDisplay => $"[Membership: Id={Id.ToInvariantString()}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/MembershipRole.cs b/src/redmine-net-api/Types/MembershipRole.cs index 06770cb9..1de8fbfc 100644 --- a/src/redmine-net-api/Types/MembershipRole.cs +++ b/src/redmine-net-api/Types/MembershipRole.cs @@ -179,7 +179,6 @@ public override int GetHashCode() /// ///
/// - private string DebuggerDisplay => $"[MembershipRole: {ToString()}, Inherited={Inherited.ToString(CultureInfo.InvariantCulture)}]"; - + private string DebuggerDisplay => $"[MembershipRole: Id={Id.ToInvariantString()}, Name={Name}, Inherited={Inherited.ToInvariantString()}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/MyAccount.cs b/src/redmine-net-api/Types/MyAccount.cs index 86c4bead..806123b4 100644 --- a/src/redmine-net-api/Types/MyAccount.cs +++ b/src/redmine-net-api/Types/MyAccount.cs @@ -252,16 +252,6 @@ public override int GetHashCode() return !Equals(left, right); } - private string DebuggerDisplay => $@"[ {nameof(MyAccount)}: -Id={Id.ToString(CultureInfo.InvariantCulture)}, -Login={Login}, -ApiKey={ApiKey}, -FirstName={FirstName}, -LastName={LastName}, -Email={Email}, -IsAdmin={IsAdmin.ToString(CultureInfo.InvariantCulture).ToLowerInv()}, -CreatedOn={CreatedOn?.ToString("u", CultureInfo.InvariantCulture)}, -LastLoginOn={LastLoginOn?.ToString("u", CultureInfo.InvariantCulture)}, -CustomFields={CustomFields.Dump()}]"; + private string DebuggerDisplay => $"[MyAccount: Id={Id.ToInvariantString()}, Login={Login}, IsAdmin={IsAdmin.ToInvariantString()}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/MyAccountCustomField.cs b/src/redmine-net-api/Types/MyAccountCustomField.cs index 5e895a7c..55372989 100644 --- a/src/redmine-net-api/Types/MyAccountCustomField.cs +++ b/src/redmine-net-api/Types/MyAccountCustomField.cs @@ -168,7 +168,7 @@ public override int GetHashCode() /// ///
/// - private string DebuggerDisplay => $"[{nameof(MyAccountCustomField)}: {ToString()}, Value: {Value}]"; + private string DebuggerDisplay => $"[MyAccountCustomField: Id={Id.ToInvariantString()}, Name={Name}, Value: {Value}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/News.cs b/src/redmine-net-api/Types/News.cs index e8a9866f..78280368 100644 --- a/src/redmine-net-api/Types/News.cs +++ b/src/redmine-net-api/Types/News.cs @@ -282,7 +282,7 @@ public override int GetHashCode() /// ///
/// - private string DebuggerDisplay => $"[{nameof(News)}: {ToString()}, Project={Project}, Author={Author}, Title={Title}, Summary={Summary}, Description={Description}, CreatedOn={CreatedOn?.ToString("u", CultureInfo.InvariantCulture)}]"; + private string DebuggerDisplay => $"[News: Id={Id.ToInvariantString()}, Title={Title}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/NewsComment.cs b/src/redmine-net-api/Types/NewsComment.cs index 567c1b7f..86eca5ed 100644 --- a/src/redmine-net-api/Types/NewsComment.cs +++ b/src/redmine-net-api/Types/NewsComment.cs @@ -19,6 +19,7 @@ limitations under the License. using System.Xml; using System.Xml.Serialization; using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; namespace Redmine.Net.Api.Types @@ -157,9 +158,6 @@ public override int GetHashCode() return !Equals(left, right); } - private string DebuggerDisplay => $@"[{nameof(IssueAllowedStatus)}: {ToString()}, -{nameof(NewsComment)}: {ToString()}, -Author={Author}, -CONTENT={Content}]"; + private string DebuggerDisplay => $"[NewsComment: Id={Id.ToInvariantString()}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Permission.cs b/src/redmine-net-api/Types/Permission.cs index 9f2de487..8d300411 100644 --- a/src/redmine-net-api/Types/Permission.cs +++ b/src/redmine-net-api/Types/Permission.cs @@ -155,7 +155,7 @@ public override int GetHashCode() /// ///
/// - private string DebuggerDisplay => $"[{nameof(Permission)}: {Info}]"; + private string DebuggerDisplay => $"[Permission: {Info}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs index f3e327c0..798ab732 100644 --- a/src/redmine-net-api/Types/Project.cs +++ b/src/redmine-net-api/Types/Project.cs @@ -397,23 +397,6 @@ public override int GetHashCode() /// ///
/// - private string DebuggerDisplay => - $@"[Project: {ToString()}, -Identifier={Identifier}, -Description={Description}, -Parent={Parent}, -HomePage={HomePage}, -CreatedOn={CreatedOn?.ToString("u", CultureInfo.InvariantCulture)}, -UpdatedOn={UpdatedOn?.ToString("u", CultureInfo.InvariantCulture)}, -Status={Status:G}, -IsPublic={IsPublic.ToString(CultureInfo.InvariantCulture)}, -InheritMembers={InheritMembers.ToString(CultureInfo.InvariantCulture)}, -DefaultAssignee={DefaultAssignee}, -DefaultVersion={DefaultVersion}, -Trackers={Trackers.Dump()}, -CustomFields={CustomFields.Dump()}, -IssueCategories={IssueCategories.Dump()}, -EnabledModules={EnabledModules.Dump()}, -TimeEntryActivities = {TimeEntryActivities.Dump()}]"; + private string DebuggerDisplay => $"[Project: Id={Id.ToInvariantString()}, Name={Name}, Identifier={Identifier}, Status={Status:G}, IsPublic={IsPublic.ToInvariantString()}]"; } } diff --git a/src/redmine-net-api/Types/ProjectEnabledModule.cs b/src/redmine-net-api/Types/ProjectEnabledModule.cs index e90971a8..02d7f60c 100644 --- a/src/redmine-net-api/Types/ProjectEnabledModule.cs +++ b/src/redmine-net-api/Types/ProjectEnabledModule.cs @@ -62,7 +62,7 @@ public ProjectEnabledModule(string moduleName) /// ///
/// - private string DebuggerDisplay => $"[{nameof(ProjectEnabledModule)}: {ToString()}]"; + private string DebuggerDisplay => $"[ProjectEnabledModule: Id={Id.ToInvariantString()}, Name={Name}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/ProjectIssueCategory.cs b/src/redmine-net-api/Types/ProjectIssueCategory.cs index 76958c50..2ef3b006 100644 --- a/src/redmine-net-api/Types/ProjectIssueCategory.cs +++ b/src/redmine-net-api/Types/ProjectIssueCategory.cs @@ -16,6 +16,7 @@ limitations under the License. using System.Diagnostics; using System.Xml.Serialization; +using Redmine.Net.Api.Extensions; namespace Redmine.Net.Api.Types { @@ -40,7 +41,7 @@ internal ProjectIssueCategory(int id, string name) /// ///
/// - private string DebuggerDisplay => $"[{nameof(ProjectIssueCategory)}: {ToString()}]"; + private string DebuggerDisplay => $"[ProjectIssueCategory: Id={Id.ToInvariantString()}, Name={Name}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/ProjectMembership.cs b/src/redmine-net-api/Types/ProjectMembership.cs index 9979ab64..cd0cb010 100644 --- a/src/redmine-net-api/Types/ProjectMembership.cs +++ b/src/redmine-net-api/Types/ProjectMembership.cs @@ -225,7 +225,7 @@ public override int GetHashCode() /// ///
/// - private string DebuggerDisplay => $"[{nameof(ProjectMembership)}: {ToString()}, Project={Project}, User={User}, Group={Group}, Roles={Roles.Dump()}]"; + private string DebuggerDisplay => $"[ProjectMembership: Id={Id.ToInvariantString()}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs b/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs index 69daaca1..eded97b3 100644 --- a/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs +++ b/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs @@ -16,6 +16,7 @@ limitations under the License. using System.Diagnostics; using System.Xml.Serialization; +using Redmine.Net.Api.Extensions; namespace Redmine.Net.Api.Types { @@ -40,7 +41,7 @@ internal ProjectTimeEntryActivity(int id, string name) /// ///
/// - private string DebuggerDisplay => $"[{nameof(ProjectTimeEntryActivity)}: {ToString()}]"; + private string DebuggerDisplay => $"[ProjectTimeEntryActivity: Id={Id.ToInvariantString()}, Name={Name}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/ProjectTracker.cs b/src/redmine-net-api/Types/ProjectTracker.cs index fa5a2533..fa026ee7 100644 --- a/src/redmine-net-api/Types/ProjectTracker.cs +++ b/src/redmine-net-api/Types/ProjectTracker.cs @@ -17,6 +17,7 @@ limitations under the License. using System.Diagnostics; using System.Globalization; using System.Xml.Serialization; +using Redmine.Net.Api.Extensions; namespace Redmine.Net.Api.Types { @@ -64,7 +65,7 @@ internal ProjectTracker(int trackerId) /// ///
/// - private string DebuggerDisplay => $"[{nameof(ProjectTracker)}: {ToString()}]"; + private string DebuggerDisplay => $"[ProjectTracker: Id={Id.ToInvariantString()}, Name={Name}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Query.cs b/src/redmine-net-api/Types/Query.cs index 2506982c..09dc4f56 100644 --- a/src/redmine-net-api/Types/Query.cs +++ b/src/redmine-net-api/Types/Query.cs @@ -176,6 +176,6 @@ public override int GetHashCode() /// ///
/// - private string DebuggerDisplay => $"[{nameof(Query)}: {ToString()}, IsPublic={IsPublic.ToString(CultureInfo.InvariantCulture)}, ProjectId={ProjectId?.ToString(CultureInfo.InvariantCulture)}]"; + private string DebuggerDisplay => $"[Query: Id={Id.ToInvariantString()}, Name={Name}, IsPublic={IsPublic.ToInvariantString()}, ProjectId={ProjectId?.ToInvariantString()}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Role.cs b/src/redmine-net-api/Types/Role.cs index a922fff5..fd9dbab2 100644 --- a/src/redmine-net-api/Types/Role.cs +++ b/src/redmine-net-api/Types/Role.cs @@ -205,7 +205,7 @@ public override int GetHashCode() /// ///
/// - private string DebuggerDisplay => $"[{nameof(Role)}: {ToString()}, Permissions={Permissions}]"; + private string DebuggerDisplay => $"[Role: Id={Id.ToInvariantString()}, Name={Name}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Search.cs b/src/redmine-net-api/Types/Search.cs index c17f8367..a0ec1835 100644 --- a/src/redmine-net-api/Types/Search.cs +++ b/src/redmine-net-api/Types/Search.cs @@ -182,6 +182,6 @@ public override int GetHashCode() return !Equals(left, right); } - private string DebuggerDisplay => $@"[{nameof(Search)}:Id={Id.ToString(CultureInfo.InvariantCulture)},Title={Title},Type={Type},Url={Url},Description={Description}, DateTime={DateTime?.ToString("u", CultureInfo.InvariantCulture)}]"; + private string DebuggerDisplay => $"[Search: Id={Id.ToInvariantString()}, Title={Title}, Type={Type}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/TimeEntry.cs b/src/redmine-net-api/Types/TimeEntry.cs index 5af98492..bdc8cd64 100644 --- a/src/redmine-net-api/Types/TimeEntry.cs +++ b/src/redmine-net-api/Types/TimeEntry.cs @@ -325,16 +325,7 @@ public override int GetHashCode() /// ///
/// - private string DebuggerDisplay => - $@"[{nameof(TimeEntry)}: {ToString()}, Issue={Issue}, Project={Project}, -SpentOn={SpentOn?.ToString("u", CultureInfo.InvariantCulture)}, -Hours={Hours.ToString("F", CultureInfo.InvariantCulture)}, -Activity={Activity}, -User={User}, -Comments={Comments}, -CreatedOn={CreatedOn?.ToString("u", CultureInfo.InvariantCulture)}, -UpdatedOn={UpdatedOn?.ToString("u", CultureInfo.InvariantCulture)}, -CustomFields={CustomFields.Dump()}]"; + private string DebuggerDisplay => $"[TimeEntry: Id={Id.ToInvariantString()}, SpentOn={SpentOn?.ToString("u", CultureInfo.InvariantCulture)}, Hours={Hours.ToString("F", CultureInfo.InvariantCulture)}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/TimeEntryActivity.cs b/src/redmine-net-api/Types/TimeEntryActivity.cs index 4fd873e3..dc571fff 100644 --- a/src/redmine-net-api/Types/TimeEntryActivity.cs +++ b/src/redmine-net-api/Types/TimeEntryActivity.cs @@ -192,7 +192,7 @@ public override int GetHashCode() /// ///
/// - private string DebuggerDisplay => $"[{nameof(TimeEntryActivity)}:{ToString()}, IsDefault={IsDefault.ToString(CultureInfo.InvariantCulture)}, IsActive={IsActive.ToString(CultureInfo.InvariantCulture)}]"; + private string DebuggerDisplay => $"[TimeEntryActivity: Id={Id.ToInvariantString()}, Name={Name}, IsDefault={IsDefault.ToInvariantString()}, IsActive={IsActive.ToInvariantString()}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Tracker.cs b/src/redmine-net-api/Types/Tracker.cs index ded9c388..be916923 100644 --- a/src/redmine-net-api/Types/Tracker.cs +++ b/src/redmine-net-api/Types/Tracker.cs @@ -183,6 +183,6 @@ public override int GetHashCode() /// ///
/// - private string DebuggerDisplay => $"[{nameof(Tracker)}: {base.ToString()}]"; + private string DebuggerDisplay => $"[Tracker: Id={Id.ToInvariantString()}, Name={Name}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/TrackerCoreField.cs b/src/redmine-net-api/Types/TrackerCoreField.cs index 54b5f1be..2677a1eb 100644 --- a/src/redmine-net-api/Types/TrackerCoreField.cs +++ b/src/redmine-net-api/Types/TrackerCoreField.cs @@ -36,7 +36,7 @@ internal TrackerCoreField(string name) /// ///
/// - private string DebuggerDisplay => $"[{nameof(TrackerCoreField)}: {ToString()}]"; + private string DebuggerDisplay => $"[TrackerCoreField: Name={Name}]"; /// /// diff --git a/src/redmine-net-api/Types/TrackerCustomField.cs b/src/redmine-net-api/Types/TrackerCustomField.cs index 5372039b..8464d220 100644 --- a/src/redmine-net-api/Types/TrackerCustomField.cs +++ b/src/redmine-net-api/Types/TrackerCustomField.cs @@ -76,7 +76,7 @@ public override void ReadJson(JsonReader reader) /// /// /// - private string DebuggerDisplay => $"[{nameof(TrackerCustomField)}: {ToString()}]"; + private string DebuggerDisplay => $"[TrackerCustomField: Id={Id.ToInvariantString()}, Name={Name}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Upload.cs b/src/redmine-net-api/Types/Upload.cs index 3948d5a5..12d57053 100644 --- a/src/redmine-net-api/Types/Upload.cs +++ b/src/redmine-net-api/Types/Upload.cs @@ -233,7 +233,7 @@ public override int GetHashCode() /// ///
/// - private string DebuggerDisplay => $"[Upload: Token={Token}, FileName={FileName}, ContentType={ContentType}, Description={Description}]"; + private string DebuggerDisplay => $"[Upload: Token={Token}, FileName={FileName}]"; /// /// diff --git a/src/redmine-net-api/Types/User.cs b/src/redmine-net-api/Types/User.cs index 481409bd..19679607 100644 --- a/src/redmine-net-api/Types/User.cs +++ b/src/redmine-net-api/Types/User.cs @@ -425,27 +425,7 @@ public override int GetHashCode() /// /// /// - private string DebuggerDisplay => - $@"[{nameof(User)}: {Groups}, -Login={Login}, Password={Password}, -FirstName={FirstName}, -LastName={LastName}, -AvatarUrl={AvatarUrl}, -IsAdmin={IsAdmin.ToString(CultureInfo.InvariantCulture)}, -TwoFactorAuthenticationScheme={TwoFactorAuthenticationScheme} -Email={Email}, -EmailNotification={MailNotification}, -AuthenticationModeId={AuthenticationModeId?.ToString(CultureInfo.InvariantCulture)}, -CreatedOn={CreatedOn?.ToString("u", CultureInfo.InvariantCulture)}, -UpdatedOn={UpdatedOn?.ToString("u", CultureInfo.InvariantCulture)} -PasswordChangedOn={PasswordChangedOn?.ToString("u", CultureInfo.InvariantCulture)} -LastLoginOn={LastLoginOn?.ToString("u", CultureInfo.InvariantCulture)}, -ApiKey={ApiKey}, -Status={Status:G}, -MustChangePassword={MustChangePassword.ToString(CultureInfo.InvariantCulture)}, -CustomFields={CustomFields.Dump()}, -Memberships={Memberships.Dump()}, -Groups={Groups.Dump()}]"; + private string DebuggerDisplay => $"[User: Id={Id.ToInvariantString()}, Login={Login}, IsAdmin={IsAdmin.ToString(CultureInfo.InvariantCulture)}, Status={Status:G}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/UserGroup.cs b/src/redmine-net-api/Types/UserGroup.cs index 57c7c0a8..660c9051 100644 --- a/src/redmine-net-api/Types/UserGroup.cs +++ b/src/redmine-net-api/Types/UserGroup.cs @@ -16,6 +16,7 @@ limitations under the License. using System.Diagnostics; using System.Xml.Serialization; +using Redmine.Net.Api.Extensions; namespace Redmine.Net.Api.Types { @@ -30,7 +31,7 @@ public sealed class UserGroup : IdentifiableName /// ///
/// - private string DebuggerDisplay => $"[{nameof(UserGroup)}: {ToString()}]"; + private string DebuggerDisplay => $"[UserGroup: Id={Id.ToInvariantString()}, Name={Name}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/Version.cs b/src/redmine-net-api/Types/Version.cs index 78dc24eb..2e4dd096 100644 --- a/src/redmine-net-api/Types/Version.cs +++ b/src/redmine-net-api/Types/Version.cs @@ -308,18 +308,7 @@ public override int GetHashCode() /// ///
/// - private string DebuggerDisplay => $@"[{nameof(Version)}: {ToString()}, -Project={Project}, -Description={Description}, -Status={Status:G}, -DueDate={DueDate?.ToString("u", CultureInfo.InvariantCulture)}, -Sharing={Sharing:G}, -CreatedOn={CreatedOn?.ToString("u", CultureInfo.InvariantCulture)}, -UpdatedOn={UpdatedOn?.ToString("u", CultureInfo.InvariantCulture)}, -EstimatedHours={EstimatedHours?.ToString("F", CultureInfo.InvariantCulture)}, -SpentHours={SpentHours?.ToString("F", CultureInfo.InvariantCulture)}, -WikiPageTitle={WikiPageTitle} -CustomFields={CustomFields.Dump()}]"; + private string DebuggerDisplay => $"[Version: Id={Id.ToInvariantString()}, Name={Name}, Status={Status:G}]"; } } diff --git a/src/redmine-net-api/Types/WikiPage.cs b/src/redmine-net-api/Types/WikiPage.cs index b2879278..76125cef 100644 --- a/src/redmine-net-api/Types/WikiPage.cs +++ b/src/redmine-net-api/Types/WikiPage.cs @@ -17,7 +17,6 @@ limitations under the License. using System; using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; using System.Xml; using System.Xml.Serialization; using Newtonsoft.Json; @@ -288,12 +287,7 @@ public override int GetHashCode() /// ///
/// - private string DebuggerDisplay => $@"[{nameof(WikiPage)}: {ToString()}, Title={Title}, Text={Text}, Comments={Comments}, -Version={Version.ToString(CultureInfo.InvariantCulture)}, -Author={Author}, -CreatedOn={CreatedOn?.ToString("u", CultureInfo.InvariantCulture)}, -UpdatedOn={UpdatedOn?.ToString("u", CultureInfo.InvariantCulture)}, -Attachments={Attachments.Dump()}]"; + private string DebuggerDisplay => $"[WikiPage: Id={Id.ToInvariantString()}, Title={Title}]"; } } \ No newline at end of file From 5ac5085d12b24b256e980cc5434bcda460e60b8a Mon Sep 17 00:00:00 2001 From: Padi Date: Thu, 8 May 2025 11:15:01 +0300 Subject: [PATCH 438/549] [IntegrationTest] Add TestContainers --- redmine-net-api.sln | 9 + src/redmine-net-api/redmine-net-api.csproj | 5 +- .../Constants.cs | 6 + .../RedmineTestContainerCollection.cs | 4 + .../Fixtures/RedmineTestContainerFixture.cs | 149 +++++++++++ .../RedmineIntegrationTestsAsync.cs | 249 ++++++++++++++++++ .../RedmineIntegrationTestsSync.cs | 234 ++++++++++++++++ .../TestData/init-redmine.sql | 67 +++++ .../redmine-net-api.Integration.Tests.csproj | 34 +++ 9 files changed, 756 insertions(+), 1 deletion(-) create mode 100644 tests/redmine-net-api.Integration.Tests/Constants.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Fixtures/RedmineTestContainerCollection.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Fixtures/RedmineTestContainerFixture.cs create mode 100644 tests/redmine-net-api.Integration.Tests/RedmineIntegrationTestsAsync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/RedmineIntegrationTestsSync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/TestData/init-redmine.sql create mode 100644 tests/redmine-net-api.Integration.Tests/redmine-net-api.Integration.Tests.csproj diff --git a/redmine-net-api.sln b/redmine-net-api.sln index 7cbed138..86cc3bbe 100644 --- a/redmine-net-api.sln +++ b/redmine-net-api.sln @@ -52,6 +52,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Others", "Others", "{4ADECA global.json = global.json EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "redmine-net-api.Integration.Tests", "tests\redmine-net-api.Integration.Tests\redmine-net-api.Integration.Tests.csproj", "{254DABFE-7C92-4C16-84A5-630330D56D4D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -70,6 +72,12 @@ Global {900EF0B3-0233-45DA-811F-4C59483E8452}.DebugJson|Any CPU.ActiveCfg = DebugJson|Any CPU {900EF0B3-0233-45DA-811F-4C59483E8452}.DebugJson|Any CPU.Build.0 = DebugJson|Any CPU {900EF0B3-0233-45DA-811F-4C59483E8452}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {254DABFE-7C92-4C16-84A5-630330D56D4D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {254DABFE-7C92-4C16-84A5-630330D56D4D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {254DABFE-7C92-4C16-84A5-630330D56D4D}.DebugJson|Any CPU.ActiveCfg = Debug|Any CPU + {254DABFE-7C92-4C16-84A5-630330D56D4D}.DebugJson|Any CPU.Build.0 = Debug|Any CPU + {254DABFE-7C92-4C16-84A5-630330D56D4D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {254DABFE-7C92-4C16-84A5-630330D56D4D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -82,6 +90,7 @@ Global {707B6A3F-1A2C-4EFE-851F-1DB0E68CFFFB} = {E8C35EC2-DD90-46E8-9B63-84EFD5F2FDE3} {1D340EEB-C535-45D4-80D7-ADD4434D7B77} = {E8C35EC2-DD90-46E8-9B63-84EFD5F2FDE3} {4ADECA2A-4D7B-4F05-85A2-0C0963A83689} = {E8C35EC2-DD90-46E8-9B63-84EFD5F2FDE3} + {254DABFE-7C92-4C16-84A5-630330D56D4D} = {F3F4278D-6271-4F77-BA88-41555D53CBD1} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4AA87D90-ABD0-4793-BE47-955B35FAE2BB} diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 15c2aed6..4e1ea59f 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -100,7 +100,10 @@ - <_Parameter1>Padi.DotNet.RedmineAPI.Tests + <_Parameter1>Padi.DotNet.RedmineAPI.Tests + + + <_Parameter1>Padi.DotNet.RedmineAPI.Integration.Tests diff --git a/tests/redmine-net-api.Integration.Tests/Constants.cs b/tests/redmine-net-api.Integration.Tests/Constants.cs new file mode 100644 index 00000000..93861d62 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Constants.cs @@ -0,0 +1,6 @@ +namespace Padi.DotNet.RedmineAPI.Integration.Tests; + +public static class Constants +{ + public const string RedmineTestContainerCollection = nameof(RedmineTestContainerCollection); +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Fixtures/RedmineTestContainerCollection.cs b/tests/redmine-net-api.Integration.Tests/Fixtures/RedmineTestContainerCollection.cs new file mode 100644 index 00000000..61b0cafb --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Fixtures/RedmineTestContainerCollection.cs @@ -0,0 +1,4 @@ +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; + +[CollectionDefinition(Constants.RedmineTestContainerCollection)] +public sealed class RedmineTestContainerCollection : ICollectionFixture { } \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Fixtures/RedmineTestContainerFixture.cs b/tests/redmine-net-api.Integration.Tests/Fixtures/RedmineTestContainerFixture.cs new file mode 100644 index 00000000..a9564c8e --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Fixtures/RedmineTestContainerFixture.cs @@ -0,0 +1,149 @@ +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Configurations; +using DotNet.Testcontainers.Containers; +using DotNet.Testcontainers.Networks; +using Npgsql; +using Redmine.Net.Api; +using Testcontainers.PostgreSql; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; + +public class RedmineTestContainerFixture : IAsyncLifetime +{ + private const int RedminePort = 3000; + private const int PostgresPort = 5432; + private const string PostgresImage = "postgres:17.4-alpine"; + private const string RedmineImage = "redmine:6.0.5-alpine"; + private const string PostgresDb = "postgres"; + private const string PostgresUser = "postgres"; + private const string PostgresPassword = "postgres"; + private const string RedmineSqlFilePath = "TestData/init-redmine.sql"; + + public const string RedmineApiKey = "029a9d38-17e8-41ae-bc8c-fbf71e193c57"; + + private readonly string RedmineNetworkAlias = Guid.NewGuid().ToString(); + private INetwork Network { get; set; } + private PostgreSqlContainer PostgresContainer { get; set; } + private IContainer RedmineContainer { get; set; } + public RedmineManager RedmineManager { get; private set; } + public string RedmineHost { get; private set; } + + public RedmineTestContainerFixture() + { + BuildContainers(); + } + + private void BuildContainers() + { + Network = new NetworkBuilder() + .WithDriver(NetworkDriver.Bridge) + .Build(); + + PostgresContainer = new PostgreSqlBuilder() + .WithImage(PostgresImage) + .WithNetwork(Network) + .WithNetworkAliases(RedmineNetworkAlias) + .WithPortBinding(PostgresPort, assignRandomHostPort: true) + .WithEnvironment(new Dictionary + { + { "POSTGRES_DB", PostgresDb }, + { "POSTGRES_USER", PostgresUser }, + { "POSTGRES_PASSWORD", PostgresPassword }, + }) + .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(PostgresPort)) + .Build(); + + RedmineContainer = new ContainerBuilder() + .WithImage(RedmineImage) + .WithNetwork(Network) + .WithPortBinding(RedminePort, assignRandomHostPort: true) + .WithEnvironment(new Dictionary + { + { "REDMINE_DB_POSTGRES", RedmineNetworkAlias }, + { "REDMINE_DB_PORT", PostgresPort.ToString() }, + { "REDMINE_DB_DATABASE", PostgresDb }, + { "REDMINE_DB_USERNAME", PostgresUser }, + { "REDMINE_DB_PASSWORD", PostgresPassword }, + }) + .DependsOn(PostgresContainer) + .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request => request.ForPort(RedminePort).ForPath("/"))) + .Build(); + } + + public async Task InitializeAsync() + { + await Network.CreateAsync(); + + await PostgresContainer.StartAsync(); + + await RedmineContainer.StartAsync(); + + await SeedTestDataAsync(PostgresContainer, CancellationToken.None); + + RedmineHost = $"http://{RedmineContainer.Hostname}:{RedmineContainer.GetMappedPublicPort(RedminePort)}"; + + var rmgBuilder = new RedmineManagerOptionsBuilder() + .WithHost(RedmineHost) + .WithBasicAuthentication("adminuser", "1qaz2wsx"); + + RedmineManager = new RedmineManager(rmgBuilder); + } + + public async Task DisposeAsync() + { + var exceptions = new List(); + + await SafeDisposeAsync(() => RedmineContainer.StopAsync()); + await SafeDisposeAsync(() => PostgresContainer.StopAsync()); + await SafeDisposeAsync(() => Network.DisposeAsync().AsTask()); + + if (exceptions.Count > 0) + { + throw new AggregateException(exceptions); + } + + return; + + async Task SafeDisposeAsync(Func disposeFunc) + { + try + { + await disposeFunc(); + } + catch (Exception ex) + { + exceptions.Add(ex); + } + } + } + + private static async Task SeedTestDataAsync(PostgreSqlContainer container, CancellationToken ct) + { + const int maxDbAttempts = 10; + var dbRetryDelay = TimeSpan.FromSeconds(2); + var connectionString = container.GetConnectionString(); + for (var attempt = 1; attempt <= maxDbAttempts; attempt++) + { + try + { + await using var conn = new NpgsqlConnection(connectionString); + await conn.OpenAsync(ct); + break; + } + catch + { + if (attempt == maxDbAttempts) + { + throw; + } + await Task.Delay(dbRetryDelay, ct); + } + } + var sql = await System.IO.File.ReadAllTextAsync(RedmineSqlFilePath, ct); + var res = await container.ExecScriptAsync(sql, ct); + if (!string.IsNullOrWhiteSpace(res.Stderr)) + { + // Optionally log stderr + } + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/RedmineIntegrationTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/RedmineIntegrationTestsAsync.cs new file mode 100644 index 00000000..b90554ee --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/RedmineIntegrationTestsAsync.cs @@ -0,0 +1,249 @@ +using System.Collections.Specialized; +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Net; +using Redmine.Net.Api.Types; +using Version = Redmine.Net.Api.Types.Version; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests; + +[Collection(Constants.RedmineTestContainerCollection)] +public class RedmineIntegrationTestsAsync(RedmineTestContainerFixture fixture) +{ + private readonly RedmineManager _redmineManager = fixture.RedmineManager; + + [Fact] + public async Task Should_ReturnProjectsAsync() + { + var list = await _redmineManager.GetAsync(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public async Task Should_ReturnRolesAsync() + { + var list = await _redmineManager.GetAsync(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public async Task Should_ReturnAttachmentsAsync() + { + var list = await _redmineManager.GetAsync(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public async Task Should_ReturnCustomFieldsAsync() + { + var list = await _redmineManager.GetAsync(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public async Task Should_ReturnGroupsAsync() + { + var list = await _redmineManager.GetAsync(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public async Task Should_ReturnFilesAsync() + { + var list = await _redmineManager.GetAsync(new RequestOptions() + { + QueryString = new NameValueCollection() + { + { RedmineKeys.PROJECT_ID, 1.ToString() } + } + }); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public async Task Should_ReturnIssuesAsync() + { + var list = await _redmineManager.GetAsync(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public async Task GetIssue_WithVersions_ShouldReturnAsync() + { + var issue = await _redmineManager.GetAsync(5.ToInvariantString(), + new RequestOptions { + QueryString = new NameValueCollection() + { + { RedmineKeys.INCLUDE, RedmineKeys.WATCHERS } + } + } + ); + Assert.NotNull(issue); + } + + [Fact] + public async Task Should_ReturnIssueCategoriesAsync() + { + var list = await _redmineManager.GetAsync(new RequestOptions() + { + QueryString = new NameValueCollection() + { + { RedmineKeys.PROJECT_ID, 1.ToString() } + } + }); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public async Task Should_ReturnIssueCustomFieldsAsync() + { + var list = await _redmineManager.GetAsync(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public async Task Should_ReturnIssuePrioritiesAsync() + { + var list = await _redmineManager.GetAsync(new RequestOptions() + { + QueryString = new NameValueCollection() + { + { RedmineKeys.ISSUE_ID, 1.ToString() } + } + }); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public async Task Should_ReturnIssueRelationsAsync() + { + var list = await _redmineManager.GetAsync(new RequestOptions() + { + QueryString = new NameValueCollection() + { + { RedmineKeys.ISSUE_ID, 1.ToString() } + } + }); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public async Task Should_ReturnIssueStatusesAsync() + { + var list = await _redmineManager.GetAsync(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public async Task Should_ReturnJournalsAsync() + { + var list = await _redmineManager.GetAsync(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public async Task Should_ReturnNewsAsync() + { + var list = await _redmineManager.GetAsync(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public async Task Should_ReturnProjectMembershipsAsync() + { + var list = await _redmineManager.GetAsync(new RequestOptions() + { + QueryString = new NameValueCollection() + { + { RedmineKeys.PROJECT_ID, 1.ToString() } + } + }); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public async Task Should_ReturnQueriesAsync() + { + var list = await _redmineManager.GetAsync(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public async Task Should_ReturnSearchesAsync() + { + var list = await _redmineManager.GetAsync(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public async Task Should_ReturnTimeEntriesAsync() + { + var list = await _redmineManager.GetAsync(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public async Task Should_ReturnTimeEntryActivitiesAsync() + { + var list = await _redmineManager.GetAsync(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public async Task Should_ReturnTrackersAsync() + { + var list = await _redmineManager.GetAsync(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public async Task Should_ReturnUsersAsync() + { + var list = await _redmineManager.GetAsync(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public async Task Should_ReturnVersionsAsync() + { + var list = await _redmineManager.GetAsync(new RequestOptions() + { + QueryString = new NameValueCollection() + { + { RedmineKeys.PROJECT_ID, 1.ToString() } + } + }); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public async Task Should_ReturnWatchersAsync() + { + var list = await _redmineManager.GetAsync(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/RedmineIntegrationTestsSync.cs b/tests/redmine-net-api.Integration.Tests/RedmineIntegrationTestsSync.cs new file mode 100644 index 00000000..024b719b --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/RedmineIntegrationTestsSync.cs @@ -0,0 +1,234 @@ +using System.Collections.Specialized; +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api; +using Redmine.Net.Api.Net; +using Redmine.Net.Api.Types; +using Version = Redmine.Net.Api.Types.Version; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests; + +[Collection(Constants.RedmineTestContainerCollection)] +public class RedmineIntegrationTestsSync(RedmineTestContainerFixture fixture) +{ + private readonly RedmineManager _redmineManager = fixture.RedmineManager; + + [Fact] + public void Should_ReturnProjects() + { + var list = _redmineManager.Get(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public void Should_ReturnRoles() + { + var list = _redmineManager.Get(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public void Should_ReturnAttachments() + { + var list = _redmineManager.Get(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public void Should_ReturnCustomFields() + { + var list = _redmineManager.Get(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public void Should_ReturnGroups() + { + var list = _redmineManager.Get(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public void Should_ReturnFiles() + { + var list = _redmineManager.Get(new RequestOptions() + { + QueryString = new NameValueCollection() + { + { RedmineKeys.PROJECT_ID, 1.ToString() } + } + }); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public void Should_ReturnIssues() + { + var list = _redmineManager.Get(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public void Should_ReturnIssueCategories() + { + var list = _redmineManager.Get(new RequestOptions() + { + QueryString = new NameValueCollection() + { + { RedmineKeys.PROJECT_ID, 1.ToString() } + } + }); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public void Should_ReturnIssueCustomFields() + { + var list = _redmineManager.Get(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public void Should_ReturnIssuePriorities() + { + var list = _redmineManager.Get(new RequestOptions() + { + QueryString = new NameValueCollection() + { + { RedmineKeys.ISSUE_ID, 1.ToString() } + } + }); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public void Should_ReturnIssueRelations() + { + var list = _redmineManager.Get(new RequestOptions() + { + QueryString = new NameValueCollection() + { + { RedmineKeys.ISSUE_ID, 1.ToString() } + } + }); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public void Should_ReturnIssueStatuses() + { + var list = _redmineManager.Get(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public void Should_ReturnJournals() + { + var list = _redmineManager.Get(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public void Should_ReturnNews() + { + var list = _redmineManager.Get(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public void Should_ReturnProjectMemberships() + { + var list = _redmineManager.Get(new RequestOptions() + { + QueryString = new NameValueCollection() + { + { RedmineKeys.PROJECT_ID, 1.ToString() } + } + }); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public void Should_ReturnQueries() + { + var list = _redmineManager.Get(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public void Should_ReturnSearches() + { + var list = _redmineManager.Get(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public void Should_ReturnTimeEntries() + { + var list = _redmineManager.Get(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public void Should_ReturnTimeEntryActivities() + { + var list = _redmineManager.Get(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public void Should_ReturnTrackers() + { + var list = _redmineManager.Get(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public void Should_ReturnUsers() + { + var list = _redmineManager.Get(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public void Should_ReturnVersions() + { + var list = _redmineManager.Get(new RequestOptions() + { + QueryString = new NameValueCollection() + { + { RedmineKeys.PROJECT_ID, 1.ToString() } + } + }); + Assert.NotNull(list); + Assert.NotEmpty(list); + } + + [Fact] + public void Should_ReturnWatchers() + { + var list = _redmineManager.Get(); + Assert.NotNull(list); + Assert.NotEmpty(list); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/TestData/init-redmine.sql b/tests/redmine-net-api.Integration.Tests/TestData/init-redmine.sql new file mode 100644 index 00000000..d511ec2b --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/TestData/init-redmine.sql @@ -0,0 +1,67 @@ +-- 1. Insert users +INSERT INTO users (id, login, hashed_password, salt, firstname, lastname, admin, status, type, created_on, updated_on) +VALUES (90, 'adminuser', '5cfe86e41de3a143be90ae5f7ced76841a0830bf', 'e71a2bcb922bede1becc396b326b93ff', 'Admin', 'User', true, 1, 'User', NOW(), NOW()), + (91, 'normaluser', '3c4afd1d5042356c7fdd19e0527db108919624f9', '6030b2ed3c7eb797eb706a325bb227ad', 'Normal', 'User', false, 1, 'User', NOW(), NOW()); + +-- 2. Insert API keys +INSERT INTO tokens (user_id, action, value, created_on) +VALUES + (90, 'api', '029a9d38-17e8-41ae-bc8c-fbf71e193c57', NOW()), + (91, 'api', 'b94da108-c6d0-483a-9c21-2648fe54521d', NOW()); + +INSERT INTO settings (id, name, "value", updated_on) +values (99, 'rest_api_enabled', 1, now()); + +insert into enabled_modules (id, project_id, name) +values (1, 1, 'issue_tracking'), + (2, 1, 'time_tracking'), + (3, 1, 'news'), + (4, 1, 'documents'), + (5, 1, 'files'), + (6, 1, 'wiki'), + (7, 1, 'repository'), + (8, 1, 'boards'), + (9, 1, 'calendar'), + (10, 1, 'gantt'); + + +insert into enumerations (id, name, position, is_default, type, active, project_id, parent_id, position_name) +values (1, 'Low', 1, false, 'IssuePriority', true, null, null, 'lowest'), + (2, 'Normal', 2, true, 'IssuePriority', true, null, null, 'default'), + (3, 'High', 3, false, 'IssuePriority', true, null, null, 'high3'), + (4, 'Urgent', 4, false, 'IssuePriority', true, null, null, 'high2'), + (5, 'Immediate', 5, false, 'IssuePriority', true, null, null, 'highest'), + (6, 'User documentation', 1, false, 'DocumentCategory', true, null, null, null), + (7, 'Technical documentation', 2, false, 'DocumentCategory', true, null, null, null), + (8, 'Design', 1, false, 'TimeEntryActivity', true, null, null, null), + (9, 'Development', 2, false, 'TimeEntryActivity', true, null, null, null); + +insert into issue_statuses (id, name, is_closed, position, default_done_ratio, description) +values (1, 'New', false, 1, null, null), + (2, 'In Progress', false, 2, null, null), + (3, 'Resolved', false, 3, null, null), + (4, 'Feedback', false, 4, null, null), + (5, 'Closed', true, 5, null, null), + (6, 'Rejected', true, 6, null, null); + + +insert into trackers (id, name, position, is_in_roadmap, fields_bits, default_status_id, description) +values (1, 'Bug', 1, false, 0, 1, null), + (2, 'Feature', 2, true, 0, 1, null), + (3, 'Support', 3, false, 0, 1, null); + +insert into projects (id, name, description, homepage, is_public, parent_id, created_on, updated_on, identifier, status, lft, rgt, inherit_members, default_version_id, default_assigned_to_id, default_issue_query_id) +values (1, 'Project-Test', null, '', true, null, '2024-09-02 10:14:33.789394', '2024-09-02 10:14:33.789394', 'project-test', 1, 1, 2, false, null, null, null); + + +insert into versions (id, project_id, name, description, effective_date, created_on, updated_on, wiki_page_title, status, sharing) +values (1, 1, 'version1', '', null, '2025-04-28 17:56:49.245993', '2025-04-28 17:56:49.245993', '', 'open', 'none'), + (2, 1, 'version2', '', null, '2025-04-28 17:57:05.138915', '2025-04-28 17:57:05.138915', '', 'open', 'descendants'); + +insert into issues (id, tracker_id, project_id, subject, description, due_date, category_id, status_id, assigned_to_id, priority_id, fixed_version_id, author_id, lock_version, created_on, updated_on, start_date, done_ratio, estimated_hours, parent_id, root_id, lft, rgt, is_private, closed_on) +values (5, 1, 1, '#380', '', null, 1, 1, null, 2, 2, 90, 1, '2025-04-28 17:58:42.818731', '2025-04-28 17:58:42.818731', '2025-04-28', 0, null, null, 5, 1, 2, false, null), + (6, 1, 1, 'issue with file', '', null, null, 1, null, 3, 2, 90, 1, '2025-04-28 18:00:07.296872', '2025-04-28 18:00:07.296872', '2025-04-28', 0, null, null, 6, 1, 2, false, null); + +insert into watchers (id, watchable_type, watchable_id, user_id) +values (8, 'Issue', 5, 90), + (9, 'Issue', 5, 91); diff --git a/tests/redmine-net-api.Integration.Tests/redmine-net-api.Integration.Tests.csproj b/tests/redmine-net-api.Integration.Tests/redmine-net-api.Integration.Tests.csproj new file mode 100644 index 00000000..5cdab1a7 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/redmine-net-api.Integration.Tests.csproj @@ -0,0 +1,34 @@ + + + + net9.0 + redmine_net_api.Integration.Tests + enable + enable + false + Padi.DotNet.RedmineAPI.Integration.Tests + $(AssemblyName) + + + + + + + + + + + + + + + + + + + + + + + + From fc89105853dc798395058d9828ec369e3ef9398e Mon Sep 17 00:00:00 2001 From: Padi Date: Thu, 8 May 2025 11:07:07 +0300 Subject: [PATCH 439/549] Some cleanup & code arrange --- src/redmine-net-api/Extensions/RedmineManagerExtensions.cs | 4 ++-- src/redmine-net-api/Net/RedmineApiUrls.cs | 2 +- src/redmine-net-api/Types/IssueRelation.cs | 4 ++++ src/redmine-net-api/Types/News.cs | 6 ++---- src/redmine-net-api/Types/PagedResults.cs | 2 +- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs index 18d0b5e6..fb94205f 100644 --- a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs +++ b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs @@ -702,7 +702,7 @@ public static async Task GetCurrentUserAsync(this RedmineManager redmineMa } /// - /// Creates the or update wiki page asynchronous. + /// Creates or updates wiki page asynchronous. /// /// The redmine manager. /// The project identifier. @@ -730,7 +730,7 @@ public static async Task CreateWikiPageAsync(this RedmineManager redmi } /// - /// Creates or update wiki page asynchronous. + /// Creates or updates wiki page asynchronous. /// /// The redmine manager. /// The project identifier. diff --git a/src/redmine-net-api/Net/RedmineApiUrls.cs b/src/redmine-net-api/Net/RedmineApiUrls.cs index f893c338..54a9d452 100644 --- a/src/redmine-net-api/Net/RedmineApiUrls.cs +++ b/src/redmine-net-api/Net/RedmineApiUrls.cs @@ -194,7 +194,7 @@ public string DeleteFragment(string id) return DeleteFragment(type, id); } - internal string DeleteFragment(Type type,string id) + internal string DeleteFragment(Type type, string id) { return $"{TypeFragment(TypeUrlFragments, type)}/{id}.{Format}"; } diff --git a/src/redmine-net-api/Types/IssueRelation.cs b/src/redmine-net-api/Types/IssueRelation.cs index f5b254c6..f913221b 100644 --- a/src/redmine-net-api/Types/IssueRelation.cs +++ b/src/redmine-net-api/Types/IssueRelation.cs @@ -205,7 +205,11 @@ private static IssueRelationType ReadIssueRelationType(string value) return IssueRelationType.CopiedFrom; } +#if NETFRAMEWORK return (IssueRelationType)Enum.Parse(typeof(IssueRelationType), value, true); +#else + return Enum.Parse(value, true); +#endif } #endregion diff --git a/src/redmine-net-api/Types/News.cs b/src/redmine-net-api/Types/News.cs index 78280368..c989c6f2 100644 --- a/src/redmine-net-api/Types/News.cs +++ b/src/redmine-net-api/Types/News.cs @@ -167,10 +167,8 @@ public override void ReadJson(JsonReader reader) case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; case RedmineKeys.SUMMARY: Summary = reader.ReadAsString(); break; case RedmineKeys.TITLE: Title = reader.ReadAsString(); break; - case RedmineKeys.ATTACHMENTS: Attachments = reader.ReadAsCollection(); - break; - case RedmineKeys.COMMENTS: Comments = reader.ReadAsCollection(); - break; + case RedmineKeys.ATTACHMENTS: Attachments = reader.ReadAsCollection(); break; + case RedmineKeys.COMMENTS: Comments = reader.ReadAsCollection(); break; default: reader.Read(); break; } } diff --git a/src/redmine-net-api/Types/PagedResults.cs b/src/redmine-net-api/Types/PagedResults.cs index 5721c2f5..eab0c680 100644 --- a/src/redmine-net-api/Types/PagedResults.cs +++ b/src/redmine-net-api/Types/PagedResults.cs @@ -37,7 +37,7 @@ public PagedResults(IEnumerable items, int total, int offset, int pageSize Offset = offset; PageSize = pageSize; - if (pageSize <= 0) + if (pageSize <= 0 || total == 0) { return; } From 3ed237f6949b9fee05aec70832c1ffe27f901a4e Mon Sep 17 00:00:00 2001 From: zapadi Date: Fri, 25 Apr 2025 17:03:44 +0300 Subject: [PATCH 440/549] Update docker-compose.yml --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 5a788f19..6e7274ac 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: redmine: ports: - '8089:3000' - image: 'redmine:5.1.1-alpine' + image: 'redmine:6.0.5-alpine' container_name: 'redmine-web' depends_on: - db-postgres @@ -33,7 +33,7 @@ services: POSTGRES_USER: redmine-usr POSTGRES_PASSWORD: redmine-pswd container_name: 'redmine-db' - image: 'postgres:16-alpine' + image: 'postgres:17.4-alpine' healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 20s From 4cea7f69a893683b55283fb89111828ec919a1e1 Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 11 May 2025 15:10:20 +0300 Subject: [PATCH 441/549] Set sdk version to 9.0.203 --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 6223f1b6..1f044567 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "9.0.101", + "version": "9.0.203", "allowPrerelease": false, "rollForward": "latestMajor" } From d248fa8a5c4e156e9c953ea05011b37ed30e9d45 Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 11 May 2025 15:02:00 +0300 Subject: [PATCH 442/549] Bump up packages version --- .../redmine-net-api.Integration.Tests.csproj | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/redmine-net-api.Integration.Tests/redmine-net-api.Integration.Tests.csproj b/tests/redmine-net-api.Integration.Tests/redmine-net-api.Integration.Tests.csproj index 5cdab1a7..76821b85 100644 --- a/tests/redmine-net-api.Integration.Tests/redmine-net-api.Integration.Tests.csproj +++ b/tests/redmine-net-api.Integration.Tests/redmine-net-api.Integration.Tests.csproj @@ -4,19 +4,19 @@ net9.0 redmine_net_api.Integration.Tests enable - enable + disable false Padi.DotNet.RedmineAPI.Integration.Tests $(AssemblyName) - - + + - - + + From cab32cbc7d59b2997b693937ca1bb412424a732e Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 11 May 2025 15:36:31 +0300 Subject: [PATCH 443/549] Disable nullable and enable implicit usings --- tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj index 87deb601..25cbe1c5 100644 --- a/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj +++ b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj @@ -4,6 +4,8 @@ Padi.DotNet.RedmineAPI.Tests + disable + enable $(AssemblyName) false net481 From 6408ef592d943d6126439b797fdd63e03f998b0f Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 11 May 2025 15:08:22 +0300 Subject: [PATCH 444/549] [New] Directory.Packages.props --- Directory.Build.props | 12 +++++++++ Directory.Packages.props | 12 +++++++++ redmine-net-api.sln | 1 + src/redmine-net-api/redmine-net-api.csproj | 31 ++++++++++++++++------ 4 files changed, 48 insertions(+), 8 deletions(-) create mode 100644 Directory.Packages.props diff --git a/Directory.Build.props b/Directory.Build.props index 3d16a055..f2b9a699 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,5 +5,17 @@ strict true + + + true + true + true + true + + + + + + \ No newline at end of file diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 00000000..1c4eba97 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/redmine-net-api.sln b/redmine-net-api.sln index 86cc3bbe..6e9f665f 100644 --- a/redmine-net-api.sln +++ b/redmine-net-api.sln @@ -38,6 +38,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{707B6A3F releasenotes.props = releasenotes.props signing.props = signing.props version.props = version.props + Directory.Packages.props = Directory.Packages.props EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docker", "Docker", "{1D340EEB-C535-45D4-80D7-ADD4434D7B77}" diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 4e1ea59f..28cc2c83 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -25,6 +25,21 @@ CS0618; CA1002; + + NU5105; + CA1303; + CA1056; + CA1062; + CA1707; + CA1716; + CA1724; + CA1806; + CA2227; + CS0612; + CS0618; + CA1002; + SYSLIB0014; + @@ -41,10 +56,6 @@ $(SolutionDir)/artifacts - - - - Adrian Popescu Redmine Api is a .NET rest client for Redmine. @@ -73,15 +84,19 @@ snupkg true + + + + - - - + + + @@ -98,7 +113,7 @@ - + <_Parameter1>Padi.DotNet.RedmineAPI.Tests From 2bde0d02b9a2c1ed9603501fea6c6006239b1572 Mon Sep 17 00:00:00 2001 From: zapadi Date: Sun, 30 Mar 2025 23:03:44 +0300 Subject: [PATCH 445/549] AType --- tests/redmine-net-api.Tests/Infrastructure/AType.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 tests/redmine-net-api.Tests/Infrastructure/AType.cs diff --git a/tests/redmine-net-api.Tests/Infrastructure/AType.cs b/tests/redmine-net-api.Tests/Infrastructure/AType.cs new file mode 100644 index 00000000..37087266 --- /dev/null +++ b/tests/redmine-net-api.Tests/Infrastructure/AType.cs @@ -0,0 +1,13 @@ +using System; +using System.Runtime.CompilerServices; + +namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure; + +internal readonly struct A{ + public static A Is => default; +#pragma warning disable CS0184 // 'is' expression's given expression is never of the provided type + public static bool IsEqual() => Is is A; +#pragma warning restore CS0184 // 'is' expression's given expression is never of the provided type + public static Type Value => typeof(T); + +} \ No newline at end of file From 4bfe64b0335ff8c9dd21fab943caa29cd84dea15 Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 14 May 2025 11:05:22 +0300 Subject: [PATCH 446/549] Add System.Memory --- Directory.Packages.props | 11 ++++++++++- src/redmine-net-api/redmine-net-api.csproj | 5 +++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 1c4eba97..f1465d5b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,4 +1,8 @@ + + |net45|net451|net452|net46|net461| + |net20|net40|net45|net451|net452|net46|net461| + @@ -8,5 +12,10 @@ - + + + + + + \ No newline at end of file diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 28cc2c83..81743a04 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -1,5 +1,9 @@ + + + |net20|net40| + Redmine.Net.Api @@ -87,6 +91,7 @@ + From ce7ca1c3e78947341573a4047a999e5788cd8f61 Mon Sep 17 00:00:00 2001 From: zapadi Date: Mon, 6 Jan 2025 18:35:23 +0200 Subject: [PATCH 447/549] [RedmineConstants] Add AUTHORIZATION_HEADER_KEY --- src/redmine-net-api/RedmineConstants.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/redmine-net-api/RedmineConstants.cs b/src/redmine-net-api/RedmineConstants.cs index e9d00588..f50a9cec 100644 --- a/src/redmine-net-api/RedmineConstants.cs +++ b/src/redmine-net-api/RedmineConstants.cs @@ -48,6 +48,11 @@ public static class RedmineConstants ///
public const string IMPERSONATE_HEADER_KEY = "X-Redmine-Switch-User"; + /// + /// + /// + public const string AUTHORIZATION_HEADER_KEY = "Authorization"; + /// /// /// From 74bd74b557561ac19f98605daa21d5f5ba0c3fa3 Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 14 May 2025 11:21:34 +0300 Subject: [PATCH 448/549] [RedmineConstants] Add more keys ENUMERATION_DOCUMENT_CATEGORIES, GENERATE_PASSWORD, ISSUE_CUSTOM_FIELDS, SEND_INFORMATION --- src/redmine-net-api/RedmineKeys.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/redmine-net-api/RedmineKeys.cs b/src/redmine-net-api/RedmineKeys.cs index 045533cb..03ee061d 100644 --- a/src/redmine-net-api/RedmineKeys.cs +++ b/src/redmine-net-api/RedmineKeys.cs @@ -269,6 +269,10 @@ public static class RedmineKeys /// /// /// + public const string ENUMERATION_DOCUMENT_CATEGORIES = "enumerations/document_categories"; + /// + /// + /// public const string ENUMERATION_ISSUE_PRIORITIES = "enumerations/issue_priorities"; /// /// @@ -326,6 +330,10 @@ public static class RedmineKeys /// /// /// + public const string GENERATE_PASSWORD = "generate_password"; + /// + /// + /// public const string GROUP = "group"; /// /// @@ -383,7 +391,10 @@ public static class RedmineKeys /// /// public const string ISSUE_CATEGORY = "issue_category"; - + /// + /// + /// + public const string ISSUE_CUSTOM_FIELDS = "issue_custom_fields"; /// /// /// @@ -684,6 +695,10 @@ public static class RedmineKeys /// /// /// + public const string SEND_INFORMATION = "send_information"; + /// + /// + /// public const string SEARCH = "search"; /// /// From 3b48664c99641f04526fb47b22c6c46ecb074403 Mon Sep 17 00:00:00 2001 From: zapadi Date: Fri, 25 Apr 2025 17:07:04 +0300 Subject: [PATCH 449/549] RedmineSerializationException --- .../RedmineSerializationException.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/redmine-net-api/Exceptions/RedmineSerializationException.cs diff --git a/src/redmine-net-api/Exceptions/RedmineSerializationException.cs b/src/redmine-net-api/Exceptions/RedmineSerializationException.cs new file mode 100644 index 00000000..0f6b779f --- /dev/null +++ b/src/redmine-net-api/Exceptions/RedmineSerializationException.cs @@ -0,0 +1,48 @@ +using System; + +namespace Redmine.Net.Api.Exceptions; + +/// +/// Represents an error that occurs during JSON serialization or deserialization. +/// +public class RedmineSerializationException : RedmineException +{ + /// + /// Initializes a new instance of the class. + /// + public RedmineSerializationException() + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The error message that explains the reason for the exception. + public RedmineSerializationException(string message) : base(message) + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The error message that explains the reason for the exception. + /// /// The name of the parameter that caused the exception. + public RedmineSerializationException(string message, string paramName) : base(message) + { + ParamName = paramName; + } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public RedmineSerializationException(string message, Exception innerException) : base(message, innerException) + { + } + + /// + /// Gets the name of the parameter that caused the current exception. + /// + public string ParamName { get; } +} \ No newline at end of file From c038f82ff3e4557978d26836bb9e54504d152179 Mon Sep 17 00:00:00 2001 From: zapadi Date: Fri, 25 Apr 2025 17:07:17 +0300 Subject: [PATCH 450/549] WebClientExtensions --- .../Extensions/WebClientExtensions.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/redmine-net-api/Net/WebClient/Extensions/WebClientExtensions.cs diff --git a/src/redmine-net-api/Net/WebClient/Extensions/WebClientExtensions.cs b/src/redmine-net-api/Net/WebClient/Extensions/WebClientExtensions.cs new file mode 100644 index 00000000..ce89706f --- /dev/null +++ b/src/redmine-net-api/Net/WebClient/Extensions/WebClientExtensions.cs @@ -0,0 +1,30 @@ +using Redmine.Net.Api; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Net; +using Redmine.Net.Api.Serialization; + +internal static class WebClientExtensions +{ + public static void ApplyHeaders(this System.Net.WebClient client, RequestOptions options, IRedmineSerializer serializer) + { + client.Headers.Add("Content-Type", options.ContentType ?? serializer.ContentType); + + if (!options.UserAgent.IsNullOrWhiteSpace()) + { + client.Headers.Add("User-Agent", options.UserAgent); + } + + if (!options.ImpersonateUser.IsNullOrWhiteSpace()) + { + client.Headers.Add(RedmineConstants.IMPERSONATE_HEADER_KEY, options.ImpersonateUser); + } + + if (options.Headers is { Count: > 0 }) + { + foreach (var header in options.Headers) + { + client.Headers.Add(header.Key, header.Value); + } + } + } +} \ No newline at end of file From 215623acbf8b03ebf507ecacc16b2ff346d65cb6 Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 11 May 2025 15:00:04 +0300 Subject: [PATCH 451/549] [New] IdentifiableNameExtensions --- .../Extensions/IdentifiableNameExtensions.cs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/redmine-net-api/Extensions/IdentifiableNameExtensions.cs diff --git a/src/redmine-net-api/Extensions/IdentifiableNameExtensions.cs b/src/redmine-net-api/Extensions/IdentifiableNameExtensions.cs new file mode 100644 index 00000000..9e03082e --- /dev/null +++ b/src/redmine-net-api/Extensions/IdentifiableNameExtensions.cs @@ -0,0 +1,65 @@ +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.Extensions +{ + /// + /// + /// + public static class IdentifiableNameExtensions + { + /// + /// Converts an object of type into an object. + /// + /// The type of the entity to convert. Expected to be one of the supported Redmine entity types. + /// The entity object to be converted into an . + /// An object populated with the identifier and name of the specified entity, or null if the entity type is not supported. + public static IdentifiableName ToIdentifiableName(this T entity) where T : class + { + return entity switch + { + CustomField customField => IdentifiableName.Create(customField.Id, customField.Name), + CustomFieldRole customFieldRole => IdentifiableName.Create(customFieldRole.Id, customFieldRole.Name), + DocumentCategory documentCategory => IdentifiableName.Create(documentCategory.Id, documentCategory.Name), + Group group => IdentifiableName.Create(group.Id, group.Name), + GroupUser groupUser => IdentifiableName.Create(groupUser.Id, groupUser.Name), + IssueAllowedStatus issueAllowedStatus => IdentifiableName.Create(issueAllowedStatus.Id, issueAllowedStatus.Name), + IssueCustomField issueCustomField => IdentifiableName.Create(issueCustomField.Id, issueCustomField.Name), + IssuePriority issuePriority => IdentifiableName.Create(issuePriority.Id, issuePriority.Name), + IssueStatus issueStatus => IdentifiableName.Create(issueStatus.Id, issueStatus.Name), + MembershipRole membershipRole => IdentifiableName.Create(membershipRole.Id, membershipRole.Name), + MyAccountCustomField myAccountCustomField => IdentifiableName.Create(myAccountCustomField.Id, myAccountCustomField.Name), + Project project => IdentifiableName.Create(project.Id, project.Name), + ProjectEnabledModule projectEnabledModule => IdentifiableName.Create(projectEnabledModule.Id, projectEnabledModule.Name), + ProjectIssueCategory projectIssueCategory => IdentifiableName.Create(projectIssueCategory.Id, projectIssueCategory.Name), + ProjectTimeEntryActivity projectTimeEntryActivity => IdentifiableName.Create(projectTimeEntryActivity.Id, projectTimeEntryActivity.Name), + ProjectTracker projectTracker => IdentifiableName.Create(projectTracker.Id, projectTracker.Name), + Query query => IdentifiableName.Create(query.Id, query.Name), + Role role => IdentifiableName.Create(role.Id, role.Name), + TimeEntryActivity timeEntryActivity => IdentifiableName.Create(timeEntryActivity.Id, timeEntryActivity.Name), + Tracker tracker => IdentifiableName.Create(tracker.Id, tracker.Name), + UserGroup userGroup => IdentifiableName.Create(userGroup.Id, userGroup.Name), + Version version => IdentifiableName.Create(version.Id, version.Name), + Watcher watcher => IdentifiableName.Create(watcher.Id, watcher.Name), + _ => null + }; + } + + + /// + /// Converts an integer value to an object. + /// + /// An integer value representing the identifier. Must be greater than zero. + /// An object with the specified identifier and a null name. + /// Thrown when the given value is less than or equal to zero. + public static IdentifiableName ToIdentifier(this int val) + { + if (val <= 0) + { + throw new RedmineException(nameof(val), "Value must be greater than zero"); + } + + return new IdentifiableName(val, null); + } + } +} \ No newline at end of file From 1e732fc7403d17bdbecf51ed7fcbe597b4100b46 Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 11 May 2025 15:00:37 +0300 Subject: [PATCH 452/549] [New] EnumExtensions --- .../Extensions/EnumExtensions.cs | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 src/redmine-net-api/Extensions/EnumExtensions.cs diff --git a/src/redmine-net-api/Extensions/EnumExtensions.cs b/src/redmine-net-api/Extensions/EnumExtensions.cs new file mode 100644 index 00000000..5ae7d77f --- /dev/null +++ b/src/redmine-net-api/Extensions/EnumExtensions.cs @@ -0,0 +1,101 @@ +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.Extensions; + +/// +/// Provides extension methods for enumerations used in the Redmine.Net.Api.Types namespace. +/// +public static class EnumExtensions +{ + /// + /// Converts the specified enumeration value to its lowercase invariant string representation. + /// + /// The enumeration value to be converted. + /// A string representation of the IssueRelationType enumeration value in a lowercase, or "undefined" if the value does not match a defined case. + public static string ToLowerInvariant(this IssueRelationType @enum) + { + return @enum switch + { + IssueRelationType.Relates => "relates", + IssueRelationType.Duplicates => "duplicates", + IssueRelationType.Duplicated => "duplicated", + IssueRelationType.Blocks => "blocks", + IssueRelationType.Blocked => "blocked", + IssueRelationType.Precedes => "precedes", + IssueRelationType.Follows => "follows", + IssueRelationType.CopiedTo => "copied_to", + IssueRelationType.CopiedFrom => "copied_from", + _ => "undefined" + }; + } + + /// + /// Converts the specified VersionSharing enumeration value to its lowercase invariant string representation. + /// + /// The VersionSharing enumeration value to be converted. + /// A string representation of the VersionSharing enumeration value in a lowercase, or "undefined" if the value does not match a valid case. + public static string ToLowerInvariant(this VersionSharing @enum) + { + return @enum switch + { + VersionSharing.Unknown => "unknown", + VersionSharing.None => "none", + VersionSharing.Descendants => "descendants", + VersionSharing.Hierarchy => "hierarchy", + VersionSharing.Tree => "tree", + VersionSharing.System => "system", + _ => "undefined" + }; + } + + /// + /// Converts the specified enumeration value to its lowercase invariant string representation. + /// + /// The enumeration value to be converted. + /// A lowercase string representation of the enumeration value, or "undefined" if the value does not match a defined case. + public static string ToLowerInvariant(this VersionStatus @enum) + { + return @enum switch + { + VersionStatus.None => "none", + VersionStatus.Open => "open", + VersionStatus.Closed => "closed", + VersionStatus.Locked => "locked", + _ => "undefined" + }; + } + + /// + /// Converts the specified ProjectStatus enumeration value to its lowercase invariant string representation. + /// + /// The ProjectStatus enumeration value to be converted. + /// A string representation of the ProjectStatus enumeration value in a lowercase, or "undefined" if the value does not match a defined case. + public static string ToLowerInvariant(this ProjectStatus @enum) + { + return @enum switch + { + ProjectStatus.None => "none", + ProjectStatus.Active => "active", + ProjectStatus.Archived => "archived", + ProjectStatus.Closed => "closed", + _ => "undefined" + }; + } + + /// + /// Converts the specified enumeration value to its lowercase invariant string representation. + /// + /// The enumeration value to be converted. + /// A string representation of the UserStatus enumeration value in a lowercase, or "undefined" if the value does not match a defined case. + public static string ToLowerInvariant(this UserStatus @enum) + { + return @enum switch + { + UserStatus.StatusActive => "status_active", + UserStatus.StatusAnonymous => "status_anonymous", + UserStatus.StatusLocked => "status_locked", + UserStatus.StatusRegistered => "status_registered", + _ => "undefined" + }; + } +} \ No newline at end of file From dc05688b88fbfac9b8a063f868e76e714850e069 Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 11 May 2025 15:05:23 +0300 Subject: [PATCH 453/549] [DebuggerDisplay] Replace + with interpolation --- src/redmine-net-api/Types/Attachment.cs | 3 +-- src/redmine-net-api/Types/ChangeSet.cs | 2 +- src/redmine-net-api/Types/CustomField.cs | 3 +-- src/redmine-net-api/Types/CustomFieldPossibleValue.cs | 2 +- src/redmine-net-api/Types/CustomFieldRole.cs | 2 +- src/redmine-net-api/Types/CustomFieldValue.cs | 2 +- src/redmine-net-api/Types/Detail.cs | 2 +- src/redmine-net-api/Types/DocumentCategory.cs | 3 +-- src/redmine-net-api/Types/Error.cs | 2 +- src/redmine-net-api/Types/File.cs | 2 +- src/redmine-net-api/Types/Group.cs | 2 +- src/redmine-net-api/Types/GroupUser.cs | 2 +- src/redmine-net-api/Types/Identifiable.cs | 4 +--- src/redmine-net-api/Types/IdentifiableName.cs | 2 +- src/redmine-net-api/Types/Issue.cs | 2 +- src/redmine-net-api/Types/IssueAllowedStatus.cs | 2 +- src/redmine-net-api/Types/IssueCategory.cs | 2 +- src/redmine-net-api/Types/IssueChild.cs | 2 +- src/redmine-net-api/Types/IssueCustomField.cs | 2 +- src/redmine-net-api/Types/IssuePriority.cs | 3 +-- src/redmine-net-api/Types/IssueRelation.cs | 7 ++----- src/redmine-net-api/Types/IssueStatus.cs | 3 +-- src/redmine-net-api/Types/Journal.cs | 2 +- src/redmine-net-api/Types/Membership.cs | 2 +- src/redmine-net-api/Types/MembershipRole.cs | 2 +- src/redmine-net-api/Types/MyAccount.cs | 3 +-- src/redmine-net-api/Types/MyAccountCustomField.cs | 2 +- src/redmine-net-api/Types/News.cs | 3 +-- src/redmine-net-api/Types/NewsComment.cs | 2 +- src/redmine-net-api/Types/Permission.cs | 2 +- src/redmine-net-api/Types/Project.cs | 3 +-- src/redmine-net-api/Types/ProjectEnabledModule.cs | 2 +- src/redmine-net-api/Types/ProjectIssueCategory.cs | 2 +- src/redmine-net-api/Types/ProjectMembership.cs | 2 +- src/redmine-net-api/Types/ProjectTimeEntryActivity.cs | 2 +- src/redmine-net-api/Types/ProjectTracker.cs | 2 +- src/redmine-net-api/Types/Query.cs | 3 +-- src/redmine-net-api/Types/Role.cs | 2 +- src/redmine-net-api/Types/Search.cs | 3 +-- src/redmine-net-api/Types/TimeEntry.cs | 2 +- src/redmine-net-api/Types/TimeEntryActivity.cs | 3 +-- src/redmine-net-api/Types/Tracker.cs | 2 +- src/redmine-net-api/Types/TrackerCoreField.cs | 2 +- src/redmine-net-api/Types/TrackerCustomField.cs | 2 +- src/redmine-net-api/Types/Upload.cs | 2 +- src/redmine-net-api/Types/User.cs | 2 +- src/redmine-net-api/Types/UserGroup.cs | 2 +- src/redmine-net-api/Types/Version.cs | 3 +-- src/redmine-net-api/Types/Watcher.cs | 2 +- src/redmine-net-api/Types/WikiPage.cs | 2 +- 50 files changed, 51 insertions(+), 68 deletions(-) diff --git a/src/redmine-net-api/Types/Attachment.cs b/src/redmine-net-api/Types/Attachment.cs index 49b6476c..98b937e6 100644 --- a/src/redmine-net-api/Types/Attachment.cs +++ b/src/redmine-net-api/Types/Attachment.cs @@ -16,7 +16,6 @@ limitations under the License. using System; using System.Diagnostics; -using System.Globalization; using System.Xml; using System.Xml.Serialization; using Newtonsoft.Json; @@ -29,7 +28,7 @@ namespace Redmine.Net.Api.Types /// /// Availability 1.3 /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.ATTACHMENT)] public sealed class Attachment : Identifiable diff --git a/src/redmine-net-api/Types/ChangeSet.cs b/src/redmine-net-api/Types/ChangeSet.cs index be873273..22d7156c 100644 --- a/src/redmine-net-api/Types/ChangeSet.cs +++ b/src/redmine-net-api/Types/ChangeSet.cs @@ -30,7 +30,7 @@ namespace Redmine.Net.Api.Types /// /// /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.CHANGE_SET)] public sealed class ChangeSet : IXmlSerializable, IJsonSerializable, IEquatable ,ICloneable diff --git a/src/redmine-net-api/Types/CustomField.cs b/src/redmine-net-api/Types/CustomField.cs index c99a1490..c0ab41ab 100644 --- a/src/redmine-net-api/Types/CustomField.cs +++ b/src/redmine-net-api/Types/CustomField.cs @@ -17,7 +17,6 @@ limitations under the License. using System; using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; using System.Xml; using System.Xml.Serialization; using Newtonsoft.Json; @@ -29,7 +28,7 @@ namespace Redmine.Net.Api.Types /// /// /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.CUSTOM_FIELD)] public sealed class CustomField : IdentifiableName, IEquatable { diff --git a/src/redmine-net-api/Types/CustomFieldPossibleValue.cs b/src/redmine-net-api/Types/CustomFieldPossibleValue.cs index 4a8854f7..571386d5 100644 --- a/src/redmine-net-api/Types/CustomFieldPossibleValue.cs +++ b/src/redmine-net-api/Types/CustomFieldPossibleValue.cs @@ -28,7 +28,7 @@ namespace Redmine.Net.Api.Types /// /// /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.POSSIBLE_VALUE)] public sealed class CustomFieldPossibleValue : IXmlSerializable, IJsonSerializable, IEquatable { diff --git a/src/redmine-net-api/Types/CustomFieldRole.cs b/src/redmine-net-api/Types/CustomFieldRole.cs index bfffbc71..7b1b20a9 100644 --- a/src/redmine-net-api/Types/CustomFieldRole.cs +++ b/src/redmine-net-api/Types/CustomFieldRole.cs @@ -23,7 +23,7 @@ namespace Redmine.Net.Api.Types /// /// /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.ROLE)] public sealed class CustomFieldRole : IdentifiableName { diff --git a/src/redmine-net-api/Types/CustomFieldValue.cs b/src/redmine-net-api/Types/CustomFieldValue.cs index 32a70959..17f0a442 100644 --- a/src/redmine-net-api/Types/CustomFieldValue.cs +++ b/src/redmine-net-api/Types/CustomFieldValue.cs @@ -28,7 +28,7 @@ namespace Redmine.Net.Api.Types /// /// /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.VALUE)] public class CustomFieldValue : IXmlSerializable diff --git a/src/redmine-net-api/Types/Detail.cs b/src/redmine-net-api/Types/Detail.cs index b4738792..8fd71956 100644 --- a/src/redmine-net-api/Types/Detail.cs +++ b/src/redmine-net-api/Types/Detail.cs @@ -28,7 +28,7 @@ namespace Redmine.Net.Api.Types /// /// /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.DETAIL)] public sealed class Detail : IXmlSerializable diff --git a/src/redmine-net-api/Types/DocumentCategory.cs b/src/redmine-net-api/Types/DocumentCategory.cs index 4e01bc96..beda071d 100644 --- a/src/redmine-net-api/Types/DocumentCategory.cs +++ b/src/redmine-net-api/Types/DocumentCategory.cs @@ -16,7 +16,6 @@ limitations under the License. using System; using System.Diagnostics; -using System.Globalization; using System.Xml; using System.Xml.Serialization; using Newtonsoft.Json; @@ -28,7 +27,7 @@ namespace Redmine.Net.Api.Types /// /// Availability 2.2 /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.DOCUMENT_CATEGORY)] public sealed class DocumentCategory : IdentifiableName, IEquatable { diff --git a/src/redmine-net-api/Types/Error.cs b/src/redmine-net-api/Types/Error.cs index f5285573..0ad45ae2 100644 --- a/src/redmine-net-api/Types/Error.cs +++ b/src/redmine-net-api/Types/Error.cs @@ -28,7 +28,7 @@ namespace Redmine.Net.Api.Types /// /// /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.ERROR)] public sealed class Error : IXmlSerializable, IJsonSerializable, IEquatable { diff --git a/src/redmine-net-api/Types/File.cs b/src/redmine-net-api/Types/File.cs index f4f09e30..d218e41b 100644 --- a/src/redmine-net-api/Types/File.cs +++ b/src/redmine-net-api/Types/File.cs @@ -30,7 +30,7 @@ namespace Redmine.Net.Api.Types /// /// /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.FILE)] public sealed class File : Identifiable { diff --git a/src/redmine-net-api/Types/Group.cs b/src/redmine-net-api/Types/Group.cs index 27bc3773..770ce1ec 100644 --- a/src/redmine-net-api/Types/Group.cs +++ b/src/redmine-net-api/Types/Group.cs @@ -29,7 +29,7 @@ namespace Redmine.Net.Api.Types /// /// Availability 2.1 /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.GROUP)] public sealed class Group : IdentifiableName, IEquatable { diff --git a/src/redmine-net-api/Types/GroupUser.cs b/src/redmine-net-api/Types/GroupUser.cs index 598e9462..6a7d9760 100644 --- a/src/redmine-net-api/Types/GroupUser.cs +++ b/src/redmine-net-api/Types/GroupUser.cs @@ -24,7 +24,7 @@ namespace Redmine.Net.Api.Types /// /// /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.USER)] public sealed class GroupUser : IdentifiableName, IValue { diff --git a/src/redmine-net-api/Types/Identifiable.cs b/src/redmine-net-api/Types/Identifiable.cs index 02307d1d..9973b232 100644 --- a/src/redmine-net-api/Types/Identifiable.cs +++ b/src/redmine-net-api/Types/Identifiable.cs @@ -16,7 +16,6 @@ limitations under the License. using System; using System.Diagnostics; -using System.Globalization; using System.Xml; using System.Xml.Schema; using System.Xml.Serialization; @@ -24,7 +23,6 @@ limitations under the License. using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; using Redmine.Net.Api.Serialization; -using NotImplementedException = System.NotImplementedException; namespace Redmine.Net.Api.Types { @@ -32,7 +30,7 @@ namespace Redmine.Net.Api.Types /// /// /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] public abstract class Identifiable : IXmlSerializable, IJsonSerializable, IEquatable , ICloneable> where T : Identifiable diff --git a/src/redmine-net-api/Types/IdentifiableName.cs b/src/redmine-net-api/Types/IdentifiableName.cs index 2a985283..4f95ee02 100644 --- a/src/redmine-net-api/Types/IdentifiableName.cs +++ b/src/redmine-net-api/Types/IdentifiableName.cs @@ -27,7 +27,7 @@ namespace Redmine.Net.Api.Types /// /// /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] public class IdentifiableName : Identifiable , ICloneable { diff --git a/src/redmine-net-api/Types/Issue.cs b/src/redmine-net-api/Types/Issue.cs index 14f55a6f..91c5f2a1 100644 --- a/src/redmine-net-api/Types/Issue.cs +++ b/src/redmine-net-api/Types/Issue.cs @@ -36,7 +36,7 @@ namespace Redmine.Net.Api.Types /// Possible values: children, attachments, relations, changesets and journals. To fetch multiple associations use comma (e.g ?include=relations,journals). /// See Issue journals for more information. /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.ISSUE)] public sealed class Issue : Identifiable diff --git a/src/redmine-net-api/Types/IssueAllowedStatus.cs b/src/redmine-net-api/Types/IssueAllowedStatus.cs index 9a47811a..d2b78d19 100644 --- a/src/redmine-net-api/Types/IssueAllowedStatus.cs +++ b/src/redmine-net-api/Types/IssueAllowedStatus.cs @@ -26,7 +26,7 @@ namespace Redmine.Net.Api.Types /// /// /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.STATUS)] public sealed class IssueAllowedStatus : IdentifiableName { diff --git a/src/redmine-net-api/Types/IssueCategory.cs b/src/redmine-net-api/Types/IssueCategory.cs index 39c5781c..ae25040e 100644 --- a/src/redmine-net-api/Types/IssueCategory.cs +++ b/src/redmine-net-api/Types/IssueCategory.cs @@ -27,7 +27,7 @@ namespace Redmine.Net.Api.Types /// /// Availability 1.3 /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.ISSUE_CATEGORY)] public sealed class IssueCategory : Identifiable { diff --git a/src/redmine-net-api/Types/IssueChild.cs b/src/redmine-net-api/Types/IssueChild.cs index b6d3fcf1..42e1ed29 100644 --- a/src/redmine-net-api/Types/IssueChild.cs +++ b/src/redmine-net-api/Types/IssueChild.cs @@ -28,7 +28,7 @@ namespace Redmine.Net.Api.Types /// /// /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.ISSUE)] public sealed class IssueChild : Identifiable ,ICloneable diff --git a/src/redmine-net-api/Types/IssueCustomField.cs b/src/redmine-net-api/Types/IssueCustomField.cs index da8935c9..f4e4a717 100644 --- a/src/redmine-net-api/Types/IssueCustomField.cs +++ b/src/redmine-net-api/Types/IssueCustomField.cs @@ -29,7 +29,7 @@ namespace Redmine.Net.Api.Types /// /// /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.CUSTOM_FIELD)] public sealed class IssueCustomField : IdentifiableName diff --git a/src/redmine-net-api/Types/IssuePriority.cs b/src/redmine-net-api/Types/IssuePriority.cs index 82522462..b9a5a7a0 100644 --- a/src/redmine-net-api/Types/IssuePriority.cs +++ b/src/redmine-net-api/Types/IssuePriority.cs @@ -16,7 +16,6 @@ limitations under the License. using System; using System.Diagnostics; -using System.Globalization; using System.Xml; using System.Xml.Serialization; using Newtonsoft.Json; @@ -28,7 +27,7 @@ namespace Redmine.Net.Api.Types /// /// Availability 2.2 /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.ISSUE_PRIORITY)] public sealed class IssuePriority : IdentifiableName diff --git a/src/redmine-net-api/Types/IssueRelation.cs b/src/redmine-net-api/Types/IssueRelation.cs index f913221b..fad07f49 100644 --- a/src/redmine-net-api/Types/IssueRelation.cs +++ b/src/redmine-net-api/Types/IssueRelation.cs @@ -16,7 +16,6 @@ limitations under the License. using System; using System.Diagnostics; -using System.Globalization; using System.Xml; using System.Xml.Serialization; using Newtonsoft.Json; @@ -30,11 +29,9 @@ namespace Redmine.Net.Api.Types /// /// Availability 1.3 /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.RELATION)] - public sealed class IssueRelation : - Identifiable - ,ICloneable + public sealed class IssueRelation : Identifiable, ICloneable { #region Properties /// diff --git a/src/redmine-net-api/Types/IssueStatus.cs b/src/redmine-net-api/Types/IssueStatus.cs index 25d2a30a..76d7863e 100644 --- a/src/redmine-net-api/Types/IssueStatus.cs +++ b/src/redmine-net-api/Types/IssueStatus.cs @@ -16,7 +16,6 @@ limitations under the License. using System; using System.Diagnostics; -using System.Globalization; using System.Xml; using System.Xml.Serialization; using Newtonsoft.Json; @@ -28,7 +27,7 @@ namespace Redmine.Net.Api.Types /// /// Availability 1.3 /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.ISSUE_STATUS)] public sealed class IssueStatus : IdentifiableName, IEquatable, ICloneable { diff --git a/src/redmine-net-api/Types/Journal.cs b/src/redmine-net-api/Types/Journal.cs index bf016b1b..ee50b101 100644 --- a/src/redmine-net-api/Types/Journal.cs +++ b/src/redmine-net-api/Types/Journal.cs @@ -29,7 +29,7 @@ namespace Redmine.Net.Api.Types /// /// /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.JOURNAL)] public sealed class Journal : Identifiable diff --git a/src/redmine-net-api/Types/Membership.cs b/src/redmine-net-api/Types/Membership.cs index 4b563614..a33c97ef 100644 --- a/src/redmine-net-api/Types/Membership.cs +++ b/src/redmine-net-api/Types/Membership.cs @@ -27,7 +27,7 @@ namespace Redmine.Net.Api.Types /// /// Only the roles can be updated, the project and the user of a membership are read-only. /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.MEMBERSHIP)] public sealed class Membership : Identifiable { diff --git a/src/redmine-net-api/Types/MembershipRole.cs b/src/redmine-net-api/Types/MembershipRole.cs index 1de8fbfc..2df6f835 100644 --- a/src/redmine-net-api/Types/MembershipRole.cs +++ b/src/redmine-net-api/Types/MembershipRole.cs @@ -28,7 +28,7 @@ namespace Redmine.Net.Api.Types /// /// /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.ROLE)] public sealed class MembershipRole : IdentifiableName, IEquatable, IValue { diff --git a/src/redmine-net-api/Types/MyAccount.cs b/src/redmine-net-api/Types/MyAccount.cs index 806123b4..944a9d32 100644 --- a/src/redmine-net-api/Types/MyAccount.cs +++ b/src/redmine-net-api/Types/MyAccount.cs @@ -17,7 +17,6 @@ limitations under the License. using System; using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; using System.Xml; using System.Xml.Serialization; using Newtonsoft.Json; @@ -31,7 +30,7 @@ namespace Redmine.Net.Api.Types /// /// /// Availability 4.1 - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.USER)] public sealed class MyAccount : Identifiable { diff --git a/src/redmine-net-api/Types/MyAccountCustomField.cs b/src/redmine-net-api/Types/MyAccountCustomField.cs index 55372989..469e198e 100644 --- a/src/redmine-net-api/Types/MyAccountCustomField.cs +++ b/src/redmine-net-api/Types/MyAccountCustomField.cs @@ -27,7 +27,7 @@ namespace Redmine.Net.Api.Types /// /// /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.CUSTOM_FIELD)] public sealed class MyAccountCustomField : IdentifiableName, IEquatable { diff --git a/src/redmine-net-api/Types/News.cs b/src/redmine-net-api/Types/News.cs index c989c6f2..4701ceb3 100644 --- a/src/redmine-net-api/Types/News.cs +++ b/src/redmine-net-api/Types/News.cs @@ -17,7 +17,6 @@ limitations under the License. using System; using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; using System.Xml; using System.Xml.Serialization; using Newtonsoft.Json; @@ -30,7 +29,7 @@ namespace Redmine.Net.Api.Types /// /// Availability 1.1 /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.NEWS)] public sealed class News : Identifiable { diff --git a/src/redmine-net-api/Types/NewsComment.cs b/src/redmine-net-api/Types/NewsComment.cs index 86eca5ed..b80953e4 100644 --- a/src/redmine-net-api/Types/NewsComment.cs +++ b/src/redmine-net-api/Types/NewsComment.cs @@ -27,7 +27,7 @@ namespace Redmine.Net.Api.Types /// /// /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.COMMENT)] public sealed class NewsComment: Identifiable { diff --git a/src/redmine-net-api/Types/Permission.cs b/src/redmine-net-api/Types/Permission.cs index 8d300411..06811090 100644 --- a/src/redmine-net-api/Types/Permission.cs +++ b/src/redmine-net-api/Types/Permission.cs @@ -28,7 +28,7 @@ namespace Redmine.Net.Api.Types /// /// /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.PERMISSION)] #pragma warning disable CA1711 public sealed class Permission : IXmlSerializable, IJsonSerializable, IEquatable diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs index 798ab732..f3041a7b 100644 --- a/src/redmine-net-api/Types/Project.cs +++ b/src/redmine-net-api/Types/Project.cs @@ -17,7 +17,6 @@ limitations under the License. using System; using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; using System.Xml; using System.Xml.Serialization; using Newtonsoft.Json; @@ -30,7 +29,7 @@ namespace Redmine.Net.Api.Types /// /// Availability 1.0 /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.PROJECT)] public sealed class Project : IdentifiableName, IEquatable { diff --git a/src/redmine-net-api/Types/ProjectEnabledModule.cs b/src/redmine-net-api/Types/ProjectEnabledModule.cs index 02d7f60c..343b6006 100644 --- a/src/redmine-net-api/Types/ProjectEnabledModule.cs +++ b/src/redmine-net-api/Types/ProjectEnabledModule.cs @@ -24,7 +24,7 @@ namespace Redmine.Net.Api.Types /// /// the module name: boards, calendar, documents, files, gant, issue_tracking, news, repository, time_tracking, wiki. /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.ENABLED_MODULE)] public sealed class ProjectEnabledModule : IdentifiableName, IValue { diff --git a/src/redmine-net-api/Types/ProjectIssueCategory.cs b/src/redmine-net-api/Types/ProjectIssueCategory.cs index 2ef3b006..6f214692 100644 --- a/src/redmine-net-api/Types/ProjectIssueCategory.cs +++ b/src/redmine-net-api/Types/ProjectIssueCategory.cs @@ -23,7 +23,7 @@ namespace Redmine.Net.Api.Types /// /// /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.ISSUE_CATEGORY)] public sealed class ProjectIssueCategory : IdentifiableName { diff --git a/src/redmine-net-api/Types/ProjectMembership.cs b/src/redmine-net-api/Types/ProjectMembership.cs index cd0cb010..dd1622b9 100644 --- a/src/redmine-net-api/Types/ProjectMembership.cs +++ b/src/redmine-net-api/Types/ProjectMembership.cs @@ -34,7 +34,7 @@ namespace Redmine.Net.Api.Types /// PUT - Updates the membership of given :id. Only the roles can be updated, the project and the user of a membership are read-only. /// DELETE - Deletes a memberships. Memberships inherited from a group membership can not be deleted. You must delete the group membership. /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.MEMBERSHIP)] public sealed class ProjectMembership : Identifiable { diff --git a/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs b/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs index eded97b3..823de240 100644 --- a/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs +++ b/src/redmine-net-api/Types/ProjectTimeEntryActivity.cs @@ -23,7 +23,7 @@ namespace Redmine.Net.Api.Types /// /// /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.TIME_ENTRY_ACTIVITY)] public sealed class ProjectTimeEntryActivity : IdentifiableName { diff --git a/src/redmine-net-api/Types/ProjectTracker.cs b/src/redmine-net-api/Types/ProjectTracker.cs index fa026ee7..d6c64130 100644 --- a/src/redmine-net-api/Types/ProjectTracker.cs +++ b/src/redmine-net-api/Types/ProjectTracker.cs @@ -24,7 +24,7 @@ namespace Redmine.Net.Api.Types /// /// /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.TRACKER)] public sealed class ProjectTracker : IdentifiableName, IValue { diff --git a/src/redmine-net-api/Types/Query.cs b/src/redmine-net-api/Types/Query.cs index 09dc4f56..f0e51d8a 100644 --- a/src/redmine-net-api/Types/Query.cs +++ b/src/redmine-net-api/Types/Query.cs @@ -16,7 +16,6 @@ limitations under the License. using System; using System.Diagnostics; -using System.Globalization; using System.Xml; using System.Xml.Serialization; using Newtonsoft.Json; @@ -28,7 +27,7 @@ namespace Redmine.Net.Api.Types /// /// Availability 1.3 /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.QUERY)] public sealed class Query : IdentifiableName, IEquatable { diff --git a/src/redmine-net-api/Types/Role.cs b/src/redmine-net-api/Types/Role.cs index fd9dbab2..7a4718a5 100644 --- a/src/redmine-net-api/Types/Role.cs +++ b/src/redmine-net-api/Types/Role.cs @@ -28,7 +28,7 @@ namespace Redmine.Net.Api.Types /// /// Availability 1.4 /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.ROLE)] public sealed class Role : IdentifiableName, IEquatable { diff --git a/src/redmine-net-api/Types/Search.cs b/src/redmine-net-api/Types/Search.cs index a0ec1835..2397f7fa 100644 --- a/src/redmine-net-api/Types/Search.cs +++ b/src/redmine-net-api/Types/Search.cs @@ -16,7 +16,6 @@ limitations under the License. using System; using System.Diagnostics; -using System.Globalization; using System.Xml; using System.Xml.Schema; using System.Xml.Serialization; @@ -30,7 +29,7 @@ namespace Redmine.Net.Api.Types /// /// /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.RESULT)] public sealed class Search: IXmlSerializable, IJsonSerializable, IEquatable { diff --git a/src/redmine-net-api/Types/TimeEntry.cs b/src/redmine-net-api/Types/TimeEntry.cs index bdc8cd64..e8c147c9 100644 --- a/src/redmine-net-api/Types/TimeEntry.cs +++ b/src/redmine-net-api/Types/TimeEntry.cs @@ -31,7 +31,7 @@ namespace Redmine.Net.Api.Types /// /// Availability 1.1 /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.TIME_ENTRY)] public sealed class TimeEntry : Identifiable , ICloneable diff --git a/src/redmine-net-api/Types/TimeEntryActivity.cs b/src/redmine-net-api/Types/TimeEntryActivity.cs index dc571fff..42f31826 100644 --- a/src/redmine-net-api/Types/TimeEntryActivity.cs +++ b/src/redmine-net-api/Types/TimeEntryActivity.cs @@ -16,7 +16,6 @@ limitations under the License. using System; using System.Diagnostics; -using System.Globalization; using System.Xml; using System.Xml.Serialization; using Newtonsoft.Json; @@ -28,7 +27,7 @@ namespace Redmine.Net.Api.Types /// /// Availability 2.2 /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.TIME_ENTRY_ACTIVITY)] public sealed class TimeEntryActivity : IdentifiableName, IEquatable { diff --git a/src/redmine-net-api/Types/Tracker.cs b/src/redmine-net-api/Types/Tracker.cs index be916923..f13d8f6b 100644 --- a/src/redmine-net-api/Types/Tracker.cs +++ b/src/redmine-net-api/Types/Tracker.cs @@ -28,7 +28,7 @@ namespace Redmine.Net.Api.Types /// /// Availability 1.3 /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.TRACKER)] public class Tracker : IdentifiableName, IEquatable { diff --git a/src/redmine-net-api/Types/TrackerCoreField.cs b/src/redmine-net-api/Types/TrackerCoreField.cs index 2677a1eb..a91c25e6 100644 --- a/src/redmine-net-api/Types/TrackerCoreField.cs +++ b/src/redmine-net-api/Types/TrackerCoreField.cs @@ -12,7 +12,7 @@ namespace Redmine.Net.Api.Types /// /// /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.FIELD)] public sealed class TrackerCoreField: IXmlSerializable, IJsonSerializable, IEquatable { diff --git a/src/redmine-net-api/Types/TrackerCustomField.cs b/src/redmine-net-api/Types/TrackerCustomField.cs index 8464d220..f4250d4b 100644 --- a/src/redmine-net-api/Types/TrackerCustomField.cs +++ b/src/redmine-net-api/Types/TrackerCustomField.cs @@ -25,7 +25,7 @@ namespace Redmine.Net.Api.Types /// /// /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.TRACKER)] public sealed class TrackerCustomField : Tracker { diff --git a/src/redmine-net-api/Types/Upload.cs b/src/redmine-net-api/Types/Upload.cs index 12d57053..616a8868 100644 --- a/src/redmine-net-api/Types/Upload.cs +++ b/src/redmine-net-api/Types/Upload.cs @@ -29,7 +29,7 @@ namespace Redmine.Net.Api.Types /// /// Support for adding attachments through the REST API is added in Redmine 1.4.0. /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.UPLOAD)] public sealed class Upload : IXmlSerializable, IJsonSerializable, IEquatable , ICloneable diff --git a/src/redmine-net-api/Types/User.cs b/src/redmine-net-api/Types/User.cs index 19679607..368ead09 100644 --- a/src/redmine-net-api/Types/User.cs +++ b/src/redmine-net-api/Types/User.cs @@ -30,7 +30,7 @@ namespace Redmine.Net.Api.Types /// /// Availability 1.1 /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.USER)] public sealed class User : Identifiable { diff --git a/src/redmine-net-api/Types/UserGroup.cs b/src/redmine-net-api/Types/UserGroup.cs index 660c9051..55697d26 100644 --- a/src/redmine-net-api/Types/UserGroup.cs +++ b/src/redmine-net-api/Types/UserGroup.cs @@ -23,7 +23,7 @@ namespace Redmine.Net.Api.Types /// /// /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.GROUP)] public sealed class UserGroup : IdentifiableName { diff --git a/src/redmine-net-api/Types/Version.cs b/src/redmine-net-api/Types/Version.cs index 2e4dd096..884aca64 100644 --- a/src/redmine-net-api/Types/Version.cs +++ b/src/redmine-net-api/Types/Version.cs @@ -17,7 +17,6 @@ limitations under the License. using System; using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; using System.Xml; using System.Xml.Serialization; using Newtonsoft.Json; @@ -30,7 +29,7 @@ namespace Redmine.Net.Api.Types /// /// Availability 1.3 /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.VERSION)] public sealed class Version : Identifiable { diff --git a/src/redmine-net-api/Types/Watcher.cs b/src/redmine-net-api/Types/Watcher.cs index b990a464..24d39693 100644 --- a/src/redmine-net-api/Types/Watcher.cs +++ b/src/redmine-net-api/Types/Watcher.cs @@ -25,7 +25,7 @@ namespace Redmine.Net.Api.Types /// /// /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.USER)] public sealed class Watcher : IdentifiableName ,IEquatable diff --git a/src/redmine-net-api/Types/WikiPage.cs b/src/redmine-net-api/Types/WikiPage.cs index 76125cef..b2c0dfad 100644 --- a/src/redmine-net-api/Types/WikiPage.cs +++ b/src/redmine-net-api/Types/WikiPage.cs @@ -29,7 +29,7 @@ namespace Redmine.Net.Api.Types /// /// Availability 2.2 /// - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.WIKI_PAGE)] public sealed class WikiPage : Identifiable { From 937d3c3fbfde6c407d31bbf85735add3c2369b28 Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 11 May 2025 15:06:36 +0300 Subject: [PATCH 454/549] [IRedmineSerializer] Add ContentType --- src/redmine-net-api/Serialization/IRedmineSerializer.cs | 5 +++++ .../Serialization/Json/JsonRedmineSerializer.cs | 2 ++ .../Serialization/Xml/XmlRedmineSerializer.cs | 2 ++ 3 files changed, 9 insertions(+) diff --git a/src/redmine-net-api/Serialization/IRedmineSerializer.cs b/src/redmine-net-api/Serialization/IRedmineSerializer.cs index e6e064f4..6116b15d 100644 --- a/src/redmine-net-api/Serialization/IRedmineSerializer.cs +++ b/src/redmine-net-api/Serialization/IRedmineSerializer.cs @@ -25,6 +25,11 @@ internal interface IRedmineSerializer /// Gets the application format this serializer supports (e.g. "json", "xml"). /// string Format { get; } + + /// + /// + /// + string ContentType { get; } /// /// Serializes the specified object into a string. diff --git a/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs b/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs index 41f66958..f279abe3 100644 --- a/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs +++ b/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs @@ -136,6 +136,8 @@ internal sealed class JsonRedmineSerializer : IRedmineSerializer #pragma warning restore CA1822 public string Format { get; } = "json"; + + public string ContentType { get; } = "application/json"; public string Serialize(T entity) where T : class { diff --git a/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs b/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs index 253543c7..134fac5f 100644 --- a/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs +++ b/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs @@ -79,6 +79,8 @@ public XmlRedmineSerializer(XmlWriterSettings xmlWriterSettings) #pragma warning restore CA1822 public string Format => RedmineConstants.XML; + + public string ContentType { get; } = "application/xml"; public string Serialize(T entity) where T : class { From e128a7406c0904ede247f365a1b2d0a380adf400 Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 11 May 2025 15:08:56 +0300 Subject: [PATCH 455/549] Split ICollectionExtensions into List & Enumerable extensions --- .../Extensions/CollectionExtensions.cs | 113 --------------- .../Extensions/EnumerableExtensions.cs | 47 +++++++ .../Extensions/ListExtensions.cs | 129 ++++++++++++++++++ 3 files changed, 176 insertions(+), 113 deletions(-) delete mode 100755 src/redmine-net-api/Extensions/CollectionExtensions.cs create mode 100644 src/redmine-net-api/Extensions/EnumerableExtensions.cs create mode 100755 src/redmine-net-api/Extensions/ListExtensions.cs diff --git a/src/redmine-net-api/Extensions/CollectionExtensions.cs b/src/redmine-net-api/Extensions/CollectionExtensions.cs deleted file mode 100755 index 6b56e79c..00000000 --- a/src/redmine-net-api/Extensions/CollectionExtensions.cs +++ /dev/null @@ -1,113 +0,0 @@ -/* - Copyright 2011 - 2025 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Text; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.Extensions -{ - /// - /// - /// - public static class CollectionExtensions - { - /// - /// Clones the specified list to clone. - /// - /// - /// The list to clone. - /// - /// - public static IList Clone(this IList listToClone, bool resetId) where T : ICloneable - { - if (listToClone == null) - { - return null; - } - - var clonedList = new List(); - - for (var index = 0; index < listToClone.Count; index++) - { - var item = listToClone[index]; - clonedList.Add(item.Clone(resetId)); - } - - return clonedList; - } - - /// - /// - /// - /// - /// The list. - /// The list to compare. - /// - public static bool Equals(this IList list, IList listToCompare) where T : class - { - if (list == null || listToCompare == null) - { - return false; - } - - if (list.Count != listToCompare.Count) - { - return false; - } - - var index = 0; - while (index < list.Count && list[index].Equals(listToCompare[index])) - { - index++; - } - - return index == list.Count; - } - - /// - /// - /// - /// - public static string Dump(this IEnumerable collection) where TIn : class - { - if (collection == null) - { - return null; - } - - var sb = new StringBuilder("{"); - - foreach (var item in collection) - { - sb.Append(item).Append(','); - } - - if (sb.Length > 1) - { - sb.Length -= 1; - } - - sb.Append('}'); - - var str = sb.ToString(); - sb.Length = 0; - - return str; - } - } -} \ No newline at end of file diff --git a/src/redmine-net-api/Extensions/EnumerableExtensions.cs b/src/redmine-net-api/Extensions/EnumerableExtensions.cs new file mode 100644 index 00000000..e5ae149f --- /dev/null +++ b/src/redmine-net-api/Extensions/EnumerableExtensions.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Text; + +namespace Redmine.Net.Api.Extensions; + +/// +/// Provides extension methods for IEnumerable types. +/// +public static class EnumerableExtensions +{ + /// + /// Converts a collection of objects into a string representation with each item separated by a comma + /// and enclosed within curly braces. + /// + /// The type of items in the collection. The type must be a reference type. + /// The collection of items to convert to a string representation. + /// + /// Returns a string containing all the items from the collection, separated by commas and + /// enclosed within curly braces. Returns null if the collection is null. + /// + public static string Dump(this IEnumerable collection) where TIn : class + { + if (collection == null) + { + return null; + } + + var sb = new StringBuilder("{"); + + foreach (var item in collection) + { + sb.Append(item).Append(','); + } + + if (sb.Length > 1) + { + sb.Length -= 1; + } + + sb.Append('}'); + + var str = sb.ToString(); + sb.Length = 0; + + return str; + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Extensions/ListExtensions.cs b/src/redmine-net-api/Extensions/ListExtensions.cs new file mode 100755 index 00000000..a8dce8c1 --- /dev/null +++ b/src/redmine-net-api/Extensions/ListExtensions.cs @@ -0,0 +1,129 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System.Collections.Generic; + +namespace Redmine.Net.Api.Extensions +{ + /// + /// Provides extension methods for operations on lists. + /// + public static class ListExtensions + { + /// + /// Creates a deep clone of the specified list. + /// + /// The type of elements in the list. Must implement . + /// The list to be cloned. + /// Specifies whether to reset the ID for each cloned item. + /// A new list containing cloned copies of the elements from the original list. Returns null if the original list is null. + public static IList Clone(this IList listToClone, bool resetId) where T : ICloneable + { + if (listToClone == null) + { + return null; + } + + var clonedList = new List(listToClone.Count); + + foreach (var item in listToClone) + { + clonedList.Add(item.Clone(resetId)); + } + + return clonedList; + } + + /// + /// Creates a deep clone of the specified list. + /// + /// The type of elements in the list. Must implement . + /// The list to be cloned. + /// Specifies whether to reset the ID for each cloned item. + /// A new list containing cloned copies of the elements from the original list. Returns null if the original list is null. + public static IList Clone(this List listToClone, bool resetId) where T : ICloneable + { + if (listToClone == null) + { + return null; + } + + var clonedList = new List(listToClone.Count); + + foreach (var item in listToClone) + { + clonedList.Add(item.Clone(resetId)); + } + return clonedList; + } + + /// + /// Compares two lists for equality by checking if they contain the same elements in the same order. + /// + /// The type of elements in the lists. Must be a reference type. + /// The first list to be compared. + /// The second list to be compared. + /// True if both lists contain the same elements in the same order; otherwise, false. Returns false if either list is null. + public static bool Equals(this IList list, IList listToCompare) where T : class + { + if (list == null || listToCompare == null) + { + return false; + } + + if (list.Count != listToCompare.Count) + { + return false; + } + + var index = 0; + while (index < list.Count && list[index].Equals(listToCompare[index])) + { + index++; + } + + return index == list.Count; + } + + /// + /// Compares two lists for equality based on their elements. + /// + /// The type of elements in the lists. Must be a reference type. + /// The first list to compare. + /// The second list to compare. + /// True if both lists are non-null, have the same count, and all corresponding elements are equal; otherwise, false. + public static bool Equals(this List list, List listToCompare) where T : class + { + if (list == null || listToCompare == null) + { + return false; + } + + if (list.Count != listToCompare.Count) + { + return false; + } + + var index = 0; + while (index < list.Count && list[index].Equals(listToCompare[index])) + { + index++; + } + + return index == list.Count; + } + } +} \ No newline at end of file From 3e5a9614e2048ff82dbf87a4fc7eb593330c30c7 Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 11 May 2025 15:12:54 +0300 Subject: [PATCH 456/549] Split IRedmineApiClient into IAsync|SyncRedmineApiClient --- .../Net/IAsyncRedmineApiClient.cs | 41 +++++++++++++++++++ src/redmine-net-api/Net/IRedmineApiClient.cs | 29 ++----------- .../Net/ISyncRedmineApiClient.cs | 36 ++++++++++++++++ 3 files changed, 81 insertions(+), 25 deletions(-) create mode 100644 src/redmine-net-api/Net/IAsyncRedmineApiClient.cs create mode 100644 src/redmine-net-api/Net/ISyncRedmineApiClient.cs diff --git a/src/redmine-net-api/Net/IAsyncRedmineApiClient.cs b/src/redmine-net-api/Net/IAsyncRedmineApiClient.cs new file mode 100644 index 00000000..b4b7bc50 --- /dev/null +++ b/src/redmine-net-api/Net/IAsyncRedmineApiClient.cs @@ -0,0 +1,41 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#if !(NET20 || NET35) +using System.Threading; +using System.Threading.Tasks; + +namespace Redmine.Net.Api.Net; + +internal interface IAsyncRedmineApiClient +{ + Task GetAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + + Task GetPagedAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + + Task CreateAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + + Task UpdateAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + + Task PatchAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + + Task DeleteAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + + Task UploadFileAsync(string address, byte[] data, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + + Task DownloadAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); +} +#endif \ No newline at end of file diff --git a/src/redmine-net-api/Net/IRedmineApiClient.cs b/src/redmine-net-api/Net/IRedmineApiClient.cs index f9ffc4f8..2116dedf 100644 --- a/src/redmine-net-api/Net/IRedmineApiClient.cs +++ b/src/redmine-net-api/Net/IRedmineApiClient.cs @@ -14,35 +14,14 @@ You may obtain a copy of the License at limitations under the License. */ -using System.Threading; -#if!(NET20) -using System.Threading.Tasks; -#endif - namespace Redmine.Net.Api.Net; /// /// /// -internal interface IRedmineApiClient +internal interface IRedmineApiClient : ISyncRedmineApiClient +#if !(NET20 || NET35) + , IAsyncRedmineApiClient +#endif { - ApiResponseMessage Get(string address, RequestOptions requestOptions = null); - ApiResponseMessage GetPaged(string address, RequestOptions requestOptions = null); - ApiResponseMessage Create(string address, string payload, RequestOptions requestOptions = null); - ApiResponseMessage Update(string address, string payload, RequestOptions requestOptions = null); - ApiResponseMessage Patch(string address, string payload, RequestOptions requestOptions = null); - ApiResponseMessage Delete(string address, RequestOptions requestOptions = null); - ApiResponseMessage Upload(string address, byte[] data, RequestOptions requestOptions = null); - ApiResponseMessage Download(string address, RequestOptions requestOptions = null); - - #if !(NET20) - Task GetAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); - Task GetPagedAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); - Task CreateAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); - Task UpdateAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); - Task PatchAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); - Task DeleteAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); - Task UploadFileAsync(string address, byte[] data, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); - Task DownloadAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); - #endif } \ No newline at end of file diff --git a/src/redmine-net-api/Net/ISyncRedmineApiClient.cs b/src/redmine-net-api/Net/ISyncRedmineApiClient.cs new file mode 100644 index 00000000..8141ae0e --- /dev/null +++ b/src/redmine-net-api/Net/ISyncRedmineApiClient.cs @@ -0,0 +1,36 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +namespace Redmine.Net.Api.Net; + +internal interface ISyncRedmineApiClient +{ + ApiResponseMessage Get(string address, RequestOptions requestOptions = null); + + ApiResponseMessage GetPaged(string address, RequestOptions requestOptions = null); + + ApiResponseMessage Create(string address, string payload, RequestOptions requestOptions = null); + + ApiResponseMessage Update(string address, string payload, RequestOptions requestOptions = null); + + ApiResponseMessage Patch(string address, string payload, RequestOptions requestOptions = null); + + ApiResponseMessage Delete(string address, RequestOptions requestOptions = null); + + ApiResponseMessage Upload(string address, byte[] data, RequestOptions requestOptions = null); + + ApiResponseMessage Download(string address, RequestOptions requestOptions = null); +} \ No newline at end of file From 8c4796aa4e34cde0956e8bd5348fb90444238e36 Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 11 May 2025 15:20:17 +0300 Subject: [PATCH 457/549] Split InternalRedmineApiWebClient into sync|async --- .../InternalRedmineApiWebClient.Async.cs | 140 ++++++++++++++++++ .../WebClient/InternalRedmineApiWebClient.cs | 124 +--------------- 2 files changed, 144 insertions(+), 120 deletions(-) create mode 100644 src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.Async.cs diff --git a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.Async.cs b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.Async.cs new file mode 100644 index 00000000..f2db7185 --- /dev/null +++ b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.Async.cs @@ -0,0 +1,140 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#if!(NET20) +using System.Collections.Specialized; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Net.WebClient.MessageContent; + +namespace Redmine.Net.Api.Net.WebClient +{ + /// + /// + /// + internal sealed partial class InternalRedmineApiWebClient + { + public async Task GetAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + return await HandleRequestAsync(address, HttpVerbs.GET, requestOptions, cancellationToken:cancellationToken).ConfigureAwait(false); + } + + public async Task GetPagedAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + return await GetAsync(address, requestOptions, cancellationToken).ConfigureAwait(false); + } + + public async Task CreateAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var content = new StringApiRequestMessageContent(payload, _serializer.ContentType); + return await HandleRequestAsync(address, HttpVerbs.POST, requestOptions, content, cancellationToken:cancellationToken).ConfigureAwait(false); + } + + public async Task UpdateAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var content = new StringApiRequestMessageContent(payload, _serializer.ContentType); + return await HandleRequestAsync(address, HttpVerbs.PUT, requestOptions, content, cancellationToken:cancellationToken).ConfigureAwait(false); + } + + public async Task UploadFileAsync(string address, byte[] data, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var content = new StreamApiRequestMessageContent(data); + return await HandleRequestAsync(address, HttpVerbs.POST, requestOptions, content, cancellationToken:cancellationToken).ConfigureAwait(false); + } + + public async Task PatchAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var content = new StringApiRequestMessageContent(payload, _serializer.ContentType); + return await HandleRequestAsync(address, HttpVerbs.PATCH, requestOptions, content, cancellationToken:cancellationToken).ConfigureAwait(false); + } + + public async Task DeleteAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + return await HandleRequestAsync(address, HttpVerbs.DELETE, requestOptions, cancellationToken:cancellationToken).ConfigureAwait(false); + } + + public async Task DownloadAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + return await HandleRequestAsync(address, HttpVerbs.DOWNLOAD, requestOptions, cancellationToken:cancellationToken).ConfigureAwait(false); + } + + private async Task HandleRequestAsync(string address, string verb, RequestOptions requestOptions = null, ApiRequestMessageContent content = null, CancellationToken cancellationToken = default) + { + return await SendAsync(CreateRequestMessage(address, verb, requestOptions, content), cancellationToken).ConfigureAwait(false); + } + + private async Task SendAsync(ApiRequestMessage requestMessage, CancellationToken cancellationToken) + { + System.Net.WebClient webClient = null; + byte[] response = null; + NameValueCollection responseHeaders = null; + try + { + webClient = _webClientFunc(); + + cancellationToken.Register(webClient.CancelAsync); + + SetWebClientHeaders(webClient, requestMessage); + + if(IsGetOrDownload(requestMessage.Method)) + { + response = await webClient.DownloadDataTaskAsync(requestMessage.RequestUri) + .ConfigureAwait(false); + } + else + { + byte[] payload; + if (requestMessage.Content != null) + { + webClient.Headers.Add(HttpRequestHeader.ContentType, requestMessage.Content.ContentType); + payload = requestMessage.Content.Body; + } + else + { + payload = EmptyBytes; + } + + response = await webClient.UploadDataTaskAsync(requestMessage.RequestUri, requestMessage.Method, payload) + .ConfigureAwait(false); + } + + responseHeaders = webClient.ResponseHeaders; + } + catch (WebException ex) when (ex.Status == WebExceptionStatus.RequestCanceled) + { + //TODO: Handle cancellation... + } + catch (WebException webException) + { + webException.HandleWebException(_serializer); + } + finally + { + webClient?.Dispose(); + } + + return new ApiResponseMessage() + { + Headers = responseHeaders, + Content = response + }; + } + } +} + +#endif \ No newline at end of file diff --git a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs index df94c7fa..1b3ee390 100644 --- a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs +++ b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs @@ -18,11 +18,7 @@ limitations under the License. using System.Collections.Specialized; using System.Net; using System.Text; -using System.Threading; using Redmine.Net.Api.Authentication; -#if!(NET20) -using System.Threading.Tasks; -#endif using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Net.WebClient.MessageContent; using Redmine.Net.Api.Serialization; @@ -32,7 +28,7 @@ namespace Redmine.Net.Api.Net.WebClient /// /// /// - internal sealed class InternalRedmineApiWebClient : IRedmineApiClient + internal sealed partial class InternalRedmineApiWebClient : IRedmineApiClient { private static readonly byte[] EmptyBytes = Encoding.UTF8.GetBytes(string.Empty); private readonly Func _webClientFunc; @@ -111,19 +107,19 @@ public ApiResponseMessage GetPaged(string address, RequestOptions requestOptions public ApiResponseMessage Create(string address, string payload, RequestOptions requestOptions = null) { - var content = new StringApiRequestMessageContent(payload, GetContentType(_serializer)); + var content = new StringApiRequestMessageContent(payload, _serializer.ContentType); return HandleRequest(address, HttpVerbs.POST, requestOptions, content); } public ApiResponseMessage Update(string address, string payload, RequestOptions requestOptions = null) { - var content = new StringApiRequestMessageContent(payload, GetContentType(_serializer)); + var content = new StringApiRequestMessageContent(payload, _serializer.ContentType); return HandleRequest(address, HttpVerbs.PUT, requestOptions, content); } public ApiResponseMessage Patch(string address, string payload, RequestOptions requestOptions = null) { - var content = new StringApiRequestMessageContent(payload, GetContentType(_serializer)); + var content = new StringApiRequestMessageContent(payload, _serializer.ContentType); return HandleRequest(address, HttpVerbs.PATCH, requestOptions, content); } @@ -143,113 +139,6 @@ public ApiResponseMessage Upload(string address, byte[] data, RequestOptions req return HandleRequest(address, HttpVerbs.POST, requestOptions, content); } - #if !(NET20) - public async Task GetAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) - { - return await HandleRequestAsync(address, HttpVerbs.GET, requestOptions, cancellationToken:cancellationToken).ConfigureAwait(false); - } - - public Task GetPagedAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) - { - return GetAsync(address, requestOptions, cancellationToken); - } - - public async Task CreateAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) - { - var content = new StringApiRequestMessageContent(payload, GetContentType(_serializer)); - return await HandleRequestAsync(address, HttpVerbs.POST, requestOptions, content, cancellationToken:cancellationToken).ConfigureAwait(false); - } - - public async Task UpdateAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) - { - var content = new StringApiRequestMessageContent(payload, GetContentType(_serializer)); - return await HandleRequestAsync(address, HttpVerbs.PUT, requestOptions, content, cancellationToken:cancellationToken).ConfigureAwait(false); - } - - public async Task UploadFileAsync(string address, byte[] data, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) - { - var content = new StreamApiRequestMessageContent(data); - return await HandleRequestAsync(address, HttpVerbs.POST, requestOptions, content, cancellationToken:cancellationToken).ConfigureAwait(false); - } - - public async Task PatchAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) - { - var content = new StringApiRequestMessageContent(payload, GetContentType(_serializer)); - return await HandleRequestAsync(address, HttpVerbs.PATCH, requestOptions, content, cancellationToken:cancellationToken).ConfigureAwait(false); - } - - public async Task DeleteAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) - { - return await HandleRequestAsync(address, HttpVerbs.DELETE, requestOptions, cancellationToken:cancellationToken).ConfigureAwait(false); - } - - public async Task DownloadAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) - { - return await HandleRequestAsync(address, HttpVerbs.DOWNLOAD, requestOptions, cancellationToken:cancellationToken).ConfigureAwait(false); - } - - private Task HandleRequestAsync(string address, string verb, RequestOptions requestOptions = null, ApiRequestMessageContent content = null, CancellationToken cancellationToken = default) - { - return SendAsync(CreateRequestMessage(address, verb, requestOptions, content), cancellationToken); - } - - private async Task SendAsync(ApiRequestMessage requestMessage, CancellationToken cancellationToken) - { - System.Net.WebClient webClient = null; - byte[] response = null; - NameValueCollection responseHeaders = null; - try - { - webClient = _webClientFunc(); - - cancellationToken.Register(webClient.CancelAsync); - - SetWebClientHeaders(webClient, requestMessage); - - if(IsGetOrDownload(requestMessage.Method)) - { - response = await webClient.DownloadDataTaskAsync(requestMessage.RequestUri).ConfigureAwait(false); - } - else - { - byte[] payload; - if (requestMessage.Content != null) - { - webClient.Headers.Add(HttpRequestHeader.ContentType, requestMessage.Content.ContentType); - payload = requestMessage.Content.Body; - } - else - { - payload = EmptyBytes; - } - - response = await webClient.UploadDataTaskAsync(requestMessage.RequestUri, requestMessage.Method, payload).ConfigureAwait(false); - } - - responseHeaders = webClient.ResponseHeaders; - } - catch (WebException ex) when (ex.Status == WebExceptionStatus.RequestCanceled) - { - //TODO: Handle cancellation... - } - catch (WebException webException) - { - webException.HandleWebException(_serializer); - } - finally - { - webClient?.Dispose(); - } - - return new ApiResponseMessage() - { - Headers = responseHeaders, - Content = response - }; - } - #endif - - private static ApiRequestMessage CreateRequestMessage(string address, string verb, RequestOptions requestOptions = null, ApiRequestMessageContent content = null) { var req = new ApiRequestMessage() @@ -353,10 +242,5 @@ private static bool IsGetOrDownload(string method) { return method is HttpVerbs.GET or HttpVerbs.DOWNLOAD; } - - private static string GetContentType(IRedmineSerializer serializer) - { - return serializer.Format == RedmineConstants.XML ? RedmineConstants.CONTENT_TYPE_APPLICATION_XML : RedmineConstants.CONTENT_TYPE_APPLICATION_JSON; - } } } From e27ed8ee0ac8ce1ea51b26adf80fffd6b721bb45 Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 11 May 2025 15:15:08 +0300 Subject: [PATCH 458/549] [RedmineManager] Small ctor refactor --- .../RedmineManager.Obsolete.cs | 6 +- src/redmine-net-api/RedmineManager.cs | 83 ++++++++++--------- 2 files changed, 47 insertions(+), 42 deletions(-) diff --git a/src/redmine-net-api/RedmineManager.Obsolete.cs b/src/redmine-net-api/RedmineManager.Obsolete.cs index 950ecafb..5927a39a 100644 --- a/src/redmine-net-api/RedmineManager.Obsolete.cs +++ b/src/redmine-net-api/RedmineManager.Obsolete.cs @@ -154,7 +154,7 @@ public RedmineManager(string host, string login, string password, MimeFormat mim /// /// [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - public TimeSpan? Timeout { get; } + public TimeSpan? Timeout { get; private set; } /// /// Gets the host. @@ -190,7 +190,7 @@ public RedmineManager(string host, string login, string password, MimeFormat mim /// The proxy. /// [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - public IWebProxy Proxy { get; } + public IWebProxy Proxy { get; private set; } /// /// Gets the type of the security protocol. @@ -199,7 +199,7 @@ public RedmineManager(string host, string login, string password, MimeFormat mim /// The type of the security protocol. /// [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - public SecurityProtocolType SecurityProtocolType { get; } + public SecurityProtocolType SecurityProtocolType { get; private set; } /// diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index 6e10e7b4..41b64118 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -50,29 +50,10 @@ public RedmineManager(RedmineManagerOptionsBuilder optionsBuilder) ArgumentNullThrowHelper.ThrowIfNull(optionsBuilder, nameof(optionsBuilder)); _redmineManagerOptions = optionsBuilder.Build(); - #if NET45_OR_GREATER - if (_redmineManagerOptions.VerifyServerCert) - { - _redmineManagerOptions.WebClientOptions.ServerCertificateValidationCallback = RemoteCertValidate; - } - #endif - - if (_redmineManagerOptions.WebClientOptions is RedmineWebClientOptions) - { - Proxy = _redmineManagerOptions.WebClientOptions.Proxy; - Timeout = _redmineManagerOptions.WebClientOptions.Timeout; - SecurityProtocolType = _redmineManagerOptions.WebClientOptions.SecurityProtocolType.GetValueOrDefault(); - #pragma warning disable SYSLIB0014 - _redmineManagerOptions.WebClientOptions.SecurityProtocolType ??= ServicePointManager.SecurityProtocol; - #pragma warning restore SYSLIB0014 - } - - if (_redmineManagerOptions.Authentication is RedmineApiKeyAuthentication) - { - ApiKey = _redmineManagerOptions.Authentication.Token; - } - + Serializer = _redmineManagerOptions.Serializer; + RedmineApiUrls = new RedmineApiUrls(_redmineManagerOptions.Serializer.Format); + Host = _redmineManagerOptions.BaseAddress.ToString(); PageSize = _redmineManagerOptions.PageSize; Scheme = _redmineManagerOptions.BaseAddress.Scheme; @@ -81,23 +62,50 @@ public RedmineManager(RedmineManagerOptionsBuilder optionsBuilder) ? MimeFormat.Xml : MimeFormat.Json; - RedmineApiUrls = new RedmineApiUrls(Serializer.Format); - #if NET45_OR_GREATER || NETCOREAPP - if (_redmineManagerOptions.WebClientOptions is RedmineWebClientOptions) + if (_redmineManagerOptions.Authentication is RedmineApiKeyAuthentication) { - ApiClient = _redmineManagerOptions.ClientFunc != null - ? new InternalRedmineApiWebClient(_redmineManagerOptions.ClientFunc, _redmineManagerOptions.Authentication, _redmineManagerOptions.Serializer) - : new InternalRedmineApiWebClient(_redmineManagerOptions); + ApiKey = _redmineManagerOptions.Authentication.Token; } - else + + ApiClient = +#if NET45_OR_GREATER || NETCOREAPP + _redmineManagerOptions.WebClientOptions switch { - + RedmineWebClientOptions => CreateWebClient(_redmineManagerOptions), + _ => CreateHttpClient(_redmineManagerOptions) + }; +#else + CreateWebClient(_redmineManagerOptions); +#endif + } + + private InternalRedmineApiWebClient CreateWebClient(RedmineManagerOptions options) + { + if (options.ClientFunc != null) + { + return new InternalRedmineApiWebClient(options.ClientFunc, options.Authentication, options.Serializer); } - #else - ApiClient = _redmineManagerOptions.ClientFunc != null - ? new InternalRedmineApiWebClient(_redmineManagerOptions.ClientFunc, _redmineManagerOptions.Authentication, _redmineManagerOptions.Serializer) - : new InternalRedmineApiWebClient(_redmineManagerOptions); - #endif + +#pragma warning disable SYSLIB0014 + options.WebClientOptions.SecurityProtocolType ??= ServicePointManager.SecurityProtocol; +#pragma warning restore SYSLIB0014 + + Proxy = options.WebClientOptions.Proxy; + Timeout = options.WebClientOptions.Timeout; + SecurityProtocolType = options.WebClientOptions.SecurityProtocolType.GetValueOrDefault(); + +#if NET45_OR_GREATER + if (options.VerifyServerCert) + { + options.WebClientOptions.ServerCertificateValidationCallback = RemoteCertValidate; + } +#endif + return new InternalRedmineApiWebClient(options); + } + + private IRedmineApiClient CreateHttpClient(RedmineManagerOptions options) + { + throw new NotImplementedException(); } /// @@ -108,10 +116,7 @@ public int Count(RequestOptions requestOptions = null) const int PAGE_SIZE = 1; const int OFFSET = 0; - if (requestOptions == null) - { - requestOptions = new RequestOptions(); - } + requestOptions ??= new RequestOptions(); requestOptions.QueryString = requestOptions.QueryString.AddPagingParameters(PAGE_SIZE, OFFSET); From 99eea491e82bb51fe462d9eaba5e667d1601cff9 Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 11 May 2025 15:16:39 +0300 Subject: [PATCH 459/549] [RequestOptions] Add Include static method --- src/redmine-net-api/Net/RequestOptions.cs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/redmine-net-api/Net/RequestOptions.cs b/src/redmine-net-api/Net/RequestOptions.cs index 1c31f7a6..42b9ff21 100644 --- a/src/redmine-net-api/Net/RequestOptions.cs +++ b/src/redmine-net-api/Net/RequestOptions.cs @@ -15,6 +15,7 @@ limitations under the License. */ using System.Collections.Specialized; +using Redmine.Net.Api.Extensions; namespace Redmine.Net.Api.Net; @@ -59,4 +60,26 @@ public RequestOptions Clone() UserAgent = UserAgent }; } + /// + /// + /// + /// + /// + public static RequestOptions Include(string include) + { + if (include.IsNullOrWhiteSpace()) + { + return null; + } + + var requestOptions = new RequestOptions + { + QueryString = new NameValueCollection + { + {RedmineKeys.INCLUDE, include} + } + }; + + return requestOptions; + } } \ No newline at end of file From 2ef93ad1262cfa879c91989ac2c836e1b87d1460 Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 11 May 2025 15:16:56 +0300 Subject: [PATCH 460/549] [RequestOptions] Headers --- src/redmine-net-api/Net/RequestOptions.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/redmine-net-api/Net/RequestOptions.cs b/src/redmine-net-api/Net/RequestOptions.cs index 42b9ff21..8bffb391 100644 --- a/src/redmine-net-api/Net/RequestOptions.cs +++ b/src/redmine-net-api/Net/RequestOptions.cs @@ -14,6 +14,7 @@ You may obtain a copy of the License at limitations under the License. */ +using System.Collections.Generic; using System.Collections.Specialized; using Redmine.Net.Api.Extensions; @@ -44,6 +45,11 @@ public sealed class RequestOptions /// /// public string UserAgent { get; set; } + + /// + /// + /// + public Dictionary Headers { get; set; } /// /// @@ -57,9 +63,11 @@ public RequestOptions Clone() ImpersonateUser = ImpersonateUser, ContentType = ContentType, Accept = Accept, - UserAgent = UserAgent + UserAgent = UserAgent, + Headers = new Dictionary(Headers), }; } + /// /// /// From 959d62391e2a0d6a9ff3051dac66a61935ef6609 Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 14 May 2025 11:17:33 +0300 Subject: [PATCH 461/549] [New] RandomHelper --- .../RandomHelper.cs | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 tests/redmine-net-api.Integration.Tests/RandomHelper.cs diff --git a/tests/redmine-net-api.Integration.Tests/RandomHelper.cs b/tests/redmine-net-api.Integration.Tests/RandomHelper.cs new file mode 100644 index 00000000..7e78005c --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/RandomHelper.cs @@ -0,0 +1,89 @@ +using System.Text; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests; + +public static class ThreadSafeRandom +{ + /// + /// Generates a cryptographically strong, random string suffix. + /// This method is thread-safe as Guid.NewGuid() is thread-safe. + /// + /// A random string, 32 characters long, consisting of hexadecimal characters, without hyphens. + public static string GenerateSuffix() + { + return Guid.NewGuid().ToString("N"); + } + + private static readonly char[] EnglishAlphabetChars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".ToCharArray(); + + // ThreadLocal ensures that each thread has its own instance of Random, + // which is important because System.Random is not thread-safe for concurrent use. + // Seed with Guid for better randomness across instances + private static readonly ThreadLocal ThreadRandom = + new ThreadLocal(() => new Random(Guid.NewGuid().GetHashCode())); + + /// + /// Generates a random string of a specified length using only English alphabet characters. + /// This method is thread-safe. + /// + /// The desired length of the random string. Defaults to 10. + /// A random string composed of English alphabet characters. + private static string GenerateRandomAlphaNumericString(int length = 10) + { + if (length <= 0) + { + throw new ArgumentOutOfRangeException(nameof(length), "Length must be a positive integer."); + } + + var random = ThreadRandom.Value; + var result = new StringBuilder(length); + for (var i = 0; i < length; i++) + { + result.Append(EnglishAlphabetChars[random.Next(EnglishAlphabetChars.Length)]); + } + + return result.ToString(); + } + + /// + /// Generates a random alphabetic suffix, defaulting to 10 characters. + /// This method is thread-safe. + /// + /// The desired length of the suffix. Defaults to 10. + /// A random alphabetic string. + public static string GenerateText(int length = 10) + { + return GenerateRandomAlphaNumericString(length); + } + + /// + /// Generates a random name by combining a specified prefix and a random alphabetic suffix. + /// This method is thread-safe. + /// Example: if prefix is "MyItem", the result could be "MyItem_aBcDeFgHiJ". + /// + /// The prefix for the name. A '_' separator will be added. + /// The desired length of the random suffix. Defaults to 10. + /// A string combining the prefix, an underscore, and a random alphabetic suffix. + /// If the prefix is null or empty, it returns just the random suffix. + public static string GenerateText(string prefix = null, int suffixLength = 10) + { + var suffix = GenerateRandomAlphaNumericString(suffixLength); + return string.IsNullOrEmpty(prefix) ? suffix : $"{prefix}_{suffix}"; + } + + // Fisher-Yates shuffle algorithm + public static void Shuffle(this IList list) + { + var n = list.Count; + var random = ThreadRandom.Value; + while (n > 1) + { + n--; + var k = random.Next(n + 1); + var value = list[k]; + list[k] = list[n]; + list[n] = value; + } + } +} \ No newline at end of file From 94640f4e9417ad99fbbba3e5ef6eaf35adaf42df Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 11 May 2025 15:21:35 +0300 Subject: [PATCH 462/549] [IntegrationTests] More tests --- .../RedmineIntegrationTestsAsync.cs | 2 +- .../Tests/Async/AttachmentTestsAsync.cs | 51 ++++++ .../Tests/Async/CustomFieldAsyncTests.cs | 18 ++ .../Tests/Async/EnumerationTestsAsync.cs | 38 ++++ .../Tests/Async/FileTestsAsync.cs | 85 +++++++++ .../Tests/Async/GroupTestsAsync.cs | 144 +++++++++++++++ .../Async/IssueAttachmentUploadTestsAsync.cs | 54 ++++++ .../Tests/Async/IssueCategoryTestsAsync.cs | 103 +++++++++++ .../Tests/Async/IssueJournalTestsAsync.cs | 49 +++++ .../Tests/Async/IssueRelationTestsAsync.cs | 91 ++++++++++ .../Tests/Async/IssueStatusAsyncTests.cs | 19 ++ .../Tests/Async/IssueTestsAsync.cs | 135 ++++++++++++++ .../Tests/Async/IssueWatcherTestsAsync.cs | 71 ++++++++ .../Tests/Async/JournalTestsAsync.cs | 49 +++++ .../Tests/Async/MembershipTestsAsync.cs | 131 ++++++++++++++ .../Tests/Async/NewsAsyncTests.cs | 55 ++++++ .../Async/ProjectInformationTestsAsync.cs | 19 ++ .../Tests/Async/ProjectTestsAsync.cs | 86 +++++++++ .../Tests/Async/QueryTestsAsync.cs | 18 ++ .../Tests/Async/RoleTestsAsync.cs | 18 ++ .../Tests/Async/SearchTestsAsync.cs | 27 +++ .../Tests/Async/TimeEntryActivityTests.cs | 19 ++ .../Tests/Async/TimeEntryTests.cs | 110 ++++++++++++ .../Tests/Async/TrackerTestsAsync.cs | 19 ++ .../Tests/Async/UploadTestsAsync.cs | 85 +++++++++ .../Tests/Async/UserTestsAsync.cs | 112 ++++++++++++ .../Tests/Async/VersionTestsAsync.cs | 109 +++++++++++ .../Tests/Async/WikiTestsAsync.cs | 170 ++++++++++++++++++ .../Fixtures/JsonSerializerFixture.cs | 1 + .../Fixtures/XmlSerializerFixture.cs | 1 + 30 files changed, 1888 insertions(+), 1 deletion(-) create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/AttachmentTestsAsync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/CustomFieldAsyncTests.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/EnumerationTestsAsync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/FileTestsAsync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/GroupTestsAsync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/IssueAttachmentUploadTestsAsync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/IssueCategoryTestsAsync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/IssueJournalTestsAsync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/IssueRelationTestsAsync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/IssueStatusAsyncTests.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/IssueTestsAsync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/IssueWatcherTestsAsync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/JournalTestsAsync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/MembershipTestsAsync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/NewsAsyncTests.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/ProjectInformationTestsAsync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/ProjectTestsAsync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/QueryTestsAsync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/RoleTestsAsync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/SearchTestsAsync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/TimeEntryActivityTests.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/TimeEntryTests.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/TrackerTestsAsync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/UploadTestsAsync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/UserTestsAsync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/VersionTestsAsync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/WikiTestsAsync.cs diff --git a/tests/redmine-net-api.Integration.Tests/RedmineIntegrationTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/RedmineIntegrationTestsAsync.cs index b90554ee..356fa51e 100644 --- a/tests/redmine-net-api.Integration.Tests/RedmineIntegrationTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/RedmineIntegrationTestsAsync.cs @@ -76,7 +76,7 @@ public async Task Should_ReturnIssuesAsync() } [Fact] - public async Task GetIssue_WithVersions_ShouldReturnAsync() + public async Task Should_ReturnIssueWithVersionsAsync() { var issue = await _redmineManager.GetAsync(5.ToInvariantString(), new RequestOptions { diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/AttachmentTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/AttachmentTestsAsync.cs new file mode 100644 index 00000000..88ca740c --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/AttachmentTestsAsync.cs @@ -0,0 +1,51 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api.Net; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; + +[Collection(Constants.RedmineTestContainerCollection)] +public class AttachmentTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task UploadAndGetAttachment_Should_Succeed() + { + // Arrange + var fileContent = "Test attachment content"u8.ToArray(); + var filename = "test_attachment.txt"; + + // Upload the file + var upload = await fixture.RedmineManager.UploadFileAsync(fileContent, filename); + Assert.NotNull(upload); + Assert.NotEmpty(upload.Token); + + // Create an issue with the attachment + var issue = new Issue + { + Project = new IdentifiableName { Id = 1 }, + Tracker = new IdentifiableName { Id = 1 }, + Status = new IssueStatus { Id = 1 }, + Priority = new IdentifiableName { Id = 4 }, + Subject = $"Test issue with attachment {Guid.NewGuid()}", + Description = "Test issue description", + Uploads = [upload] + }; + + var createdIssue = await fixture.RedmineManager.CreateAsync(issue); + Assert.NotNull(createdIssue); + + // Get the issue with attachments + var retrievedIssue = await fixture.RedmineManager.GetAsync(createdIssue.Id.ToString(), RequestOptions.Include("attachments")); + + // Act + var attachment = retrievedIssue.Attachments.FirstOrDefault(); + Assert.NotNull(attachment); + + var downloadedAttachment = await fixture.RedmineManager.GetAsync(attachment.Id.ToString()); + + // Assert + Assert.NotNull(downloadedAttachment); + Assert.Equal(attachment.Id, downloadedAttachment.Id); + Assert.Equal(attachment.FileName, downloadedAttachment.FileName); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/CustomFieldAsyncTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/CustomFieldAsyncTests.cs new file mode 100644 index 00000000..47cf9a7e --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/CustomFieldAsyncTests.cs @@ -0,0 +1,18 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; + +[Collection(Constants.RedmineTestContainerCollection)] +public class CustomFieldTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task GetAllCustomFields_Should_Return_Null() + { + // Act + var customFields = await fixture.RedmineManager.GetAsync(); + + // Assert + Assert.Null(customFields); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/EnumerationTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/EnumerationTestsAsync.cs new file mode 100644 index 00000000..00448051 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/EnumerationTestsAsync.cs @@ -0,0 +1,38 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; + +[Collection(Constants.RedmineTestContainerCollection)] +public class EnumerationTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task GetDocumentCategories_Should_Succeed() + { + // Act + var categories = await fixture.RedmineManager.GetAsync(); + + // Assert + Assert.NotNull(categories); + } + + [Fact] + public async Task GetIssuePriorities_Should_Succeed() + { + // Act + var issuePriorities = await fixture.RedmineManager.GetAsync(); + + // Assert + Assert.NotNull(issuePriorities); + } + + [Fact] + public async Task GetTimeEntryActivities_Should_Succeed() + { + // Act + var activities = await fixture.RedmineManager.GetAsync(); + + // Assert + Assert.NotNull(activities); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/FileTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/FileTestsAsync.cs new file mode 100644 index 00000000..1ca94aa1 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/FileTestsAsync.cs @@ -0,0 +1,85 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api.Extensions; +using File = Redmine.Net.Api.Types.File; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; + +[Collection(Constants.RedmineTestContainerCollection)] +public class FileTestsAsync(RedmineTestContainerFixture fixture) +{ + private const string PROJECT_ID = "1"; + + [Fact] + public async Task CreateFile_Should_Succeed() + { + var (_, token) = await UploadFileAsync(); + + var filePayload = new File + { + Token = token, + }; + + var createdFile = await fixture.RedmineManager.CreateAsync(filePayload, PROJECT_ID); + Assert.Null(createdFile); + + var files = await fixture.RedmineManager.GetProjectFilesAsync(PROJECT_ID); + + //Assert + Assert.NotNull(files); + Assert.NotEmpty(files.Items); + } + + [Fact] + public async Task CreateFile_Without_Token_Should_Fail() + { + await Assert.ThrowsAsync(() => fixture.RedmineManager.CreateAsync( + new File { Filename = "project_file.zip" }, PROJECT_ID)); + } + + [Fact] + public async Task CreateFile_With_OptionalParameters_Should_Succeed() + { + var (fileName, token) = await UploadFileAsync(); + + var filePayload = new File + { + Token = token, + Filename = fileName, + Description = ThreadSafeRandom.GenerateText(9), + ContentType = "text/plain", + }; + + var createdFile = await fixture.RedmineManager.CreateAsync(filePayload, PROJECT_ID); + Assert.NotNull(createdFile); + } + + [Fact] + public async Task CreateFile_With_Version_Should_Succeed() + { + var (fileName, token) = await UploadFileAsync(); + + var filePayload = new File + { + Token = token, + Filename = fileName, + Description = ThreadSafeRandom.GenerateText(9), + ContentType = "text/plain", + Version = 1.ToIdentifier(), + }; + + var createdFile = await fixture.RedmineManager.CreateAsync(filePayload, PROJECT_ID); + Assert.NotNull(createdFile); + } + + private async Task<(string,string)> UploadFileAsync() + { + var bytes = "Hello World!"u8.ToArray(); + var fileName = $"{ThreadSafeRandom.GenerateText(5)}.txt"; + var upload = await fixture.RedmineManager.UploadFileAsync(bytes, fileName); + + Assert.NotNull(upload); + Assert.NotNull(upload.Token); + + return (fileName, upload.Token); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/GroupTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/GroupTestsAsync.cs new file mode 100644 index 00000000..313a7b67 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/GroupTestsAsync.cs @@ -0,0 +1,144 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Net; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; + +[Collection(Constants.RedmineTestContainerCollection)] +public class GroupTestsAsync(RedmineTestContainerFixture fixture) +{ + private async Task CreateTestGroupAsync() + { + var group = new Group + { + Name = $"Test Group {Guid.NewGuid()}" + }; + + return await fixture.RedmineManager.CreateAsync(group); + } + + [Fact] + public async Task GetAllGroups_Should_Succeed() + { + // Act + var groups = await fixture.RedmineManager.GetAsync(); + + // Assert + Assert.NotNull(groups); + } + + [Fact] + public async Task CreateGroup_Should_Succeed() + { + // Arrange + var group = new Group + { + Name = $"Test Group {Guid.NewGuid()}" + }; + + // Act + var createdGroup = await fixture.RedmineManager.CreateAsync(group); + + // Assert + Assert.NotNull(createdGroup); + Assert.True(createdGroup.Id > 0); + Assert.Equal(group.Name, createdGroup.Name); + } + + [Fact] + public async Task GetGroup_Should_Succeed() + { + // Arrange + var createdGroup = await CreateTestGroupAsync(); + Assert.NotNull(createdGroup); + + // Act + var retrievedGroup = await fixture.RedmineManager.GetAsync(createdGroup.Id.ToInvariantString()); + + // Assert + Assert.NotNull(retrievedGroup); + Assert.Equal(createdGroup.Id, retrievedGroup.Id); + Assert.Equal(createdGroup.Name, retrievedGroup.Name); + } + + [Fact] + public async Task UpdateGroup_Should_Succeed() + { + // Arrange + var createdGroup = await CreateTestGroupAsync(); + Assert.NotNull(createdGroup); + + var updatedName = $"Updated Test Group {Guid.NewGuid()}"; + createdGroup.Name = updatedName; + + // Act + await fixture.RedmineManager.UpdateAsync(createdGroup.Id.ToInvariantString(), createdGroup); + var retrievedGroup = await fixture.RedmineManager.GetAsync(createdGroup.Id.ToInvariantString()); + + // Assert + Assert.NotNull(retrievedGroup); + Assert.Equal(createdGroup.Id, retrievedGroup.Id); + Assert.Equal(updatedName, retrievedGroup.Name); + } + + [Fact] + public async Task DeleteGroup_Should_Succeed() + { + // Arrange + var createdGroup = await CreateTestGroupAsync(); + Assert.NotNull(createdGroup); + + var groupId = createdGroup.Id.ToInvariantString(); + + // Act + await fixture.RedmineManager.DeleteAsync(groupId); + + // Assert + await Assert.ThrowsAsync(async () => + await fixture.RedmineManager.GetAsync(groupId)); + } + + [Fact] + public async Task AddUserToGroup_Should_Succeed() + { + // Arrange + var group = await CreateTestGroupAsync(); + Assert.NotNull(group); + + // Assuming there's at least one user in the system (typically Admin with ID 1) + var userId = 1; + + // Act + await fixture.RedmineManager.AddUserToGroupAsync(group.Id, userId); + var updatedGroup = await fixture.RedmineManager.GetAsync(group.Id.ToString(), RequestOptions.Include("users")); + + // Assert + Assert.NotNull(updatedGroup); + Assert.NotNull(updatedGroup.Users); + Assert.Contains(updatedGroup.Users, u => u.Id == userId); + } + + [Fact] + public async Task RemoveUserFromGroup_Should_Succeed() + { + // Arrange + var group = await CreateTestGroupAsync(); + Assert.NotNull(group); + + // Assuming there's at least one user in the system (typically Admin with ID 1) + var userId = 1; + + // First add the user to the group + await fixture.RedmineManager.AddUserToGroupAsync(group.Id, userId); + + // Act + await fixture.RedmineManager.RemoveUserFromGroupAsync(group.Id, userId); + var updatedGroup = await fixture.RedmineManager.GetAsync(group.Id.ToString(), RequestOptions.Include("users")); + + // Assert + Assert.NotNull(updatedGroup); + // Assert.DoesNotContain(updatedGroup.Users ?? new List(), u => u.Id == userId); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueAttachmentUploadTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueAttachmentUploadTestsAsync.cs new file mode 100644 index 00000000..679244d1 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueAttachmentUploadTestsAsync.cs @@ -0,0 +1,54 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api.Net; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; + +[Collection(Constants.RedmineTestContainerCollection)] +public class IssueAttachmentTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task UploadAttachmentAndAttachToIssue_Should_Succeed() + { + // Arrange + var issue = new Issue + { + Project = new IdentifiableName { Id = 1 }, + Tracker = new IdentifiableName { Id = 1 }, + Status = new IssueStatus() { Id = 1 }, + Priority = new IdentifiableName { Id = 4 }, + Subject = $"Test issue for attachment {Guid.NewGuid()}", + Description = "Test issue description" + }; + + var createdIssue = await fixture.RedmineManager.CreateAsync(issue); + Assert.NotNull(createdIssue); + + // Upload a file + var fileContent = "Test attachment content"u8.ToArray(); + var filename = "test_attachment.txt"; + + var upload = await fixture.RedmineManager.UploadFileAsync(fileContent, filename); + Assert.NotNull(upload); + Assert.NotEmpty(upload.Token); + + // Prepare issue with attachment + var updateIssue = new Issue + { + Subject = $"Test issue for attachment {ThreadSafeRandom.GenerateText(5)}", + Uploads = [upload] + }; + + // Act + await fixture.RedmineManager.UpdateAsync(createdIssue.Id.ToString(), updateIssue); + + var retrievedIssue = + await fixture.RedmineManager.GetAsync(createdIssue.Id.ToString(), RequestOptions.Include("attachments")); + + // Assert + Assert.NotNull(retrievedIssue); + Assert.NotNull(retrievedIssue.Attachments); + Assert.NotEmpty(retrievedIssue.Attachments); + Assert.Contains(retrievedIssue.Attachments, a => a.FileName == filename); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueCategoryTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueCategoryTestsAsync.cs new file mode 100644 index 00000000..62b772cd --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueCategoryTestsAsync.cs @@ -0,0 +1,103 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; + +[Collection(Constants.RedmineTestContainerCollection)] +public class IssueCategoryTestsAsync(RedmineTestContainerFixture fixture) +{ + private const string PROJECT_ID = "1"; + + private async Task CreateTestIssueCategoryAsync() + { + var category = new IssueCategory + { + Name = $"Test Category {Guid.NewGuid()}" + }; + + return await fixture.RedmineManager.CreateAsync(category, PROJECT_ID); + } + + [Fact] + public async Task GetProjectIssueCategories_Should_Succeed() + { + // Act + var categories = await fixture.RedmineManager.GetAsync(PROJECT_ID); + + // Assert + Assert.NotNull(categories); + } + + [Fact] + public async Task CreateIssueCategory_Should_Succeed() + { + // Arrange + var category = new IssueCategory + { + Name = $"Test Category {Guid.NewGuid()}" + }; + + // Act + var createdCategory = await fixture.RedmineManager.CreateAsync(category, PROJECT_ID); + + // Assert + Assert.NotNull(createdCategory); + Assert.True(createdCategory.Id > 0); + Assert.Equal(category.Name, createdCategory.Name); + } + + [Fact] + public async Task GetIssueCategory_Should_Succeed() + { + // Arrange + var createdCategory = await CreateTestIssueCategoryAsync(); + Assert.NotNull(createdCategory); + + // Act + var retrievedCategory = await fixture.RedmineManager.GetAsync(createdCategory.Id.ToInvariantString()); + + // Assert + Assert.NotNull(retrievedCategory); + Assert.Equal(createdCategory.Id, retrievedCategory.Id); + Assert.Equal(createdCategory.Name, retrievedCategory.Name); + } + + [Fact] + public async Task UpdateIssueCategory_Should_Succeed() + { + // Arrange + var createdCategory = await CreateTestIssueCategoryAsync(); + Assert.NotNull(createdCategory); + + var updatedName = $"Updated Test Category {Guid.NewGuid()}"; + createdCategory.Name = updatedName; + + // Act + await fixture.RedmineManager.UpdateAsync(createdCategory.Id.ToInvariantString(), createdCategory); + var retrievedCategory = await fixture.RedmineManager.GetAsync(createdCategory.Id.ToInvariantString()); + + // Assert + Assert.NotNull(retrievedCategory); + Assert.Equal(createdCategory.Id, retrievedCategory.Id); + Assert.Equal(updatedName, retrievedCategory.Name); + } + + [Fact] + public async Task DeleteIssueCategory_Should_Succeed() + { + // Arrange + var createdCategory = await CreateTestIssueCategoryAsync(); + Assert.NotNull(createdCategory); + + var categoryId = createdCategory.Id.ToInvariantString(); + + // Act + await fixture.RedmineManager.DeleteAsync(categoryId); + + // Assert + await Assert.ThrowsAsync(async () => + await fixture.RedmineManager.GetAsync(categoryId)); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueJournalTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueJournalTestsAsync.cs new file mode 100644 index 00000000..73dffec4 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueJournalTestsAsync.cs @@ -0,0 +1,49 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api.Net; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; + +[Collection(Constants.RedmineTestContainerCollection)] +public class IssueJournalTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task GetIssueWithJournals_Should_Succeed() + { + // Arrange + // Create an issue + var issue = new Issue + { + Project = new IdentifiableName { Id = 1 }, + Tracker = new IdentifiableName { Id = 1 }, + Status = new IssueStatus { Id = 1 }, + Priority = new IdentifiableName { Id = 4 }, + Subject = $"Test issue for journals {Guid.NewGuid()}", + Description = "Test issue description" + }; + + var createdIssue = await fixture.RedmineManager.CreateAsync(issue); + Assert.NotNull(createdIssue); + + // Update the issue to create a journal entry + var updateIssue = new Issue + { + Notes = "This is a test note that should appear in journals", + Subject = $"Updated subject {Guid.NewGuid()}" + }; + + await fixture.RedmineManager.UpdateAsync(createdIssue.Id.ToString(), updateIssue); + + // Act + // Get the issue with journals + var retrievedIssue = + await fixture.RedmineManager.GetAsync(createdIssue.Id.ToString(), + RequestOptions.Include("journals")); + + // Assert + Assert.NotNull(retrievedIssue); + Assert.NotNull(retrievedIssue.Journals); + Assert.NotEmpty(retrievedIssue.Journals); + Assert.Contains(retrievedIssue.Journals, j => j.Notes?.Contains("test note") == true); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueRelationTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueRelationTestsAsync.cs new file mode 100644 index 00000000..1cf208cc --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueRelationTestsAsync.cs @@ -0,0 +1,91 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api.Net; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; + +[Collection(Constants.RedmineTestContainerCollection)] +public class IssueRelationTestsAsync(RedmineTestContainerFixture fixture) +{ + private async Task<(Issue firstIssue, Issue secondIssue)> CreateTestIssuesAsync() + { + var issue1 = new Issue + { + Project = new IdentifiableName { Id = 1 }, + Tracker = new IdentifiableName { Id = 1 }, + Status = new IssueStatus { Id = 1 }, + Priority = new IdentifiableName { Id = 4 }, + Subject = $"Test issue 1 subject {Guid.NewGuid()}", + Description = "Test issue 1 description" + }; + + var issue2 = new Issue + { + Project = new IdentifiableName { Id = 1 }, + Tracker = new IdentifiableName { Id = 1 }, + Status = new IssueStatus { Id = 1 }, + Priority = new IdentifiableName { Id = 4 }, + Subject = $"Test issue 2 subject {Guid.NewGuid()}", + Description = "Test issue 2 description" + }; + + var createdIssue1 = await fixture.RedmineManager.CreateAsync(issue1); + var createdIssue2 = await fixture.RedmineManager.CreateAsync(issue2); + + return (createdIssue1, createdIssue2); + } + + private async Task CreateTestIssueRelationAsync() + { + var (issue1, issue2) = await CreateTestIssuesAsync(); + + var relation = new IssueRelation + { + IssueId = issue1.Id, + IssueToId = issue2.Id, + Type = IssueRelationType.Relates + }; + + return await fixture.RedmineManager.CreateAsync( relation, issue1.Id.ToString()); + } + + [Fact] + public async Task CreateIssueRelation_Should_Succeed() + { + // Arrange + var (issue1, issue2) = await CreateTestIssuesAsync(); + + var relation = new IssueRelation + { + IssueId = issue1.Id, + IssueToId = issue2.Id, + Type = IssueRelationType.Relates + }; + + // Act + var createdRelation = await fixture.RedmineManager.CreateAsync(relation, issue1.Id.ToString()); + + // Assert + Assert.NotNull(createdRelation); + Assert.True(createdRelation.Id > 0); + Assert.Equal(relation.IssueId, createdRelation.IssueId); + Assert.Equal(relation.IssueToId, createdRelation.IssueToId); + Assert.Equal(relation.Type, createdRelation.Type); + } + + [Fact] + public async Task DeleteIssueRelation_Should_Succeed() + { + // Arrange + var relation = await CreateTestIssueRelationAsync(); + Assert.NotNull(relation); + + // Act & Assert + await fixture.RedmineManager.DeleteAsync(relation.Id.ToString()); + + // Verify the relation no longer exists by checking the issue doesn't have it + var issue = await fixture.RedmineManager.GetAsync(relation.IssueId.ToString(), RequestOptions.Include("relations")); + + Assert.Null(issue.Relations?.FirstOrDefault(r => r.Id == relation.Id)); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueStatusAsyncTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueStatusAsyncTests.cs new file mode 100644 index 00000000..75d1bdbf --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueStatusAsyncTests.cs @@ -0,0 +1,19 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; + +[Collection(Constants.RedmineTestContainerCollection)] +public class IssueStatusTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task GetAllIssueStatuses_Should_Succeed() + { + // Act + var statuses = await fixture.RedmineManager.GetAsync(); + + // Assert + Assert.NotNull(statuses); + Assert.NotEmpty(statuses); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueTestsAsync.cs new file mode 100644 index 00000000..390efb17 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueTestsAsync.cs @@ -0,0 +1,135 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; + +[Collection(Constants.RedmineTestContainerCollection)] +public class IssueTestsAsync(RedmineTestContainerFixture fixture) +{ + private static readonly IdentifiableName ProjectIdName = IdentifiableName.Create(1); + + private async Task CreateTestIssueAsync() + { + var issue = new Issue + { + Project = ProjectIdName, + Subject = ThreadSafeRandom.GenerateText(9), + Description = ThreadSafeRandom.GenerateText(18), + Tracker = 1.ToIdentifier(), + Status = 1.ToIssueStatusIdentifier(), + Priority = 2.ToIdentifier(), + CustomFields = + [ + IssueCustomField.CreateMultiple(1, ThreadSafeRandom.GenerateText(8), [ThreadSafeRandom.GenerateText(4), ThreadSafeRandom.GenerateText(4)]) + ] + }; + return await fixture.RedmineManager.CreateAsync(issue); + } + + [Fact] + public async Task CreateIssue_Should_Succeed() + { + //Arrange + var issueData = new Issue + { + Project = ProjectIdName, + Subject = ThreadSafeRandom.GenerateText(9), + Description = ThreadSafeRandom.GenerateText(18), + Tracker = 2.ToIdentifier(), + Status = 1.ToIssueStatusIdentifier(), + Priority = 3.ToIdentifier(), + StartDate = DateTime.Now.Date, + DueDate = DateTime.Now.Date.AddDays(7), + EstimatedHours = 8, + CustomFields = + [ + IssueCustomField.CreateSingle(1, ThreadSafeRandom.GenerateText(8), ThreadSafeRandom.GenerateText(4)) + ] + }; + + //Act + var cr = await fixture.RedmineManager.CreateAsync(issueData); + var createdIssue = await fixture.RedmineManager.GetAsync(cr.Id.ToString()); + + //Assert + Assert.NotNull(createdIssue); + Assert.True(createdIssue.Id > 0); + Assert.Equal(issueData.Subject, createdIssue.Subject); + Assert.Equal(issueData.Description, createdIssue.Description); + Assert.Equal(issueData.Project.Id, createdIssue.Project.Id); + Assert.Equal(issueData.Tracker.Id, createdIssue.Tracker.Id); + Assert.Equal(issueData.Status.Id, createdIssue.Status.Id); + Assert.Equal(issueData.Priority.Id, createdIssue.Priority.Id); + Assert.Equal(issueData.StartDate, createdIssue.StartDate); + Assert.Equal(issueData.DueDate, createdIssue.DueDate); + // Assert.Equal(issueData.EstimatedHours, createdIssue.EstimatedHours); + } + + [Fact] + public async Task GetIssue_Should_Succeed() + { + //Arrange + var createdIssue = await CreateTestIssueAsync(); + Assert.NotNull(createdIssue); + + var issueId = createdIssue.Id.ToInvariantString(); + + //Act + var retrievedIssue = await fixture.RedmineManager.GetAsync(issueId); + + //Assert + Assert.NotNull(retrievedIssue); + Assert.Equal(createdIssue.Id, retrievedIssue.Id); + Assert.Equal(createdIssue.Subject, retrievedIssue.Subject); + Assert.Equal(createdIssue.Description, retrievedIssue.Description); + Assert.Equal(createdIssue.Project.Id, retrievedIssue.Project.Id); + } + + [Fact] + public async Task UpdateIssue_Should_Succeed() + { + //Arrange + var createdIssue = await CreateTestIssueAsync(); + Assert.NotNull(createdIssue); + + var updatedSubject = ThreadSafeRandom.GenerateText(9); + var updatedDescription = ThreadSafeRandom.GenerateText(18); + var updatedStatusId = 2; + + createdIssue.Subject = updatedSubject; + createdIssue.Description = updatedDescription; + createdIssue.Status = updatedStatusId.ToIssueStatusIdentifier(); + createdIssue.Notes = ThreadSafeRandom.GenerateText("Note"); + + var issueId = createdIssue.Id.ToInvariantString(); + + //Act + await fixture.RedmineManager.UpdateAsync(issueId, createdIssue); + var retrievedIssue = await fixture.RedmineManager.GetAsync(issueId); + + //Assert + Assert.NotNull(retrievedIssue); + Assert.Equal(createdIssue.Id, retrievedIssue.Id); + Assert.Equal(updatedSubject, retrievedIssue.Subject); + Assert.Equal(updatedDescription, retrievedIssue.Description); + Assert.Equal(updatedStatusId, retrievedIssue.Status.Id); + } + + [Fact] + public async Task DeleteIssue_Should_Succeed() + { + //Arrange + var createdIssue = await CreateTestIssueAsync(); + Assert.NotNull(createdIssue); + + var issueId = createdIssue.Id.ToInvariantString(); + + //Act + await fixture.RedmineManager.DeleteAsync(issueId); + + //Assert + await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetAsync(issueId)); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueWatcherTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueWatcherTestsAsync.cs new file mode 100644 index 00000000..6da2a071 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueWatcherTestsAsync.cs @@ -0,0 +1,71 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Net; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; + +[Collection(Constants.RedmineTestContainerCollection)] +public class IssueWatcherTestsAsync(RedmineTestContainerFixture fixture) +{ + private async Task CreateTestIssueAsync() + { + var issue = new Issue + { + Project = new IdentifiableName { Id = 1 }, + Tracker = new IdentifiableName { Id = 1 }, + Status = new IssueStatus { Id = 1 }, + Priority = new IdentifiableName { Id = 4 }, + Subject = $"Test issue subject {Guid.NewGuid()}", + Description = "Test issue description" + }; + + return await fixture.RedmineManager.CreateAsync(issue); + } + + [Fact] + public async Task AddWatcher_Should_Succeed() + { + // Arrange + var issue = await CreateTestIssueAsync(); + Assert.NotNull(issue); + + // Assuming there's at least one user in the system (typically Admin with ID 1) + var userId = 1; + + // Act + await fixture.RedmineManager.AddWatcherToIssueAsync(issue.Id, userId); + + // Get updated issue with watchers + var updatedIssue = await fixture.RedmineManager.GetAsync(issue.Id.ToString(), RequestOptions.Include("watchers")); + + // Assert + Assert.NotNull(updatedIssue); + Assert.NotNull(updatedIssue.Watchers); + Assert.Contains(updatedIssue.Watchers, w => w.Id == userId); + } + + [Fact] + public async Task RemoveWatcher_Should_Succeed() + { + // Arrange + var issue = await CreateTestIssueAsync(); + Assert.NotNull(issue); + + // Assuming there's at least one user in the system (typically Admin with ID 1) + var userId = 1; + + // Add watcher first + await fixture.RedmineManager.AddWatcherToIssueAsync(issue.Id, userId); + + // Act + await fixture.RedmineManager.RemoveWatcherFromIssueAsync(issue.Id, userId); + + // Get updated issue with watchers + var updatedIssue = await fixture.RedmineManager.GetAsync(issue.Id.ToString(), RequestOptions.Include("watchers")); + + // Assert + Assert.NotNull(updatedIssue); + Assert.DoesNotContain(updatedIssue.Watchers ?? [], w => w.Id == userId); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/JournalTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/JournalTestsAsync.cs new file mode 100644 index 00000000..413b1bc0 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/JournalTestsAsync.cs @@ -0,0 +1,49 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Net; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; + +[Collection(Constants.RedmineTestContainerCollection)] +public class JournalTestsAsync(RedmineTestContainerFixture fixture) +{ + private async Task CreateTestIssueAsync() + { + var issue = new Issue + { + Project = IdentifiableName.Create(1), + Subject = ThreadSafeRandom.GenerateText(13), + Description = ThreadSafeRandom.GenerateText(19), + Tracker = 1.ToIdentifier(), + Status = 1.ToIssueStatusIdentifier(), + Priority = 2.ToIdentifier(), + }; + return await fixture.RedmineManager.CreateAsync(issue); + } + + [Fact] + public async Task Get_Issue_With_Journals_Should_Succeed() + { + //Arrange + var testIssue = await CreateTestIssueAsync(); + Assert.NotNull(testIssue); + + var issueIdToTest = testIssue.Id.ToInvariantString(); + + testIssue.Notes = "This is a test note to create a journal entry."; + await fixture.RedmineManager.UpdateAsync(issueIdToTest, testIssue); + + //Act + var issueWithJournals = await fixture.RedmineManager.GetAsync( + issueIdToTest, + RequestOptions.Include(RedmineKeys.JOURNALS)); + + //Assert + Assert.NotNull(issueWithJournals); + Assert.NotNull(issueWithJournals.Journals); + Assert.True(issueWithJournals.Journals.Count > 0, "Issue should have journal entries."); + Assert.Contains(issueWithJournals.Journals, j => j.Notes == testIssue.Notes); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/MembershipTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/MembershipTestsAsync.cs new file mode 100644 index 00000000..f3e7f6a2 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/MembershipTestsAsync.cs @@ -0,0 +1,131 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; + +[Collection(Constants.RedmineTestContainerCollection)] +public class MembershipTestsAsync(RedmineTestContainerFixture fixture) +{ + private const string PROJECT_ID = "1"; + + private async Task CreateTestMembershipAsync() + { + var roles = await fixture.RedmineManager.GetAsync(); + Assert.NotEmpty(roles); + + var user = new User + { + Login = ThreadSafeRandom.GenerateText(10), + FirstName = ThreadSafeRandom.GenerateText(8), + LastName = ThreadSafeRandom.GenerateText(9), + Email = $"{ThreadSafeRandom.GenerateText(5)}@example.com", + Password = "password123", + MustChangePassword = false, + Status = UserStatus.StatusActive + }; + + var createdUser = await fixture.RedmineManager.CreateAsync(user); + Assert.NotNull(createdUser); + + var membership = new ProjectMembership + { + User = new IdentifiableName { Id = createdUser.Id }, + Roles = [new MembershipRole { Id = roles[0].Id }] + }; + + return await fixture.RedmineManager.CreateAsync(membership, PROJECT_ID); + } + + [Fact] + public async Task GetProjectMemberships_Should_Succeed() + { + // Act + var memberships = await fixture.RedmineManager.GetProjectMembershipsAsync(PROJECT_ID); + + // Assert + Assert.NotNull(memberships); + } + + [Fact] + public async Task CreateMembership_Should_Succeed() + { + // Arrange + var roles = await fixture.RedmineManager.GetAsync(); + Assert.NotEmpty(roles); + + var user = new User + { + Login = ThreadSafeRandom.GenerateText(10), + FirstName = ThreadSafeRandom.GenerateText(8), + LastName = ThreadSafeRandom.GenerateText(9), + Email = $"{ThreadSafeRandom.GenerateText(5)}@example.com", + Password = "password123", + MustChangePassword = false, + Status = UserStatus.StatusActive + }; + + var createdUser = await fixture.RedmineManager.CreateAsync(user); + Assert.NotNull(createdUser); + + var membership = new ProjectMembership + { + User = new IdentifiableName { Id = createdUser.Id }, + Roles = [new MembershipRole { Id = roles[0].Id }] + }; + + // Act + var createdMembership = await fixture.RedmineManager.CreateAsync(membership, PROJECT_ID); + + // Assert + Assert.NotNull(createdMembership); + Assert.True(createdMembership.Id > 0); + Assert.Equal(membership.User.Id, createdMembership.User.Id); + Assert.NotEmpty(createdMembership.Roles); + } + + [Fact] + public async Task UpdateMembership_Should_Succeed() + { + // Arrange + var membership = await CreateTestMembershipAsync(); + Assert.NotNull(membership); + + var roles = await fixture.RedmineManager.GetAsync(); + Assert.NotEmpty(roles); + + // Change roles + var newRoleId = roles.FirstOrDefault(r => membership.Roles.All(mr => mr.Id != r.Id))?.Id ?? roles.First().Id; + membership.Roles = [new MembershipRole { Id = newRoleId }]; + + // Act + await fixture.RedmineManager.UpdateAsync(membership.Id.ToString(), membership); + + // Get the updated membership from project memberships + var updatedMemberships = await fixture.RedmineManager.GetProjectMembershipsAsync(PROJECT_ID); + var updatedMembership = updatedMemberships.Items.FirstOrDefault(m => m.Id == membership.Id); + + // Assert + Assert.NotNull(updatedMembership); + Assert.Contains(updatedMembership.Roles, r => r.Id == newRoleId); + } + + [Fact] + public async Task DeleteMembership_Should_Succeed() + { + // Arrange + var membership = await CreateTestMembershipAsync(); + Assert.NotNull(membership); + + var membershipId = membership.Id.ToString(); + + // Act + await fixture.RedmineManager.DeleteAsync(membershipId); + + // Get project memberships + var updatedMemberships = await fixture.RedmineManager.GetProjectMembershipsAsync(PROJECT_ID); + + // Assert + Assert.DoesNotContain(updatedMemberships.Items, m => m.Id == membership.Id); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/NewsAsyncTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/NewsAsyncTests.cs new file mode 100644 index 00000000..6cfd86bf --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/NewsAsyncTests.cs @@ -0,0 +1,55 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; + +[Collection(Constants.RedmineTestContainerCollection)] +public class NewsTestsAsync(RedmineTestContainerFixture fixture) +{ + private const string PROJECT_ID = "1"; + + [Fact] + public async Task GetAllNews_Should_Succeed() + { + // Arrange + _ = await fixture.RedmineManager.AddProjectNewsAsync(PROJECT_ID, new News() + { + Title = ThreadSafeRandom.GenerateText(5), + Summary = ThreadSafeRandom.GenerateText(10), + Description = ThreadSafeRandom.GenerateText(20), + }); + + _ = await fixture.RedmineManager.AddProjectNewsAsync("2", new News() + { + Title = ThreadSafeRandom.GenerateText(5), + Summary = ThreadSafeRandom.GenerateText(10), + Description = ThreadSafeRandom.GenerateText(20), + }); + + + // Act + var news = await fixture.RedmineManager.GetAsync(); + + // Assert + Assert.NotNull(news); + } + + [Fact] + public async Task GetProjectNews_Should_Succeed() + { + // Arrange + var newsCreated = await fixture.RedmineManager.AddProjectNewsAsync(PROJECT_ID, new News() + { + Title = ThreadSafeRandom.GenerateText(5), + Summary = ThreadSafeRandom.GenerateText(10), + Description = ThreadSafeRandom.GenerateText(20), + }); + + // Act + var news = await fixture.RedmineManager.GetProjectNewsAsync(PROJECT_ID); + + // Assert + Assert.NotNull(news); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/ProjectInformationTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/ProjectInformationTestsAsync.cs new file mode 100644 index 00000000..7997d845 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/ProjectInformationTestsAsync.cs @@ -0,0 +1,19 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api.Extensions; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; + +[Collection(Constants.RedmineTestContainerCollection)] +public class ProjectInformationTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task GetCurrentUserInfo_Should_Succeed() + { + // Act + var currentUser = await fixture.RedmineManager.GetCurrentUserAsync(); + + // Assert + Assert.NotNull(currentUser); + Assert.True(currentUser.Id > 0); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/ProjectTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/ProjectTestsAsync.cs new file mode 100644 index 00000000..9c45065a --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/ProjectTestsAsync.cs @@ -0,0 +1,86 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; + +[Collection(Constants.RedmineTestContainerCollection)] +public class ProjectTestsAsync(RedmineTestContainerFixture fixture) +{ + private async Task CreateEntityAsync(string subjectSuffix = null) + { + var entity = new Project + { + Identifier = Guid.NewGuid().ToString("N"), + Name = "test-random", + }; + + return await fixture.RedmineManager.CreateAsync(entity); + } + + [Fact] + public async Task CreateProject_Should_Succeed() + { + var data = new Project + { + IsPublic = true, + EnabledModules = [ + new ProjectEnabledModule("files"), + new ProjectEnabledModule("wiki") + ], + Identifier = Guid.NewGuid().ToString("N"), + InheritMembers = true, + Name = "test-random", + HomePage = "test-homepage", + Trackers = + [ + new ProjectTracker(1), + new ProjectTracker(2), + new ProjectTracker(3), + ], + Description = $"Description for create test", + CustomFields = + [ + new IssueCustomField + { + Id = 1, + Values = [ + new CustomFieldValue + { + Info = "Custom field test value" + } + ] + } + ] + }; + + //Act + var createdProject = await fixture.RedmineManager.CreateAsync(data); + Assert.NotNull(createdProject); + } + + [Fact] + public async Task DeleteIssue_Should_Succeed() + { + //Arrange + var createdEntity = await CreateEntityAsync("DeleteTest"); + Assert.NotNull(createdEntity); + + var id = createdEntity.Id.ToInvariantString(); + + //Act + await fixture.RedmineManager.DeleteAsync(id); + + await Task.Delay(200); + + //Assert + await Assert.ThrowsAsync(TestCode); + return; + + async Task TestCode() + { + await fixture.RedmineManager.GetAsync(id); + } + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/QueryTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/QueryTestsAsync.cs new file mode 100644 index 00000000..0ee98155 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/QueryTestsAsync.cs @@ -0,0 +1,18 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; + +[Collection(Constants.RedmineTestContainerCollection)] +public class QueryTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task GetAllQueries_Should_Succeed() + { + // Act + var queries = await fixture.RedmineManager.GetAsync(); + + // Assert + Assert.NotNull(queries); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/RoleTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/RoleTestsAsync.cs new file mode 100644 index 00000000..cc163d64 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/RoleTestsAsync.cs @@ -0,0 +1,18 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; + +[Collection(Constants.RedmineTestContainerCollection)] +public class RoleTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task Get_All_Roles_Should_Succeed() + { + //Act + var roles = await fixture.RedmineManager.GetAsync(); + + //Assert + Assert.NotNull(roles); + Assert.NotEmpty(roles); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/SearchTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/SearchTestsAsync.cs new file mode 100644 index 00000000..e07b128a --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/SearchTestsAsync.cs @@ -0,0 +1,27 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api; +using Redmine.Net.Api.Extensions; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; + +[Collection(Constants.RedmineTestContainerCollection)] +public class SearchTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task Search_Should_Succeed() + { + // Arrange + var searchBuilder = new SearchFilterBuilder + { + IncludeIssues = true, + IncludeWikiPages = true + }; + + // Act + var results = await fixture.RedmineManager.SearchAsync("query_string",100, searchFilter:searchBuilder); + + // Assert + Assert.NotNull(results); + Assert.Empty(results.Items); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/TimeEntryActivityTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/TimeEntryActivityTests.cs new file mode 100644 index 00000000..79260dcb --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/TimeEntryActivityTests.cs @@ -0,0 +1,19 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; + +[Collection(Constants.RedmineTestContainerCollection)] +public class TimeEntryActivityTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task GetAllTimeEntryActivities_Should_Succeed() + { + // Act + var activities = await fixture.RedmineManager.GetAsync(); + + // Assert + Assert.NotNull(activities); + Assert.NotEmpty(activities); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/TimeEntryTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/TimeEntryTests.cs new file mode 100644 index 00000000..31b603e5 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/TimeEntryTests.cs @@ -0,0 +1,110 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; + +[Collection(Constants.RedmineTestContainerCollection)] +public class TimeEntryTestsAsync(RedmineTestContainerFixture fixture) +{ + private async Task CreateTestTimeEntryAsync() + { + var project = await fixture.RedmineManager.GetAsync(1.ToInvariantString()); + var issue = await fixture.RedmineManager.GetAsync(1.ToInvariantString()); + + var timeEntry = new TimeEntry + { + Project = project, + Issue = issue.ToIdentifiableName(), + SpentOn = DateTime.Now.Date, + Hours = 1.5m, + Activity = 8.ToIdentifier(), + Comments = $"Test time entry comments {Guid.NewGuid()}", + }; + return await fixture.RedmineManager.CreateAsync(timeEntry); + } + + [Fact] + public async Task CreateTimeEntry_Should_Succeed() + { + //Arrange + var timeEntryData = new TimeEntry + { + Project = 1.ToIdentifier(), + Issue = 1.ToIdentifier(), + SpentOn = DateTime.Now.Date, + Hours = 1.5m, + Activity = 8.ToIdentifier(), + Comments = $"Initial create test comments {Guid.NewGuid()}", + }; + + //Act + var createdTimeEntry = await fixture.RedmineManager.CreateAsync(timeEntryData); + + //Assert + Assert.NotNull(createdTimeEntry); + Assert.True(createdTimeEntry.Id > 0); + Assert.Equal(timeEntryData.Hours, createdTimeEntry.Hours); + Assert.Equal(timeEntryData.Comments, createdTimeEntry.Comments); + Assert.Equal(timeEntryData.Project.Id, createdTimeEntry.Project.Id); + Assert.Equal(timeEntryData.Issue.Id, createdTimeEntry.Issue.Id); + Assert.Equal(timeEntryData.Activity.Id, createdTimeEntry.Activity.Id); + } + + [Fact] + public async Task GetTimeEntry_Should_Succeed() + { + //Arrange + var createdTimeEntry = await CreateTestTimeEntryAsync(); + Assert.NotNull(createdTimeEntry); + + //Act + var retrievedTimeEntry = await fixture.RedmineManager.GetAsync(createdTimeEntry.Id.ToInvariantString()); + + //Assert + Assert.NotNull(retrievedTimeEntry); + Assert.Equal(createdTimeEntry.Id, retrievedTimeEntry.Id); + Assert.Equal(createdTimeEntry.Hours, retrievedTimeEntry.Hours); + Assert.Equal(createdTimeEntry.Comments, retrievedTimeEntry.Comments); + } + + [Fact] + public async Task UpdateTimeEntry_Should_Succeed() + { + //Arrange + var createdTimeEntry = await CreateTestTimeEntryAsync(); + Assert.NotNull(createdTimeEntry); + + var updatedComments = $"Updated test time entry comments {Guid.NewGuid()}"; + var updatedHours = 2.5m; + createdTimeEntry.Comments = updatedComments; + createdTimeEntry.Hours = updatedHours; + + //Act + await fixture.RedmineManager.UpdateAsync(createdTimeEntry.Id.ToInvariantString(), createdTimeEntry); + var retrievedTimeEntry = await fixture.RedmineManager.GetAsync(createdTimeEntry.Id.ToInvariantString()); + + //Assert + Assert.NotNull(retrievedTimeEntry); + Assert.Equal(createdTimeEntry.Id, retrievedTimeEntry.Id); + Assert.Equal(updatedComments, retrievedTimeEntry.Comments); + Assert.Equal(updatedHours, retrievedTimeEntry.Hours); + } + + [Fact] + public async Task DeleteTimeEntry_Should_Succeed() + { + //Arrange + var createdTimeEntry = await CreateTestTimeEntryAsync(); + Assert.NotNull(createdTimeEntry); + + var timeEntryId = createdTimeEntry.Id.ToInvariantString(); + + //Act + await fixture.RedmineManager.DeleteAsync(timeEntryId); + + //Assert + await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetAsync(timeEntryId)); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/TrackerTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/TrackerTestsAsync.cs new file mode 100644 index 00000000..0b549009 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/TrackerTestsAsync.cs @@ -0,0 +1,19 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; + +[Collection(Constants.RedmineTestContainerCollection)] +public class TrackerTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task Get_All_Trackers_Should_Succeed() + { + //Act + var trackers = await fixture.RedmineManager.GetAsync(); + + //Assert + Assert.NotNull(trackers); + Assert.NotEmpty(trackers); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/UploadTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/UploadTestsAsync.cs new file mode 100644 index 00000000..1b925d57 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/UploadTestsAsync.cs @@ -0,0 +1,85 @@ +using System.Text; +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Net; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; + +[Collection(Constants.RedmineTestContainerCollection)] +public class UploadTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task Upload_Attachment_To_Issue_Should_Succeed() + { + var bytes = "Hello World!"u8.ToArray(); + var uploadFile = await fixture.RedmineManager.UploadFileAsync(bytes, "hello-world.txt"); + + Assert.NotNull(uploadFile); + Assert.NotNull(uploadFile.Token); + + var issue = await fixture.RedmineManager.CreateAsync(new Issue() + { + Project = 1.ToIdentifier(), + Subject = "Creating an issue with a uploaded file", + Tracker = 1.ToIdentifier(), + Status = 1.ToIssueStatusIdentifier(), + Uploads = [ + new Upload() + { + Token = uploadFile.Token, + ContentType = "text/plain", + Description = "An optional description here", + FileName = "hello-world.txt" + } + ] + }); + + Assert.NotNull(issue); + + var files = await fixture.RedmineManager.GetAsync(issue.Id.ToInvariantString(), RequestOptions.Include(RedmineKeys.ATTACHMENTS)); + + Assert.NotNull(files); + Assert.Single(files.Attachments); + } + + [Fact] + public async Task Upload_Attachment_To_Wiki_Should_Succeed() + { + var bytes = Encoding.UTF8.GetBytes(ThreadSafeRandom.GenerateText("Hello Wiki!",10)); + var fileName = $"{ThreadSafeRandom.GenerateText("wiki-",5)}.txt"; + var uploadFile = await fixture.RedmineManager.UploadFileAsync(bytes, fileName); + + Assert.NotNull(uploadFile); + Assert.NotNull(uploadFile.Token); + + var wikiPageName = ThreadSafeRandom.GenerateText(7); + + var wikiPageInfo = new WikiPage() + { + Version = 0, + Comments = ThreadSafeRandom.GenerateText(15), + Text = ThreadSafeRandom.GenerateText(10), + Uploads = + [ + new Upload() + { + Token = uploadFile.Token, + ContentType = "text/plain", + Description = ThreadSafeRandom.GenerateText(15), + FileName = fileName, + } + ] + }; + + var wiki = await fixture.RedmineManager.CreateWikiPageAsync(1.ToInvariantString(), wikiPageName, wikiPageInfo); + + Assert.NotNull(wiki); + + var files = await fixture.RedmineManager.GetWikiPageAsync(1.ToInvariantString(), wikiPageName, RequestOptions.Include(RedmineKeys.ATTACHMENTS)); + + Assert.NotNull(files); + Assert.Single(files.Attachments); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/UserTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/UserTestsAsync.cs new file mode 100644 index 00000000..b5bbd987 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/UserTestsAsync.cs @@ -0,0 +1,112 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; + +[Collection(Constants.RedmineTestContainerCollection)] +public class UserTestsAsync(RedmineTestContainerFixture fixture) +{ + private async Task CreateTestUserAsync() + { + var user = new User + { + Login = ThreadSafeRandom.GenerateText(12), + FirstName = ThreadSafeRandom.GenerateText(8), + LastName = ThreadSafeRandom.GenerateText(10), + Email = $"{ThreadSafeRandom.GenerateText(5)}.{ThreadSafeRandom.GenerateText(4)}@gmail.com", + Password = "password123", + AuthenticationModeId = null, + MustChangePassword = false, + Status = UserStatus.StatusActive + }; + return await fixture.RedmineManager.CreateAsync(user); + } + + [Fact] + public async Task CreateUser_Should_Succeed() + { + //Arrange + var userData = new User + { + Login = ThreadSafeRandom.GenerateText(5), + FirstName = ThreadSafeRandom.GenerateText(5), + LastName = ThreadSafeRandom.GenerateText(5), + Password = "password123", + MailNotification = "only_my_events", + AuthenticationModeId = null, + MustChangePassword = false, + Status = UserStatus.StatusActive, + }; + + userData.Email = $"{userData.FirstName}.{userData.LastName}@gmail.com"; + + //Act + var createdUser = await fixture.RedmineManager.CreateAsync(userData); + + //Assert + Assert.NotNull(createdUser); + Assert.True(createdUser.Id > 0); + Assert.Equal(userData.Login, createdUser.Login); + Assert.Equal(userData.FirstName, createdUser.FirstName); + Assert.Equal(userData.LastName, createdUser.LastName); + Assert.Equal(userData.Email, createdUser.Email); + } + + [Fact] + public async Task GetUser_Should_Succeed() + { + + //Arrange + var createdUser = await CreateTestUserAsync(); + Assert.NotNull(createdUser); + + //Act + var retrievedUser = + await fixture.RedmineManager.GetAsync(createdUser.Id.ToInvariantString()); + + //Assert + Assert.NotNull(retrievedUser); + Assert.Equal(createdUser.Id, retrievedUser.Id); + Assert.Equal(createdUser.Login, retrievedUser.Login); + Assert.Equal(createdUser.FirstName, retrievedUser.FirstName); + } + + [Fact] + public async Task UpdateUser_Should_Succeed() + { + + //Arrange + var createdUser = await CreateTestUserAsync(); + Assert.NotNull(createdUser); + + var updatedFirstName = ThreadSafeRandom.GenerateText(10); + createdUser.FirstName = updatedFirstName; + + //Act + await fixture.RedmineManager.UpdateAsync(createdUser.Id.ToInvariantString(), createdUser); + var retrievedUser = + await fixture.RedmineManager.GetAsync(createdUser.Id.ToInvariantString()); + + //Assert + Assert.NotNull(retrievedUser); + Assert.Equal(createdUser.Id, retrievedUser.Id); + Assert.Equal(updatedFirstName, retrievedUser.FirstName); + } + + [Fact] + public async Task DeleteUser_Should_Succeed() + { + //Arrange + var createdUser = await CreateTestUserAsync(); + Assert.NotNull(createdUser); + var userId = createdUser.Id.ToInvariantString(); + + //Act + await fixture.RedmineManager.DeleteAsync(userId); + + //Assert + await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetAsync(userId)); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/VersionTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/VersionTestsAsync.cs new file mode 100644 index 00000000..71cd8e4c --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/VersionTestsAsync.cs @@ -0,0 +1,109 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; +using Version = Redmine.Net.Api.Types.Version; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; + +[Collection(Constants.RedmineTestContainerCollection)] +public class VersionTestsAsync(RedmineTestContainerFixture fixture) +{ + private const string PROJECT_ID = "1"; + + private async Task CreateTestVersionAsync() + { + var version = new Version + { + Name = ThreadSafeRandom.GenerateText(10), + Description = ThreadSafeRandom.GenerateText(15), + Status = VersionStatus.Open, + Sharing = VersionSharing.None, + DueDate = DateTime.Now.Date.AddDays(30) + }; + return await fixture.RedmineManager.CreateAsync(version, PROJECT_ID); + } + + [Fact] + public async Task CreateVersion_Should_Succeed() + { + //Arrange + var versionSuffix = Guid.NewGuid().ToString("N"); + var versionData = new Version + { + Name = $"Test Version Create {versionSuffix}", + Description = $"Initial create test description {Guid.NewGuid()}", + Status = VersionStatus.Open, + Sharing = VersionSharing.System, + DueDate = DateTime.Now.Date.AddDays(10) + }; + + //Act + var createdVersion = await fixture.RedmineManager.CreateAsync(versionData, PROJECT_ID); + + //Assert + Assert.NotNull(createdVersion); + Assert.True(createdVersion.Id > 0); + Assert.Equal(versionData.Name, createdVersion.Name); + Assert.Equal(versionData.Description, createdVersion.Description); + Assert.Equal(versionData.Status, createdVersion.Status); + Assert.Equal(PROJECT_ID, createdVersion.Project.Id.ToInvariantString()); + } + + [Fact] + public async Task GetVersion_Should_Succeed() + { + + //Arrange + var createdVersion = await CreateTestVersionAsync(); + Assert.NotNull(createdVersion); + + //Act + var retrievedVersion = await fixture.RedmineManager.GetAsync(createdVersion.Id.ToInvariantString()); + + //Assert + Assert.NotNull(retrievedVersion); + Assert.Equal(createdVersion.Id, retrievedVersion.Id); + Assert.Equal(createdVersion.Name, retrievedVersion.Name); + Assert.Equal(createdVersion.Description, retrievedVersion.Description); + } + + [Fact] + public async Task UpdateVersion_Should_Succeed() + { + //Arrange + var createdVersion = await CreateTestVersionAsync(); + Assert.NotNull(createdVersion); + + var updatedDescription = ThreadSafeRandom.GenerateText(20); + var updatedStatus = VersionStatus.Locked; + createdVersion.Description = updatedDescription; + createdVersion.Status = updatedStatus; + + //Act + await fixture.RedmineManager.UpdateAsync(createdVersion.Id.ToInvariantString(), createdVersion); + var retrievedVersion = await fixture.RedmineManager.GetAsync(createdVersion.Id.ToInvariantString()); + + //Assert + Assert.NotNull(retrievedVersion); + Assert.Equal(createdVersion.Id, retrievedVersion.Id); + Assert.Equal(updatedDescription, retrievedVersion.Description); + Assert.Equal(updatedStatus, retrievedVersion.Status); + } + + [Fact] + public async Task DeleteVersion_Should_Succeed() + { + //Arrange + var createdVersion = await CreateTestVersionAsync(); + Assert.NotNull(createdVersion); + var versionId = createdVersion.Id.ToInvariantString(); + + //Act + await fixture.RedmineManager.DeleteAsync(versionId); + + //Assert + await Assert.ThrowsAsync(async () => + await fixture.RedmineManager.GetAsync(versionId)); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/WikiTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/WikiTestsAsync.cs new file mode 100644 index 00000000..9ef9a3a0 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/WikiTestsAsync.cs @@ -0,0 +1,170 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; + +[Collection(Constants.RedmineTestContainerCollection)] +public class WikiTestsAsync(RedmineTestContainerFixture fixture) +{ + private const string PROJECT_ID = "1"; + private const string WIKI_PAGE_TITLE = "TestWikiPage"; + + private async Task CreateOrUpdateTestWikiPageAsync() + { + var wikiPage = new WikiPage + { + Title = WIKI_PAGE_TITLE, + Text = $"Test wiki page content {Guid.NewGuid()}", + Comments = "Initial wiki page creation" + }; + + return await fixture.RedmineManager.CreateWikiPageAsync(PROJECT_ID, "wikiPageName", wikiPage); + } + + [Fact] + public async Task CreateOrUpdateWikiPage_Should_Succeed() + { + // Arrange + var wikiPage = new WikiPage + { + Title = $"TestWikiPage_{Guid.NewGuid()}".Replace("-", "").Substring(0, 20), + Text = "Test wiki page content", + Comments = "Initial wiki page creation" + }; + + // Act + var createdPage = await fixture.RedmineManager.CreateWikiPageAsync(PROJECT_ID, "wikiPageName", wikiPage); + + // Assert + Assert.NotNull(createdPage); + Assert.Equal(wikiPage.Title, createdPage.Title); + Assert.Equal(wikiPage.Text, createdPage.Text); + } + + [Fact] + public async Task GetWikiPage_Should_Succeed() + { + // Arrange + var createdPage = await CreateOrUpdateTestWikiPageAsync(); + Assert.NotNull(createdPage); + + // Act + var retrievedPage = await fixture.RedmineManager.GetWikiPageAsync(PROJECT_ID, createdPage.Title); + + // Assert + Assert.NotNull(retrievedPage); + Assert.Equal(createdPage.Title, retrievedPage.Title); + Assert.Equal(createdPage.Text, retrievedPage.Text); + } + + [Fact] + public async Task GetAllWikiPages_Should_Succeed() + { + // Arrange + await CreateOrUpdateTestWikiPageAsync(); + + // Act + var wikiPages = await fixture.RedmineManager.GetAllWikiPagesAsync(PROJECT_ID); + + // Assert + Assert.NotNull(wikiPages); + Assert.NotEmpty(wikiPages); + } + + [Fact] + public async Task DeleteWikiPage_Should_Succeed() + { + // Arrange + var wikiPageName = ThreadSafeRandom.GenerateText(7); + + var wikiPage = new WikiPage + { + Title = ThreadSafeRandom.GenerateText(5), + Text = "Test wiki page content for deletion", + Comments = "Initial wiki page creation for deletion test" + }; + + var createdPage = await fixture.RedmineManager.CreateWikiPageAsync(PROJECT_ID, wikiPageName, wikiPage); + Assert.NotNull(createdPage); + + // Act + await fixture.RedmineManager.DeleteWikiPageAsync(PROJECT_ID, wikiPageName); + + // Assert + await Assert.ThrowsAsync(async () => + await fixture.RedmineManager.GetWikiPageAsync(PROJECT_ID, wikiPageName)); + } + + private async Task<(WikiPage Page, string ProjectId, string PageTitle)> CreateTestWikiPageAsync( + string pageTitleSuffix = null, + string initialText = "Default initial text for wiki page.", + string initialComments = "Initial comments for wiki page.") + { + var pageTitle = $"TestWikiPage_{(pageTitleSuffix ?? Guid.NewGuid().ToString("N"))}"; + var wikiPageData = new WikiPage + { + Text = initialText, + Comments = initialComments + }; + + var createdPage = await fixture.RedmineManager.CreateWikiPageAsync(PROJECT_ID, pageTitle, wikiPageData); + + Assert.NotNull(createdPage); + Assert.Equal(pageTitle, createdPage.Title); + Assert.True(createdPage.Id > 0, "Created WikiPage should have a valid ID."); + Assert.Equal(initialText, createdPage.Text); + + return (createdPage, PROJECT_ID, pageTitle); + } + + [Fact] + public async Task CreateWikiPage_Should_Succeed() + { + //Arrange + var pageTitle = ThreadSafeRandom.GenerateText("NewWikiPage"); + var text = "This is the content of a new wiki page."; + var comments = "Creation comment for new wiki page."; + var wikiPageData = new WikiPage { Text = text, Comments = comments }; + + //Act + var createdPage = await fixture.RedmineManager.CreateWikiPageAsync(PROJECT_ID, pageTitle, wikiPageData); + + //Assert + Assert.NotNull(createdPage); + Assert.Equal(pageTitle, createdPage.Title); + Assert.Equal(text, createdPage.Text); + Assert.True(createdPage.Version >= 0); + + } + + [Fact] + public async Task UpdateWikiPage_Should_Succeed() + { + //Arrange + var (initialPage, projectId, pageTitle) = await CreateTestWikiPageAsync("UpdateTest", "Original Text.", "Original Comments."); + + var updatedText = $"Updated wiki text content {Guid.NewGuid():N}"; + var updatedComments = "These are updated comments for the wiki page update."; + + var wikiPageToUpdate = new WikiPage + { + Text = updatedText, + Comments = updatedComments, + Version = ++initialPage.Version + }; + + //Act + await fixture.RedmineManager.UpdateWikiPageAsync(projectId, pageTitle, wikiPageToUpdate); + var retrievedPage = await fixture.RedmineManager.GetAsync(initialPage.Id.ToInvariantString()); + + //Assert + Assert.NotNull(retrievedPage); + Assert.Equal(updatedText, retrievedPage.Text); + Assert.Equal(updatedComments, retrievedPage.Comments); + Assert.True(retrievedPage.Version > initialPage.Version + || (retrievedPage.Version == 1 && initialPage.Version == 0) + || (initialPage.Version ==0 && retrievedPage.Version ==0)); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Infrastructure/Fixtures/JsonSerializerFixture.cs b/tests/redmine-net-api.Tests/Infrastructure/Fixtures/JsonSerializerFixture.cs index d787dd62..35c40898 100644 --- a/tests/redmine-net-api.Tests/Infrastructure/Fixtures/JsonSerializerFixture.cs +++ b/tests/redmine-net-api.Tests/Infrastructure/Fixtures/JsonSerializerFixture.cs @@ -1,4 +1,5 @@ using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Serialization.Json; namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; diff --git a/tests/redmine-net-api.Tests/Infrastructure/Fixtures/XmlSerializerFixture.cs b/tests/redmine-net-api.Tests/Infrastructure/Fixtures/XmlSerializerFixture.cs index 700329e1..55f209ec 100644 --- a/tests/redmine-net-api.Tests/Infrastructure/Fixtures/XmlSerializerFixture.cs +++ b/tests/redmine-net-api.Tests/Infrastructure/Fixtures/XmlSerializerFixture.cs @@ -1,4 +1,5 @@ using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Serialization.Xml; namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; From 5c5e14fd1fd34278cac0757e8e87b64a7f1e2edc Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 11 May 2025 15:22:12 +0300 Subject: [PATCH 463/549] [Serialization] Add more Json tests --- .../Serialization/Json/AttachmentTests.cs | 42 ++++++++++++ .../Serialization/Json/CustomFieldTests.cs | 67 +++++++++++++++++++ .../Serialization/Json/ErrorTests.cs | 33 +++++++++ .../Json/IssueCustomFieldsTests.cs | 43 ++++++++++++ .../Serialization/Json/UserTests.cs | 50 ++++++++++++++ 5 files changed, 235 insertions(+) create mode 100644 tests/redmine-net-api.Tests/Serialization/Json/AttachmentTests.cs create mode 100644 tests/redmine-net-api.Tests/Serialization/Json/CustomFieldTests.cs create mode 100644 tests/redmine-net-api.Tests/Serialization/Json/ErrorTests.cs create mode 100644 tests/redmine-net-api.Tests/Serialization/Json/IssueCustomFieldsTests.cs create mode 100644 tests/redmine-net-api.Tests/Serialization/Json/UserTests.cs diff --git a/tests/redmine-net-api.Tests/Serialization/Json/AttachmentTests.cs b/tests/redmine-net-api.Tests/Serialization/Json/AttachmentTests.cs new file mode 100644 index 00000000..6cb7191d --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Json/AttachmentTests.cs @@ -0,0 +1,42 @@ +using System; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Json; + +[Collection(Constants.JsonRedmineSerializerCollection)] +public class AttachmentTests(JsonSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Attachment() + { + const string input = """ + { + "attachment": { + "id": 6243, + "filename": "test.txt", + "filesize": 124, + "content_type": "text/plain", + "description": "This is an attachment", + "content_url": "http://localhost:3000/attachments/download/6243/test.txt", + "author": {"name": "Jean-Philippe Lang", "id": 1}, + "created_on": "2011-07-18T22:58:40+02:00" + } + } + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + Assert.Equal(6243, output.Id); + Assert.Equal("test.txt", output.FileName); + Assert.Equal(124, output.FileSize); + Assert.Equal("text/plain", output.ContentType); + Assert.Equal("This is an attachment", output.Description); + Assert.Equal("http://localhost:3000/attachments/download/6243/test.txt", output.ContentUrl); + Assert.Equal("Jean-Philippe Lang", output.Author.Name); + Assert.Equal(1, output.Author.Id); + Assert.Equal(new DateTime(2011, 7, 18, 20, 58, 40, DateTimeKind.Utc).ToLocalTime(), output.CreatedOn); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Json/CustomFieldTests.cs b/tests/redmine-net-api.Tests/Serialization/Json/CustomFieldTests.cs new file mode 100644 index 00000000..10d14dac --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Json/CustomFieldTests.cs @@ -0,0 +1,67 @@ +using System.Linq; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Json; + +[Collection(Constants.JsonRedmineSerializerCollection)] +public sealed class CustomFieldTests(JsonSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_CustomFields() + { + const string input = """ + { + "custom_fields": [ + { + "id": 1, + "name": "Affected version", + "customized_type": "issue", + "field_format": "list", + "regexp": null, + "min_length": null, + "max_length": null, + "is_required": true, + "is_filter": true, + "searchable": true, + "multiple": true, + "default_value": null, + "visible": false, + "possible_values": [ + { + "value": "0.5.x" + }, + { + "value": "0.6.x" + } + ] + } + ], + "total_count": 1 + } + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(1, output.TotalItems); + + var customFields = output.Items.ToList(); + Assert.Equal(1, customFields[0].Id); + Assert.Equal("Affected version", customFields[0].Name); + Assert.Equal("issue", customFields[0].CustomizedType); + Assert.Equal("list", customFields[0].FieldFormat); + Assert.True(customFields[0].IsRequired); + Assert.True(customFields[0].IsFilter); + Assert.True(customFields[0].Searchable); + Assert.True(customFields[0].Multiple); + Assert.False(customFields[0].Visible); + + var possibleValues = customFields[0].PossibleValues.ToList(); + Assert.Equal(2, possibleValues.Count); + Assert.Equal("0.5.x", possibleValues[0].Value); + Assert.Equal("0.6.x", possibleValues[1].Value); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Json/ErrorTests.cs b/tests/redmine-net-api.Tests/Serialization/Json/ErrorTests.cs new file mode 100644 index 00000000..889ac9dc --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Json/ErrorTests.cs @@ -0,0 +1,33 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Json; + +[Collection(Constants.JsonRedmineSerializerCollection)] +public class ErrorTests(JsonSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Errors() + { + const string input = """ + { + "errors":[ + "First name can't be blank", + "Email is invalid" + ], + "total_count":2 + } + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(2, output.TotalItems); + + var errors = output.Items.ToList(); + Assert.Equal("First name can't be blank", errors[0].Info); + Assert.Equal("Email is invalid", errors[1].Info); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Json/IssueCustomFieldsTests.cs b/tests/redmine-net-api.Tests/Serialization/Json/IssueCustomFieldsTests.cs new file mode 100644 index 00000000..ee570b6d --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Json/IssueCustomFieldsTests.cs @@ -0,0 +1,43 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Json; + +[Collection(Constants.JsonRedmineSerializerCollection)] +public class IssueCustomFieldsTests(JsonSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_Issue_With_CustomFields_With_Multiple_Values() + { + const string input = """ + { + "custom_fields":[ + {"value":["1.0.1","1.0.2"],"multiple":true,"name":"Affected version","id":1}, + {"value":"Fixed","name":"Resolution","id":2} + ], + "total_count":2 + } + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(2, output.TotalItems); + + var customFields = output.Items.ToList(); + + Assert.Equal(1, customFields[0].Id); + Assert.Equal("Affected version", customFields[0].Name); + Assert.True(customFields[0].Multiple); + Assert.Equal(2, customFields[0].Values.Count); + Assert.Equal("1.0.1", customFields[0].Values[0].Info); + Assert.Equal("1.0.2", customFields[0].Values[1].Info); + + Assert.Equal(2, customFields[1].Id); + Assert.Equal("Resolution", customFields[1].Name); + Assert.False(customFields[1].Multiple); + Assert.Equal("Fixed", customFields[1].Values[0].Info); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Serialization/Json/UserTests.cs b/tests/redmine-net-api.Tests/Serialization/Json/UserTests.cs new file mode 100644 index 00000000..d7c13243 --- /dev/null +++ b/tests/redmine-net-api.Tests/Serialization/Json/UserTests.cs @@ -0,0 +1,50 @@ +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; +using Redmine.Net.Api.Types; +using Xunit; + +namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Json; + +[Collection(Constants.JsonRedmineSerializerCollection)] +public class UserTests(JsonSerializerFixture fixture) +{ + [Fact] + public void Should_Deserialize_User() + { + const string input = """ + { + "user":{ + "id": 3, + "login":"jplang", + "firstname": "Jean-Philippe", + "lastname":"Lang", + "mail":"jp_lang@yahoo.fr", + "created_on": "2007-09-28T00:16:04+02:00", + "updated_on":"2010-08-01T18:05:45+02:00", + "last_login_on":"2011-08-01T18:05:45+02:00", + "passwd_changed_on": "2011-08-01T18:05:45+02:00", + "api_key": "ebc3f6b781a6fb3f2b0a83ce0ebb80e0d585189d", + "avatar_url": "", + "status": 1 + } + } + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + Assert.Equal(3, output.Id); + Assert.Equal("jplang", output.Login); + Assert.Equal("Jean-Philippe", output.FirstName); + Assert.Equal("Lang", output.LastName); + Assert.Equal("jp_lang@yahoo.fr", output.Email); + Assert.Equal(new DateTime(2007, 9, 28, 0, 16, 4, DateTimeKind.Local).AddHours(1), output.CreatedOn); + Assert.Equal(new DateTime(2010, 8, 1, 18, 5, 45, DateTimeKind.Local).AddHours(1), output.UpdatedOn); + Assert.Equal(new DateTime(2011, 8, 1, 18, 5, 45, DateTimeKind.Local).AddHours(1), output.LastLoginOn); + Assert.Equal(new DateTime(2011, 8, 1, 18, 5, 45, DateTimeKind.Local).AddHours(1), output.PasswordChangedOn); + Assert.Equal("ebc3f6b781a6fb3f2b0a83ce0ebb80e0d585189d", output.ApiKey); + Assert.Empty(output.AvatarUrl); + Assert.Equal(UserStatus.StatusActive, output.Status); + } + +} \ No newline at end of file From 43eef2861ef97c3e28e902442056526f16ea146f Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 11 May 2025 15:25:47 +0300 Subject: [PATCH 464/549] [IRedmineApiClientOptions] Remove redundant properties --- .../Net/IRedmineApiClientOptions.cs | 11 ++- .../Net/WebClient/IRedmineWebClientOptions.cs | 93 ++++--------------- 2 files changed, 22 insertions(+), 82 deletions(-) diff --git a/src/redmine-net-api/Net/IRedmineApiClientOptions.cs b/src/redmine-net-api/Net/IRedmineApiClientOptions.cs index 3a11601f..57431696 100644 --- a/src/redmine-net-api/Net/IRedmineApiClientOptions.cs +++ b/src/redmine-net-api/Net/IRedmineApiClientOptions.cs @@ -78,6 +78,7 @@ public interface IRedmineApiClientOptions /// long? MaxResponseContentBufferSize { get; set; } +#if NET471_OR_GREATER || NETCOREAPP /// /// /// @@ -87,7 +88,7 @@ public interface IRedmineApiClientOptions /// /// int? MaxResponseHeadersLength { get; set; } - +#endif /// /// /// @@ -179,18 +180,18 @@ public interface IRedmineApiClientOptions ///
SecurityProtocolType? SecurityProtocolType { get; set; } - #if NET40_OR_GREATER || NETCOREAPP +#if NET40_OR_GREATER || NETCOREAPP /// /// /// public X509CertificateCollection ClientCertificates { get; set; } - #endif +#endif - #if(NET46_OR_GREATER || NETCOREAPP) +#if(NET46_OR_GREATER || NETCOREAPP) /// /// /// public bool? ReusePort { get; set; } - #endif +#endif } } \ No newline at end of file diff --git a/src/redmine-net-api/Net/WebClient/IRedmineWebClientOptions.cs b/src/redmine-net-api/Net/WebClient/IRedmineWebClientOptions.cs index 809d9afb..1c7635f0 100644 --- a/src/redmine-net-api/Net/WebClient/IRedmineWebClientOptions.cs +++ b/src/redmine-net-api/Net/WebClient/IRedmineWebClientOptions.cs @@ -1,83 +1,22 @@ -using System; -using System.Collections.Generic; -using System.Net.Cache; -using System.Net.Security; -using System.Security.Cryptography.X509Certificates; +/* + Copyright 2011 - 2025 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ namespace Redmine.Net.Api.Net.WebClient; /// /// /// -public interface IRedmineWebClientOptions : IRedmineApiClientOptions -{ -#if NET40_OR_GREATER || NETCOREAPP - /// - /// - /// - public X509CertificateCollection ClientCertificates { get; set; } -#endif - - /// - /// - /// - int? DefaultConnectionLimit { get; set; } - - /// - /// - /// - Dictionary DefaultHeaders { get; set; } - - /// - /// - /// - int? DnsRefreshTimeout { get; set; } - - /// - /// - /// - bool? EnableDnsRoundRobin { get; set; } - - /// - /// - /// - bool? KeepAlive { get; set; } - - /// - /// - /// - int? MaxServicePoints { get; set; } - - /// - /// - /// - int? MaxServicePointIdleTime { get; set; } - - /// - /// - /// - RequestCachePolicy RequestCachePolicy { get; set; } - -#if(NET46_OR_GREATER || NETCOREAPP) - /// - /// - /// - public bool? ReusePort { get; set; } -#endif - - /// - /// - /// - RemoteCertificateValidationCallback ServerCertificateValidationCallback { get; set; } - - /// - /// - /// - bool? UnsafeAuthenticatedConnectionSharing { get; set; } - - /// - /// - /// - /// Only HTTP/1.0 and HTTP/1.1 version requests are currently supported. - Version ProtocolVersion { get; set; } -} \ No newline at end of file +public interface IRedmineWebClientOptions : IRedmineApiClientOptions; \ No newline at end of file From 0ce2e07d960b5e5271bebb47c911b2cf59c06527 Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 11 May 2025 15:26:29 +0300 Subject: [PATCH 465/549] [IdentifiableName] Add string operator --- src/redmine-net-api/Types/IdentifiableName.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/redmine-net-api/Types/IdentifiableName.cs b/src/redmine-net-api/Types/IdentifiableName.cs index 4f95ee02..42060a69 100644 --- a/src/redmine-net-api/Types/IdentifiableName.cs +++ b/src/redmine-net-api/Types/IdentifiableName.cs @@ -219,7 +219,24 @@ public override int GetHashCode() return !Equals(left, right); } #endregion + + /// + /// + /// + /// + /// + public static implicit operator string(IdentifiableName identifiableName) => FromIdentifiableName(identifiableName); + /// + /// + /// + /// + /// + public static string FromIdentifiableName(IdentifiableName identifiableName) + { + return identifiableName?.Id.ToInvariantString(); + } + /// /// /// From 03146bc3f91ebbc86b15b1e8da2cb78a8b075a6b Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 11 May 2025 15:28:52 +0300 Subject: [PATCH 466/549] Enums ToLowerInvariant instead of ToString().ToLowerInv() --- src/redmine-net-api/Types/IssueRelation.cs | 6 +++--- src/redmine-net-api/Types/Version.cs | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/redmine-net-api/Types/IssueRelation.cs b/src/redmine-net-api/Types/IssueRelation.cs index fad07f49..4f5da762 100644 --- a/src/redmine-net-api/Types/IssueRelation.cs +++ b/src/redmine-net-api/Types/IssueRelation.cs @@ -112,8 +112,8 @@ public override void WriteXml(XmlWriter writer) { AssertValidIssueRelationType(); - writer.WriteElementString(RedmineKeys.ISSUE_TO_ID, IssueToId.ToString(CultureInfo.InvariantCulture)); - writer.WriteElementString(RedmineKeys.RELATION_TYPE, Type.ToString().ToLowerInv()); + writer.WriteElementString(RedmineKeys.ISSUE_TO_ID, IssueToId.ToInvariantString()); + writer.WriteElementString(RedmineKeys.RELATION_TYPE, Type.ToLowerInvariant()); if (Type == IssueRelationType.Precedes || Type == IssueRelationType.Follows) { @@ -134,7 +134,7 @@ public override void WriteJson(JsonWriter writer) using (new JsonObject(writer, RedmineKeys.RELATION)) { writer.WriteProperty(RedmineKeys.ISSUE_TO_ID, IssueToId); - writer.WriteProperty(RedmineKeys.RELATION_TYPE, Type.ToString().ToLowerInv()); + writer.WriteProperty(RedmineKeys.RELATION_TYPE, Type.ToLowerInvariant()); if (Type == IssueRelationType.Precedes || Type == IssueRelationType.Follows) { diff --git a/src/redmine-net-api/Types/Version.cs b/src/redmine-net-api/Types/Version.cs index 884aca64..84f8ecd4 100644 --- a/src/redmine-net-api/Types/Version.cs +++ b/src/redmine-net-api/Types/Version.cs @@ -140,10 +140,10 @@ public override void ReadXml(XmlReader reader) public override void WriteXml(XmlWriter writer) { writer.WriteElementString(RedmineKeys.NAME, Name); - writer.WriteElementString(RedmineKeys.STATUS, Status.ToInvariantString()); + writer.WriteElementString(RedmineKeys.STATUS, Status.ToLowerInvariant()); if (Sharing != VersionSharing.Unknown) { - writer.WriteElementString(RedmineKeys.SHARING, Sharing.ToInvariantString()); + writer.WriteElementString(RedmineKeys.SHARING, Sharing.ToLowerInvariant()); } writer.WriteDateOrEmpty(RedmineKeys.DUE_DATE, DueDate); @@ -206,8 +206,8 @@ public override void WriteJson(JsonWriter writer) using (new JsonObject(writer, RedmineKeys.VERSION)) { writer.WriteProperty(RedmineKeys.NAME, Name); - writer.WriteProperty(RedmineKeys.STATUS, Status.ToString().ToLowerInv()); - writer.WriteProperty(RedmineKeys.SHARING, Sharing.ToString().ToLowerInv()); + writer.WriteProperty(RedmineKeys.STATUS, Status.ToLowerInvariant()); + writer.WriteProperty(RedmineKeys.SHARING, Sharing.ToLowerInvariant()); writer.WriteProperty(RedmineKeys.DESCRIPTION, Description); writer.WriteDateOrEmpty(RedmineKeys.DUE_DATE, DueDate); if (CustomFields != null) From d5811ce11307dd9a577d37917af3fb6b4d0265a1 Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 14 May 2025 11:57:49 +0300 Subject: [PATCH 467/549] Add icons --- .github/workflows/build-and-test.yml | 2 +- .github/workflows/publish.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index ef7d0fcc..bdc96713 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -99,7 +99,7 @@ jobs: - name: Restore run: dotnet restore "${{ env.PROJECT_PATH }}" - - name: Build + - name: 🔨 Build run: >- dotnet build "${{ env.PROJECT_PATH }}" --configuration "${{ env.CONFIGURATION }}" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f3c32f01..a20b2882 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -128,7 +128,7 @@ jobs: - name: Install dependencies run: dotnet restore "${{ env.PROJECT_PATH }}" - - name: Create the package + - name: 📦 Create the package run: >- dotnet pack "${{ env.PROJECT_PATH }}" --output ./artifacts @@ -140,7 +140,7 @@ jobs: -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg - - name: Create the package - Signed + - name: 📦 Create the package - Signed run: >- dotnet pack "${{ env.PROJECT_PATH }}" --output ./artifacts From faeee90fa5139f1264723b99b2c966c145e6db2c Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 14 May 2025 11:20:15 +0300 Subject: [PATCH 468/549] Add ArgumentVerifier --- .../Common/ArgumentVerifier.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 src/redmine-net-api/Common/ArgumentVerifier.cs diff --git a/src/redmine-net-api/Common/ArgumentVerifier.cs b/src/redmine-net-api/Common/ArgumentVerifier.cs new file mode 100644 index 00000000..c4e7ede7 --- /dev/null +++ b/src/redmine-net-api/Common/ArgumentVerifier.cs @@ -0,0 +1,43 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Redmine.Net.Api.Common; + +/// +/// A utility class to perform argument validations. +/// +internal static class ArgumentVerifier +{ + /// + /// Throws ArgumentNullException if the argument is null. + /// + /// Argument value to check. + /// Name of Argument. + public static void ThrowIfNull([NotNull] object value, string name) + { + if (value == null) + { + throw new ArgumentNullException(name); + } + } + + /// + /// Validates string and throws: + /// ArgumentNullException if the argument is null. + /// ArgumentException if the argument is empty. + /// + /// Argument value to check. + /// Name of Argument. + public static void ThrowIfNullOrEmpty([NotNull] string value, string name) + { + if (value == null) + { + throw new ArgumentNullException(name); + } + + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentException("The value cannot be null or empty", name); + } + } +} \ No newline at end of file From 7614fad15461c1a1eefd061ee7c55cc565478cc3 Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 14 May 2025 11:27:45 +0300 Subject: [PATCH 469/549] Add Include options (Group, Issue, Project, User & Wiki) --- src/redmine-net-api/Types/Include.cs | 142 +++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 src/redmine-net-api/Types/Include.cs diff --git a/src/redmine-net-api/Types/Include.cs b/src/redmine-net-api/Types/Include.cs new file mode 100644 index 00000000..ef4fa09a --- /dev/null +++ b/src/redmine-net-api/Types/Include.cs @@ -0,0 +1,142 @@ +namespace Redmine.Net.Api.Types; + +/// +/// +/// +public static class Include +{ + /// + /// + /// + public static class Group + { + /// + /// + /// + public const string Users = RedmineKeys.USERS; + + /// + /// Adds extra information about user's memberships and roles on the projects + /// + public const string Memberships = RedmineKeys.MEMBERSHIPS; + } + + /// + /// Associated data that can be retrieved + /// + public static class Issue + { + /// + /// Specifies whether to include child issues. + /// This parameter is applicable when retrieving details for a specific issue. + /// Corresponds to the Redmine API include parameter: children. + /// + public const string Children = RedmineKeys.CHILDREN; + + /// + /// Specifies whether to include attachments. + /// This parameter is applicable when retrieving a list of issues or details for a specific issue. + /// Corresponds to the Redmine API include parameter: attachments. + /// + public const string Attachments = RedmineKeys.ATTACHMENTS; + + /// + /// Specifies whether to include issue relations. + /// This parameter is applicable when retrieving a list of issues or details for a specific issue. + /// Corresponds to the Redmine API include parameter: relations. + /// + public const string Relations = RedmineKeys.RELATIONS; + + /// + /// Specifies whether to include associated changesets. + /// This parameter is applicable when retrieving details for a specific issue. + /// Corresponds to the Redmine API include parameter: changesets. + /// + public const string Changesets = RedmineKeys.CHANGE_SETS; + + /// + /// Specifies whether to include journal entries (notes and history). + /// This parameter is applicable when retrieving details for a specific issue. + /// Corresponds to the Redmine API include parameter: journals. + /// + public const string Journals = RedmineKeys.JOURNALS; + + /// + /// Specifies whether to include watchers of the issue. + /// This parameter is applicable when retrieving details for a specific issue. + /// Corresponds to the Redmine API include parameter: watchers. + /// + public const string Watchers = RedmineKeys.WATCHERS; + + /// + /// Specifies whether to include allowed statuses of the issue. + /// This parameter is applicable when retrieving details for a specific issue. + /// Corresponds to the Redmine API include parameter: watchers. + /// Since 5.0.x, Returns the available allowed statuses (the same values as provided in the issue edit form) based on: + /// the issue's current tracker, the issue's current status, and the member's role (the defined workflow); + /// the existence of any open subtask(s); + /// the existence of any open blocking issue(s); + /// the existence of a closed parent issue. + /// + public const string AllowedStatuses = RedmineKeys.ALLOWED_STATUSES; + } + + /// + /// + /// + public static class Project + { + /// + /// + /// + public const string Trackers = RedmineKeys.TRACKERS; + + /// + /// since 2.6.0 + /// + public const string EnabledModules = RedmineKeys.ENABLED_MODULES; + + /// + /// + /// + public const string IssueCategories = RedmineKeys.ISSUE_CATEGORIES; + + /// + /// since 3.4.0 + /// + public const string TimeEntryActivities = RedmineKeys.TIME_ENTRY_ACTIVITIES; + + /// + /// since 4.2.0 + /// + public const string IssueCustomFields = RedmineKeys.ISSUE_CUSTOM_FIELDS; + } + + /// + /// + /// + public static class User + { + /// + /// Adds extra information about user's memberships and roles on the projects + /// + public const string Memberships = RedmineKeys.MEMBERSHIPS; + + /// + /// Adds extra information about user's groups + /// added in 2.1 + /// + public const string Groups = RedmineKeys.GROUPS; + } + + /// + /// + /// + public static class WikiPage + { + /// + /// + /// + public const string Attachments = RedmineKeys.ATTACHMENTS; + } +} \ No newline at end of file From 9cecd0e533a1c7ce83d215d854de11b5deced9a4 Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 11 May 2025 15:29:46 +0300 Subject: [PATCH 470/549] Add Create(id, name) overload --- src/redmine-net-api/Types/IdentifiableName.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/redmine-net-api/Types/IdentifiableName.cs b/src/redmine-net-api/Types/IdentifiableName.cs index 42060a69..3d087f3a 100644 --- a/src/redmine-net-api/Types/IdentifiableName.cs +++ b/src/redmine-net-api/Types/IdentifiableName.cs @@ -38,7 +38,19 @@ public class IdentifiableName : Identifiable /// public static T Create(int id) where T: IdentifiableName, new() { - var t = new T (){Id = id}; + var t = new T + { + Id = id + }; + return t; + } + + internal static T Create(int id, string name) where T: IdentifiableName, new() + { + var t = new T + { + Id = id, Name = name + }; return t; } From 1ba04d38c774c85078f67c956f0b79b8590c212e Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 11 May 2025 15:30:31 +0300 Subject: [PATCH 471/549] Add ctor with parameter --- src/redmine-net-api/Types/IssueStatus.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/redmine-net-api/Types/IssueStatus.cs b/src/redmine-net-api/Types/IssueStatus.cs index 76d7863e..8478746e 100644 --- a/src/redmine-net-api/Types/IssueStatus.cs +++ b/src/redmine-net-api/Types/IssueStatus.cs @@ -31,11 +31,23 @@ namespace Redmine.Net.Api.Types [XmlRoot(RedmineKeys.ISSUE_STATUS)] public sealed class IssueStatus : IdentifiableName, IEquatable, ICloneable { + /// + /// + /// public IssueStatus() { } + /// + /// + /// + /// + public IssueStatus(int id) + { + Id = id; + } + /// /// /// From 119941940405eeeb8c51d16a5d3af0f8a65f2ba1 Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 14 May 2025 11:35:09 +0300 Subject: [PATCH 472/549] Add ToIssueStatusIdentifier --- .../Extensions/IdentifiableNameExtensions.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/redmine-net-api/Extensions/IdentifiableNameExtensions.cs b/src/redmine-net-api/Extensions/IdentifiableNameExtensions.cs index 9e03082e..a0daf466 100644 --- a/src/redmine-net-api/Extensions/IdentifiableNameExtensions.cs +++ b/src/redmine-net-api/Extensions/IdentifiableNameExtensions.cs @@ -61,5 +61,21 @@ public static IdentifiableName ToIdentifier(this int val) return new IdentifiableName(val, null); } + + /// + /// Converts an integer value into an object. + /// + /// The integer value representing the ID of an issue status. + /// An object initialized with the specified identifier. + /// Thrown when the specified value is less than or equal to zero. + public static IssueStatus ToIssueStatusIdentifier(this int val) + { + if (val <= 0) + { + throw new RedmineException(nameof(val), "Value must be greater than zero"); + } + + return new IssueStatus(val, null); + } } } \ No newline at end of file From f90c88625e469b205db3cb85f6c27b1986c915b9 Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 14 May 2025 11:39:30 +0300 Subject: [PATCH 473/549] Add GeneratePassword & SendInformation props --- src/redmine-net-api/Types/User.cs | 80 ++++++++++++++++++------------- 1 file changed, 47 insertions(+), 33 deletions(-) diff --git a/src/redmine-net-api/Types/User.cs b/src/redmine-net-api/Types/User.cs index 368ead09..fb5c71c1 100644 --- a/src/redmine-net-api/Types/User.cs +++ b/src/redmine-net-api/Types/User.cs @@ -17,7 +17,6 @@ limitations under the License. using System; using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; using System.Xml; using System.Xml.Serialization; using Newtonsoft.Json; @@ -115,6 +114,10 @@ public sealed class User : Identifiable ///
public bool MustChangePassword { get; set; } + /// + /// + /// + public bool GeneratePassword { get; set; } /// /// @@ -152,9 +155,15 @@ public sealed class User : Identifiable /// Gets or sets the user's mail_notification. /// /// - /// only_my_events, only_assigned, [...] + /// only_my_events, only_assigned, ... /// public string MailNotification { get; set; } + + /// + /// Send account information to the user + /// + public bool SendInformation { get; set; } + #endregion #region Implementation of IXmlSerialization @@ -207,32 +216,33 @@ public override void ReadXml(XmlReader reader) public override void WriteXml(XmlWriter writer) { writer.WriteElementString(RedmineKeys.LOGIN, Login); - writer.WriteElementString(RedmineKeys.FIRST_NAME, FirstName); - writer.WriteElementString(RedmineKeys.LAST_NAME, LastName); - writer.WriteElementString(RedmineKeys.MAIL, Email); - if(!string.IsNullOrEmpty(MailNotification)) - { - writer.WriteElementString(RedmineKeys.MAIL_NOTIFICATION, MailNotification); - } - - if (!string.IsNullOrEmpty(Password)) + if (!Password.IsNullOrWhiteSpace()) { writer.WriteElementString(RedmineKeys.PASSWORD, Password); } - + + writer.WriteElementString(RedmineKeys.FIRST_NAME, FirstName); + writer.WriteElementString(RedmineKeys.LAST_NAME, LastName); + writer.WriteElementString(RedmineKeys.MAIL, Email); + if(AuthenticationModeId.HasValue) { writer.WriteValueOrEmpty(RedmineKeys.AUTH_SOURCE_ID, AuthenticationModeId); } + if(!MailNotification.IsNullOrWhiteSpace()) + { + writer.WriteElementString(RedmineKeys.MAIL_NOTIFICATION, MailNotification); + } + writer.WriteBoolean(RedmineKeys.MUST_CHANGE_PASSWORD, MustChangePassword); - writer.WriteElementString(RedmineKeys.STATUS, ((int)Status).ToString(CultureInfo.InvariantCulture)); + writer.WriteBoolean(RedmineKeys.GENERATE_PASSWORD, GeneratePassword); + writer.WriteBoolean(RedmineKeys.SEND_INFORMATION, SendInformation); - if(CustomFields != null) - { - writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); - } + writer.WriteElementString(RedmineKeys.STATUS, ((int)Status).ToInvariantString()); + + writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); } #endregion @@ -291,32 +301,33 @@ public override void WriteJson(JsonWriter writer) using (new JsonObject(writer, RedmineKeys.USER)) { writer.WriteProperty(RedmineKeys.LOGIN, Login); - writer.WriteProperty(RedmineKeys.FIRST_NAME, FirstName); - writer.WriteProperty(RedmineKeys.LAST_NAME, LastName); - writer.WriteProperty(RedmineKeys.MAIL, Email); - if(!string.IsNullOrEmpty(MailNotification)) - { - writer.WriteProperty(RedmineKeys.MAIL_NOTIFICATION, MailNotification); - } - if (!string.IsNullOrEmpty(Password)) { writer.WriteProperty(RedmineKeys.PASSWORD, Password); } - + + writer.WriteProperty(RedmineKeys.FIRST_NAME, FirstName); + writer.WriteProperty(RedmineKeys.LAST_NAME, LastName); + writer.WriteProperty(RedmineKeys.MAIL, Email); + if(AuthenticationModeId.HasValue) { writer.WriteValueOrEmpty(RedmineKeys.AUTH_SOURCE_ID, AuthenticationModeId); } + + if(!MailNotification.IsNullOrWhiteSpace()) + { + writer.WriteProperty(RedmineKeys.MAIL_NOTIFICATION, MailNotification); + } writer.WriteBoolean(RedmineKeys.MUST_CHANGE_PASSWORD, MustChangePassword); - writer.WriteProperty(RedmineKeys.STATUS, ((int)Status).ToString(CultureInfo.InvariantCulture)); + writer.WriteBoolean(RedmineKeys.GENERATE_PASSWORD, GeneratePassword); + writer.WriteBoolean(RedmineKeys.SEND_INFORMATION, SendInformation); + + writer.WriteProperty(RedmineKeys.STATUS, ((int)Status).ToInvariantString()); - if(CustomFields != null) - { - writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); - } + writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); } } #endregion @@ -344,6 +355,8 @@ public override bool Equals(User other) && LastLoginOn == other.LastLoginOn && Status == other.Status && MustChangePassword == other.MustChangePassword + && GeneratePassword == other.GeneratePassword + && SendInformation == other.SendInformation && IsAdmin == other.IsAdmin && PasswordChangedOn == other.PasswordChangedOn && UpdatedOn == other.UpdatedOn @@ -394,6 +407,8 @@ public override int GetHashCode() hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); hashCode = HashCodeHelper.GetHashCode(Memberships, hashCode); hashCode = HashCodeHelper.GetHashCode(Groups, hashCode); + hashCode = HashCodeHelper.GetHashCode(GeneratePassword, hashCode); + hashCode = HashCodeHelper.GetHashCode(SendInformation, hashCode); return hashCode; } } @@ -425,7 +440,6 @@ public override int GetHashCode() /// ///
/// - private string DebuggerDisplay => $"[User: Id={Id.ToInvariantString()}, Login={Login}, IsAdmin={IsAdmin.ToString(CultureInfo.InvariantCulture)}, Status={Status:G}]"; - + private string DebuggerDisplay => $"[User: Id={Id.ToInvariantString()}, Login={Login}, IsAdmin={IsAdmin.ToInvariantString()}, Status={Status:G}]"; } } \ No newline at end of file From deeb32df7c866de2f02b620ac9a3ae50c4d65210 Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 14 May 2025 11:34:40 +0300 Subject: [PATCH 474/549] Add static methods to create single/multiple custom fields --- src/redmine-net-api/Types/IssueCustomField.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/redmine-net-api/Types/IssueCustomField.cs b/src/redmine-net-api/Types/IssueCustomField.cs index f4e4a717..0acdffb0 100644 --- a/src/redmine-net-api/Types/IssueCustomField.cs +++ b/src/redmine-net-api/Types/IssueCustomField.cs @@ -326,5 +326,53 @@ public static string GetValue(object item) ///
/// private string DebuggerDisplay => $"[IssueCustomField: Id={Id.ToInvariantString()}, Name={Name}, Multiple={Multiple.ToInvariantString()}]"; + + /// + /// + /// + /// + /// + /// + /// + public static IssueCustomField CreateSingle(int id, string name, string value) + { + return new IssueCustomField + { + Id = id, + Name = name, + Values = [new CustomFieldValue { Info = value }] + }; + } + + /// + /// + /// + /// + /// + /// + /// + public static IssueCustomField CreateMultiple(int id, string name, string[] values) + { + var isf = new IssueCustomField + { + Id = id, + Name = name, + Multiple = true, + }; + + if (values is not { Length: > 0 }) + { + return isf; + } + + isf.Values = new List(values.Length); + + foreach (var value in values) + { + isf.Values.Add(new CustomFieldValue { Info = value }); + } + + return isf; + } } } \ No newline at end of file From c77f1b7c1c5ac98590331edd1462ead15ac7f02c Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 14 May 2025 11:38:39 +0300 Subject: [PATCH 475/549] Serialize UserId only at create --- src/redmine-net-api/Types/ProjectMembership.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/redmine-net-api/Types/ProjectMembership.cs b/src/redmine-net-api/Types/ProjectMembership.cs index dd1622b9..714b0030 100644 --- a/src/redmine-net-api/Types/ProjectMembership.cs +++ b/src/redmine-net-api/Types/ProjectMembership.cs @@ -102,7 +102,11 @@ public override void ReadXml(XmlReader reader) /// public override void WriteXml(XmlWriter writer) { - writer.WriteIdIfNotNull(RedmineKeys.USER_ID, User); + if (Id <= 0) + { + writer.WriteIdIfNotNull(RedmineKeys.USER_ID, User); + } + writer.WriteArray(RedmineKeys.ROLE_IDS, Roles, typeof(MembershipRole), RedmineKeys.ROLE_ID); } #endregion @@ -146,7 +150,11 @@ public override void WriteJson(JsonWriter writer) { using (new JsonObject(writer, RedmineKeys.MEMBERSHIP)) { - writer.WriteIdIfNotNull(RedmineKeys.USER_ID, User); + if (Id <= 0) + { + writer.WriteIdIfNotNull(RedmineKeys.USER_ID, User); + } + writer.WriteRepeatableElement(RedmineKeys.ROLE_IDS, (IEnumerable)Roles); } } From eb961fe02be0f03b7a261c0af82b2fd72eec5078 Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 14 May 2025 11:40:17 +0300 Subject: [PATCH 476/549] Remove ProjectId from serialization --- src/redmine-net-api/Types/IssueCategory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/redmine-net-api/Types/IssueCategory.cs b/src/redmine-net-api/Types/IssueCategory.cs index ae25040e..ebb68087 100644 --- a/src/redmine-net-api/Types/IssueCategory.cs +++ b/src/redmine-net-api/Types/IssueCategory.cs @@ -90,7 +90,7 @@ public override void ReadXml(XmlReader reader) /// public override void WriteXml(XmlWriter writer) { - writer.WriteIdIfNotNull(RedmineKeys.PROJECT_ID, Project); + // writer.WriteIdIfNotNull(RedmineKeys.PROJECT_ID, Project); writer.WriteElementString(RedmineKeys.NAME, Name); writer.WriteIdIfNotNull(RedmineKeys.ASSIGNED_TO_ID, AssignTo); } From dee70f24ee44198866e8fa8aef9c47b2bf4e35bd Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 14 May 2025 11:38:02 +0300 Subject: [PATCH 477/549] Serialize default assigned/version --- src/redmine-net-api/Types/Project.cs | 35 ++++++++++++---------------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs index f3041a7b..b3d5953a 100644 --- a/src/redmine-net-api/Types/Project.cs +++ b/src/redmine-net-api/Types/Project.cs @@ -197,30 +197,25 @@ public override void WriteXml(XmlWriter writer) { writer.WriteElementString(RedmineKeys.NAME, Name); writer.WriteElementString(RedmineKeys.IDENTIFIER, Identifier); - writer.WriteIfNotDefaultOrNull(RedmineKeys.DESCRIPTION, Description); - writer.WriteBoolean(RedmineKeys.INHERIT_MEMBERS, InheritMembers); - writer.WriteBoolean(RedmineKeys.IS_PUBLIC, IsPublic); writer.WriteIfNotDefaultOrNull(RedmineKeys.HOMEPAGE, HomePage); - + writer.WriteBoolean(RedmineKeys.IS_PUBLIC, IsPublic); writer.WriteIdIfNotNull(RedmineKeys.PARENT_ID, Parent); + writer.WriteBoolean(RedmineKeys.INHERIT_MEMBERS, InheritMembers); + + //It works only when the new project is a subproject and it inherits the members. + writer.WriteIdIfNotNull(RedmineKeys.DEFAULT_ASSIGNED_TO_ID, DefaultAssignee); + //It works only with existing shared versions. + writer.WriteIdIfNotNull(RedmineKeys.DEFAULT_VERSION_ID, DefaultVersion); writer.WriteRepeatableElement(RedmineKeys.TRACKER_IDS, (IEnumerable)Trackers); writer.WriteRepeatableElement(RedmineKeys.ENABLED_MODULE_NAMES, (IEnumerable)EnabledModules); - - if (Id == 0) - { - writer.WriteRepeatableElement(RedmineKeys.ISSUE_CUSTOM_FIELD_IDS, (IEnumerable)CustomFields); - return; - } - + writer.WriteRepeatableElement(RedmineKeys.ISSUE_CUSTOM_FIELD_IDS, (IEnumerable)CustomFields); writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); } #endregion #region Implementation of IJsonSerialization - - /// /// /// @@ -279,15 +274,15 @@ public override void WriteJson(JsonWriter writer) writer.WriteBoolean(RedmineKeys.INHERIT_MEMBERS, InheritMembers); writer.WriteBoolean(RedmineKeys.IS_PUBLIC, IsPublic); writer.WriteIdIfNotNull(RedmineKeys.PARENT_ID, Parent); + + //It works only when the new project is a subproject and it inherits the members. + writer.WriteIdIfNotNull(RedmineKeys.DEFAULT_ASSIGNED_TO_ID, DefaultAssignee); + //It works only with existing shared versions. + writer.WriteIdIfNotNull(RedmineKeys.DEFAULT_VERSION_ID, DefaultVersion); + writer.WriteRepeatableElement(RedmineKeys.TRACKER_IDS, (IEnumerable)Trackers); writer.WriteRepeatableElement(RedmineKeys.ENABLED_MODULE_NAMES, (IEnumerable)EnabledModules); - - if (Id == 0) - { - writer.WriteRepeatableElement(RedmineKeys.ISSUE_CUSTOM_FIELD_IDS, (IEnumerable)CustomFields); - return; - } - + writer.WriteRepeatableElement(RedmineKeys.ISSUE_CUSTOM_FIELD_IDS, (IEnumerable)CustomFields); writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); } } From c2076f842063207a94d78fbb88c877a0f62d24b1 Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 14 May 2025 11:24:43 +0300 Subject: [PATCH 478/549] Add DocumentCategory --- src/redmine-net-api/Net/RedmineApiUrls.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/redmine-net-api/Net/RedmineApiUrls.cs b/src/redmine-net-api/Net/RedmineApiUrls.cs index 54a9d452..c50d0fc7 100644 --- a/src/redmine-net-api/Net/RedmineApiUrls.cs +++ b/src/redmine-net-api/Net/RedmineApiUrls.cs @@ -31,6 +31,7 @@ internal sealed class RedmineApiUrls { {typeof(Attachment), RedmineKeys.ATTACHMENTS}, {typeof(CustomField), RedmineKeys.CUSTOM_FIELDS}, + {typeof(DocumentCategory), RedmineKeys.ENUMERATION_DOCUMENT_CATEGORIES}, {typeof(Group), RedmineKeys.GROUPS}, {typeof(Issue), RedmineKeys.ISSUES}, {typeof(IssueCategory), RedmineKeys.ISSUE_CATEGORIES}, From 8b6c466b114ddefae5572f5af653efef5b472ab5 Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 11 May 2025 15:31:56 +0300 Subject: [PATCH 479/549] Code arrange --- src/redmine-net-api/Extensions/StringExtensions.cs | 6 +++--- src/redmine-net-api/RedmineManagerAsync.cs | 4 ++-- .../RedmineManagerOptionsBuilder.cs | 14 ++++++++------ 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/redmine-net-api/Extensions/StringExtensions.cs b/src/redmine-net-api/Extensions/StringExtensions.cs index 03fdc059..d7b4d1a2 100644 --- a/src/redmine-net-api/Extensions/StringExtensions.cs +++ b/src/redmine-net-api/Extensions/StringExtensions.cs @@ -158,9 +158,9 @@ internal static string ToInvariantString(this T value) where T : struct }; } - private const string CRLR = "\r\n"; private const string CR = "\r"; private const string LR = "\n"; + private const string CRLR = $"{CR}{LR}"; internal static string ReplaceEndings(this string input, string replacement = CRLR) { @@ -170,9 +170,9 @@ internal static string ReplaceEndings(this string input, string replacement = CR } #if NET6_0_OR_GREATER - input = input.ReplaceLineEndings(CRLR); + input = input.ReplaceLineEndings(replacement); #else - input = Regex.Replace(input, $"{CRLR}|{CR}|{LR}", CRLR); + input = Regex.Replace(input, $"{CRLR}|{CR}|{LR}", replacement); #endif return input; } diff --git a/src/redmine-net-api/RedmineManagerAsync.cs b/src/redmine-net-api/RedmineManagerAsync.cs index 72e2d6a6..3a3f5085 100644 --- a/src/redmine-net-api/RedmineManagerAsync.cs +++ b/src/redmine-net-api/RedmineManagerAsync.cs @@ -211,7 +211,7 @@ public async Task UploadFileAsync(byte[] data, string fileName = null, R { var url = RedmineApiUrls.UploadFragment(fileName); - var response = await ApiClient.UploadFileAsync(url, data,requestOptions , cancellationToken: cancellationToken).ConfigureAwait(false); + var response = await ApiClient.UploadFileAsync(url, data, requestOptions, cancellationToken: cancellationToken).ConfigureAwait(false); return response.DeserializeTo(Serializer); } @@ -219,7 +219,7 @@ public async Task UploadFileAsync(byte[] data, string fileName = null, R /// public async Task DownloadFileAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) { - var response = await ApiClient.DownloadAsync(address, requestOptions,cancellationToken: cancellationToken).ConfigureAwait(false); + var response = await ApiClient.DownloadAsync(address, requestOptions, cancellationToken: cancellationToken).ConfigureAwait(false); return response.Content; } diff --git a/src/redmine-net-api/RedmineManagerOptionsBuilder.cs b/src/redmine-net-api/RedmineManagerOptionsBuilder.cs index b54db5b3..aadd7712 100644 --- a/src/redmine-net-api/RedmineManagerOptionsBuilder.cs +++ b/src/redmine-net-api/RedmineManagerOptionsBuilder.cs @@ -214,12 +214,13 @@ internal RedmineManagerOptions Build() { const string defaultUserAgent = "Redmine.Net.Api.Net"; var defaultDecompressionFormat = - #if NETFRAMEWORK +#if NETFRAMEWORK DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.None; - #else +#else DecompressionMethods.All; - #endif - #if NET45_OR_GREATER || NETCOREAPP +#endif + +#if NET45_OR_GREATER || NETCOREAPP WebClientOptions ??= _clientType switch { ClientType.WebClient => new RedmineWebClientOptions() @@ -229,13 +230,14 @@ internal RedmineManagerOptions Build() }, _ => throw new ArgumentOutOfRangeException() }; - #else +#else WebClientOptions ??= new RedmineWebClientOptions() { UserAgent = defaultUserAgent, DecompressionFormat = defaultDecompressionFormat, }; - #endif +#endif + var baseAddress = CreateRedmineUri(Host, WebClientOptions.Scheme); var options = new RedmineManagerOptions() From 07cd9380a85ac7ab418810066c4856e4e14ecf4d Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 11 May 2025 15:32:56 +0300 Subject: [PATCH 480/549] Update summary --- .../RedmineManagerAsyncExtensions.Obsolete.cs | 4 +- .../Extensions/RedmineManagerExtensions.cs | 330 +++++++++--------- .../Extensions/StringExtensions.cs | 43 ++- .../RedmineManagerOptionsBuilder.cs | 2 +- .../Serialization/RedmineSerializerFactory.cs | 12 + .../Serialization/SerializationHelper.cs | 14 +- .../Serialization/SerializationType.cs | 6 +- src/redmine-net-api/Types/Issue.cs | 2 +- 8 files changed, 236 insertions(+), 177 deletions(-) diff --git a/src/redmine-net-api/Extensions/RedmineManagerAsyncExtensions.Obsolete.cs b/src/redmine-net-api/Extensions/RedmineManagerAsyncExtensions.Obsolete.cs index d2b42862..c6a1373a 100644 --- a/src/redmine-net-api/Extensions/RedmineManagerAsyncExtensions.Obsolete.cs +++ b/src/redmine-net-api/Extensions/RedmineManagerAsyncExtensions.Obsolete.cs @@ -64,7 +64,7 @@ public static async Task CreateWikiPageAsync(this RedmineManager redmi } /// - /// Creates the or update wiki page asynchronous. + /// Creates or updates wiki page asynchronous. /// /// The redmine manager. /// The project identifier. @@ -94,7 +94,7 @@ public static async Task DeleteWikiPageAsync(this RedmineManager redmineManager, /// /// Support for adding attachments through the REST API is added in Redmine 1.4.0. - /// Upload a file to server. This method does not block the calling thread. + /// Upload a file to the server. This method does not block the calling thread. /// /// The redmine manager. /// The content of the file that will be uploaded on server. diff --git a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs index fb94205f..20d600a9 100644 --- a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs +++ b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs @@ -35,13 +35,13 @@ namespace Redmine.Net.Api.Extensions public static class RedmineManagerExtensions { /// - /// + /// Archives a project in Redmine based on the specified project identifier. /// - /// - /// - /// - /// - public static void ArchiveProject(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null) + /// The instance of the RedmineManager used to manage the API requests. + /// The unique identifier of the project to be archived. + /// Additional request options to include in the API call. + public static void ArchiveProject(this RedmineManager redmineManager, string projectIdentifier, + RequestOptions requestOptions = null) { var uri = redmineManager.RedmineApiUrls.ProjectArchive(projectIdentifier); @@ -49,15 +49,15 @@ public static void ArchiveProject(this RedmineManager redmineManager, string pro redmineManager.ApiClient.Update(escapedUri, string.Empty ,requestOptions); } - + /// - /// + /// Unarchives a project in Redmine based on the specified project identifier. /// - /// - /// - /// - /// - public static void UnarchiveProject(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null) + /// The instance of the RedmineManager used to manage the API requests. + /// The unique identifier of the project to be unarchived. + /// Additional request options to include in the API call. + public static void UnarchiveProject(this RedmineManager redmineManager, string projectIdentifier, + RequestOptions requestOptions = null) { var uri = redmineManager.RedmineApiUrls.ProjectUnarchive(projectIdentifier); @@ -65,15 +65,15 @@ public static void UnarchiveProject(this RedmineManager redmineManager, string p redmineManager.ApiClient.Update(escapedUri, string.Empty ,requestOptions); } - + /// - /// + /// Reopens a previously closed project in Redmine based on the specified project identifier. /// - /// - /// - /// - /// - public static void ReopenProject(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null) + /// The instance of the RedmineManager used to execute the API request. + /// The unique identifier of the project to be reopened. + /// Additional request options to include in the API call. + public static void ReopenProject(this RedmineManager redmineManager, string projectIdentifier, + RequestOptions requestOptions = null) { var uri = redmineManager.RedmineApiUrls.ProjectReopen(projectIdentifier); @@ -81,15 +81,15 @@ public static void ReopenProject(this RedmineManager redmineManager, string proj redmineManager.ApiClient.Update(escapedUri, string.Empty ,requestOptions); } - + /// - /// + /// Closes a project in Redmine based on the specified project identifier. /// - /// - /// - /// - /// - public static void CloseProject(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null) + /// The instance of the RedmineManager used to manage the API requests. + /// The unique identifier of the project to be closed. + /// Additional request options to include in the API call. + public static void CloseProject(this RedmineManager redmineManager, string projectIdentifier, + RequestOptions requestOptions = null) { var uri = redmineManager.RedmineApiUrls.ProjectClose(projectIdentifier); @@ -97,16 +97,18 @@ public static void CloseProject(this RedmineManager redmineManager, string proje redmineManager.ApiClient.Update(escapedUri,string.Empty, requestOptions); } - + /// - /// + /// Adds a related issue to a project repository in Redmine based on the specified parameters. /// - /// - /// - /// - /// - /// - public static void ProjectRepositoryAddRelatedIssue(this RedmineManager redmineManager, string projectIdentifier, string repositoryIdentifier, string revision, RequestOptions requestOptions = null) + /// The instance of the RedmineManager used to manage the API requests. + /// The unique identifier of the project to which the repository belongs. + /// The unique identifier of the repository within the project. + /// The revision or commit ID to relate the issue to. + /// Additional request options to include in the API call. + public static void ProjectRepositoryAddRelatedIssue(this RedmineManager redmineManager, + string projectIdentifier, string repositoryIdentifier, string revision, + RequestOptions requestOptions = null) { var uri = redmineManager.RedmineApiUrls.ProjectRepositoryAddRelatedIssue(projectIdentifier, repositoryIdentifier, revision); @@ -116,15 +118,17 @@ public static void ProjectRepositoryAddRelatedIssue(this RedmineManager redmineM } /// - /// + /// Removes a related issue from the specified repository revision of a project in Redmine. /// - /// - /// - /// - /// - /// - /// - public static void ProjectRepositoryRemoveRelatedIssue(this RedmineManager redmineManager, string projectIdentifier, string repositoryIdentifier, string revision, string issueIdentifier, RequestOptions requestOptions = null) + /// The instance of the RedmineManager used to manage the API requests. + /// The unique identifier of the project containing the repository. + /// The unique identifier of the repository from which the related issue will be removed. + /// The specific revision of the repository to disassociate the issue from. + /// The unique identifier of the issue to be removed as related. + /// Additional request options to include in the API call. + public static void ProjectRepositoryRemoveRelatedIssue(this RedmineManager redmineManager, + string projectIdentifier, string repositoryIdentifier, string revision, string issueIdentifier, + RequestOptions requestOptions = null) { var uri = redmineManager.RedmineApiUrls.ProjectRepositoryRemoveRelatedIssue(projectIdentifier, repositoryIdentifier, revision, issueIdentifier); @@ -132,15 +136,16 @@ public static void ProjectRepositoryRemoveRelatedIssue(this RedmineManager redmi _ = redmineManager.ApiClient.Delete(escapedUri, requestOptions); } - + /// - /// + /// Retrieves a paginated list of news for a specific project in Redmine. /// - /// - /// - /// - /// - public static PagedResults GetProjectNews(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null) + /// The instance of the RedmineManager used to manage the API requests. + /// The unique identifier of the project for which news is being retrieved. + /// Additional request options to include in the API call, if any. + /// A paginated list of news items associated with the specified project. + public static PagedResults GetProjectNews(this RedmineManager redmineManager, string projectIdentifier, + RequestOptions requestOptions = null) { var uri = redmineManager.RedmineApiUrls.ProjectNews(projectIdentifier); @@ -152,15 +157,16 @@ public static PagedResults GetProjectNews(this RedmineManager redmineManag } /// - /// + /// Adds a news item to a project in Redmine based on the specified project identifier. /// - /// - /// - /// - /// - /// - /// - public static News AddProjectNews(this RedmineManager redmineManager, string projectIdentifier, News news, RequestOptions requestOptions = null) + /// The instance of the RedmineManager used to manage the API requests. + /// The unique identifier of the project to which the news will be added. + /// The news item to be added to the project, which must contain a valid title. + /// Additional request options to include in the API call. + /// The created news item as a response from the Redmine server. + /// Thrown when the provided news object is null or the news title is blank. + public static News AddProjectNews(this RedmineManager redmineManager, string projectIdentifier, News news, + RequestOptions requestOptions = null) { if (news == null) { @@ -184,14 +190,15 @@ public static News AddProjectNews(this RedmineManager redmineManager, string pro } /// - /// + /// Retrieves the memberships associated with the specified project in Redmine. /// - /// - /// - /// - /// - /// - public static PagedResults GetProjectMemberships(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null) + /// The instance of the RedmineManager used to manage the API requests. + /// The unique identifier of the project for which memberships are being retrieved. + /// Additional request options to include in the API call, such as pagination or filters. + /// Returns a paginated collection of project memberships for the specified project. + /// Thrown when the API request fails or an error occurs during execution. + public static PagedResults GetProjectMemberships(this RedmineManager redmineManager, + string projectIdentifier, RequestOptions requestOptions = null) { var uri = redmineManager.RedmineApiUrls.ProjectMemberships(projectIdentifier); @@ -201,14 +208,15 @@ public static PagedResults GetProjectMemberships(this Redmine } /// - /// + /// Retrieves the list of files associated with a specific project in Redmine. /// - /// - /// - /// - /// - /// - public static PagedResults GetProjectFiles(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null) + /// The instance of the RedmineManager used to manage the API requests. + /// The unique identifier of the project whose files are being retrieved. + /// Additional request options to include in the API call. + /// A paginated result containing the list of files associated with the project. + /// Thrown when the API request fails or returns an error response. + public static PagedResults GetProjectFiles(this RedmineManager redmineManager, string projectIdentifier, + RequestOptions requestOptions = null) { var uri = redmineManager.RedmineApiUrls.ProjectFilesFragment(projectIdentifier); @@ -220,9 +228,9 @@ public static PagedResults GetProjectFiles(this RedmineManager redmineMana /// /// Returns the user whose credentials are used to access the API. /// - /// - /// - /// + /// The instance of the RedmineManager used to manage the API requests. + /// Additional request options to include in the API call. + /// The authenticated user as a object. public static User GetCurrentUser(this RedmineManager redmineManager, RequestOptions requestOptions = null) { var uri = redmineManager.RedmineApiUrls.CurrentUser(); @@ -231,11 +239,13 @@ public static User GetCurrentUser(this RedmineManager redmineManager, RequestOpt return response.DeserializeTo(redmineManager.Serializer); } - + /// - /// + /// Retrieves the account details of the currently authenticated user. /// - /// Returns the my account details. + /// The instance of the RedmineManager used to perform the API call. + /// Optional configuration for the API request. + /// Returns the account details of the authenticated user as a MyAccount object. public static MyAccount GetMyAccount(this RedmineManager redmineManager, RequestOptions requestOptions = null) { var uri = redmineManager.RedmineApiUrls.MyAccount(); @@ -246,13 +256,14 @@ public static MyAccount GetMyAccount(this RedmineManager redmineManager, Request } /// - /// Adds the watcher to issue. + /// Adds a watcher to a specific issue in Redmine using the specified issue ID and user ID. /// - /// - /// The issue identifier. - /// The user identifier. - /// - public static void AddWatcherToIssue(this RedmineManager redmineManager, int issueId, int userId, RequestOptions requestOptions = null) + /// The instance of the RedmineManager used to manage the API requests. + /// The unique identifier of the issue to which the watcher will be added. + /// The unique identifier of the user to be added as a watcher. + /// Additional request options to include in the API call. + public static void AddWatcherToIssue(this RedmineManager redmineManager, int issueId, int userId, + RequestOptions requestOptions = null) { var uri = redmineManager.RedmineApiUrls.IssueWatcherAdd(issueId.ToString(CultureInfo.InvariantCulture)); @@ -262,13 +273,14 @@ public static void AddWatcherToIssue(this RedmineManager redmineManager, int iss } /// - /// Removes the watcher from issue. + /// Removes a watcher from a specific issue in Redmine based on the specified issue identifier and user identifier. /// - /// - /// The issue identifier. - /// The user identifier. - /// - public static void RemoveWatcherFromIssue(this RedmineManager redmineManager, int issueId, int userId, RequestOptions requestOptions = null) + /// The instance of the RedmineManager used to manage the API requests. + /// The unique identifier of the issue from which the watcher will be removed. + /// The unique identifier of the user to be removed as a watcher. + /// Additional request options to include in the API call. + public static void RemoveWatcherFromIssue(this RedmineManager redmineManager, int issueId, int userId, + RequestOptions requestOptions = null) { var uri = redmineManager.RedmineApiUrls.IssueWatcherRemove(issueId.ToString(CultureInfo.InvariantCulture), userId.ToString(CultureInfo.InvariantCulture)); @@ -276,13 +288,14 @@ public static void RemoveWatcherFromIssue(this RedmineManager redmineManager, in } /// - /// Adds an existing user to a group. + /// Adds a user to a specified group in Redmine. /// - /// - /// The group id. - /// The user id. - /// - public static void AddUserToGroup(this RedmineManager redmineManager, int groupId, int userId, RequestOptions requestOptions = null) + /// The instance of the RedmineManager used to manage API requests. + /// The unique identifier of the group to which the user will be added. + /// The unique identifier of the user to be added to the group. + /// Additional request options to include in the API call. + public static void AddUserToGroup(this RedmineManager redmineManager, int groupId, int userId, + RequestOptions requestOptions = null) { var uri = redmineManager.RedmineApiUrls.GroupUserAdd(groupId.ToString(CultureInfo.InvariantCulture)); @@ -292,13 +305,14 @@ public static void AddUserToGroup(this RedmineManager redmineManager, int groupI } /// - /// Removes an user from a group. + /// Removes a user from a specified group in Redmine. /// - /// - /// The group id. - /// The user id. - /// - public static void RemoveUserFromGroup(this RedmineManager redmineManager, int groupId, int userId, RequestOptions requestOptions = null) + /// The instance of the RedmineManager used to manage API requests. + /// The unique identifier of the group from which the user will be removed. + /// The unique identifier of the user to be removed from the group. + /// Additional request options to customize the API call. + public static void RemoveUserFromGroup(this RedmineManager redmineManager, int groupId, int userId, + RequestOptions requestOptions = null) { var uri = redmineManager.RedmineApiUrls.GroupUserRemove(groupId.ToString(CultureInfo.InvariantCulture), userId.ToString(CultureInfo.InvariantCulture)); @@ -306,15 +320,15 @@ public static void RemoveUserFromGroup(this RedmineManager redmineManager, int g } /// - /// Creates or updates a wiki page. + /// Updates a specified wiki page for a project in Redmine. /// - /// - /// The project id or identifier. - /// The wiki page name. - /// The wiki page to create or update. - /// - /// - public static void UpdateWikiPage(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage, RequestOptions requestOptions = null) + /// The instance of the RedmineManager used to process the request. + /// The unique identifier of the project containing the wiki page. + /// The name of the wiki page to be updated. + /// The WikiPage object containing the updated data for the page. + /// Optional parameters for customizing the API request. + public static void UpdateWikiPage(this RedmineManager redmineManager, string projectId, string pageName, + WikiPage wikiPage, RequestOptions requestOptions = null) { var payload = redmineManager.Serializer.Serialize(wikiPage); @@ -331,15 +345,17 @@ public static void UpdateWikiPage(this RedmineManager redmineManager, string pro } /// - /// + /// Creates a new wiki page within a specified project in Redmine. /// - /// - /// - /// - /// - /// - /// - public static WikiPage CreateWikiPage(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage, RequestOptions requestOptions = null) + /// The instance of the RedmineManager used to manage API requests. + /// The unique identifier of the project where the wiki page will be created. + /// The name of the new wiki page. + /// The WikiPage object containing the content and metadata for the new page. + /// Additional request options to include in the API call. + /// The created WikiPage object containing the details of the new wiki page. + /// Thrown when the request payload is empty or if the API request fails. + public static WikiPage CreateWikiPage(this RedmineManager redmineManager, string projectId, string pageName, + WikiPage wikiPage, RequestOptions requestOptions = null) { var payload = redmineManager.Serializer.Serialize(wikiPage); @@ -358,15 +374,16 @@ public static WikiPage CreateWikiPage(this RedmineManager redmineManager, string } /// - /// Gets the wiki page. + /// Retrieves a wiki page from a Redmine project using the specified project identifier and page name. /// - /// - /// The project identifier. - /// Name of the page. - /// - /// The version. - /// - public static WikiPage GetWikiPage(this RedmineManager redmineManager, string projectId, string pageName, RequestOptions requestOptions = null, uint version = 0) + /// The instance of the RedmineManager responsible for managing API requests. + /// The unique identifier of the project containing the wiki page. + /// The name of the wiki page to retrieve. + /// Additional options to include in the API request, such as headers or query parameters. + /// The specific version of the wiki page to retrieve. If 0, the latest version is retrieved. + /// A WikiPage object containing the details of the requested wiki page. + public static WikiPage GetWikiPage(this RedmineManager redmineManager, string projectId, string pageName, + RequestOptions requestOptions = null, uint version = 0) { var uri = version == 0 ? redmineManager.RedmineApiUrls.ProjectWikiPage(projectId, pageName) @@ -380,13 +397,14 @@ public static WikiPage GetWikiPage(this RedmineManager redmineManager, string pr } /// - /// Returns the list of all pages in a project wiki. + /// Retrieves all wiki pages associated with the specified project. /// - /// - /// The project id or identifier. - /// - /// - public static List GetAllWikiPages(this RedmineManager redmineManager, string projectId, RequestOptions requestOptions = null) + /// The instance of the RedmineManager used to manage API requests. + /// The unique identifier of the project whose wiki pages are to be fetched. + /// Additional request options to include in the API call. + /// A list of wiki pages associated with the specified project. + public static List GetAllWikiPages(this RedmineManager redmineManager, string projectId, + RequestOptions requestOptions = null) { var uri = redmineManager.RedmineApiUrls.ProjectWikiIndex(projectId); @@ -399,10 +417,10 @@ public static List GetAllWikiPages(this RedmineManager redmineManager, /// Deletes a wiki page, its attachments and its history. If the deleted page is a parent page, its child pages are not /// deleted but changed as root pages. ///
- /// + /// The instance of the RedmineManager used to manage API requests. /// The project id or identifier. /// The wiki page name. - /// + /// Additional request options to include in the API call. public static void DeleteWikiPage(this RedmineManager redmineManager, string projectId, string pageName, RequestOptions requestOptions = null) { var uri = redmineManager.RedmineApiUrls.ProjectWikiPageDelete(projectId, pageName); @@ -418,7 +436,7 @@ public static void DeleteWikiPage(this RedmineManager redmineManager, string pro /// /// The issue identifier. /// The attachment. - /// + /// Additional request options to include in the API call. public static void UpdateIssueAttachment(this RedmineManager redmineManager, int issueId, Attachment attachment, RequestOptions requestOptions = null) { var attachments = new Attachments @@ -478,7 +496,7 @@ private static NameValueCollection CreateSearchParameters(string q, int limit, i ///
/// /// - /// + /// Additional request options to include in the API call. /// public static async Task ArchiveProjectAsync(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) { @@ -494,7 +512,7 @@ public static async Task ArchiveProjectAsync(this RedmineManager redmineManager, ///
/// /// - /// + /// Additional request options to include in the API call. /// public static async Task UnarchiveProjectAsync(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) { @@ -510,7 +528,7 @@ public static async Task UnarchiveProjectAsync(this RedmineManager redmineManage ///
/// /// - /// + /// Additional request options to include in the API call. /// public static async Task CloseProjectAsync(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) { @@ -526,7 +544,7 @@ public static async Task CloseProjectAsync(this RedmineManager redmineManager, s ///
/// /// - /// + /// Additional request options to include in the API call. /// public static async Task ReopenProjectAsync(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) { @@ -544,7 +562,7 @@ public static async Task ReopenProjectAsync(this RedmineManager redmineManager, /// /// /// - /// + /// Additional request options to include in the API call. /// public static async Task ProjectRepositoryAddRelatedIssueAsync(this RedmineManager redmineManager, string projectIdentifier, string repositoryIdentifier, string revision, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) { @@ -563,7 +581,7 @@ public static async Task ProjectRepositoryAddRelatedIssueAsync(this RedmineManag /// /// /// - /// + /// Additional request options to include in the API call. /// public static async Task ProjectRepositoryRemoveRelatedIssueAsync(this RedmineManager redmineManager, string projectIdentifier, string repositoryIdentifier, string revision, string issueIdentifier, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) { @@ -579,7 +597,7 @@ public static async Task ProjectRepositoryRemoveRelatedIssueAsync(this RedmineMa ///
/// /// - /// + /// Additional request options to include in the API call. /// /// public static async Task> GetProjectNewsAsync(this RedmineManager redmineManager, string projectIdentifier, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) @@ -599,7 +617,7 @@ public static async Task> GetProjectNewsAsync(this RedmineMan /// /// /// - /// + /// Additional request options to include in the API call. /// /// /// @@ -631,7 +649,7 @@ public static async Task AddProjectNewsAsync(this RedmineManager redmineMa ///
/// /// - /// + /// Additional request options to include in the API call. /// /// /// @@ -649,7 +667,7 @@ public static async Task> GetProjectMembershipsA ///
/// /// - /// + /// Additional request options to include in the API call. /// /// /// @@ -677,7 +695,7 @@ public static async Task> SearchAsync(this RedmineManager r { var parameters = CreateSearchParameters(q, limit, offset, searchFilter); - var response = await redmineManager.ApiClient.GetPagedAsync("", new RequestOptions() + var response = await redmineManager.ApiClient.GetPagedAsync(string.Empty, new RequestOptions() { QueryString = parameters }, cancellationToken).ConfigureAwait(false); @@ -689,7 +707,7 @@ public static async Task> SearchAsync(this RedmineManager r /// ///
/// - /// + /// Additional request options to include in the API call. /// /// public static async Task GetCurrentUserAsync(this RedmineManager redmineManager, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) @@ -708,7 +726,7 @@ public static async Task GetCurrentUserAsync(this RedmineManager redmineMa /// The project identifier. /// Name of the page. /// The wiki page. - /// + /// Additional request options to include in the API call. /// /// public static async Task CreateWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) @@ -736,7 +754,7 @@ public static async Task CreateWikiPageAsync(this RedmineManager redmi /// The project identifier. /// Name of the page. /// The wiki page. - /// + /// Additional request options to include in the API call. /// /// public static async Task UpdateWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) @@ -761,7 +779,7 @@ public static async Task UpdateWikiPageAsync(this RedmineManager redmineManager, /// The redmine manager. /// The project identifier. /// Name of the page. - /// + /// Additional request options to include in the API call. /// /// public static async Task DeleteWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) @@ -779,7 +797,7 @@ public static async Task DeleteWikiPageAsync(this RedmineManager redmineManager, /// The redmine manager. /// The project identifier. /// Name of the page. - /// + /// Additional request options to include in the API call. /// The version. /// /// @@ -801,7 +819,7 @@ public static async Task GetWikiPageAsync(this RedmineManager redmineM ///
/// The redmine manager. /// The project identifier. - /// + /// Additional request options to include in the API call. /// /// public static async Task> GetAllWikiPagesAsync(this RedmineManager redmineManager, string projectId, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) @@ -819,7 +837,7 @@ public static async Task> GetAllWikiPagesAsync(this RedmineManage /// The redmine manager. /// The group id. /// The user id. - /// + /// Additional request options to include in the API call. /// /// /// Returns the Guid associated with the async request. @@ -839,7 +857,7 @@ public static async Task AddUserToGroupAsync(this RedmineManager redmineManager, /// The redmine manager. /// The group id. /// The user id. - /// + /// Additional request options to include in the API call. /// /// public static async Task RemoveUserFromGroupAsync(this RedmineManager redmineManager, int groupId, int userId, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) @@ -855,7 +873,7 @@ public static async Task RemoveUserFromGroupAsync(this RedmineManager redmineMan /// The redmine manager. /// The issue identifier. /// The user identifier. - /// + /// Additional request options to include in the API call. /// /// public static async Task AddWatcherToIssueAsync(this RedmineManager redmineManager, int issueId, int userId, RequestOptions requestOptions = null , CancellationToken cancellationToken = default) @@ -873,7 +891,7 @@ public static async Task AddWatcherToIssueAsync(this RedmineManager redmineManag /// The redmine manager. /// The issue identifier. /// The user identifier. - /// + /// Additional request options to include in the API call. /// /// public static async Task RemoveWatcherFromIssueAsync(this RedmineManager redmineManager, int issueId, int userId, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) diff --git a/src/redmine-net-api/Extensions/StringExtensions.cs b/src/redmine-net-api/Extensions/StringExtensions.cs index d7b4d1a2..bdd34736 100644 --- a/src/redmine-net-api/Extensions/StringExtensions.cs +++ b/src/redmine-net-api/Extensions/StringExtensions.cs @@ -28,10 +28,10 @@ namespace Redmine.Net.Api.Extensions public static class StringExtensions { /// - /// + /// Determines whether a string is null, empty, or consists only of white-space characters. /// - /// - /// + /// The string to evaluate. + /// True if the string is null, empty, or whitespace; otherwise, false. public static bool IsNullOrWhiteSpace(this string value) { if (value == null) @@ -51,11 +51,11 @@ public static bool IsNullOrWhiteSpace(this string value) } /// - /// + /// Truncates a string to the specified maximum length if it exceeds that length. /// - /// - /// - /// + /// The string to truncate. + /// The maximum allowed length for the string. + /// The truncated string if its length exceeds the maximum length; otherwise, the original string. public static string Truncate(this string text, int maximumLength) { if (text.IsNullOrWhiteSpace() || maximumLength < 1 || text.Length <= maximumLength) @@ -107,6 +107,11 @@ internal static SecureString ToSecureString(this string value) return rv; } + /// + /// Removes the trailing slash ('/' or '\') from the end of the string if it exists. + /// + /// The string to process. + /// The input string without a trailing slash, or the original string if no trailing slash exists. internal static string RemoveTrailingSlash(this string s) { if (string.IsNullOrEmpty(s)) @@ -128,12 +133,24 @@ internal static string RemoveTrailingSlash(this string s) return s; } - + + /// + /// Returns the specified string value if it is neither null, empty, nor consists only of white-space characters; otherwise, returns the fallback string. + /// + /// The primary string value to evaluate. + /// The fallback string to return if the primary string is null, empty, or consists of only white-space characters. + /// The original string if it is valid; otherwise, the fallback string. internal static string ValueOrFallback(this string value, string fallback) { return !value.IsNullOrWhiteSpace() ? value : fallback; } - + + /// + /// Converts a value of a struct type to its invariant culture string representation. + /// + /// The struct type of the value. + /// The value to convert to a string. + /// The invariant culture string representation of the value. internal static string ToInvariantString(this T value) where T : struct { return value switch @@ -161,7 +178,13 @@ internal static string ToInvariantString(this T value) where T : struct private const string CR = "\r"; private const string LR = "\n"; private const string CRLR = $"{CR}{LR}"; - + + /// + /// Replaces all line endings in the input string with the specified replacement string. + /// + /// The string in which line endings will be replaced. + /// The string to replace line endings with. Defaults to a combination of carriage return and line feed. + /// The input string with all line endings replaced by the specified replacement string. internal static string ReplaceEndings(this string input, string replacement = CRLR) { if (input.IsNullOrWhiteSpace()) diff --git a/src/redmine-net-api/RedmineManagerOptionsBuilder.cs b/src/redmine-net-api/RedmineManagerOptionsBuilder.cs index aadd7712..3b3c9f3d 100644 --- a/src/redmine-net-api/RedmineManagerOptionsBuilder.cs +++ b/src/redmine-net-api/RedmineManagerOptionsBuilder.cs @@ -191,7 +191,7 @@ public RedmineManagerOptionsBuilder WithVersion(Version version) } /// - /// + /// Gets or sets the version of the Redmine server to which this client will connect. /// public Version Version { get; set; } diff --git a/src/redmine-net-api/Serialization/RedmineSerializerFactory.cs b/src/redmine-net-api/Serialization/RedmineSerializerFactory.cs index a315ad44..6c71326e 100644 --- a/src/redmine-net-api/Serialization/RedmineSerializerFactory.cs +++ b/src/redmine-net-api/Serialization/RedmineSerializerFactory.cs @@ -15,6 +15,8 @@ limitations under the License. */ using System; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Xml; namespace Redmine.Net.Api.Serialization; @@ -23,6 +25,16 @@ namespace Redmine.Net.Api.Serialization; ///
internal static class RedmineSerializerFactory { + /// + /// Creates an instance of an IRedmineSerializer based on the specified serialization type. + /// + /// The type of serialization, either Xml or Json. + /// + /// An instance of a serializer that implements the IRedmineSerializer interface. + /// + /// + /// Thrown when the specified serialization type is not supported. + /// public static IRedmineSerializer CreateSerializer(SerializationType type) { return type switch diff --git a/src/redmine-net-api/Serialization/SerializationHelper.cs b/src/redmine-net-api/Serialization/SerializationHelper.cs index 225772aa..a43624c1 100644 --- a/src/redmine-net-api/Serialization/SerializationHelper.cs +++ b/src/redmine-net-api/Serialization/SerializationHelper.cs @@ -19,16 +19,20 @@ limitations under the License. namespace Redmine.Net.Api.Serialization { /// - /// + /// Provides helper methods for serializing user-related data for communication with the Redmine API. /// internal static class SerializationHelper { /// - /// + /// Serializes the user ID into a format suitable for communication with the Redmine API, + /// based on the specified serializer type. /// - /// - /// - /// + /// The ID of the user to be serialized. + /// The serializer used to format the user ID (e.g., XML or JSON). + /// A serialized representation of the user ID. + /// + /// Thrown when the provided serializer is not recognized or supported. + /// public static string SerializeUserId(int userId, IRedmineSerializer redmineSerializer) { return redmineSerializer is XmlRedmineSerializer diff --git a/src/redmine-net-api/Serialization/SerializationType.cs b/src/redmine-net-api/Serialization/SerializationType.cs index decd824c..3830d3fe 100644 --- a/src/redmine-net-api/Serialization/SerializationType.cs +++ b/src/redmine-net-api/Serialization/SerializationType.cs @@ -17,15 +17,17 @@ limitations under the License. namespace Redmine.Net.Api.Serialization { /// - /// + /// Specifies the serialization types supported by the Redmine API. /// public enum SerializationType { /// + /// The XML format. /// Xml, + /// - /// The json + /// The JSON format. /// Json } diff --git a/src/redmine-net-api/Types/Issue.cs b/src/redmine-net-api/Types/Issue.cs index 91c5f2a1..3fe5eb27 100644 --- a/src/redmine-net-api/Types/Issue.cs +++ b/src/redmine-net-api/Types/Issue.cs @@ -56,7 +56,7 @@ public sealed class Issue : public IdentifiableName Tracker { get; set; } /// - /// Gets or sets the status.Possible values: open, closed, * to get open and closed issues, status id + /// Gets or sets the status. Possible values: open, closed, * to get open and closed issues, status id /// /// The status. public IssueStatus Status { get; set; } From ce6a5c1b10617edfb97a5a326260873e56395377 Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 11 May 2025 15:34:38 +0300 Subject: [PATCH 481/549] Version inherits from IdentifiableName instead of Identifiable --- src/redmine-net-api/Types/Version.cs | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/redmine-net-api/Types/Version.cs b/src/redmine-net-api/Types/Version.cs index 84f8ecd4..50b1f108 100644 --- a/src/redmine-net-api/Types/Version.cs +++ b/src/redmine-net-api/Types/Version.cs @@ -31,14 +31,9 @@ namespace Redmine.Net.Api.Types ///
[DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [XmlRoot(RedmineKeys.VERSION)] - public sealed class Version : Identifiable + public sealed class Version : IdentifiableName, IEquatable { #region Properties - /// - /// Gets or sets the name. - /// - public string Name { get; set; } - /// /// Gets the project. /// @@ -226,18 +221,18 @@ public override void WriteJson(JsonWriter writer) ///
/// /// - public override bool Equals(Version other) + public bool Equals(Version other) { if (other == null) return false; return base.Equals(other) && Project == other.Project && string.Equals(Description, other.Description, StringComparison.Ordinal) - && Status == other.Status - && DueDate == other.DueDate - && Sharing == other.Sharing - && CreatedOn == other.CreatedOn - && UpdatedOn == other.UpdatedOn - && (CustomFields?.Equals(other.CustomFields) ?? other.CustomFields == null) + && Status == other.Status + && DueDate == other.DueDate + && Sharing == other.Sharing + && CreatedOn == other.CreatedOn + && UpdatedOn == other.UpdatedOn + && (CustomFields?.Equals(other.CustomFields) ?? other.CustomFields == null) && string.Equals(WikiPageTitle,other.WikiPageTitle, StringComparison.Ordinal) && EstimatedHours == other.EstimatedHours && SpentHours == other.SpentHours; From 499c058458d06b1946f1c412594c50c2dae22f51 Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 11 May 2025 15:35:52 +0300 Subject: [PATCH 482/549] Improvements --- src/redmine-net-api/Types/Version.cs | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/redmine-net-api/Types/Version.cs b/src/redmine-net-api/Types/Version.cs index 50b1f108..2b828c3d 100644 --- a/src/redmine-net-api/Types/Version.cs +++ b/src/redmine-net-api/Types/Version.cs @@ -117,8 +117,18 @@ public override void ReadXml(XmlReader reader) case RedmineKeys.DUE_DATE: DueDate = reader.ReadElementContentAsNullableDateTime(); break; case RedmineKeys.NAME: Name = reader.ReadElementContentAsString(); break; case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; - case RedmineKeys.SHARING: Sharing = (VersionSharing)Enum.Parse(typeof(VersionSharing), reader.ReadElementContentAsString(), true); break; - case RedmineKeys.STATUS: Status = (VersionStatus)Enum.Parse(typeof(VersionStatus), reader.ReadElementContentAsString(), true); break; + case RedmineKeys.SHARING: Sharing = +#if NETFRAMEWORK + (VersionSharing)Enum.Parse(typeof(VersionSharing), reader.ReadElementContentAsString(), true); break; +#else + Enum.Parse(reader.ReadElementContentAsString(), true); break; +#endif + case RedmineKeys.STATUS: Status = +#if NETFRAMEWORK + (VersionStatus)Enum.Parse(typeof(VersionStatus), reader.ReadElementContentAsString(), true); break; +#else + Enum.Parse(reader.ReadElementContentAsString(), true); break; +#endif case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadElementContentAsNullableDateTime(); break; case RedmineKeys.WIKI_PAGE_TITLE: WikiPageTitle = reader.ReadElementContentAsString(); break; case RedmineKeys.ESTIMATED_HOURS: EstimatedHours = reader.ReadElementContentAsNullableFloat(); break; @@ -179,8 +189,18 @@ public override void ReadJson(JsonReader reader) case RedmineKeys.DUE_DATE: DueDate = reader.ReadAsDateTime(); break; case RedmineKeys.NAME: Name = reader.ReadAsString(); break; case RedmineKeys.PROJECT: Project = new IdentifiableName(reader); break; - case RedmineKeys.SHARING: Sharing = (VersionSharing)Enum.Parse(typeof(VersionSharing), reader.ReadAsString() ?? string.Empty, true); break; - case RedmineKeys.STATUS: Status = (VersionStatus)Enum.Parse(typeof(VersionStatus), reader.ReadAsString() ?? string.Empty, true); break; + case RedmineKeys.SHARING: Sharing = +#if NETFRAMEWORK + (VersionSharing)Enum.Parse(typeof(VersionSharing), reader.ReadAsString() ?? string.Empty, true); break; +#else + Enum.Parse(reader.ReadAsString() ?? string.Empty, true); break; +#endif + case RedmineKeys.STATUS: Status = +#if NETFRAMEWORK + (VersionStatus)Enum.Parse(typeof(VersionStatus), reader.ReadAsString() ?? string.Empty, true); break; +#else + Enum.Parse(reader.ReadAsString() ?? string.Empty, true); break; +#endif case RedmineKeys.UPDATED_ON: UpdatedOn = reader.ReadAsDateTime(); break; case RedmineKeys.WIKI_PAGE_TITLE: WikiPageTitle = reader.ReadAsString(); break; case RedmineKeys.ESTIMATED_HOURS: EstimatedHours = (float?)reader.ReadAsDouble(); break; From 6bfcc64c83c8750f74f32a22449d68ce43c16656 Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 11 May 2025 15:39:59 +0300 Subject: [PATCH 483/549] Remove unused directives --- tests/redmine-net-api.Tests/Bugs/RedmineApi-229.cs | 1 - tests/redmine-net-api.Tests/Clone/AttachmentCloneTests.cs | 1 - tests/redmine-net-api.Tests/Clone/IssueCloneTests.cs | 1 - tests/redmine-net-api.Tests/Clone/JournalCloneTests.cs | 2 -- .../redmine-net-api.Tests/Equality/AttachmentEqualityTests.cs | 2 -- tests/redmine-net-api.Tests/Equality/BaseEqualityTests.cs | 1 - tests/redmine-net-api.Tests/Equality/IssueEqualityTests.cs | 1 - tests/redmine-net-api.Tests/Equality/JournalEqualityTests.cs | 2 -- tests/redmine-net-api.Tests/Equality/MyAccountTests.cs | 1 - tests/redmine-net-api.Tests/Equality/NewsTests.cs | 1 - tests/redmine-net-api.Tests/Equality/ProjectTests.cs | 1 - tests/redmine-net-api.Tests/Equality/SearchTests.cs | 1 - tests/redmine-net-api.Tests/Equality/TimeEntryTests.cs | 1 - tests/redmine-net-api.Tests/Equality/UserTests.cs | 1 - tests/redmine-net-api.Tests/Equality/VersionTests.cs | 1 - tests/redmine-net-api.Tests/Equality/WikiPageTests.cs | 1 - tests/redmine-net-api.Tests/Infrastructure/Order/CaseOrder.cs | 2 -- .../Infrastructure/Order/CollectionOrderer.cs | 3 --- .../Infrastructure/Order/OrderAttribute.cs | 2 -- .../Serialization/Json/AttachmentTests.cs | 1 - .../Serialization/Json/CustomFieldTests.cs | 1 - tests/redmine-net-api.Tests/Serialization/Json/IssuesTests.cs | 2 -- .../Serialization/Xml/AttachmentTests.cs | 1 - .../Serialization/Xml/CustomFieldTests.cs | 1 - .../Serialization/Xml/EnumerationTests.cs | 1 - tests/redmine-net-api.Tests/Serialization/Xml/ErrorTests.cs | 1 - tests/redmine-net-api.Tests/Serialization/Xml/FileTests.cs | 4 +--- tests/redmine-net-api.Tests/Serialization/Xml/GroupTests.cs | 2 -- .../Serialization/Xml/IssueCategoryTests.cs | 2 -- .../Serialization/Xml/IssueStatusTests.cs | 1 - tests/redmine-net-api.Tests/Serialization/Xml/IssueTests.cs | 2 -- .../Serialization/Xml/MembershipTests.cs | 1 - tests/redmine-net-api.Tests/Serialization/Xml/NewsTests.cs | 2 -- tests/redmine-net-api.Tests/Serialization/Xml/ProjectTests.cs | 2 -- tests/redmine-net-api.Tests/Serialization/Xml/QueryTests.cs | 1 - .../redmine-net-api.Tests/Serialization/Xml/RelationTests.cs | 1 - tests/redmine-net-api.Tests/Serialization/Xml/RoleTests.cs | 1 - tests/redmine-net-api.Tests/Serialization/Xml/SearchTests.cs | 2 -- tests/redmine-net-api.Tests/Serialization/Xml/TrackerTests.cs | 1 - tests/redmine-net-api.Tests/Serialization/Xml/UploadTests.cs | 1 - tests/redmine-net-api.Tests/Serialization/Xml/UserTests.cs | 3 --- tests/redmine-net-api.Tests/Serialization/Xml/VersionTests.cs | 2 -- tests/redmine-net-api.Tests/Serialization/Xml/WikiTests.cs | 2 -- tests/redmine-net-api.Tests/TestHelper.cs | 4 +--- tests/redmine-net-api.Tests/Tests/RedmineApiUrlsTests.cs | 2 +- 45 files changed, 3 insertions(+), 67 deletions(-) diff --git a/tests/redmine-net-api.Tests/Bugs/RedmineApi-229.cs b/tests/redmine-net-api.Tests/Bugs/RedmineApi-229.cs index 64ae736a..2c6bb2b8 100644 --- a/tests/redmine-net-api.Tests/Bugs/RedmineApi-229.cs +++ b/tests/redmine-net-api.Tests/Bugs/RedmineApi-229.cs @@ -1,4 +1,3 @@ -using System; using Redmine.Net.Api.Types; using Xunit; diff --git a/tests/redmine-net-api.Tests/Clone/AttachmentCloneTests.cs b/tests/redmine-net-api.Tests/Clone/AttachmentCloneTests.cs index dc8a2830..13990f54 100644 --- a/tests/redmine-net-api.Tests/Clone/AttachmentCloneTests.cs +++ b/tests/redmine-net-api.Tests/Clone/AttachmentCloneTests.cs @@ -1,4 +1,3 @@ -using System; using Redmine.Net.Api.Types; using Xunit; diff --git a/tests/redmine-net-api.Tests/Clone/IssueCloneTests.cs b/tests/redmine-net-api.Tests/Clone/IssueCloneTests.cs index 0d0b82fa..f41bcd2a 100644 --- a/tests/redmine-net-api.Tests/Clone/IssueCloneTests.cs +++ b/tests/redmine-net-api.Tests/Clone/IssueCloneTests.cs @@ -1,4 +1,3 @@ -using System; using Redmine.Net.Api.Types; using Xunit; diff --git a/tests/redmine-net-api.Tests/Clone/JournalCloneTests.cs b/tests/redmine-net-api.Tests/Clone/JournalCloneTests.cs index 37440f6e..a6c11719 100644 --- a/tests/redmine-net-api.Tests/Clone/JournalCloneTests.cs +++ b/tests/redmine-net-api.Tests/Clone/JournalCloneTests.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using Redmine.Net.Api.Types; using Xunit; diff --git a/tests/redmine-net-api.Tests/Equality/AttachmentEqualityTests.cs b/tests/redmine-net-api.Tests/Equality/AttachmentEqualityTests.cs index 6cc72dd1..c604f699 100644 --- a/tests/redmine-net-api.Tests/Equality/AttachmentEqualityTests.cs +++ b/tests/redmine-net-api.Tests/Equality/AttachmentEqualityTests.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using Redmine.Net.Api.Types; using Xunit; diff --git a/tests/redmine-net-api.Tests/Equality/BaseEqualityTests.cs b/tests/redmine-net-api.Tests/Equality/BaseEqualityTests.cs index 5ff00d44..e6d8672f 100644 --- a/tests/redmine-net-api.Tests/Equality/BaseEqualityTests.cs +++ b/tests/redmine-net-api.Tests/Equality/BaseEqualityTests.cs @@ -1,4 +1,3 @@ -using System; using Xunit; namespace Padi.DotNet.RedmineAPI.Tests.Equality; diff --git a/tests/redmine-net-api.Tests/Equality/IssueEqualityTests.cs b/tests/redmine-net-api.Tests/Equality/IssueEqualityTests.cs index 5060aece..2d137336 100644 --- a/tests/redmine-net-api.Tests/Equality/IssueEqualityTests.cs +++ b/tests/redmine-net-api.Tests/Equality/IssueEqualityTests.cs @@ -1,4 +1,3 @@ -using System; using Redmine.Net.Api.Types; using Xunit; diff --git a/tests/redmine-net-api.Tests/Equality/JournalEqualityTests.cs b/tests/redmine-net-api.Tests/Equality/JournalEqualityTests.cs index 16f2a073..c0ea67d4 100644 --- a/tests/redmine-net-api.Tests/Equality/JournalEqualityTests.cs +++ b/tests/redmine-net-api.Tests/Equality/JournalEqualityTests.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using Redmine.Net.Api.Types; using Xunit; diff --git a/tests/redmine-net-api.Tests/Equality/MyAccountTests.cs b/tests/redmine-net-api.Tests/Equality/MyAccountTests.cs index 6c0fad96..f098f5f2 100644 --- a/tests/redmine-net-api.Tests/Equality/MyAccountTests.cs +++ b/tests/redmine-net-api.Tests/Equality/MyAccountTests.cs @@ -1,4 +1,3 @@ -using System; using Redmine.Net.Api.Types; namespace Padi.DotNet.RedmineAPI.Tests.Equality; diff --git a/tests/redmine-net-api.Tests/Equality/NewsTests.cs b/tests/redmine-net-api.Tests/Equality/NewsTests.cs index 953775ea..0851518c 100644 --- a/tests/redmine-net-api.Tests/Equality/NewsTests.cs +++ b/tests/redmine-net-api.Tests/Equality/NewsTests.cs @@ -1,4 +1,3 @@ -using System; using Redmine.Net.Api.Types; namespace Padi.DotNet.RedmineAPI.Tests.Equality; diff --git a/tests/redmine-net-api.Tests/Equality/ProjectTests.cs b/tests/redmine-net-api.Tests/Equality/ProjectTests.cs index 2f2ce5a6..a8aff18a 100644 --- a/tests/redmine-net-api.Tests/Equality/ProjectTests.cs +++ b/tests/redmine-net-api.Tests/Equality/ProjectTests.cs @@ -1,4 +1,3 @@ -using System; using Redmine.Net.Api.Types; namespace Padi.DotNet.RedmineAPI.Tests.Equality; diff --git a/tests/redmine-net-api.Tests/Equality/SearchTests.cs b/tests/redmine-net-api.Tests/Equality/SearchTests.cs index 4962b131..2f5f0707 100644 --- a/tests/redmine-net-api.Tests/Equality/SearchTests.cs +++ b/tests/redmine-net-api.Tests/Equality/SearchTests.cs @@ -1,4 +1,3 @@ -using System; using Redmine.Net.Api.Types; namespace Padi.DotNet.RedmineAPI.Tests.Equality; diff --git a/tests/redmine-net-api.Tests/Equality/TimeEntryTests.cs b/tests/redmine-net-api.Tests/Equality/TimeEntryTests.cs index 6e1c3426..445358ff 100644 --- a/tests/redmine-net-api.Tests/Equality/TimeEntryTests.cs +++ b/tests/redmine-net-api.Tests/Equality/TimeEntryTests.cs @@ -1,4 +1,3 @@ -using System; using Redmine.Net.Api.Types; namespace Padi.DotNet.RedmineAPI.Tests.Equality; diff --git a/tests/redmine-net-api.Tests/Equality/UserTests.cs b/tests/redmine-net-api.Tests/Equality/UserTests.cs index ffc52415..018163ec 100644 --- a/tests/redmine-net-api.Tests/Equality/UserTests.cs +++ b/tests/redmine-net-api.Tests/Equality/UserTests.cs @@ -1,4 +1,3 @@ -using System; using Redmine.Net.Api.Types; namespace Padi.DotNet.RedmineAPI.Tests.Equality; diff --git a/tests/redmine-net-api.Tests/Equality/VersionTests.cs b/tests/redmine-net-api.Tests/Equality/VersionTests.cs index 5d3e06cc..786d82b8 100644 --- a/tests/redmine-net-api.Tests/Equality/VersionTests.cs +++ b/tests/redmine-net-api.Tests/Equality/VersionTests.cs @@ -1,4 +1,3 @@ -using System; using Redmine.Net.Api.Types; using Version = Redmine.Net.Api.Types.Version; diff --git a/tests/redmine-net-api.Tests/Equality/WikiPageTests.cs b/tests/redmine-net-api.Tests/Equality/WikiPageTests.cs index 6462f7e6..39284d57 100644 --- a/tests/redmine-net-api.Tests/Equality/WikiPageTests.cs +++ b/tests/redmine-net-api.Tests/Equality/WikiPageTests.cs @@ -1,4 +1,3 @@ -using System; using Redmine.Net.Api.Types; namespace Padi.DotNet.RedmineAPI.Tests.Equality; diff --git a/tests/redmine-net-api.Tests/Infrastructure/Order/CaseOrder.cs b/tests/redmine-net-api.Tests/Infrastructure/Order/CaseOrder.cs index 97ddb56a..b0f8f375 100644 --- a/tests/redmine-net-api.Tests/Infrastructure/Order/CaseOrder.cs +++ b/tests/redmine-net-api.Tests/Infrastructure/Order/CaseOrder.cs @@ -1,7 +1,5 @@ #if !(NET20 || NET40) using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; using System.Reflection; using Xunit.Abstractions; using Xunit.Sdk; diff --git a/tests/redmine-net-api.Tests/Infrastructure/Order/CollectionOrderer.cs b/tests/redmine-net-api.Tests/Infrastructure/Order/CollectionOrderer.cs index ae7b01da..b1ad5a08 100644 --- a/tests/redmine-net-api.Tests/Infrastructure/Order/CollectionOrderer.cs +++ b/tests/redmine-net-api.Tests/Infrastructure/Order/CollectionOrderer.cs @@ -1,8 +1,5 @@ #if !(NET20 || NET40) -using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; using Xunit; using Xunit.Abstractions; diff --git a/tests/redmine-net-api.Tests/Infrastructure/Order/OrderAttribute.cs b/tests/redmine-net-api.Tests/Infrastructure/Order/OrderAttribute.cs index c8f07627..1c7e08e7 100644 --- a/tests/redmine-net-api.Tests/Infrastructure/Order/OrderAttribute.cs +++ b/tests/redmine-net-api.Tests/Infrastructure/Order/OrderAttribute.cs @@ -1,5 +1,3 @@ -using System; - namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure.Order { public sealed class OrderAttribute : Attribute diff --git a/tests/redmine-net-api.Tests/Serialization/Json/AttachmentTests.cs b/tests/redmine-net-api.Tests/Serialization/Json/AttachmentTests.cs index 6cb7191d..bd34ff91 100644 --- a/tests/redmine-net-api.Tests/Serialization/Json/AttachmentTests.cs +++ b/tests/redmine-net-api.Tests/Serialization/Json/AttachmentTests.cs @@ -1,4 +1,3 @@ -using System; using Padi.DotNet.RedmineAPI.Tests.Infrastructure; using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; using Xunit; diff --git a/tests/redmine-net-api.Tests/Serialization/Json/CustomFieldTests.cs b/tests/redmine-net-api.Tests/Serialization/Json/CustomFieldTests.cs index 10d14dac..18366876 100644 --- a/tests/redmine-net-api.Tests/Serialization/Json/CustomFieldTests.cs +++ b/tests/redmine-net-api.Tests/Serialization/Json/CustomFieldTests.cs @@ -1,4 +1,3 @@ -using System.Linq; using Padi.DotNet.RedmineAPI.Tests.Infrastructure; using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; using Redmine.Net.Api.Types; diff --git a/tests/redmine-net-api.Tests/Serialization/Json/IssuesTests.cs b/tests/redmine-net-api.Tests/Serialization/Json/IssuesTests.cs index e1f9965c..adb73da7 100644 --- a/tests/redmine-net-api.Tests/Serialization/Json/IssuesTests.cs +++ b/tests/redmine-net-api.Tests/Serialization/Json/IssuesTests.cs @@ -1,5 +1,3 @@ -using System; -using System.Linq; using Padi.DotNet.RedmineAPI.Tests.Infrastructure; using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; using Xunit; diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/AttachmentTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/AttachmentTests.cs index 85caa2ae..9930b97d 100644 --- a/tests/redmine-net-api.Tests/Serialization/Xml/AttachmentTests.cs +++ b/tests/redmine-net-api.Tests/Serialization/Xml/AttachmentTests.cs @@ -1,4 +1,3 @@ -using System; using Padi.DotNet.RedmineAPI.Tests.Infrastructure; using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; using Xunit; diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/CustomFieldTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/CustomFieldTests.cs index 0cb541a9..daa7a02d 100644 --- a/tests/redmine-net-api.Tests/Serialization/Xml/CustomFieldTests.cs +++ b/tests/redmine-net-api.Tests/Serialization/Xml/CustomFieldTests.cs @@ -1,4 +1,3 @@ -using System.Linq; using Padi.DotNet.RedmineAPI.Tests.Infrastructure; using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; using Redmine.Net.Api.Types; diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/EnumerationTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/EnumerationTests.cs index 2d870e8c..3c0a6740 100644 --- a/tests/redmine-net-api.Tests/Serialization/Xml/EnumerationTests.cs +++ b/tests/redmine-net-api.Tests/Serialization/Xml/EnumerationTests.cs @@ -1,4 +1,3 @@ -using System.Linq; using Padi.DotNet.RedmineAPI.Tests.Infrastructure; using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; using Redmine.Net.Api.Types; diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/ErrorTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/ErrorTests.cs index c5e447a5..a40bd64a 100644 --- a/tests/redmine-net-api.Tests/Serialization/Xml/ErrorTests.cs +++ b/tests/redmine-net-api.Tests/Serialization/Xml/ErrorTests.cs @@ -1,4 +1,3 @@ -using System.Linq; using Padi.DotNet.RedmineAPI.Tests.Infrastructure; using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; using Redmine.Net.Api.Types; diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/FileTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/FileTests.cs index 72fbd208..1adc3c59 100644 --- a/tests/redmine-net-api.Tests/Serialization/Xml/FileTests.cs +++ b/tests/redmine-net-api.Tests/Serialization/Xml/FileTests.cs @@ -1,9 +1,7 @@ -using System; -using System.Collections.Generic; using Padi.DotNet.RedmineAPI.Tests.Infrastructure; using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; -using Redmine.Net.Api.Types; using Xunit; +using File = Redmine.Net.Api.Types.File; namespace Padi.DotNet.RedmineAPI.Tests.Serialization.Xml; diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/GroupTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/GroupTests.cs index 3e057eac..d563fb41 100644 --- a/tests/redmine-net-api.Tests/Serialization/Xml/GroupTests.cs +++ b/tests/redmine-net-api.Tests/Serialization/Xml/GroupTests.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.Linq; using Padi.DotNet.RedmineAPI.Tests.Infrastructure; using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; using Redmine.Net.Api.Types; diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/IssueCategoryTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/IssueCategoryTests.cs index 9bf24bdb..3b658f55 100644 --- a/tests/redmine-net-api.Tests/Serialization/Xml/IssueCategoryTests.cs +++ b/tests/redmine-net-api.Tests/Serialization/Xml/IssueCategoryTests.cs @@ -1,5 +1,3 @@ -using System.Collections.Generic; -using System.Linq; using Padi.DotNet.RedmineAPI.Tests.Infrastructure; using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; using Xunit; diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/IssueStatusTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/IssueStatusTests.cs index 8e699d0b..58daceca 100644 --- a/tests/redmine-net-api.Tests/Serialization/Xml/IssueStatusTests.cs +++ b/tests/redmine-net-api.Tests/Serialization/Xml/IssueStatusTests.cs @@ -1,4 +1,3 @@ -using System.Linq; using Padi.DotNet.RedmineAPI.Tests.Infrastructure; using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; using Xunit; diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/IssueTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/IssueTests.cs index 6791587c..a423b713 100644 --- a/tests/redmine-net-api.Tests/Serialization/Xml/IssueTests.cs +++ b/tests/redmine-net-api.Tests/Serialization/Xml/IssueTests.cs @@ -1,5 +1,3 @@ -using System; -using System.Linq; using Padi.DotNet.RedmineAPI.Tests.Infrastructure; using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; using Xunit; diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/MembershipTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/MembershipTests.cs index 9bede534..59aab9b9 100644 --- a/tests/redmine-net-api.Tests/Serialization/Xml/MembershipTests.cs +++ b/tests/redmine-net-api.Tests/Serialization/Xml/MembershipTests.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using Padi.DotNet.RedmineAPI.Tests.Infrastructure; using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; using Redmine.Net.Api.Types; diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/NewsTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/NewsTests.cs index 0b29de40..93ebe8d3 100644 --- a/tests/redmine-net-api.Tests/Serialization/Xml/NewsTests.cs +++ b/tests/redmine-net-api.Tests/Serialization/Xml/NewsTests.cs @@ -1,5 +1,3 @@ -using System; -using System.Linq; using Padi.DotNet.RedmineAPI.Tests.Infrastructure; using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; using Xunit; diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/ProjectTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/ProjectTests.cs index fd0072c7..fa1a9f44 100644 --- a/tests/redmine-net-api.Tests/Serialization/Xml/ProjectTests.cs +++ b/tests/redmine-net-api.Tests/Serialization/Xml/ProjectTests.cs @@ -1,5 +1,3 @@ -using System; -using System.Linq; using Padi.DotNet.RedmineAPI.Tests.Infrastructure; using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; using Xunit; diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/QueryTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/QueryTests.cs index 5927739a..9e830935 100644 --- a/tests/redmine-net-api.Tests/Serialization/Xml/QueryTests.cs +++ b/tests/redmine-net-api.Tests/Serialization/Xml/QueryTests.cs @@ -1,4 +1,3 @@ -using System.Linq; using Padi.DotNet.RedmineAPI.Tests.Infrastructure; using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; using Xunit; diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/RelationTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/RelationTests.cs index f06be8ac..cc9ecd5b 100644 --- a/tests/redmine-net-api.Tests/Serialization/Xml/RelationTests.cs +++ b/tests/redmine-net-api.Tests/Serialization/Xml/RelationTests.cs @@ -1,4 +1,3 @@ -using System.Linq; using Padi.DotNet.RedmineAPI.Tests.Infrastructure; using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; using Redmine.Net.Api.Types; diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/RoleTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/RoleTests.cs index 7d695ef5..2790bc89 100644 --- a/tests/redmine-net-api.Tests/Serialization/Xml/RoleTests.cs +++ b/tests/redmine-net-api.Tests/Serialization/Xml/RoleTests.cs @@ -1,4 +1,3 @@ -using System.Linq; using Padi.DotNet.RedmineAPI.Tests.Infrastructure; using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; using Redmine.Net.Api.Types; diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/SearchTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/SearchTests.cs index 5c60001f..928cd089 100644 --- a/tests/redmine-net-api.Tests/Serialization/Xml/SearchTests.cs +++ b/tests/redmine-net-api.Tests/Serialization/Xml/SearchTests.cs @@ -1,5 +1,3 @@ -using System; -using System.Linq; using Padi.DotNet.RedmineAPI.Tests.Infrastructure; using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; using Redmine.Net.Api.Types; diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/TrackerTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/TrackerTests.cs index a61410cb..4d39faa8 100644 --- a/tests/redmine-net-api.Tests/Serialization/Xml/TrackerTests.cs +++ b/tests/redmine-net-api.Tests/Serialization/Xml/TrackerTests.cs @@ -1,4 +1,3 @@ -using System.Linq; using Padi.DotNet.RedmineAPI.Tests.Infrastructure; using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; using Redmine.Net.Api.Types; diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/UploadTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/UploadTests.cs index ff191c89..b9693b1a 100644 --- a/tests/redmine-net-api.Tests/Serialization/Xml/UploadTests.cs +++ b/tests/redmine-net-api.Tests/Serialization/Xml/UploadTests.cs @@ -1,4 +1,3 @@ -using System.Linq; using Padi.DotNet.RedmineAPI.Tests.Infrastructure; using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; using Xunit; diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/UserTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/UserTests.cs index e24b0a57..39f31e9a 100644 --- a/tests/redmine-net-api.Tests/Serialization/Xml/UserTests.cs +++ b/tests/redmine-net-api.Tests/Serialization/Xml/UserTests.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections; -using System.Linq; using Padi.DotNet.RedmineAPI.Tests.Infrastructure; using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; using Redmine.Net.Api.Types; diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/VersionTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/VersionTests.cs index 860e1a93..28b7c3b2 100644 --- a/tests/redmine-net-api.Tests/Serialization/Xml/VersionTests.cs +++ b/tests/redmine-net-api.Tests/Serialization/Xml/VersionTests.cs @@ -1,5 +1,3 @@ -using System; -using System.Linq; using Padi.DotNet.RedmineAPI.Tests.Infrastructure; using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; using Redmine.Net.Api.Types; diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/WikiTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/WikiTests.cs index 768ffde0..b5d3a275 100644 --- a/tests/redmine-net-api.Tests/Serialization/Xml/WikiTests.cs +++ b/tests/redmine-net-api.Tests/Serialization/Xml/WikiTests.cs @@ -1,5 +1,3 @@ -using System; -using System.Linq; using Padi.DotNet.RedmineAPI.Tests.Infrastructure; using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; using Xunit; diff --git a/tests/redmine-net-api.Tests/TestHelper.cs b/tests/redmine-net-api.Tests/TestHelper.cs index fbc1995f..3e466bad 100644 --- a/tests/redmine-net-api.Tests/TestHelper.cs +++ b/tests/redmine-net-api.Tests/TestHelper.cs @@ -1,6 +1,4 @@ -using System; -using System.IO; -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; using Padi.DotNet.RedmineAPI.Tests.Infrastructure; namespace Padi.DotNet.RedmineAPI.Tests diff --git a/tests/redmine-net-api.Tests/Tests/RedmineApiUrlsTests.cs b/tests/redmine-net-api.Tests/Tests/RedmineApiUrlsTests.cs index 8b0121bb..ff358497 100644 --- a/tests/redmine-net-api.Tests/Tests/RedmineApiUrlsTests.cs +++ b/tests/redmine-net-api.Tests/Tests/RedmineApiUrlsTests.cs @@ -1,10 +1,10 @@ -using System; using System.Collections.Specialized; using Redmine.Net.Api; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Net; using Redmine.Net.Api.Types; using Xunit; +using File = Redmine.Net.Api.Types.File; using Version = Redmine.Net.Api.Types.Version; From 951fc814408eafc7685f81bfd08f236f463c213c Mon Sep 17 00:00:00 2001 From: Padi Date: Sun, 11 May 2025 15:41:23 +0300 Subject: [PATCH 484/549] Replace magic strings with constants --- src/redmine-net-api/RedmineConstants.cs | 5 +++++ .../Serialization/Json/JsonRedmineSerializer.cs | 4 ++-- .../Serialization/Xml/XmlRedmineSerializer.cs | 2 +- src/redmine-net-api/Types/IssueRelationType.cs | 4 ++-- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/redmine-net-api/RedmineConstants.cs b/src/redmine-net-api/RedmineConstants.cs index f50a9cec..d2b963eb 100644 --- a/src/redmine-net-api/RedmineConstants.cs +++ b/src/redmine-net-api/RedmineConstants.cs @@ -57,5 +57,10 @@ public static class RedmineConstants /// ///
public const string XML = "xml"; + + /// + /// + /// + public const string JSON = "json"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs b/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs index f279abe3..f0319cdd 100644 --- a/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs +++ b/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs @@ -135,9 +135,9 @@ internal sealed class JsonRedmineSerializer : IRedmineSerializer } #pragma warning restore CA1822 - public string Format { get; } = "json"; + public string Format { get; } = RedmineConstants.JSON; - public string ContentType { get; } = "application/json"; + public string ContentType { get; } = RedmineConstants.CONTENT_TYPE_APPLICATION_JSON; public string Serialize(T entity) where T : class { diff --git a/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs b/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs index 134fac5f..84772404 100644 --- a/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs +++ b/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs @@ -80,7 +80,7 @@ public XmlRedmineSerializer(XmlWriterSettings xmlWriterSettings) public string Format => RedmineConstants.XML; - public string ContentType { get; } = "application/xml"; + public string ContentType { get; } = RedmineConstants.CONTENT_TYPE_APPLICATION_XML; public string Serialize(T entity) where T : class { diff --git a/src/redmine-net-api/Types/IssueRelationType.cs b/src/redmine-net-api/Types/IssueRelationType.cs index fff16392..01d06292 100644 --- a/src/redmine-net-api/Types/IssueRelationType.cs +++ b/src/redmine-net-api/Types/IssueRelationType.cs @@ -68,13 +68,13 @@ public enum IssueRelationType /// ///
- [XmlEnum("copied_to")] + [XmlEnum(RedmineKeys.COPIED_TO)] CopiedTo, /// /// /// - [XmlEnum("copied_from")] + [XmlEnum(RedmineKeys.COPIED_FROM)] CopiedFrom } } \ No newline at end of file From 3e69eaab0abc9c650fc1f43b78dfe6d934179170 Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 14 May 2025 11:08:05 +0300 Subject: [PATCH 485/549] Rename extension --- ...Extensions.cs => IEnumerableExtensions.cs} | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) rename src/redmine-net-api/Extensions/{EnumerableExtensions.cs => IEnumerableExtensions.cs} (50%) diff --git a/src/redmine-net-api/Extensions/EnumerableExtensions.cs b/src/redmine-net-api/Extensions/IEnumerableExtensions.cs similarity index 50% rename from src/redmine-net-api/Extensions/EnumerableExtensions.cs rename to src/redmine-net-api/Extensions/IEnumerableExtensions.cs index e5ae149f..98d9b355 100644 --- a/src/redmine-net-api/Extensions/EnumerableExtensions.cs +++ b/src/redmine-net-api/Extensions/IEnumerableExtensions.cs @@ -1,12 +1,14 @@ +using System; using System.Collections.Generic; using System.Text; +using Redmine.Net.Api.Common; namespace Redmine.Net.Api.Extensions; /// /// Provides extension methods for IEnumerable types. /// -public static class EnumerableExtensions +public static class IEnumerableExtensions { /// /// Converts a collection of objects into a string representation with each item separated by a comma @@ -18,7 +20,7 @@ public static class EnumerableExtensions /// Returns a string containing all the items from the collection, separated by commas and /// enclosed within curly braces. Returns null if the collection is null. /// - public static string Dump(this IEnumerable collection) where TIn : class + internal static string Dump(this IEnumerable collection) where TIn : class { if (collection == null) { @@ -44,4 +46,30 @@ public static string Dump(this IEnumerable collection) where TIn : cla return str; } + + /// + /// Returns the index of the first item in the sequence that satisfies the predicate. If no item satisfies the predicate, -1 is returned. + /// + /// The type of objects in the . + /// in which to search. + /// Function performed to check whether an item satisfies the condition. + /// Return the zero-based index of the first occurrence of an element that satisfies the condition, if found; otherwise, -1. + internal static int IndexOf(this IEnumerable source, Func predicate) + { + ArgumentVerifier.ThrowIfNull(predicate, nameof(predicate)); + + var index = 0; + + foreach (var item in source) + { + if (predicate(item)) + { + return index; + } + + index++; + } + + return -1; + } } \ No newline at end of file From 368ec165e25cc3b657aa1356a4f9ccce02aaed5f Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 14 May 2025 11:13:55 +0300 Subject: [PATCH 486/549] Replace ToString(CultureInfo.InvariantCulture) with ToInvariantString() --- .../Extensions/RedmineManagerExtensions.cs | 26 +++++++++---------- .../NameValueCollectionExtensions.cs | 6 ++--- src/redmine-net-api/RedmineManager.cs | 4 +-- .../Json/Extensions/JsonWriterExtensions.cs | 6 ++--- .../Serialization/Xml/CacheKeyFactory.cs | 5 ++-- .../Xml/Extensions/XmlWriterExtensions.cs | 8 +++--- src/redmine-net-api/Types/Attachments.cs | 3 ++- src/redmine-net-api/Types/GroupUser.cs | 2 +- src/redmine-net-api/Types/IdentifiableName.cs | 2 +- src/redmine-net-api/Types/Issue.cs | 6 ++--- src/redmine-net-api/Types/IssueCustomField.cs | 4 +-- src/redmine-net-api/Types/MembershipRole.cs | 4 +-- src/redmine-net-api/Types/ProjectTracker.cs | 2 +- src/redmine-net-api/Types/Watcher.cs | 3 ++- .../_net20/RedmineManagerAsyncObsolete.cs | 4 +-- 15 files changed, 44 insertions(+), 41 deletions(-) diff --git a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs index 20d600a9..d7ff6113 100644 --- a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs +++ b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs @@ -265,7 +265,7 @@ public static MyAccount GetMyAccount(this RedmineManager redmineManager, Request public static void AddWatcherToIssue(this RedmineManager redmineManager, int issueId, int userId, RequestOptions requestOptions = null) { - var uri = redmineManager.RedmineApiUrls.IssueWatcherAdd(issueId.ToString(CultureInfo.InvariantCulture)); + var uri = redmineManager.RedmineApiUrls.IssueWatcherAdd(issueId.ToInvariantString()); var payload = SerializationHelper.SerializeUserId(userId, redmineManager.Serializer); @@ -282,7 +282,7 @@ public static void AddWatcherToIssue(this RedmineManager redmineManager, int iss public static void RemoveWatcherFromIssue(this RedmineManager redmineManager, int issueId, int userId, RequestOptions requestOptions = null) { - var uri = redmineManager.RedmineApiUrls.IssueWatcherRemove(issueId.ToString(CultureInfo.InvariantCulture), userId.ToString(CultureInfo.InvariantCulture)); + var uri = redmineManager.RedmineApiUrls.IssueWatcherRemove(issueId.ToInvariantString(), userId.ToInvariantString()); redmineManager.ApiClient.Delete(uri, requestOptions); } @@ -297,7 +297,7 @@ public static void RemoveWatcherFromIssue(this RedmineManager redmineManager, in public static void AddUserToGroup(this RedmineManager redmineManager, int groupId, int userId, RequestOptions requestOptions = null) { - var uri = redmineManager.RedmineApiUrls.GroupUserAdd(groupId.ToString(CultureInfo.InvariantCulture)); + var uri = redmineManager.RedmineApiUrls.GroupUserAdd(groupId.ToInvariantString()); var payload = SerializationHelper.SerializeUserId(userId, redmineManager.Serializer); @@ -314,7 +314,7 @@ public static void AddUserToGroup(this RedmineManager redmineManager, int groupI public static void RemoveUserFromGroup(this RedmineManager redmineManager, int groupId, int userId, RequestOptions requestOptions = null) { - var uri = redmineManager.RedmineApiUrls.GroupUserRemove(groupId.ToString(CultureInfo.InvariantCulture), userId.ToString(CultureInfo.InvariantCulture)); + var uri = redmineManager.RedmineApiUrls.GroupUserRemove(groupId.ToInvariantString(), userId.ToInvariantString()); redmineManager.ApiClient.Delete(uri, requestOptions); } @@ -387,7 +387,7 @@ public static WikiPage GetWikiPage(this RedmineManager redmineManager, string pr { var uri = version == 0 ? redmineManager.RedmineApiUrls.ProjectWikiPage(projectId, pageName) - : redmineManager.RedmineApiUrls.ProjectWikiPageVersion(projectId, pageName, version.ToString(CultureInfo.InvariantCulture)); + : redmineManager.RedmineApiUrls.ProjectWikiPageVersion(projectId, pageName, version.ToInvariantString()); var escapedUri = Uri.EscapeDataString(uri); @@ -446,7 +446,7 @@ public static void UpdateIssueAttachment(this RedmineManager redmineManager, int var data = redmineManager.Serializer.Serialize(attachments); - var uri = redmineManager.RedmineApiUrls.AttachmentUpdate(issueId.ToString(CultureInfo.InvariantCulture)); + var uri = redmineManager.RedmineApiUrls.AttachmentUpdate(issueId.ToInvariantString()); redmineManager.ApiClient.Patch(uri, data, requestOptions); } @@ -482,8 +482,8 @@ private static NameValueCollection CreateSearchParameters(string q, int limit, i var parameters = new NameValueCollection { {RedmineKeys.Q, q}, - {RedmineKeys.LIMIT, limit.ToString(CultureInfo.InvariantCulture)}, - {RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)}, + {RedmineKeys.LIMIT, limit.ToInvariantString()}, + {RedmineKeys.OFFSET, offset.ToInvariantString()}, }; return searchFilter != null ? searchFilter.Build(parameters) : parameters; @@ -805,7 +805,7 @@ public static async Task GetWikiPageAsync(this RedmineManager redmineM { var uri = version == 0 ? redmineManager.RedmineApiUrls.ProjectWikiPage(projectId, pageName) - : redmineManager.RedmineApiUrls.ProjectWikiPageVersion(projectId, pageName, version.ToString(CultureInfo.InvariantCulture)); + : redmineManager.RedmineApiUrls.ProjectWikiPageVersion(projectId, pageName, version.ToInvariantString()); var escapedUri = Uri.EscapeDataString(uri); @@ -844,7 +844,7 @@ public static async Task> GetAllWikiPagesAsync(this RedmineManage /// public static async Task AddUserToGroupAsync(this RedmineManager redmineManager, int groupId, int userId, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) { - var uri = redmineManager.RedmineApiUrls.GroupUserAdd(groupId.ToString(CultureInfo.InvariantCulture)); + var uri = redmineManager.RedmineApiUrls.GroupUserAdd(groupId.ToInvariantString()); var payload = SerializationHelper.SerializeUserId(userId, redmineManager.Serializer); @@ -862,7 +862,7 @@ public static async Task AddUserToGroupAsync(this RedmineManager redmineManager, /// public static async Task RemoveUserFromGroupAsync(this RedmineManager redmineManager, int groupId, int userId, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) { - var uri = redmineManager.RedmineApiUrls.GroupUserRemove(groupId.ToString(CultureInfo.InvariantCulture), userId.ToString(CultureInfo.InvariantCulture)); + var uri = redmineManager.RedmineApiUrls.GroupUserRemove(groupId.ToInvariantString(), userId.ToInvariantString()); await redmineManager.ApiClient.DeleteAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); } @@ -878,7 +878,7 @@ public static async Task RemoveUserFromGroupAsync(this RedmineManager redmineMan /// public static async Task AddWatcherToIssueAsync(this RedmineManager redmineManager, int issueId, int userId, RequestOptions requestOptions = null , CancellationToken cancellationToken = default) { - var uri = redmineManager.RedmineApiUrls.IssueWatcherAdd(issueId.ToString(CultureInfo.InvariantCulture)); + var uri = redmineManager.RedmineApiUrls.IssueWatcherAdd(issueId.ToInvariantString()); var payload = SerializationHelper.SerializeUserId(userId, redmineManager.Serializer); @@ -896,7 +896,7 @@ public static async Task AddWatcherToIssueAsync(this RedmineManager redmineManag /// public static async Task RemoveWatcherFromIssueAsync(this RedmineManager redmineManager, int issueId, int userId, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) { - var uri = redmineManager.RedmineApiUrls.IssueWatcherRemove(issueId.ToString(CultureInfo.InvariantCulture), userId.ToString(CultureInfo.InvariantCulture)); + var uri = redmineManager.RedmineApiUrls.IssueWatcherRemove(issueId.ToInvariantString(), userId.ToInvariantString()); await redmineManager.ApiClient.DeleteAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); } diff --git a/src/redmine-net-api/Net/WebClient/Extensions/NameValueCollectionExtensions.cs b/src/redmine-net-api/Net/WebClient/Extensions/NameValueCollectionExtensions.cs index 6d573e17..e69dffbd 100644 --- a/src/redmine-net-api/Net/WebClient/Extensions/NameValueCollectionExtensions.cs +++ b/src/redmine-net-api/Net/WebClient/Extensions/NameValueCollectionExtensions.cs @@ -101,8 +101,8 @@ internal static NameValueCollection AddPagingParameters(this NameValueCollection offset = 0; } - parameters.Set(RedmineKeys.LIMIT, pageSize.ToString(CultureInfo.InvariantCulture)); - parameters.Set(RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)); + parameters.Set(RedmineKeys.LIMIT, pageSize.ToInvariantString()); + parameters.Set(RedmineKeys.OFFSET, offset.ToInvariantString()); return parameters; } @@ -133,7 +133,7 @@ internal static void AddIfNotNull(this NameValueCollection nameValueCollection, { if (value.HasValue) { - nameValueCollection.Add(key, value.Value.ToString(CultureInfo.InvariantCulture)); + nameValueCollection.Add(key, value.Value.ToInvariantString()); } } } diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index 41b64118..a47c4b44 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -241,7 +241,7 @@ internal List GetInternal(string uri, RequestOptions requestOptions = null if (pageSize == default) { pageSize = _redmineManagerOptions.PageSize > 0 ? _redmineManagerOptions.PageSize : RedmineConstants.DEFAULT_PAGE_SIZE_VALUE; - requestOptions.QueryString.Set(RedmineKeys.LIMIT, pageSize.ToString(CultureInfo.InvariantCulture)); + requestOptions.QueryString.Set(RedmineKeys.LIMIT, pageSize.ToInvariantString()); } var hasOffset = TypesWithOffset.ContainsKey(typeof(T)); @@ -250,7 +250,7 @@ internal List GetInternal(string uri, RequestOptions requestOptions = null int totalCount; do { - requestOptions.QueryString.Set(RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)); + requestOptions.QueryString.Set(RedmineKeys.OFFSET, offset.ToInvariantString()); var tempResult = GetPaginatedInternal(uri, requestOptions); diff --git a/src/redmine-net-api/Serialization/Json/Extensions/JsonWriterExtensions.cs b/src/redmine-net-api/Serialization/Json/Extensions/JsonWriterExtensions.cs index c921d706..e5f20376 100644 --- a/src/redmine-net-api/Serialization/Json/Extensions/JsonWriterExtensions.cs +++ b/src/redmine-net-api/Serialization/Json/Extensions/JsonWriterExtensions.cs @@ -72,7 +72,7 @@ public static void WriteIfNotDefaultOrNull(this JsonWriter writer, string ele /// The property name. public static void WriteBoolean(this JsonWriter writer, string elementName, bool value) { - writer.WriteProperty(elementName, value.ToString().ToLowerInv()); + writer.WriteProperty(elementName, value.ToInvariantString()); } /// @@ -84,7 +84,7 @@ public static void WriteBoolean(this JsonWriter writer, string elementName, bool /// public static void WriteIdOrEmpty(this JsonWriter jsonWriter, string tag, IdentifiableName ident, string emptyValue = null) { - jsonWriter.WriteProperty(tag, ident != null ? ident.Id.ToString(CultureInfo.InvariantCulture) : emptyValue); + jsonWriter.WriteProperty(tag, ident != null ? ident.Id.ToInvariantString() : emptyValue); } /// @@ -212,7 +212,7 @@ public static void WriteArrayIds(this JsonWriter jsonWriter, string tag, IEnumer foreach (var identifiableName in collection) { - sb.Append(identifiableName.Id.ToString(CultureInfo.InvariantCulture)).Append(','); + sb.Append(identifiableName.Id.ToInvariantString()).Append(','); } if (sb.Length > 1) diff --git a/src/redmine-net-api/Serialization/Xml/CacheKeyFactory.cs b/src/redmine-net-api/Serialization/Xml/CacheKeyFactory.cs index 45f17b12..3671e595 100644 --- a/src/redmine-net-api/Serialization/Xml/CacheKeyFactory.cs +++ b/src/redmine-net-api/Serialization/Xml/CacheKeyFactory.cs @@ -18,6 +18,7 @@ limitations under the License. using System.Globalization; using System.Text; using System.Xml.Serialization; +using Redmine.Net.Api.Extensions; namespace Redmine.Net.Api.Serialization { @@ -46,11 +47,11 @@ public static string Create(Type type, XmlAttributeOverrides overrides, Type[] t var keyBuilder = new StringBuilder(); keyBuilder.Append(type.FullName); keyBuilder.Append( "??" ); - keyBuilder.Append(overrides?.GetHashCode().ToString(CultureInfo.InvariantCulture)); + keyBuilder.Append(overrides?.GetHashCode().ToInvariantString()); keyBuilder.Append( "??" ); keyBuilder.Append(GetTypeArraySignature(types)); keyBuilder.Append("??"); - keyBuilder.Append(root?.GetHashCode().ToString(CultureInfo.InvariantCulture)); + keyBuilder.Append(root?.GetHashCode().ToInvariantString()); keyBuilder.Append("??"); keyBuilder.Append(defaultNamespace); diff --git a/src/redmine-net-api/Serialization/Xml/Extensions/XmlWriterExtensions.cs b/src/redmine-net-api/Serialization/Xml/Extensions/XmlWriterExtensions.cs index cf33cb1c..3894309e 100644 --- a/src/redmine-net-api/Serialization/Xml/Extensions/XmlWriterExtensions.cs +++ b/src/redmine-net-api/Serialization/Xml/Extensions/XmlWriterExtensions.cs @@ -47,7 +47,7 @@ public static void WriteIdIfNotNull(this XmlWriter writer, string elementName, I { if (identifiableName != null) { - writer.WriteElementString(elementName, identifiableName.Id.ToString(CultureInfo.InvariantCulture)); + writer.WriteElementString(elementName, identifiableName.Id.ToInvariantString()); } } @@ -232,7 +232,7 @@ public static void WriteArrayStringElement(this XmlWriter writer, string element /// public static void WriteIdOrEmpty(this XmlWriter writer, string elementName, IdentifiableName ident) { - writer.WriteElementString(elementName, ident != null ? ident.Id.ToString(CultureInfo.InvariantCulture) : string.Empty); + writer.WriteElementString(elementName, ident != null ? ident.Id.ToInvariantString() : string.Empty); } /// @@ -267,7 +267,7 @@ public static void WriteIfNotDefaultOrNull(this XmlWriter writer, string elem /// The tag. public static void WriteBoolean(this XmlWriter writer, string elementName, bool value) { - writer.WriteElementString(elementName, value.ToString().ToLowerInv()); + writer.WriteElementString(elementName, value.ToInvariantString()); } /// @@ -285,7 +285,7 @@ public static void WriteValueOrEmpty(this XmlWriter writer, string elementNam } else { - writer.WriteElementString(elementName, val.Value.ToString().ToLowerInv()); + writer.WriteElementString(elementName, val.Value.ToInvariantString()); } } diff --git a/src/redmine-net-api/Types/Attachments.cs b/src/redmine-net-api/Types/Attachments.cs index 21655fec..64276c23 100644 --- a/src/redmine-net-api/Types/Attachments.cs +++ b/src/redmine-net-api/Types/Attachments.cs @@ -17,6 +17,7 @@ limitations under the License. using System.Collections.Generic; using System.Globalization; using Newtonsoft.Json; +using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api.Types @@ -43,7 +44,7 @@ public void WriteJson(JsonWriter writer) writer.WriteStartArray(); foreach (var item in this) { - writer.WritePropertyName(item.Key.ToString(CultureInfo.InvariantCulture)); + writer.WritePropertyName(item.Key.ToInvariantString()); item.Value.WriteJson(writer); } writer.WriteEndArray(); diff --git a/src/redmine-net-api/Types/GroupUser.cs b/src/redmine-net-api/Types/GroupUser.cs index 6a7d9760..a1eaf701 100644 --- a/src/redmine-net-api/Types/GroupUser.cs +++ b/src/redmine-net-api/Types/GroupUser.cs @@ -32,7 +32,7 @@ public sealed class GroupUser : IdentifiableName, IValue /// /// /// - public string Value => Id.ToString(CultureInfo.InvariantCulture); + public string Value => Id.ToInvariantString(); #endregion /// diff --git a/src/redmine-net-api/Types/IdentifiableName.cs b/src/redmine-net-api/Types/IdentifiableName.cs index 3d087f3a..27af92d7 100644 --- a/src/redmine-net-api/Types/IdentifiableName.cs +++ b/src/redmine-net-api/Types/IdentifiableName.cs @@ -124,7 +124,7 @@ public override void ReadXml(XmlReader reader) /// public override void WriteXml(XmlWriter writer) { - writer.WriteAttributeString(RedmineKeys.ID, Id.ToString(CultureInfo.InvariantCulture)); + writer.WriteAttributeString(RedmineKeys.ID, Id.ToInvariantString()); writer.WriteAttributeString(RedmineKeys.NAME, Name); } diff --git a/src/redmine-net-api/Types/Issue.cs b/src/redmine-net-api/Types/Issue.cs index 3fe5eb27..884d3c7e 100644 --- a/src/redmine-net-api/Types/Issue.cs +++ b/src/redmine-net-api/Types/Issue.cs @@ -334,7 +334,7 @@ public override void WriteXml(XmlWriter writer) } writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); - writer.WriteElementString(RedmineKeys.IS_PRIVATE, IsPrivate.ToString(CultureInfo.InvariantCulture).ToLowerInv()); + writer.WriteElementString(RedmineKeys.IS_PRIVATE, IsPrivate.ToInvariantString()); writer.WriteIdIfNotNull(RedmineKeys.PROJECT_ID, Project); writer.WriteIdIfNotNull(RedmineKeys.PRIORITY_ID, Priority); @@ -452,12 +452,12 @@ public override void WriteJson(JsonWriter writer) if (DoneRatio != null) { - writer.WriteProperty(RedmineKeys.DONE_RATIO, DoneRatio.Value.ToString(CultureInfo.InvariantCulture)); + writer.WriteProperty(RedmineKeys.DONE_RATIO, DoneRatio.Value.ToInvariantString()); } if (SpentHours != null) { - writer.WriteProperty(RedmineKeys.SPENT_HOURS, SpentHours.Value.ToString(CultureInfo.InvariantCulture)); + writer.WriteProperty(RedmineKeys.SPENT_HOURS, SpentHours.Value.ToInvariantString()); } writer.WriteArray(RedmineKeys.UPLOADS, Uploads); diff --git a/src/redmine-net-api/Types/IssueCustomField.cs b/src/redmine-net-api/Types/IssueCustomField.cs index 0acdffb0..9b65f9c0 100644 --- a/src/redmine-net-api/Types/IssueCustomField.cs +++ b/src/redmine-net-api/Types/IssueCustomField.cs @@ -113,7 +113,7 @@ public override void WriteXml(XmlWriter writer) var itemsCount = Values.Count; - writer.WriteAttributeString(RedmineKeys.ID, Id.ToString(CultureInfo.InvariantCulture)); + writer.WriteAttributeString(RedmineKeys.ID, Id.ToInvariantString()); Multiple = itemsCount > 1; @@ -307,7 +307,7 @@ public override int GetHashCode() /// /// /// - public string Value => Id.ToString(CultureInfo.InvariantCulture); + public string Value => Id.ToInvariantString(); #endregion diff --git a/src/redmine-net-api/Types/MembershipRole.cs b/src/redmine-net-api/Types/MembershipRole.cs index 2df6f835..7a01826a 100644 --- a/src/redmine-net-api/Types/MembershipRole.cs +++ b/src/redmine-net-api/Types/MembershipRole.cs @@ -100,7 +100,7 @@ public override void ReadJson(JsonReader reader) /// public override void WriteJson(JsonWriter writer) { - writer.WriteProperty(RedmineKeys.ID, Id.ToString(CultureInfo.InvariantCulture)); + writer.WriteProperty(RedmineKeys.ID, Id.ToInvariantString()); } #endregion @@ -172,7 +172,7 @@ public override int GetHashCode() /// /// /// - public string Value => Id.ToString(CultureInfo.InvariantCulture); + public string Value => Id.ToInvariantString(); #endregion /// diff --git a/src/redmine-net-api/Types/ProjectTracker.cs b/src/redmine-net-api/Types/ProjectTracker.cs index d6c64130..d77b5230 100644 --- a/src/redmine-net-api/Types/ProjectTracker.cs +++ b/src/redmine-net-api/Types/ProjectTracker.cs @@ -57,7 +57,7 @@ internal ProjectTracker(int trackerId) /// /// /// - public string Value => Id.ToString(CultureInfo.InvariantCulture); + public string Value => Id.ToInvariantString(); #endregion diff --git a/src/redmine-net-api/Types/Watcher.cs b/src/redmine-net-api/Types/Watcher.cs index 24d39693..2d6b7798 100644 --- a/src/redmine-net-api/Types/Watcher.cs +++ b/src/redmine-net-api/Types/Watcher.cs @@ -18,6 +18,7 @@ limitations under the License. using System.Diagnostics; using System.Globalization; using System.Xml.Serialization; +using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; namespace Redmine.Net.Api.Types @@ -36,7 +37,7 @@ public sealed class Watcher : IdentifiableName /// /// /// - public string Value => Id.ToString(CultureInfo.InvariantCulture); + public string Value => Id.ToInvariantString(); #endregion diff --git a/src/redmine-net-api/_net20/RedmineManagerAsyncObsolete.cs b/src/redmine-net-api/_net20/RedmineManagerAsyncObsolete.cs index 8344efba..ef03936f 100644 --- a/src/redmine-net-api/_net20/RedmineManagerAsyncObsolete.cs +++ b/src/redmine-net-api/_net20/RedmineManagerAsyncObsolete.cs @@ -306,8 +306,8 @@ public static Task> SearchAsync(this RedmineManager redmine var parameters = new NameValueCollection { {RedmineKeys.Q, q}, - {RedmineKeys.LIMIT, limit.ToString(CultureInfo.InvariantCulture)}, - {RedmineKeys.OFFSET, offset.ToString(CultureInfo.InvariantCulture)}, + {RedmineKeys.LIMIT, limit.ToInvariantString()}, + {RedmineKeys.OFFSET, offset.ToInvariantString()}, }; if (searchFilter != null) From d2fc658f1791146d68fc39c180df9a5db588a70b Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 14 May 2025 11:14:36 +0300 Subject: [PATCH 487/549] Add appsettings-local --- tests/redmine-net-api.Tests/TestHelper.cs | 3 ++- tests/redmine-net-api.Tests/appsettings-local.json | 5 +++++ tests/redmine-net-api.Tests/appsettings.json | 6 ------ tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj | 3 +++ 4 files changed, 10 insertions(+), 7 deletions(-) create mode 100644 tests/redmine-net-api.Tests/appsettings-local.json diff --git a/tests/redmine-net-api.Tests/TestHelper.cs b/tests/redmine-net-api.Tests/TestHelper.cs index 3e466bad..bd7b615a 100644 --- a/tests/redmine-net-api.Tests/TestHelper.cs +++ b/tests/redmine-net-api.Tests/TestHelper.cs @@ -13,6 +13,7 @@ private static IConfigurationRoot GetIConfigurationRoot(string outputPath) .SetBasePath(outputPath) .AddJsonFile("appsettings.json", optional: true) .AddJsonFile($"appsettings.{environment}.json", optional: true) + .AddJsonFile($"appsettings-local.json", optional: true) .AddUserSecrets("f8b9e946-b547-42f1-861c-f719dca00a84") .Build(); } @@ -29,7 +30,7 @@ public static RedmineCredentials GetApplicationConfiguration(string outputPath = var iConfig = GetIConfigurationRoot(outputPath); iConfig - .GetSection("Credentials-Local") + .GetSection("Credentials") .Bind(credentials); return credentials; diff --git a/tests/redmine-net-api.Tests/appsettings-local.json b/tests/redmine-net-api.Tests/appsettings-local.json new file mode 100644 index 00000000..07fadae6 --- /dev/null +++ b/tests/redmine-net-api.Tests/appsettings-local.json @@ -0,0 +1,5 @@ +{ + "Credentials": { + "ApiKey": "$ApiKey" + } +} diff --git a/tests/redmine-net-api.Tests/appsettings.json b/tests/redmine-net-api.Tests/appsettings.json index 75421bd0..9b28a4ca 100644 --- a/tests/redmine-net-api.Tests/appsettings.json +++ b/tests/redmine-net-api.Tests/appsettings.json @@ -4,11 +4,5 @@ "ApiKey": "$ApiKey", "Username": "$Username", "Password": "$Password" - }, - "Credentials-Local":{ - "Uri": "$Uri", - "ApiKey": "$ApiKey", - "Username": "$Username", - "Password": "$Password" } } diff --git a/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj index 25cbe1c5..adbfdce5 100644 --- a/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj +++ b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj @@ -79,6 +79,9 @@ PreserveNewest + + PreserveNewest + \ No newline at end of file From dfdf2be1d299c87678d1d8ee9d23af18c815568d Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 14 May 2025 11:20:30 +0300 Subject: [PATCH 488/549] Move to common folder --- src/redmine-net-api/{Types => Common}/IValue.cs | 0 src/redmine-net-api/{Types => Common}/PagedResults.cs | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/redmine-net-api/{Types => Common}/IValue.cs (100%) rename src/redmine-net-api/{Types => Common}/PagedResults.cs (100%) diff --git a/src/redmine-net-api/Types/IValue.cs b/src/redmine-net-api/Common/IValue.cs similarity index 100% rename from src/redmine-net-api/Types/IValue.cs rename to src/redmine-net-api/Common/IValue.cs diff --git a/src/redmine-net-api/Types/PagedResults.cs b/src/redmine-net-api/Common/PagedResults.cs similarity index 100% rename from src/redmine-net-api/Types/PagedResults.cs rename to src/redmine-net-api/Common/PagedResults.cs From 9b47ef75ccd2f84633cc7da444cfaf97fe287023 Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 14 May 2025 11:24:02 +0300 Subject: [PATCH 489/549] Cleanup --- src/redmine-net-api/Extensions/StringExtensions.cs | 10 +++++----- .../Net/WebClient/Extensions/WebClientExtensions.cs | 4 ++-- .../Net/WebClient/InternalRedmineApiWebClient.Async.cs | 1 + src/redmine-net-api/RedmineManagerAsync.cs | 2 ++ src/redmine-net-api/Types/Attachment.cs | 2 +- src/redmine-net-api/Types/Issue.cs | 7 ++++--- src/redmine-net-api/Types/WikiPage.cs | 1 - 7 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/redmine-net-api/Extensions/StringExtensions.cs b/src/redmine-net-api/Extensions/StringExtensions.cs index bdd34736..a881f14d 100644 --- a/src/redmine-net-api/Extensions/StringExtensions.cs +++ b/src/redmine-net-api/Extensions/StringExtensions.cs @@ -39,9 +39,9 @@ public static bool IsNullOrWhiteSpace(this string value) return true; } - for (var index = 0; index < value.Length; ++index) + foreach (var ch in value) { - if (!char.IsWhiteSpace(value[index])) + if (!char.IsWhiteSpace(ch)) { return false; } @@ -99,9 +99,9 @@ internal static SecureString ToSecureString(this string value) var rv = new SecureString(); - for (var index = 0; index < value.Length; ++index) + foreach (var ch in value) { - rv.AppendChar(value[index]); + rv.AppendChar(ch); } return rv; @@ -169,7 +169,7 @@ internal static string ToInvariantString(this T value) where T : struct TimeSpan ts => ts.ToString(), DateTime d => d.ToString(CultureInfo.InvariantCulture), #pragma warning disable CA1308 - bool b => b.ToString().ToLowerInvariant(), + bool b => b ? "true" : "false", #pragma warning restore CA1308 _ => value.ToString(), }; diff --git a/src/redmine-net-api/Net/WebClient/Extensions/WebClientExtensions.cs b/src/redmine-net-api/Net/WebClient/Extensions/WebClientExtensions.cs index ce89706f..83176d08 100644 --- a/src/redmine-net-api/Net/WebClient/Extensions/WebClientExtensions.cs +++ b/src/redmine-net-api/Net/WebClient/Extensions/WebClientExtensions.cs @@ -1,8 +1,8 @@ -using Redmine.Net.Api; using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Net; using Redmine.Net.Api.Serialization; +namespace Redmine.Net.Api.Net.WebClient.Extensions; + internal static class WebClientExtensions { public static void ApplyHeaders(this System.Net.WebClient client, RequestOptions options, IRedmineSerializer serializer) diff --git a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.Async.cs b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.Async.cs index f2db7185..226cea14 100644 --- a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.Async.cs +++ b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.Async.cs @@ -114,6 +114,7 @@ private async Task SendAsync(ApiRequestMessage requestMessag } responseHeaders = webClient.ResponseHeaders; + } catch (WebException ex) when (ex.Status == WebExceptionStatus.RequestCanceled) { diff --git a/src/redmine-net-api/RedmineManagerAsync.cs b/src/redmine-net-api/RedmineManagerAsync.cs index 3a3f5085..c2b9ef1f 100644 --- a/src/redmine-net-api/RedmineManagerAsync.cs +++ b/src/redmine-net-api/RedmineManagerAsync.cs @@ -24,7 +24,9 @@ limitations under the License. using Redmine.Net.Api.Net; using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Types; +#if!(NET45_OR_GREATER || NETCOREAPP) using TaskExtensions = Redmine.Net.Api.Extensions.TaskExtensions; +#endif namespace Redmine.Net.Api; diff --git a/src/redmine-net-api/Types/Attachment.cs b/src/redmine-net-api/Types/Attachment.cs index 98b937e6..24871731 100644 --- a/src/redmine-net-api/Types/Attachment.cs +++ b/src/redmine-net-api/Types/Attachment.cs @@ -122,8 +122,8 @@ public override void ReadXml(XmlReader reader) /// public override void WriteXml(XmlWriter writer) { - writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); writer.WriteElementString(RedmineKeys.FILE_NAME, FileName); + writer.WriteElementString(RedmineKeys.DESCRIPTION, Description); } #endregion diff --git a/src/redmine-net-api/Types/Issue.cs b/src/redmine-net-api/Types/Issue.cs index 884d3c7e..838cb1c8 100644 --- a/src/redmine-net-api/Types/Issue.cs +++ b/src/redmine-net-api/Types/Issue.cs @@ -645,11 +645,12 @@ public IdentifiableName AsParent() return IdentifiableName.Create(Id); } - /// - /// + /// Provides a string representation of the object for use in debugging. /// - /// + /// + /// A string that represents the object, formatted for debugging purposes. + /// private string DebuggerDisplay => $"[Issue:Id={Id.ToInvariantString()}, Status={Status?.Name}, Priority={Priority?.Name}, DoneRatio={DoneRatio?.ToString("F", CultureInfo.InvariantCulture)},IsPrivate={IsPrivate.ToInvariantString()}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Types/WikiPage.cs b/src/redmine-net-api/Types/WikiPage.cs index b2c0dfad..44d2a8a0 100644 --- a/src/redmine-net-api/Types/WikiPage.cs +++ b/src/redmine-net-api/Types/WikiPage.cs @@ -288,6 +288,5 @@ public override int GetHashCode() /// /// private string DebuggerDisplay => $"[WikiPage: Id={Id.ToInvariantString()}, Title={Title}]"; - } } \ No newline at end of file From a2b9ff203eb4d119b9155a2f2d2b1cdee128a2a4 Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 14 May 2025 11:24:55 +0300 Subject: [PATCH 490/549] Refactor --- .../Net/ApiResponseMessageExtensions.cs | 41 ++++++++----------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/src/redmine-net-api/Net/ApiResponseMessageExtensions.cs b/src/redmine-net-api/Net/ApiResponseMessageExtensions.cs index f039a451..40d630b8 100644 --- a/src/redmine-net-api/Net/ApiResponseMessageExtensions.cs +++ b/src/redmine-net-api/Net/ApiResponseMessageExtensions.cs @@ -16,6 +16,7 @@ limitations under the License. using System.Collections.Generic; using System.Text; +using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api.Net; @@ -24,37 +25,29 @@ internal static class ApiResponseMessageExtensions { internal static T DeserializeTo(this ApiResponseMessage responseMessage, IRedmineSerializer redmineSerializer) where T : new() { - if (responseMessage?.Content == null) - { - return default; - } - - var responseAsString = Encoding.UTF8.GetString(responseMessage.Content); - - return redmineSerializer.Deserialize(responseAsString); + var responseAsString = GetResponseContentAsString(responseMessage); + return responseAsString.IsNullOrWhiteSpace() ? default : redmineSerializer.Deserialize(responseAsString); } internal static PagedResults DeserializeToPagedResults(this ApiResponseMessage responseMessage, IRedmineSerializer redmineSerializer) where T : class, new() { - if (responseMessage?.Content == null) - { - return default; - } - - var responseAsString = Encoding.UTF8.GetString(responseMessage.Content); - - return redmineSerializer.DeserializeToPagedResults(responseAsString); + var responseAsString = GetResponseContentAsString(responseMessage); + return responseAsString.IsNullOrWhiteSpace() ? default : redmineSerializer.DeserializeToPagedResults(responseAsString); } internal static List DeserializeToList(this ApiResponseMessage responseMessage, IRedmineSerializer redmineSerializer) where T : class, new() { - if (responseMessage?.Content == null) - { - return default; - } - - var responseAsString = Encoding.UTF8.GetString(responseMessage.Content); - - return redmineSerializer.Deserialize>(responseAsString); + var responseAsString = GetResponseContentAsString(responseMessage); + return responseAsString.IsNullOrWhiteSpace() ? null : redmineSerializer.Deserialize>(responseAsString); + } + + /// + /// Gets the response content as a UTF-8 encoded string. + /// + /// The API response message. + /// The content as a string, or null if the response or content is null. + private static string GetResponseContentAsString(ApiResponseMessage responseMessage) + { + return responseMessage?.Content == null ? null : Encoding.UTF8.GetString(responseMessage.Content); } } \ No newline at end of file From f24547bec7d7ce47d8236ebf99bd541f35777f85 Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 14 May 2025 11:26:27 +0300 Subject: [PATCH 491/549] Fix clone error --- src/redmine-net-api/Net/RequestOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/redmine-net-api/Net/RequestOptions.cs b/src/redmine-net-api/Net/RequestOptions.cs index 8bffb391..7f3aa069 100644 --- a/src/redmine-net-api/Net/RequestOptions.cs +++ b/src/redmine-net-api/Net/RequestOptions.cs @@ -64,7 +64,7 @@ public RequestOptions Clone() ContentType = ContentType, Accept = Accept, UserAgent = UserAgent, - Headers = new Dictionary(Headers), + Headers = Headers != null ? new Dictionary(Headers) : null, }; } From 942556a285f1c1de6566af143d83442d46464ae2 Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 14 May 2025 11:41:12 +0300 Subject: [PATCH 492/549] Fix wiki create --- .../Extensions/RedmineManagerExtensions.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs index d7ff6113..0bf1b6a9 100644 --- a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs +++ b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs @@ -368,7 +368,7 @@ public static WikiPage CreateWikiPage(this RedmineManager redmineManager, string var escapedUri = Uri.EscapeDataString(uri); - var response = redmineManager.ApiClient.Create(escapedUri, payload, requestOptions); + var response = redmineManager.ApiClient.Update(escapedUri, payload, requestOptions); return response.DeserializeTo(redmineManager.Serializer); } @@ -733,16 +733,21 @@ public static async Task CreateWikiPageAsync(this RedmineManager redmi { var payload = redmineManager.Serializer.Serialize(wikiPage); + if (pageName.IsNullOrWhiteSpace()) + { + throw new RedmineException("Page name cannot be blank"); + } + if (string.IsNullOrEmpty(payload)) { throw new RedmineException("The payload is empty"); } - var url = redmineManager.RedmineApiUrls.ProjectWikiPageUpdate(projectId, pageName); + var path = redmineManager.RedmineApiUrls.ProjectWikiPageUpdate(projectId, Uri.EscapeDataString(pageName)); - var escapedUri = Uri.EscapeDataString(url); + //var escapedUri = Uri.EscapeDataString(url); - var response = await redmineManager.ApiClient.CreateAsync(escapedUri, payload,requestOptions, cancellationToken).ConfigureAwait(false); + var response = await redmineManager.ApiClient.UpdateAsync(path, payload, requestOptions, cancellationToken).ConfigureAwait(false); return response.DeserializeTo(redmineManager.Serializer); } From e5f3de5c14895136c81514b8c9947421acb3abc1 Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 14 May 2025 11:50:51 +0300 Subject: [PATCH 493/549] Fix Search --- .../Extensions/RedmineManagerExtensions.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs index 0bf1b6a9..67216c08 100644 --- a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs +++ b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs @@ -467,7 +467,11 @@ public static PagedResults Search(this RedmineManager redmineManager, st { var parameters = CreateSearchParameters(q, limit, offset, searchFilter); - var response = redmineManager.GetPaginated(new RequestOptions() {QueryString = parameters}); + var response = redmineManager.GetPaginated(new RequestOptions + { + QueryString = parameters, + ImpersonateUser = impersonateUserName + }); return response; } @@ -691,16 +695,21 @@ public static async Task> GetProjectFilesAsync(this RedmineMa /// /// /// - public static async Task> SearchAsync(this RedmineManager redmineManager, string q, int limit = RedmineManager.DEFAULT_PAGE_SIZE_VALUE, int offset = 0, SearchFilterBuilder searchFilter = null, CancellationToken cancellationToken = default) + public static async Task> SearchAsync(this RedmineManager redmineManager, + string q, + int limit = RedmineManager.DEFAULT_PAGE_SIZE_VALUE, + int offset = 0, + SearchFilterBuilder searchFilter = null, + CancellationToken cancellationToken = default) { var parameters = CreateSearchParameters(q, limit, offset, searchFilter); - var response = await redmineManager.ApiClient.GetPagedAsync(string.Empty, new RequestOptions() + var response = await redmineManager.GetPagedAsync(new RequestOptions() { QueryString = parameters }, cancellationToken).ConfigureAwait(false); - return response.DeserializeToPagedResults(redmineManager.Serializer); + return response; } /// From 347f1d826ad2e2727b98da8853aa74e53d5cd8c5 Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 14 May 2025 11:41:45 +0300 Subject: [PATCH 494/549] Add EnsureDeserializationInputIsNotNullOrWhiteSpace --- .../Serialization/SerializationHelper.cs | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/redmine-net-api/Serialization/SerializationHelper.cs b/src/redmine-net-api/Serialization/SerializationHelper.cs index a43624c1..c54ce852 100644 --- a/src/redmine-net-api/Serialization/SerializationHelper.cs +++ b/src/redmine-net-api/Serialization/SerializationHelper.cs @@ -14,7 +14,11 @@ You may obtain a copy of the License at limitations under the License. */ -using System.Globalization; +using System; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Xml; namespace Redmine.Net.Api.Serialization { @@ -35,9 +39,20 @@ internal static class SerializationHelper /// public static string SerializeUserId(int userId, IRedmineSerializer redmineSerializer) { - return redmineSerializer is XmlRedmineSerializer - ? $"{userId.ToString(CultureInfo.InvariantCulture)}" - : $"{{\"user_id\":\"{userId.ToString(CultureInfo.InvariantCulture)}\"}}"; + return redmineSerializer switch + { + XmlRedmineSerializer => $"{userId.ToInvariantString()}", + JsonRedmineSerializer => $"{{\"user_id\":\"{userId.ToInvariantString()}\"}}", + _ => throw new ArgumentOutOfRangeException(nameof(redmineSerializer), redmineSerializer, null) + }; + } + + public static void EnsureDeserializationInputIsNotNullOrWhiteSpace(string input, string paramName, Type type) + { + if (input.IsNullOrWhiteSpace()) + { + throw new RedmineSerializationException($"Could not deserialize null or empty input for type '{type.Name}'.", paramName); + } } } } \ No newline at end of file From 050996b99036e03c0a4691ebd2ba03ba93ef287d Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 14 May 2025 11:44:15 +0300 Subject: [PATCH 495/549] Throw RedmineSerializationException instead of ArgumentNullException --- .../Json/JsonRedmineSerializer.cs | 164 ++++++++---------- .../Serialization/Xml/XmlRedmineSerializer.cs | 98 +++++------ 2 files changed, 115 insertions(+), 147 deletions(-) diff --git a/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs b/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs index f0319cdd..9f84f385 100644 --- a/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs +++ b/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs @@ -22,116 +22,98 @@ limitations under the License. using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; -namespace Redmine.Net.Api.Serialization +namespace Redmine.Net.Api.Serialization.Json { internal sealed class JsonRedmineSerializer : IRedmineSerializer { - public T Deserialize(string jsonResponse) where T : new() + private static void EnsureJsonSerializable() { - if (jsonResponse.IsNullOrWhiteSpace()) - { - throw new ArgumentNullException(nameof(jsonResponse), $"Could not deserialize null or empty input for type '{typeof(T).Name}'."); - } - - var isJsonSerializable = typeof(IJsonSerializable).IsAssignableFrom(typeof(T)); - - if (!isJsonSerializable) + if (!typeof(IJsonSerializable).IsAssignableFrom(typeof(T))) { throw new RedmineException($"Entity of type '{typeof(T)}' should implement IJsonSerializable."); } + } - using (var stringReader = new StringReader(jsonResponse)) - { - using (var jsonReader = new JsonTextReader(stringReader)) - { - var obj = Activator.CreateInstance(); + public T Deserialize(string jsonResponse) where T : new() + { + SerializationHelper.EnsureDeserializationInputIsNotNullOrWhiteSpace(jsonResponse, nameof(jsonResponse), typeof(T)); - if (jsonReader.Read()) - { - if (jsonReader.Read()) - { - ((IJsonSerializable)obj).ReadJson(jsonReader); - } - } + EnsureJsonSerializable(); - return obj; - } + using var stringReader = new StringReader(jsonResponse); + using var jsonReader = new JsonTextReader(stringReader); + var obj = Activator.CreateInstance(); + + if (jsonReader.Read() && jsonReader.Read()) + { + ((IJsonSerializable)obj).ReadJson(jsonReader); } + + return obj; } public PagedResults DeserializeToPagedResults(string jsonResponse) where T : class, new() { - if (jsonResponse.IsNullOrWhiteSpace()) - { - throw new ArgumentNullException(nameof(jsonResponse), $"Could not deserialize null or empty input for type '{typeof(T).Name}'."); - } + SerializationHelper.EnsureDeserializationInputIsNotNullOrWhiteSpace(jsonResponse, nameof(jsonResponse), typeof(T)); - using (var sr = new StringReader(jsonResponse)) + using var sr = new StringReader(jsonResponse); + using var reader = new JsonTextReader(sr); + var total = 0; + var offset = 0; + var limit = 0; + List list = null; + + while (reader.Read()) { - using (var reader = new JsonTextReader(sr)) + if (reader.TokenType != JsonToken.PropertyName) + { + continue; + } + + switch (reader.Value) { - var total = 0; - var offset = 0; - var limit = 0; - List list = null; - - while (reader.Read()) - { - if (reader.TokenType != JsonToken.PropertyName) continue; - - switch (reader.Value) - { - case RedmineKeys.TOTAL_COUNT: - total = reader.ReadAsInt32().GetValueOrDefault(); - break; - case RedmineKeys.OFFSET: - offset = reader.ReadAsInt32().GetValueOrDefault(); - break; - case RedmineKeys.LIMIT: - limit = reader.ReadAsInt32().GetValueOrDefault(); - break; - default: - list = reader.ReadAsCollection(); - break; - } - } - - return new PagedResults(list, total, offset, limit); + case RedmineKeys.TOTAL_COUNT: + total = reader.ReadAsInt32().GetValueOrDefault(); + break; + case RedmineKeys.OFFSET: + offset = reader.ReadAsInt32().GetValueOrDefault(); + break; + case RedmineKeys.LIMIT: + limit = reader.ReadAsInt32().GetValueOrDefault(); + break; + default: + list = reader.ReadAsCollection(); + break; } } + + return new PagedResults(list, total, offset, limit); } #pragma warning disable CA1822 public int Count(string jsonResponse) where T : class, new() { - if (jsonResponse.IsNullOrWhiteSpace()) - { - throw new ArgumentNullException(nameof(jsonResponse), $"Could not deserialize null or empty input for type '{typeof(T).Name}'."); - } + SerializationHelper.EnsureDeserializationInputIsNotNullOrWhiteSpace(jsonResponse, nameof(jsonResponse), typeof(T)); + + using var sr = new StringReader(jsonResponse); + using var reader = new JsonTextReader(sr); + var total = 0; - using (var sr = new StringReader(jsonResponse)) + while (reader.Read()) { - using (var reader = new JsonTextReader(sr)) + if (reader.TokenType != JsonToken.PropertyName) { - var total = 0; - - while (reader.Read()) - { - if (reader.TokenType != JsonToken.PropertyName) - { - continue; - } - - if (reader.Value is RedmineKeys.TOTAL_COUNT) - { - total = reader.ReadAsInt32().GetValueOrDefault(); - return total; - } - } + continue; + } + if (reader.Value is RedmineKeys.TOTAL_COUNT) + { + total = reader.ReadAsInt32().GetValueOrDefault(); return total; } } + + return total; } #pragma warning restore CA1822 @@ -141,10 +123,12 @@ internal sealed class JsonRedmineSerializer : IRedmineSerializer public string Serialize(T entity) where T : class { - if (entity == default(T)) + if (entity == null) { - throw new ArgumentNullException(nameof(entity), $"Could not serialize null of type {typeof(T).Name}"); + throw new RedmineSerializationException($"Could not serialize null of type {typeof(T).Name}", nameof(entity)); } + + EnsureJsonSerializable(); if (entity is not IJsonSerializable jsonSerializable) { @@ -153,22 +137,18 @@ public string Serialize(T entity) where T : class var stringBuilder = new StringBuilder(); - using (var sw = new StringWriter(stringBuilder)) - { - using (var writer = new JsonTextWriter(sw)) - { - writer.Formatting = Formatting.Indented; - writer.DateFormatHandling = DateFormatHandling.IsoDateFormat; + using var sw = new StringWriter(stringBuilder); + using var writer = new JsonTextWriter(sw); + //writer.Formatting = Formatting.Indented; + writer.DateFormatHandling = DateFormatHandling.IsoDateFormat; - jsonSerializable.WriteJson(writer); + jsonSerializable.WriteJson(writer); - var json = stringBuilder.ToString(); + var json = stringBuilder.ToString(); - stringBuilder.Length = 0; + stringBuilder.Length = 0; - return json; - } - } + return json; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs b/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs index 84772404..720c3d25 100644 --- a/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs +++ b/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs @@ -22,11 +22,17 @@ limitations under the License. using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; -namespace Redmine.Net.Api.Serialization +namespace Redmine.Net.Api.Serialization.Xml { internal sealed class XmlRedmineSerializer : IRedmineSerializer { - + private static void EnsureXmlSerializable() + { + if (!typeof(IXmlSerializable).IsAssignableFrom(typeof(T))) + { + throw new RedmineException($"Entity of type '{typeof(T)}' should implement ${nameof(IXmlSerializable)}."); + } + } public XmlRedmineSerializer() : this(new XmlWriterSettings { OmitXmlDeclaration = true @@ -103,39 +109,32 @@ public string Serialize(T entity) where T : class /// private static PagedResults XmlDeserializeList(string xmlResponse, bool onlyCount) where T : class, new() { - if (xmlResponse.IsNullOrWhiteSpace()) + SerializationHelper.EnsureDeserializationInputIsNotNullOrWhiteSpace(xmlResponse, nameof(xmlResponse), typeof(T)); + + using var stringReader = new StringReader(xmlResponse); + using var xmlReader = XmlTextReaderBuilder.Create(stringReader); + while (xmlReader.NodeType == XmlNodeType.None || xmlReader.NodeType == XmlNodeType.XmlDeclaration) { - throw new ArgumentNullException(nameof(xmlResponse), $"Could not deserialize null or empty input for type '{typeof(T).Name}'."); + xmlReader.Read(); } - using (var stringReader = new StringReader(xmlResponse)) - { - using (var xmlReader = XmlTextReaderBuilder.Create(stringReader)) - { - while (xmlReader.NodeType == XmlNodeType.None || xmlReader.NodeType == XmlNodeType.XmlDeclaration) - { - xmlReader.Read(); - } - - var totalItems = xmlReader.ReadAttributeAsInt(RedmineKeys.TOTAL_COUNT); + var totalItems = xmlReader.ReadAttributeAsInt(RedmineKeys.TOTAL_COUNT); - if (onlyCount) - { - return new PagedResults(null, totalItems, 0, 0); - } + if (onlyCount) + { + return new PagedResults(null, totalItems, 0, 0); + } - var offset = xmlReader.ReadAttributeAsInt(RedmineKeys.OFFSET); - var limit = xmlReader.ReadAttributeAsInt(RedmineKeys.LIMIT); - var result = xmlReader.ReadElementContentAsCollection(); - - if (totalItems == 0 && result?.Count > 0) - { - totalItems = result.Count; - } + var offset = xmlReader.ReadAttributeAsInt(RedmineKeys.OFFSET); + var limit = xmlReader.ReadAttributeAsInt(RedmineKeys.LIMIT); + var result = xmlReader.ReadElementContentAsCollection(); - return new PagedResults(result, totalItems, offset, limit); - } + if (totalItems == 0 && result?.Count > 0) + { + totalItems = result.Count; } + + return new PagedResults(result, totalItems, offset, limit); } /// @@ -150,22 +149,18 @@ public string Serialize(T entity) where T : class // ReSharper disable once InconsistentNaming private string ToXML(T entity) where T : class { - if (entity == default(T)) + if (entity == null) { throw new ArgumentNullException(nameof(entity), $"Could not serialize null of type {typeof(T).Name}"); } - using (var stringWriter = new StringWriter()) - { - using (var xmlWriter = XmlWriter.Create(stringWriter, _xmlWriterSettings)) - { - var serializer = new XmlSerializer(typeof(T)); + using var stringWriter = new StringWriter(); + using var xmlWriter = XmlWriter.Create(stringWriter, _xmlWriterSettings); + var serializer = new XmlSerializer(typeof(T)); - serializer.Serialize(xmlWriter, entity); + serializer.Serialize(xmlWriter, entity); - return stringWriter.ToString(); - } - } + return stringWriter.ToString(); } /// @@ -183,27 +178,20 @@ private string ToXML(T entity) where T : class // ReSharper disable once InconsistentNaming private static TOut XmlDeserializeEntity(string xml) where TOut : new() { - if (xml.IsNullOrWhiteSpace()) - { - throw new ArgumentNullException(nameof(xml), $"Could not deserialize null or empty input for type '{typeof(TOut).Name}'."); - } - - using (var textReader = new StringReader(xml)) - { - using (var xmlReader = XmlTextReaderBuilder.Create(textReader)) - { - var serializer = new XmlSerializer(typeof(TOut)); + SerializationHelper.EnsureDeserializationInputIsNotNullOrWhiteSpace(xml, nameof(xml), typeof(TOut)); - var entity = serializer.Deserialize(xmlReader); + using var textReader = new StringReader(xml); + using var xmlReader = XmlTextReaderBuilder.Create(textReader); + var serializer = new XmlSerializer(typeof(TOut)); - if (entity is TOut t) - { - return t; - } + var entity = serializer.Deserialize(xmlReader); - return default; - } + if (entity is TOut t) + { + return t; } + + return default; } } } \ No newline at end of file From f3ccbcefc835a05834d9f99bbce003a3ff453785 Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 14 May 2025 15:38:03 +0300 Subject: [PATCH 496/549] Add StatusCode to ApiResponseMessage --- src/redmine-net-api/Net/ApiResponseMessage.cs | 4 ++++ .../InternalRedmineApiWebClient.Async.cs | 9 +++++-- .../WebClient/InternalRedmineApiWebClient.cs | 8 ++++++- .../Net/WebClient/InternalWebClient.cs | 24 +++++++++++++++++++ 4 files changed, 42 insertions(+), 3 deletions(-) diff --git a/src/redmine-net-api/Net/ApiResponseMessage.cs b/src/redmine-net-api/Net/ApiResponseMessage.cs index 971aaabb..3a72058c 100644 --- a/src/redmine-net-api/Net/ApiResponseMessage.cs +++ b/src/redmine-net-api/Net/ApiResponseMessage.cs @@ -15,6 +15,7 @@ limitations under the License. */ using System.Collections.Specialized; +using System.Net; namespace Redmine.Net.Api.Net; @@ -22,4 +23,7 @@ internal sealed class ApiResponseMessage { public NameValueCollection Headers { get; init; } public byte[] Content { get; init; } + + public HttpStatusCode StatusCode { get; init; } + } \ No newline at end of file diff --git a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.Async.cs b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.Async.cs index 226cea14..10ff9b95 100644 --- a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.Async.cs +++ b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.Async.cs @@ -82,6 +82,7 @@ private async Task SendAsync(ApiRequestMessage requestMessag { System.Net.WebClient webClient = null; byte[] response = null; + HttpStatusCode? statusCode = null; NameValueCollection responseHeaders = null; try { @@ -114,7 +115,10 @@ private async Task SendAsync(ApiRequestMessage requestMessag } responseHeaders = webClient.ResponseHeaders; - + if (webClient is InternalWebClient iwc) + { + statusCode = iwc.StatusCode; + } } catch (WebException ex) when (ex.Status == WebExceptionStatus.RequestCanceled) { @@ -132,7 +136,8 @@ private async Task SendAsync(ApiRequestMessage requestMessag return new ApiResponseMessage() { Headers = responseHeaders, - Content = response + Content = response, + StatusCode = statusCode ?? HttpStatusCode.OK, }; } } diff --git a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs index 1b3ee390..99ddbd3e 100644 --- a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs +++ b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs @@ -170,6 +170,7 @@ private ApiResponseMessage Send(ApiRequestMessage requestMessage) { System.Net.WebClient webClient = null; byte[] response = null; + HttpStatusCode? statusCode = null; NameValueCollection responseHeaders = null; try @@ -198,6 +199,10 @@ private ApiResponseMessage Send(ApiRequestMessage requestMessage) } responseHeaders = webClient.ResponseHeaders; + if (webClient is InternalWebClient iwc) + { + statusCode = iwc.StatusCode; + } } catch (WebException webException) { @@ -211,7 +216,8 @@ private ApiResponseMessage Send(ApiRequestMessage requestMessage) return new ApiResponseMessage() { Headers = responseHeaders, - Content = response + Content = response, + StatusCode = statusCode ?? HttpStatusCode.OK, }; } diff --git a/src/redmine-net-api/Net/WebClient/InternalWebClient.cs b/src/redmine-net-api/Net/WebClient/InternalWebClient.cs index 2bec6d92..913feb7e 100644 --- a/src/redmine-net-api/Net/WebClient/InternalWebClient.cs +++ b/src/redmine-net-api/Net/WebClient/InternalWebClient.cs @@ -101,6 +101,30 @@ protected override WebRequest GetWebRequest(Uri address) throw new RedmineException(webException.GetBaseException().Message, webException); } } + + public HttpStatusCode StatusCode { get; private set; } + + protected override WebResponse GetWebResponse(WebRequest request) + { + var response = base.GetWebResponse(request); + if (response is HttpWebResponse httpResponse) + { + StatusCode = httpResponse.StatusCode; + } + return response; + } + + protected override WebResponse GetWebResponse(WebRequest request, IAsyncResult result) + { + var response = base.GetWebResponse(request, result); + + if (response is HttpWebResponse httpResponse) + { + StatusCode = httpResponse.StatusCode; + } + + return response; + } private static void AssignIfHasValue(T? nullableValue, Action assignAction) where T : struct { From ef16f51195052aa5e943b763404eaf7ae522a36f Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 14 May 2025 15:41:00 +0300 Subject: [PATCH 497/549] Add RedmineAuthenticationType enum --- .../Authentication/RedmineApiKeyAuthentication.cs | 3 ++- .../Authentication/RedmineAuthenticationType.cs | 8 ++++++++ .../Authentication/RedmineBasicAuthentication.cs | 5 +++-- .../Authentication/RedmineNoAuthentication.cs | 3 ++- src/redmine-net-api/Extensions/EnumExtensions.cs | 13 +++++++++++++ .../Net/WebClient/InternalRedmineApiWebClient.cs | 4 ++-- src/redmine-net-api/RedmineConstants.cs | 4 ++++ 7 files changed, 34 insertions(+), 6 deletions(-) create mode 100644 src/redmine-net-api/Authentication/RedmineAuthenticationType.cs diff --git a/src/redmine-net-api/Authentication/RedmineApiKeyAuthentication.cs b/src/redmine-net-api/Authentication/RedmineApiKeyAuthentication.cs index a037a60c..c1d59744 100644 --- a/src/redmine-net-api/Authentication/RedmineApiKeyAuthentication.cs +++ b/src/redmine-net-api/Authentication/RedmineApiKeyAuthentication.cs @@ -15,6 +15,7 @@ limitations under the License. */ using System.Net; +using Redmine.Net.Api.Extensions; namespace Redmine.Net.Api.Authentication; @@ -24,7 +25,7 @@ namespace Redmine.Net.Api.Authentication; public sealed class RedmineApiKeyAuthentication: IRedmineAuthentication { /// - public string AuthenticationType => "X-Redmine-API-Key"; + public string AuthenticationType { get; } = RedmineAuthenticationType.ApiKey.ToText(); /// public string Token { get; init; } diff --git a/src/redmine-net-api/Authentication/RedmineAuthenticationType.cs b/src/redmine-net-api/Authentication/RedmineAuthenticationType.cs new file mode 100644 index 00000000..22c38cd2 --- /dev/null +++ b/src/redmine-net-api/Authentication/RedmineAuthenticationType.cs @@ -0,0 +1,8 @@ +namespace Redmine.Net.Api.Authentication; + +internal enum RedmineAuthenticationType +{ + NoAuthentication, + Basic, + ApiKey +} \ No newline at end of file diff --git a/src/redmine-net-api/Authentication/RedmineBasicAuthentication.cs b/src/redmine-net-api/Authentication/RedmineBasicAuthentication.cs index e78aa653..810da00a 100644 --- a/src/redmine-net-api/Authentication/RedmineBasicAuthentication.cs +++ b/src/redmine-net-api/Authentication/RedmineBasicAuthentication.cs @@ -18,6 +18,7 @@ limitations under the License. using System.Net; using System.Text; using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; namespace Redmine.Net.Api.Authentication { @@ -27,7 +28,7 @@ namespace Redmine.Net.Api.Authentication public sealed class RedmineBasicAuthentication: IRedmineAuthentication { /// - public string AuthenticationType => "Basic"; + public string AuthenticationType { get; } = RedmineAuthenticationType.Basic.ToText(); /// public string Token { get; init; } @@ -45,7 +46,7 @@ public RedmineBasicAuthentication(string username, string password) if (username == null) throw new RedmineException(nameof(username)); if (password == null) throw new RedmineException(nameof(password)); - Token = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}")); + Token = $"Basic {Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}"))}"; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Authentication/RedmineNoAuthentication.cs b/src/redmine-net-api/Authentication/RedmineNoAuthentication.cs index 4f2ed673..d8518828 100644 --- a/src/redmine-net-api/Authentication/RedmineNoAuthentication.cs +++ b/src/redmine-net-api/Authentication/RedmineNoAuthentication.cs @@ -15,6 +15,7 @@ limitations under the License. */ using System.Net; +using Redmine.Net.Api.Extensions; namespace Redmine.Net.Api.Authentication; @@ -24,7 +25,7 @@ namespace Redmine.Net.Api.Authentication; public sealed class RedmineNoAuthentication: IRedmineAuthentication { /// - public string AuthenticationType => "NoAuth"; + public string AuthenticationType { get; } = RedmineAuthenticationType.NoAuthentication.ToText(); /// public string Token { get; init; } diff --git a/src/redmine-net-api/Extensions/EnumExtensions.cs b/src/redmine-net-api/Extensions/EnumExtensions.cs index 5ae7d77f..60f49075 100644 --- a/src/redmine-net-api/Extensions/EnumExtensions.cs +++ b/src/redmine-net-api/Extensions/EnumExtensions.cs @@ -1,3 +1,5 @@ +using System; +using Redmine.Net.Api.Authentication; using Redmine.Net.Api.Types; namespace Redmine.Net.Api.Extensions; @@ -98,4 +100,15 @@ public static string ToLowerInvariant(this UserStatus @enum) _ => "undefined" }; } + + internal static string ToText(this RedmineAuthenticationType @enum) + { + return @enum switch + { + RedmineAuthenticationType.NoAuthentication => "NoAuth", + RedmineAuthenticationType.Basic => "Basic", + RedmineAuthenticationType.ApiKey => "ApiKey", + _ => "undefined" + }; + } } \ No newline at end of file diff --git a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs index 99ddbd3e..93f7abfd 100644 --- a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs +++ b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs @@ -231,10 +231,10 @@ private void SetWebClientHeaders(System.Net.WebClient webClient, ApiRequestMessa switch (_credentials) { case RedmineApiKeyAuthentication: - webClient.Headers.Add(_credentials.AuthenticationType,_credentials.Token); + webClient.Headers.Add(RedmineConstants.API_KEY_AUTHORIZATION_HEADER_KEY,_credentials.Token); break; case RedmineBasicAuthentication: - webClient.Headers.Add("Authorization", $"{_credentials.AuthenticationType} {_credentials.Token}"); + webClient.Headers.Add(RedmineConstants.AUTHORIZATION_HEADER_KEY, _credentials.Token); break; } diff --git a/src/redmine-net-api/RedmineConstants.cs b/src/redmine-net-api/RedmineConstants.cs index d2b963eb..106ba466 100644 --- a/src/redmine-net-api/RedmineConstants.cs +++ b/src/redmine-net-api/RedmineConstants.cs @@ -52,6 +52,10 @@ public static class RedmineConstants /// /// public const string AUTHORIZATION_HEADER_KEY = "Authorization"; + /// + /// + /// + public const string API_KEY_AUTHORIZATION_HEADER_KEY = "X-Redmine-API-Key"; /// /// From 8da41ca1727182410d9e6f79598529160665fbf4 Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 14 May 2025 15:45:32 +0300 Subject: [PATCH 498/549] Dispose register cancellation token --- .../InternalRedmineApiWebClient.Async.cs | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.Async.cs b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.Async.cs index 10ff9b95..18f81f00 100644 --- a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.Async.cs +++ b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.Async.cs @@ -84,12 +84,19 @@ private async Task SendAsync(ApiRequestMessage requestMessag byte[] response = null; HttpStatusCode? statusCode = null; NameValueCollection responseHeaders = null; + CancellationTokenRegistration cancellationTokenRegistration = default; + try { webClient = _webClientFunc(); - - cancellationToken.Register(webClient.CancelAsync); + cancellationTokenRegistration = + cancellationToken.Register( + static state => ((System.Net.WebClient)state!).CancelAsync(), + webClient + ); + cancellationToken.ThrowIfCancellationRequested(); + SetWebClientHeaders(webClient, requestMessage); if(IsGetOrDownload(requestMessage.Method)) @@ -122,7 +129,10 @@ private async Task SendAsync(ApiRequestMessage requestMessag } catch (WebException ex) when (ex.Status == WebExceptionStatus.RequestCanceled) { - //TODO: Handle cancellation... + if (cancellationToken.IsCancellationRequested) + { + throw new RedmineApiException("The operation was canceled by the user.", ex); + } } catch (WebException webException) { @@ -130,6 +140,12 @@ private async Task SendAsync(ApiRequestMessage requestMessag } finally { + #if NETFRAMEWORK + cancellationTokenRegistration.Dispose(); + #else + await cancellationTokenRegistration.DisposeAsync().ConfigureAwait(false); + #endif + webClient?.Dispose(); } From 7319a2e516fa7a93078a56408f24180d1d6d50cd Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 14 May 2025 15:47:00 +0300 Subject: [PATCH 499/549] Add async download file progress --- .../Net/IAsyncRedmineApiClient.cs | 3 +- .../Net/ISyncRedmineApiClient.cs | 4 +- .../InternalRedmineApiWebClient.Async.cs | 18 +++++-- .../WebClient/InternalRedmineApiWebClient.cs | 17 ++++-- src/redmine-net-api/_net20/IProgress{T}.cs | 54 +++++++++++++++++++ 5 files changed, 86 insertions(+), 10 deletions(-) create mode 100644 src/redmine-net-api/_net20/IProgress{T}.cs diff --git a/src/redmine-net-api/Net/IAsyncRedmineApiClient.cs b/src/redmine-net-api/Net/IAsyncRedmineApiClient.cs index b4b7bc50..0400084b 100644 --- a/src/redmine-net-api/Net/IAsyncRedmineApiClient.cs +++ b/src/redmine-net-api/Net/IAsyncRedmineApiClient.cs @@ -15,6 +15,7 @@ limitations under the License. */ #if !(NET20 || NET35) +using System; using System.Threading; using System.Threading.Tasks; @@ -36,6 +37,6 @@ internal interface IAsyncRedmineApiClient Task UploadFileAsync(string address, byte[] data, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); - Task DownloadAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + Task DownloadAsync(string address, RequestOptions requestOptions = null, IProgress progress = null, CancellationToken cancellationToken = default); } #endif \ No newline at end of file diff --git a/src/redmine-net-api/Net/ISyncRedmineApiClient.cs b/src/redmine-net-api/Net/ISyncRedmineApiClient.cs index 8141ae0e..a657feac 100644 --- a/src/redmine-net-api/Net/ISyncRedmineApiClient.cs +++ b/src/redmine-net-api/Net/ISyncRedmineApiClient.cs @@ -14,6 +14,8 @@ You may obtain a copy of the License at limitations under the License. */ +using System; + namespace Redmine.Net.Api.Net; internal interface ISyncRedmineApiClient @@ -32,5 +34,5 @@ internal interface ISyncRedmineApiClient ApiResponseMessage Upload(string address, byte[] data, RequestOptions requestOptions = null); - ApiResponseMessage Download(string address, RequestOptions requestOptions = null); + ApiResponseMessage Download(string address, RequestOptions requestOptions = null, IProgress progress = null); } \ No newline at end of file diff --git a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.Async.cs b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.Async.cs index 18f81f00..e21ab209 100644 --- a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.Async.cs +++ b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.Async.cs @@ -15,10 +15,12 @@ limitations under the License. */ #if!(NET20) +using System; using System.Collections.Specialized; using System.Net; using System.Threading; using System.Threading.Tasks; +using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Net.WebClient.MessageContent; @@ -68,17 +70,17 @@ public async Task DeleteAsync(string address, RequestOptions return await HandleRequestAsync(address, HttpVerbs.DELETE, requestOptions, cancellationToken:cancellationToken).ConfigureAwait(false); } - public async Task DownloadAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + public async Task DownloadAsync(string address, RequestOptions requestOptions = null, IProgress progress = null, CancellationToken cancellationToken = default) { return await HandleRequestAsync(address, HttpVerbs.DOWNLOAD, requestOptions, cancellationToken:cancellationToken).ConfigureAwait(false); } - private async Task HandleRequestAsync(string address, string verb, RequestOptions requestOptions = null, ApiRequestMessageContent content = null, CancellationToken cancellationToken = default) + private async Task HandleRequestAsync(string address, string verb, RequestOptions requestOptions = null, ApiRequestMessageContent content = null, IProgress progress = null, CancellationToken cancellationToken = default) { - return await SendAsync(CreateRequestMessage(address, verb, requestOptions, content), cancellationToken).ConfigureAwait(false); + return await SendAsync(CreateRequestMessage(address, verb, requestOptions, content), progress, cancellationToken: cancellationToken).ConfigureAwait(false); } - private async Task SendAsync(ApiRequestMessage requestMessage, CancellationToken cancellationToken) + private async Task SendAsync(ApiRequestMessage requestMessage, IProgress progress = null, CancellationToken cancellationToken = default) { System.Net.WebClient webClient = null; byte[] response = null; @@ -97,6 +99,14 @@ private async Task SendAsync(ApiRequestMessage requestMessag cancellationToken.ThrowIfCancellationRequested(); + if (progress != null) + { + webClient.DownloadProgressChanged += (_, e) => + { + progress.Report(e.ProgressPercentage); + }; + } + SetWebClientHeaders(webClient, requestMessage); if(IsGetOrDownload(requestMessage.Method)) diff --git a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs index 93f7abfd..6988c5a2 100644 --- a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs +++ b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs @@ -128,7 +128,7 @@ public ApiResponseMessage Delete(string address, RequestOptions requestOptions = return HandleRequest(address, HttpVerbs.DELETE, requestOptions); } - public ApiResponseMessage Download(string address, RequestOptions requestOptions = null) + public ApiResponseMessage Download(string address, RequestOptions requestOptions = null, IProgress progress = null) { return HandleRequest(address, HttpVerbs.DOWNLOAD, requestOptions); } @@ -161,12 +161,12 @@ private static ApiRequestMessage CreateRequestMessage(string address, string ver return req; } - private ApiResponseMessage HandleRequest(string address, string verb, RequestOptions requestOptions = null, ApiRequestMessageContent content = null) + private ApiResponseMessage HandleRequest(string address, string verb, RequestOptions requestOptions = null, ApiRequestMessageContent content = null, IProgress progress = null) { - return Send(CreateRequestMessage(address, verb, requestOptions, content)); + return Send(CreateRequestMessage(address, verb, requestOptions, content), progress); } - private ApiResponseMessage Send(ApiRequestMessage requestMessage) + private ApiResponseMessage Send(ApiRequestMessage requestMessage, IProgress progress = null) { System.Net.WebClient webClient = null; byte[] response = null; @@ -176,6 +176,15 @@ private ApiResponseMessage Send(ApiRequestMessage requestMessage) try { webClient = _webClientFunc(); + + if (progress != null) + { + webClient.DownloadProgressChanged += (_, e) => + { + progress.Report(e.ProgressPercentage); + }; + } + SetWebClientHeaders(webClient, requestMessage); if (IsGetOrDownload(requestMessage.Method)) diff --git a/src/redmine-net-api/_net20/IProgress{T}.cs b/src/redmine-net-api/_net20/IProgress{T}.cs new file mode 100644 index 00000000..add86bde --- /dev/null +++ b/src/redmine-net-api/_net20/IProgress{T}.cs @@ -0,0 +1,54 @@ +#if NET20 +/* + Copyright 2011 - 2025 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +namespace System; + +/// Defines a provider for progress updates. +/// The type of progress update value. +public interface IProgress +{ + /// Reports a progress update. + /// The value of the updated progress. + void Report(T value); +} + +/// +/// +/// +/// +public sealed class Progress : IProgress +{ + private readonly Action _handler; + + /// + /// + /// + /// + public Progress(Action handler) + { + _handler = handler; + } + + /// + /// + /// + /// + public void Report(T value) + { + _handler(value); + } +} +#endif \ No newline at end of file From 1cad44b9dcb9a14b810a7670fb14dd9e2971a87f Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 14 May 2025 15:47:20 +0300 Subject: [PATCH 500/549] Suppress warning --- src/redmine-net-api/Types/Include.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/redmine-net-api/Types/Include.cs b/src/redmine-net-api/Types/Include.cs index ef4fa09a..1c8b2d9c 100644 --- a/src/redmine-net-api/Types/Include.cs +++ b/src/redmine-net-api/Types/Include.cs @@ -3,6 +3,11 @@ namespace Redmine.Net.Api.Types; /// /// /// + [System.Diagnostics.CodeAnalysis.SuppressMessage( +"Design", +"CA1034:Nested types should not be visible", +Justification = "Deliberately exposed")] + public static class Include { /// From 68122d2be47cb09bf0a9d40db4d6c266764cd294 Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 14 May 2025 15:47:41 +0300 Subject: [PATCH 501/549] Enable nullale --- src/redmine-net-api/Internals/ArgumentNullThrowHelper.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/redmine-net-api/Internals/ArgumentNullThrowHelper.cs b/src/redmine-net-api/Internals/ArgumentNullThrowHelper.cs index 86f45107..e7daa84e 100644 --- a/src/redmine-net-api/Internals/ArgumentNullThrowHelper.cs +++ b/src/redmine-net-api/Internals/ArgumentNullThrowHelper.cs @@ -2,6 +2,8 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; +#nullable enable + namespace Redmine.Net.Api.Internals; internal static class ArgumentNullThrowHelper From 6045b4c759504d99592fcb5e35f9261da5280d4c Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 14 May 2025 15:54:34 +0300 Subject: [PATCH 502/549] ApiUrl --- src/redmine-net-api/Net/RedmineApiUrls.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/redmine-net-api/Net/RedmineApiUrls.cs b/src/redmine-net-api/Net/RedmineApiUrls.cs index c50d0fc7..704a4e48 100644 --- a/src/redmine-net-api/Net/RedmineApiUrls.cs +++ b/src/redmine-net-api/Net/RedmineApiUrls.cs @@ -151,7 +151,7 @@ internal string CreateEntityFragment(Type type, string ownerId = null) if (type == typeof(Upload)) { - return $"{RedmineKeys.UPLOADS}.{Format}"; + return UploadFragment(ownerId); //$"{RedmineKeys.UPLOADS}.{Format}"; } if (type == typeof(Attachment) || type == typeof(Attachments)) From 2dfd0d8c79de25b66728b8eae0824ae62ad01392 Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 14 May 2025 16:53:14 +0300 Subject: [PATCH 503/549] [WebClientExtensions] Fix namespace --- src/redmine-net-api/Net/WebClient/Extensions/WebExtensions.cs | 3 ++- ...dmineWebClientObsolete.cs => IRedmineWebClient.Obsolete.cs} | 0 .../Net/WebClient/InternalRedmineApiWebClient.Async.cs | 1 + .../Net/WebClient/InternalRedmineApiWebClient.cs | 1 + src/redmine-net-api/Net/WebClient/RedmineWebClient.Obsolete.cs | 1 + 5 files changed, 5 insertions(+), 1 deletion(-) rename src/redmine-net-api/Net/WebClient/{IRedmineWebClientObsolete.cs => IRedmineWebClient.Obsolete.cs} (100%) diff --git a/src/redmine-net-api/Net/WebClient/Extensions/WebExtensions.cs b/src/redmine-net-api/Net/WebClient/Extensions/WebExtensions.cs index 3a3b1a2a..b5fd04d3 100644 --- a/src/redmine-net-api/Net/WebClient/Extensions/WebExtensions.cs +++ b/src/redmine-net-api/Net/WebClient/Extensions/WebExtensions.cs @@ -20,10 +20,11 @@ limitations under the License. using System.Net; using System.Text; using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Types; -namespace Redmine.Net.Api.Extensions +namespace Redmine.Net.Api.Net.WebClient.Extensions { /// /// diff --git a/src/redmine-net-api/Net/WebClient/IRedmineWebClientObsolete.cs b/src/redmine-net-api/Net/WebClient/IRedmineWebClient.Obsolete.cs similarity index 100% rename from src/redmine-net-api/Net/WebClient/IRedmineWebClientObsolete.cs rename to src/redmine-net-api/Net/WebClient/IRedmineWebClient.Obsolete.cs diff --git a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.Async.cs b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.Async.cs index e21ab209..bcf45e4f 100644 --- a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.Async.cs +++ b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.Async.cs @@ -22,6 +22,7 @@ limitations under the License. using System.Threading.Tasks; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Net.WebClient.Extensions; using Redmine.Net.Api.Net.WebClient.MessageContent; namespace Redmine.Net.Api.Net.WebClient diff --git a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs index 6988c5a2..4c68f026 100644 --- a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs +++ b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs @@ -20,6 +20,7 @@ limitations under the License. using System.Text; using Redmine.Net.Api.Authentication; using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Net.WebClient.Extensions; using Redmine.Net.Api.Net.WebClient.MessageContent; using Redmine.Net.Api.Serialization; diff --git a/src/redmine-net-api/Net/WebClient/RedmineWebClient.Obsolete.cs b/src/redmine-net-api/Net/WebClient/RedmineWebClient.Obsolete.cs index 0276931c..90ab613e 100644 --- a/src/redmine-net-api/Net/WebClient/RedmineWebClient.Obsolete.cs +++ b/src/redmine-net-api/Net/WebClient/RedmineWebClient.Obsolete.cs @@ -17,6 +17,7 @@ limitations under the License. using System; using System.Net; using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Net.WebClient.Extensions; using Redmine.Net.Api.Serialization; namespace Redmine.Net.Api From 4d4bfc63bd1a259ae480436458de63a5973dc8c5 Mon Sep 17 00:00:00 2001 From: Padi Date: Thu, 15 May 2025 00:09:16 +0300 Subject: [PATCH 504/549] Rearrange --- src/redmine-net-api/Extensions/RedmineManagerExtensions.cs | 1 + src/redmine-net-api/Net/{ => Internal}/ApiRequestMessage.cs | 2 +- .../Net/{ => Internal}/ApiRequestMessageContent.cs | 2 +- src/redmine-net-api/Net/{ => Internal}/ApiResponseMessage.cs | 2 +- .../Net/{ => Internal}/ApiResponseMessageExtensions.cs | 2 +- .../Net/{ => Internal}/IAsyncRedmineApiClient.cs | 2 +- src/redmine-net-api/Net/{ => Internal}/IRedmineApiClient.cs | 2 +- src/redmine-net-api/Net/{ => Internal}/ISyncRedmineApiClient.cs | 2 +- src/redmine-net-api/Net/{ => Internal}/RedmineApiUrls.cs | 2 +- .../Net/{ => Internal}/RedmineApiUrlsExtensions.cs | 2 +- .../Net/WebClient/InternalRedmineApiWebClient.Async.cs | 1 + .../Net/WebClient/InternalRedmineApiWebClient.cs | 1 + .../MessageContent/ByteArrayApiRequestMessageContent.cs | 2 ++ src/redmine-net-api/RedmineManager.cs | 1 + src/redmine-net-api/RedmineManagerAsync.cs | 1 + .../Infrastructure/Fixtures/RedmineApiUrlsFixture.cs | 2 +- tests/redmine-net-api.Tests/Tests/RedmineApiUrlsTests.cs | 1 + 17 files changed, 18 insertions(+), 10 deletions(-) rename src/redmine-net-api/Net/{ => Internal}/ApiRequestMessage.cs (96%) rename src/redmine-net-api/Net/{ => Internal}/ApiRequestMessageContent.cs (94%) rename src/redmine-net-api/Net/{ => Internal}/ApiResponseMessage.cs (95%) rename src/redmine-net-api/Net/{ => Internal}/ApiResponseMessageExtensions.cs (98%) rename src/redmine-net-api/Net/{ => Internal}/IAsyncRedmineApiClient.cs (98%) rename src/redmine-net-api/Net/{ => Internal}/IRedmineApiClient.cs (94%) rename src/redmine-net-api/Net/{ => Internal}/ISyncRedmineApiClient.cs (97%) rename src/redmine-net-api/Net/{ => Internal}/RedmineApiUrls.cs (99%) rename src/redmine-net-api/Net/{ => Internal}/RedmineApiUrlsExtensions.cs (99%) diff --git a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs index 67216c08..4f8cc441 100644 --- a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs +++ b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs @@ -24,6 +24,7 @@ limitations under the License. #endif using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Net; +using Redmine.Net.Api.Net.Internal; using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Types; diff --git a/src/redmine-net-api/Net/ApiRequestMessage.cs b/src/redmine-net-api/Net/Internal/ApiRequestMessage.cs similarity index 96% rename from src/redmine-net-api/Net/ApiRequestMessage.cs rename to src/redmine-net-api/Net/Internal/ApiRequestMessage.cs index b0b7a2fb..bbd31924 100644 --- a/src/redmine-net-api/Net/ApiRequestMessage.cs +++ b/src/redmine-net-api/Net/Internal/ApiRequestMessage.cs @@ -16,7 +16,7 @@ limitations under the License. using System.Collections.Specialized; -namespace Redmine.Net.Api.Net; +namespace Redmine.Net.Api.Net.Internal; internal sealed class ApiRequestMessage { diff --git a/src/redmine-net-api/Net/ApiRequestMessageContent.cs b/src/redmine-net-api/Net/Internal/ApiRequestMessageContent.cs similarity index 94% rename from src/redmine-net-api/Net/ApiRequestMessageContent.cs rename to src/redmine-net-api/Net/Internal/ApiRequestMessageContent.cs index 94c5f5e9..296d7a4d 100644 --- a/src/redmine-net-api/Net/ApiRequestMessageContent.cs +++ b/src/redmine-net-api/Net/Internal/ApiRequestMessageContent.cs @@ -14,7 +14,7 @@ You may obtain a copy of the License at limitations under the License. */ -namespace Redmine.Net.Api.Net; +namespace Redmine.Net.Api.Net.Internal; internal abstract class ApiRequestMessageContent { diff --git a/src/redmine-net-api/Net/ApiResponseMessage.cs b/src/redmine-net-api/Net/Internal/ApiResponseMessage.cs similarity index 95% rename from src/redmine-net-api/Net/ApiResponseMessage.cs rename to src/redmine-net-api/Net/Internal/ApiResponseMessage.cs index 3a72058c..f04c45d1 100644 --- a/src/redmine-net-api/Net/ApiResponseMessage.cs +++ b/src/redmine-net-api/Net/Internal/ApiResponseMessage.cs @@ -17,7 +17,7 @@ limitations under the License. using System.Collections.Specialized; using System.Net; -namespace Redmine.Net.Api.Net; +namespace Redmine.Net.Api.Net.Internal; internal sealed class ApiResponseMessage { diff --git a/src/redmine-net-api/Net/ApiResponseMessageExtensions.cs b/src/redmine-net-api/Net/Internal/ApiResponseMessageExtensions.cs similarity index 98% rename from src/redmine-net-api/Net/ApiResponseMessageExtensions.cs rename to src/redmine-net-api/Net/Internal/ApiResponseMessageExtensions.cs index 40d630b8..4d2c4518 100644 --- a/src/redmine-net-api/Net/ApiResponseMessageExtensions.cs +++ b/src/redmine-net-api/Net/Internal/ApiResponseMessageExtensions.cs @@ -19,7 +19,7 @@ limitations under the License. using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Serialization; -namespace Redmine.Net.Api.Net; +namespace Redmine.Net.Api.Net.Internal; internal static class ApiResponseMessageExtensions { diff --git a/src/redmine-net-api/Net/IAsyncRedmineApiClient.cs b/src/redmine-net-api/Net/Internal/IAsyncRedmineApiClient.cs similarity index 98% rename from src/redmine-net-api/Net/IAsyncRedmineApiClient.cs rename to src/redmine-net-api/Net/Internal/IAsyncRedmineApiClient.cs index 0400084b..92210bd6 100644 --- a/src/redmine-net-api/Net/IAsyncRedmineApiClient.cs +++ b/src/redmine-net-api/Net/Internal/IAsyncRedmineApiClient.cs @@ -19,7 +19,7 @@ limitations under the License. using System.Threading; using System.Threading.Tasks; -namespace Redmine.Net.Api.Net; +namespace Redmine.Net.Api.Net.Internal; internal interface IAsyncRedmineApiClient { diff --git a/src/redmine-net-api/Net/IRedmineApiClient.cs b/src/redmine-net-api/Net/Internal/IRedmineApiClient.cs similarity index 94% rename from src/redmine-net-api/Net/IRedmineApiClient.cs rename to src/redmine-net-api/Net/Internal/IRedmineApiClient.cs index 2116dedf..29b90d0f 100644 --- a/src/redmine-net-api/Net/IRedmineApiClient.cs +++ b/src/redmine-net-api/Net/Internal/IRedmineApiClient.cs @@ -14,7 +14,7 @@ You may obtain a copy of the License at limitations under the License. */ -namespace Redmine.Net.Api.Net; +namespace Redmine.Net.Api.Net.Internal; /// /// diff --git a/src/redmine-net-api/Net/ISyncRedmineApiClient.cs b/src/redmine-net-api/Net/Internal/ISyncRedmineApiClient.cs similarity index 97% rename from src/redmine-net-api/Net/ISyncRedmineApiClient.cs rename to src/redmine-net-api/Net/Internal/ISyncRedmineApiClient.cs index a657feac..5fd2e239 100644 --- a/src/redmine-net-api/Net/ISyncRedmineApiClient.cs +++ b/src/redmine-net-api/Net/Internal/ISyncRedmineApiClient.cs @@ -16,7 +16,7 @@ limitations under the License. using System; -namespace Redmine.Net.Api.Net; +namespace Redmine.Net.Api.Net.Internal; internal interface ISyncRedmineApiClient { diff --git a/src/redmine-net-api/Net/RedmineApiUrls.cs b/src/redmine-net-api/Net/Internal/RedmineApiUrls.cs similarity index 99% rename from src/redmine-net-api/Net/RedmineApiUrls.cs rename to src/redmine-net-api/Net/Internal/RedmineApiUrls.cs index 704a4e48..8d83883e 100644 --- a/src/redmine-net-api/Net/RedmineApiUrls.cs +++ b/src/redmine-net-api/Net/Internal/RedmineApiUrls.cs @@ -21,7 +21,7 @@ limitations under the License. using Redmine.Net.Api.Types; using Version = Redmine.Net.Api.Types.Version; -namespace Redmine.Net.Api.Net +namespace Redmine.Net.Api.Net.Internal { internal sealed class RedmineApiUrls { diff --git a/src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs b/src/redmine-net-api/Net/Internal/RedmineApiUrlsExtensions.cs similarity index 99% rename from src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs rename to src/redmine-net-api/Net/Internal/RedmineApiUrlsExtensions.cs index 4312ba9e..103dc7f7 100644 --- a/src/redmine-net-api/Net/RedmineApiUrlsExtensions.cs +++ b/src/redmine-net-api/Net/Internal/RedmineApiUrlsExtensions.cs @@ -17,7 +17,7 @@ limitations under the License. using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; -namespace Redmine.Net.Api.Net; +namespace Redmine.Net.Api.Net.Internal; internal static class RedmineApiUrlsExtensions { diff --git a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.Async.cs b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.Async.cs index bcf45e4f..9fa4317f 100644 --- a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.Async.cs +++ b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.Async.cs @@ -22,6 +22,7 @@ limitations under the License. using System.Threading.Tasks; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Net.Internal; using Redmine.Net.Api.Net.WebClient.Extensions; using Redmine.Net.Api.Net.WebClient.MessageContent; diff --git a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs index 4c68f026..25dbc941 100644 --- a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs +++ b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs @@ -20,6 +20,7 @@ limitations under the License. using System.Text; using Redmine.Net.Api.Authentication; using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Net.Internal; using Redmine.Net.Api.Net.WebClient.Extensions; using Redmine.Net.Api.Net.WebClient.MessageContent; using Redmine.Net.Api.Serialization; diff --git a/src/redmine-net-api/Net/WebClient/MessageContent/ByteArrayApiRequestMessageContent.cs b/src/redmine-net-api/Net/WebClient/MessageContent/ByteArrayApiRequestMessageContent.cs index a1456ad2..13602f4b 100644 --- a/src/redmine-net-api/Net/WebClient/MessageContent/ByteArrayApiRequestMessageContent.cs +++ b/src/redmine-net-api/Net/WebClient/MessageContent/ByteArrayApiRequestMessageContent.cs @@ -14,6 +14,8 @@ You may obtain a copy of the License at limitations under the License. */ +using Redmine.Net.Api.Net.Internal; + namespace Redmine.Net.Api.Net.WebClient.MessageContent; internal class ByteArrayApiRequestMessageContent : ApiRequestMessageContent diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index a47c4b44..4bcc144b 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -23,6 +23,7 @@ limitations under the License. using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; using Redmine.Net.Api.Net; +using Redmine.Net.Api.Net.Internal; using Redmine.Net.Api.Net.WebClient; using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Types; diff --git a/src/redmine-net-api/RedmineManagerAsync.cs b/src/redmine-net-api/RedmineManagerAsync.cs index c2b9ef1f..3d708bf1 100644 --- a/src/redmine-net-api/RedmineManagerAsync.cs +++ b/src/redmine-net-api/RedmineManagerAsync.cs @@ -22,6 +22,7 @@ limitations under the License. using System.Threading.Tasks; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Net; +using Redmine.Net.Api.Net.Internal; using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Types; #if!(NET45_OR_GREATER || NETCOREAPP) diff --git a/tests/redmine-net-api.Tests/Infrastructure/Fixtures/RedmineApiUrlsFixture.cs b/tests/redmine-net-api.Tests/Infrastructure/Fixtures/RedmineApiUrlsFixture.cs index 7a914dae..b1647407 100644 --- a/tests/redmine-net-api.Tests/Infrastructure/Fixtures/RedmineApiUrlsFixture.cs +++ b/tests/redmine-net-api.Tests/Infrastructure/Fixtures/RedmineApiUrlsFixture.cs @@ -1,5 +1,5 @@ using System.Diagnostics; -using Redmine.Net.Api.Net; +using Redmine.Net.Api.Net.Internal; namespace Padi.DotNet.RedmineAPI.Tests.Tests; diff --git a/tests/redmine-net-api.Tests/Tests/RedmineApiUrlsTests.cs b/tests/redmine-net-api.Tests/Tests/RedmineApiUrlsTests.cs index ff358497..068db9ed 100644 --- a/tests/redmine-net-api.Tests/Tests/RedmineApiUrlsTests.cs +++ b/tests/redmine-net-api.Tests/Tests/RedmineApiUrlsTests.cs @@ -2,6 +2,7 @@ using Redmine.Net.Api; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Net; +using Redmine.Net.Api.Net.Internal; using Redmine.Net.Api.Types; using Xunit; using File = Redmine.Net.Api.Types.File; From 3aeed2742fc42709547166f786cafa36d88d9d22 Mon Sep 17 00:00:00 2001 From: Padi Date: Thu, 15 May 2025 00:10:22 +0300 Subject: [PATCH 505/549] IProgress --- src/redmine-net-api/IRedmineManager.cs | 4 +++- src/redmine-net-api/IRedmineManagerAsync.cs | 4 +++- src/redmine-net-api/RedmineManager.cs | 4 ++-- src/redmine-net-api/RedmineManagerAsync.cs | 4 ++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/redmine-net-api/IRedmineManager.cs b/src/redmine-net-api/IRedmineManager.cs index 5e4f5437..acd1b8cb 100644 --- a/src/redmine-net-api/IRedmineManager.cs +++ b/src/redmine-net-api/IRedmineManager.cs @@ -14,6 +14,7 @@ You may obtain a copy of the License at limitations under the License. */ +using System; using System.Collections.Generic; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Net; @@ -110,7 +111,8 @@ void Delete(string id, RequestOptions requestOptions = null) /// Downloads a file from the specified address. /// /// The address. + /// /// The content of the downloaded file as a byte array. /// - byte[] DownloadFile(string address); + byte[] DownloadFile(string address, IProgress progress = null); } \ No newline at end of file diff --git a/src/redmine-net-api/IRedmineManagerAsync.cs b/src/redmine-net-api/IRedmineManagerAsync.cs index 4d2343d6..8d0098e5 100644 --- a/src/redmine-net-api/IRedmineManagerAsync.cs +++ b/src/redmine-net-api/IRedmineManagerAsync.cs @@ -15,6 +15,7 @@ limitations under the License. */ #if !(NET20) +using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -147,9 +148,10 @@ Task DeleteAsync(string id, RequestOptions requestOptions = null, Cancellatio /// /// The address. /// + /// /// /// - Task DownloadFileAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + Task DownloadFileAsync(string address, RequestOptions requestOptions = null, IProgress progress = null, CancellationToken cancellationToken = default); } } #endif \ No newline at end of file diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index 4bcc144b..a64f6cd7 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -206,9 +206,9 @@ public Upload UploadFile(byte[] data, string fileName = null) } /// - public byte[] DownloadFile(string address) + public byte[] DownloadFile(string address, IProgress progress = null) { - var response = ApiClient.Download(address); + var response = ApiClient.Download(address, progress: progress); return response.Content; } diff --git a/src/redmine-net-api/RedmineManagerAsync.cs b/src/redmine-net-api/RedmineManagerAsync.cs index 3d708bf1..5b8472b8 100644 --- a/src/redmine-net-api/RedmineManagerAsync.cs +++ b/src/redmine-net-api/RedmineManagerAsync.cs @@ -220,9 +220,9 @@ public async Task UploadFileAsync(byte[] data, string fileName = null, R } /// - public async Task DownloadFileAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + public async Task DownloadFileAsync(string address, RequestOptions requestOptions = null, IProgress progress = null, CancellationToken cancellationToken = default) { - var response = await ApiClient.DownloadAsync(address, requestOptions, cancellationToken: cancellationToken).ConfigureAwait(false); + var response = await ApiClient.DownloadAsync(address, requestOptions, progress, cancellationToken: cancellationToken).ConfigureAwait(false); return response.Content; } From 98920e4fbdb88ca920a41066103c9fb10a1950df Mon Sep 17 00:00:00 2001 From: Padi Date: Thu, 15 May 2025 11:39:04 +0300 Subject: [PATCH 506/549] Refactor exception handling --- .../Net/Internal/HttpStatusHelper.cs | 98 ++++++++++++ .../Extensions/WebExceptionExtensions.cs | 70 +++++++++ .../Net/WebClient/Extensions/WebExtensions.cs | 147 ------------------ 3 files changed, 168 insertions(+), 147 deletions(-) create mode 100644 src/redmine-net-api/Net/Internal/HttpStatusHelper.cs create mode 100644 src/redmine-net-api/Net/WebClient/Extensions/WebExceptionExtensions.cs delete mode 100644 src/redmine-net-api/Net/WebClient/Extensions/WebExtensions.cs diff --git a/src/redmine-net-api/Net/Internal/HttpStatusHelper.cs b/src/redmine-net-api/Net/Internal/HttpStatusHelper.cs new file mode 100644 index 00000000..90fe88c9 --- /dev/null +++ b/src/redmine-net-api/Net/Internal/HttpStatusHelper.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.Net.Internal; + +internal static class HttpStatusHelper +{ + internal static void MapStatusCodeToException(int statusCode, Stream responseStream, Exception inner, IRedmineSerializer serializer) + { + switch (statusCode) + { + case (int)HttpStatusCode.NotFound: + throw new NotFoundException("Not found.", inner); + + case (int)HttpStatusCode.Unauthorized: + throw new UnauthorizedException("Unauthorized.", inner); + + case (int)HttpStatusCode.Forbidden: + throw new ForbiddenException("Forbidden.", inner); + + case (int)HttpStatusCode.Conflict: + throw new ConflictException("The page that you are trying to update is stale!", inner); + + case 422: + var exception = CreateUnprocessableEntityException(responseStream, inner, serializer); + throw exception; + + case (int)HttpStatusCode.NotAcceptable: + throw new NotAcceptableException("Not acceptable.", inner); + + case (int)HttpStatusCode.InternalServerError: + throw new InternalServerErrorException("Internal server error.", inner); + + default: + throw new RedmineException($"HTTP {(int)statusCode} – {statusCode}", inner); + } + } + + private static RedmineException CreateUnprocessableEntityException( + Stream responseStream, + Exception inner, + IRedmineSerializer serializer) + { + var errors = GetRedmineErrors(responseStream, serializer); + + if (errors is null) + { + return new RedmineException("Unprocessable Content", inner); + } + + var sb = new StringBuilder(); + foreach (var error in errors) + { + sb.Append(error.Info).Append(Environment.NewLine); + } + + sb.Length -= 1; + return new RedmineException($"Unprocessable Content: {sb}", inner); + } + + + /// + /// Gets the redmine exceptions. + /// + /// + /// + /// + private static List GetRedmineErrors(Stream responseStream, IRedmineSerializer serializer) + { + if (responseStream == null) + { + return null; + } + + using (responseStream) + { + using var streamReader = new StreamReader(responseStream); + var responseContent = streamReader.ReadToEnd(); + + return GetRedmineErrors(responseContent, serializer); + } + } + + private static List GetRedmineErrors(string content, IRedmineSerializer serializer) + { + if (content.IsNullOrWhiteSpace()) return null; + + var paged = serializer.DeserializeToPagedResults(content); + return (List)paged.Items; + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Net/WebClient/Extensions/WebExceptionExtensions.cs b/src/redmine-net-api/Net/WebClient/Extensions/WebExceptionExtensions.cs new file mode 100644 index 00000000..4bd89063 --- /dev/null +++ b/src/redmine-net-api/Net/WebClient/Extensions/WebExceptionExtensions.cs @@ -0,0 +1,70 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System.Net; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Net.Internal; +using Redmine.Net.Api.Serialization; + +namespace Redmine.Net.Api.Net.WebClient.Extensions +{ + /// + /// + /// + internal static class WebExceptionExtensions + { + /// + /// Handles the web exception. + /// + /// The exception. + /// + /// Timeout! + /// Bad domain name! + /// + /// + /// + /// + /// The page that you are trying to update is staled! + /// + /// + public static void HandleWebException(this WebException exception, IRedmineSerializer serializer) + { + if (exception == null) + { + return; + } + + var innerException = exception.InnerException ?? exception; + + switch (exception.Status) + { + case WebExceptionStatus.Timeout: + throw new RedmineTimeoutException(nameof(WebExceptionStatus.Timeout), innerException); + case WebExceptionStatus.NameResolutionFailure: + throw new NameResolutionFailureException("Bad domain name.", innerException); + case WebExceptionStatus.ProtocolError: + if (exception.Response != null) + { + using var responseStream = exception.Response.GetResponseStream(); + HttpStatusHelper.MapStatusCodeToException((int)exception.Status, responseStream, innerException, serializer); + } + + break; + } + throw new RedmineException(exception.Message, innerException); + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Net/WebClient/Extensions/WebExtensions.cs b/src/redmine-net-api/Net/WebClient/Extensions/WebExtensions.cs deleted file mode 100644 index b5fd04d3..00000000 --- a/src/redmine-net-api/Net/WebClient/Extensions/WebExtensions.cs +++ /dev/null @@ -1,147 +0,0 @@ -/* - Copyright 2011 - 2025 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Text; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.Net.WebClient.Extensions -{ - /// - /// - /// - internal static class WebExceptionExtensions - { - /// - /// Handles the web exception. - /// - /// The exception. - /// - /// Timeout! - /// Bad domain name! - /// - /// - /// - /// - /// The page that you are trying to update is staled! - /// - /// - /// - public static void HandleWebException(this WebException exception, IRedmineSerializer serializer) - { - if (exception == null) - { - return; - } - - var innerException = exception.InnerException ?? exception; - - switch (exception.Status) - { - case WebExceptionStatus.Timeout: - throw new RedmineTimeoutException(nameof(WebExceptionStatus.Timeout), innerException); - case WebExceptionStatus.NameResolutionFailure: - throw new NameResolutionFailureException("Bad domain name.", innerException); - - case WebExceptionStatus.ProtocolError: - { - var response = (HttpWebResponse)exception.Response; - switch ((int)response.StatusCode) - { - case (int)HttpStatusCode.NotFound: - throw new NotFoundException(response.StatusDescription, innerException); - - case (int)HttpStatusCode.Unauthorized: - throw new UnauthorizedException(response.StatusDescription, innerException); - - case (int)HttpStatusCode.Forbidden: - throw new ForbiddenException(response.StatusDescription, innerException); - - case (int)HttpStatusCode.Conflict: - throw new ConflictException("The page that you are trying to update is staled!", innerException); - - case 422: - RedmineException redmineException; - var errors = GetRedmineExceptions(exception.Response, serializer); - - if (errors != null) - { - var sb = new StringBuilder(); - foreach (var error in errors) - { - sb.Append(error.Info).Append(Environment.NewLine); - } - - redmineException = new RedmineException($"Invalid or missing attribute parameters: {sb}", innerException, "Unprocessable Content"); - sb.Length = 0; - } - else - { - redmineException = new RedmineException("Invalid or missing attribute parameters", innerException); - } - - throw redmineException; - - case (int)HttpStatusCode.NotAcceptable: - throw new NotAcceptableException(response.StatusDescription, innerException); - - default: - throw new RedmineException(response.StatusDescription, innerException); - } - } - - default: - throw new RedmineException(exception.Message, innerException); - } - } - - /// - /// Gets the redmine exceptions. - /// - /// The web response. - /// - /// - private static IEnumerable GetRedmineExceptions(this WebResponse webResponse, IRedmineSerializer serializer) - { - using (var responseStream = webResponse.GetResponseStream()) - { - if (responseStream == null) - { - return null; - } - - using (var streamReader = new StreamReader(responseStream)) - { - var responseContent = streamReader.ReadToEnd(); - - if (responseContent.IsNullOrWhiteSpace()) - { - return null; - } - - var result = serializer.DeserializeToPagedResults(responseContent); - return result.Items; - } - } - } - } -} \ No newline at end of file From a8f6ed9d511d8a4b62799f9ce87d8d7583aac6b4 Mon Sep 17 00:00:00 2001 From: Padi Date: Thu, 15 May 2025 11:40:17 +0300 Subject: [PATCH 507/549] Replace magic strings with constants --- src/redmine-net-api/IRedmineManager.cs | 6 +++--- .../WebClient/Extensions/WebClientExtensions.cs | 16 +++++++++------- src/redmine-net-api/RedmineConstants.cs | 3 +++ 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/redmine-net-api/IRedmineManager.cs b/src/redmine-net-api/IRedmineManager.cs index acd1b8cb..8642a6e8 100644 --- a/src/redmine-net-api/IRedmineManager.cs +++ b/src/redmine-net-api/IRedmineManager.cs @@ -97,16 +97,16 @@ void Delete(string id, RequestOptions requestOptions = null) /// /// Support for adding attachments through the REST API is added in Redmine 1.4.0. - /// Upload a file to server. + /// Upload a file to the server. /// /// The content of the file that will be uploaded on server. /// /// - /// Returns the token for uploaded file. + /// Returns the token for the uploaded file. /// /// Upload UploadFile(byte[] data, string fileName = null); - + /// /// Downloads a file from the specified address. /// diff --git a/src/redmine-net-api/Net/WebClient/Extensions/WebClientExtensions.cs b/src/redmine-net-api/Net/WebClient/Extensions/WebClientExtensions.cs index 83176d08..2cb3c8c0 100644 --- a/src/redmine-net-api/Net/WebClient/Extensions/WebClientExtensions.cs +++ b/src/redmine-net-api/Net/WebClient/Extensions/WebClientExtensions.cs @@ -7,11 +7,11 @@ internal static class WebClientExtensions { public static void ApplyHeaders(this System.Net.WebClient client, RequestOptions options, IRedmineSerializer serializer) { - client.Headers.Add("Content-Type", options.ContentType ?? serializer.ContentType); + client.Headers.Add(RedmineConstants.CONTENT_TYPE_HEADER_KEY, options.ContentType ?? serializer.ContentType); if (!options.UserAgent.IsNullOrWhiteSpace()) { - client.Headers.Add("User-Agent", options.UserAgent); + client.Headers.Add(RedmineConstants.USER_AGENT_HEADER_KEY, options.UserAgent); } if (!options.ImpersonateUser.IsNullOrWhiteSpace()) @@ -19,12 +19,14 @@ public static void ApplyHeaders(this System.Net.WebClient client, RequestOptions client.Headers.Add(RedmineConstants.IMPERSONATE_HEADER_KEY, options.ImpersonateUser); } - if (options.Headers is { Count: > 0 }) + if (options.Headers is not { Count: > 0 }) { - foreach (var header in options.Headers) - { - client.Headers.Add(header.Key, header.Value); - } + return; + } + + foreach (var header in options.Headers) + { + client.Headers.Add(header.Key, header.Value); } } } \ No newline at end of file diff --git a/src/redmine-net-api/RedmineConstants.cs b/src/redmine-net-api/RedmineConstants.cs index 106ba466..fa752ce4 100644 --- a/src/redmine-net-api/RedmineConstants.cs +++ b/src/redmine-net-api/RedmineConstants.cs @@ -66,5 +66,8 @@ public static class RedmineConstants /// /// public const string JSON = "json"; + + internal const string USER_AGENT_HEADER_KEY = "User-Agent"; + internal const string CONTENT_TYPE_HEADER_KEY = "Content-Type"; } } \ No newline at end of file From 9622cfb8e5750ffa894b08392665a5016130aec5 Mon Sep 17 00:00:00 2001 From: Padi Date: Thu, 15 May 2025 11:41:08 +0300 Subject: [PATCH 508/549] Fix fixture namespace --- tests/redmine-net-api.Tests/Bugs/RedmineApi-371.cs | 2 +- .../Infrastructure/Fixtures/RedmineApiUrlsFixture.cs | 2 +- tests/redmine-net-api.Tests/Tests/RedmineApiUrlsTests.cs | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/redmine-net-api.Tests/Bugs/RedmineApi-371.cs b/tests/redmine-net-api.Tests/Bugs/RedmineApi-371.cs index d83e4db1..b145b739 100644 --- a/tests/redmine-net-api.Tests/Bugs/RedmineApi-371.cs +++ b/tests/redmine-net-api.Tests/Bugs/RedmineApi-371.cs @@ -1,5 +1,5 @@ using System.Collections.Specialized; -using Padi.DotNet.RedmineAPI.Tests.Tests; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; using Redmine.Net.Api; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Net; diff --git a/tests/redmine-net-api.Tests/Infrastructure/Fixtures/RedmineApiUrlsFixture.cs b/tests/redmine-net-api.Tests/Infrastructure/Fixtures/RedmineApiUrlsFixture.cs index b1647407..34ec15e8 100644 --- a/tests/redmine-net-api.Tests/Infrastructure/Fixtures/RedmineApiUrlsFixture.cs +++ b/tests/redmine-net-api.Tests/Infrastructure/Fixtures/RedmineApiUrlsFixture.cs @@ -1,7 +1,7 @@ using System.Diagnostics; using Redmine.Net.Api.Net.Internal; -namespace Padi.DotNet.RedmineAPI.Tests.Tests; +namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; public sealed class RedmineApiUrlsFixture { diff --git a/tests/redmine-net-api.Tests/Tests/RedmineApiUrlsTests.cs b/tests/redmine-net-api.Tests/Tests/RedmineApiUrlsTests.cs index 068db9ed..35ffd7cc 100644 --- a/tests/redmine-net-api.Tests/Tests/RedmineApiUrlsTests.cs +++ b/tests/redmine-net-api.Tests/Tests/RedmineApiUrlsTests.cs @@ -1,4 +1,5 @@ using System.Collections.Specialized; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; using Redmine.Net.Api; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Net; From 48d87c7e606c497853ca9ca92bf8e884aaaeb059 Mon Sep 17 00:00:00 2001 From: Padi Date: Thu, 15 May 2025 11:41:29 +0300 Subject: [PATCH 509/549] Fix test type -> xml --- .../Infrastructure/Fixtures/RedmineApiUrlsFixture.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/redmine-net-api.Tests/Infrastructure/Fixtures/RedmineApiUrlsFixture.cs b/tests/redmine-net-api.Tests/Infrastructure/Fixtures/RedmineApiUrlsFixture.cs index 34ec15e8..a19b44ac 100644 --- a/tests/redmine-net-api.Tests/Infrastructure/Fixtures/RedmineApiUrlsFixture.cs +++ b/tests/redmine-net-api.Tests/Infrastructure/Fixtures/RedmineApiUrlsFixture.cs @@ -26,6 +26,6 @@ private void SetMimeTypeJson() [Conditional("DEBUG_XML")] private void SetMimeTypeXml() { - Format = "json"; + Format = "xml"; } } \ No newline at end of file From e5626fe98ae8d75c8a7d7c728e18d28bddeb12ee Mon Sep 17 00:00:00 2001 From: Padi Date: Thu, 15 May 2025 11:43:20 +0300 Subject: [PATCH 510/549] [New] GetIssue_With_Watchers_And_Relations_Should_Succeed test --- .../Tests/Async/IssueTestsAsync.cs | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueTestsAsync.cs index 390efb17..9b355210 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueTestsAsync.cs @@ -1,6 +1,7 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Net; using Redmine.Net.Api.Types; namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; @@ -10,7 +11,7 @@ public class IssueTestsAsync(RedmineTestContainerFixture fixture) { private static readonly IdentifiableName ProjectIdName = IdentifiableName.Create(1); - private async Task CreateTestIssueAsync() + private async Task CreateTestIssueAsync(List customFields = null, List watchers = null) { var issue = new Issue { @@ -20,10 +21,8 @@ private async Task CreateTestIssueAsync() Tracker = 1.ToIdentifier(), Status = 1.ToIssueStatusIdentifier(), Priority = 2.ToIdentifier(), - CustomFields = - [ - IssueCustomField.CreateMultiple(1, ThreadSafeRandom.GenerateText(8), [ThreadSafeRandom.GenerateText(4), ThreadSafeRandom.GenerateText(4)]) - ] + CustomFields = customFields, + Watchers = watchers }; return await fixture.RedmineManager.CreateAsync(issue); } @@ -132,4 +131,24 @@ public async Task DeleteIssue_Should_Succeed() //Assert await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetAsync(issueId)); } + + [Fact] + public async Task GetIssue_With_Watchers_And_Relations_Should_Succeed() + { + var createdIssue = await CreateTestIssueAsync( + [ + IssueCustomField.CreateMultiple(1, ThreadSafeRandom.GenerateText(8), + [ThreadSafeRandom.GenerateText(4), ThreadSafeRandom.GenerateText(4)]) + ], + [new Watcher() { Id = 1 }, new Watcher(){Id = 2}]); + + Assert.NotNull(createdIssue); + + //Act + var retrievedIssue = await fixture.RedmineManager.GetAsync(createdIssue.Id.ToInvariantString(), + RequestOptions.Include($"{Include.Issue.Watchers},{Include.Issue.Relations}")); + + //Assert + Assert.NotNull(retrievedIssue); + } } \ No newline at end of file From e7aa5ba94fd62e35185edea4ff28f6466ae146e3 Mon Sep 17 00:00:00 2001 From: Padi Date: Thu, 15 May 2025 11:50:27 +0300 Subject: [PATCH 511/549] Rename ThreadSafeRandom to RandomHelper --- .../RandomHelper.cs | 10 +++++----- .../Tests/Async/FileTestsAsync.cs | 6 +++--- .../Async/IssueAttachmentUploadTestsAsync.cs | 2 +- .../Tests/Async/IssueTestsAsync.cs | 20 +++++++++---------- .../Tests/Async/JournalTestsAsync.cs | 4 ++-- .../Tests/Async/MembershipTestsAsync.cs | 16 +++++++-------- .../Tests/Async/NewsAsyncTests.cs | 18 ++++++++--------- .../Tests/Async/UploadTestsAsync.cs | 12 +++++------ .../Tests/Async/UserTestsAsync.cs | 16 +++++++-------- .../Tests/Async/VersionTestsAsync.cs | 6 +++--- .../Tests/Async/WikiTestsAsync.cs | 6 +++--- 11 files changed, 58 insertions(+), 58 deletions(-) diff --git a/tests/redmine-net-api.Integration.Tests/RandomHelper.cs b/tests/redmine-net-api.Integration.Tests/RandomHelper.cs index 7e78005c..95a8129d 100644 --- a/tests/redmine-net-api.Integration.Tests/RandomHelper.cs +++ b/tests/redmine-net-api.Integration.Tests/RandomHelper.cs @@ -2,20 +2,20 @@ namespace Padi.DotNet.RedmineAPI.Integration.Tests; -public static class ThreadSafeRandom +internal static class RandomHelper { /// /// Generates a cryptographically strong, random string suffix. /// This method is thread-safe as Guid.NewGuid() is thread-safe. /// /// A random string, 32 characters long, consisting of hexadecimal characters, without hyphens. - public static string GenerateSuffix() + private static string GenerateSuffix() { return Guid.NewGuid().ToString("N"); } - private static readonly char[] EnglishAlphabetChars = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".ToCharArray(); + private static readonly char[] EnglishAlphabetChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + .ToCharArray(); // ThreadLocal ensures that each thread has its own instance of Random, // which is important because System.Random is not thread-safe for concurrent use. @@ -60,7 +60,7 @@ public static string GenerateText(int length = 10) /// /// Generates a random name by combining a specified prefix and a random alphabetic suffix. /// This method is thread-safe. - /// Example: if prefix is "MyItem", the result could be "MyItem_aBcDeFgHiJ". + /// Example: if the prefix is "MyItem", the result could be "MyItem_aBcDeFgHiJ". /// /// The prefix for the name. A '_' separator will be added. /// The desired length of the random suffix. Defaults to 10. diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/FileTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/FileTestsAsync.cs index 1ca94aa1..251d8451 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/FileTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/FileTestsAsync.cs @@ -45,7 +45,7 @@ public async Task CreateFile_With_OptionalParameters_Should_Succeed() { Token = token, Filename = fileName, - Description = ThreadSafeRandom.GenerateText(9), + Description = RandomHelper.GenerateText(9), ContentType = "text/plain", }; @@ -62,7 +62,7 @@ public async Task CreateFile_With_Version_Should_Succeed() { Token = token, Filename = fileName, - Description = ThreadSafeRandom.GenerateText(9), + Description = RandomHelper.GenerateText(9), ContentType = "text/plain", Version = 1.ToIdentifier(), }; @@ -74,7 +74,7 @@ public async Task CreateFile_With_Version_Should_Succeed() private async Task<(string,string)> UploadFileAsync() { var bytes = "Hello World!"u8.ToArray(); - var fileName = $"{ThreadSafeRandom.GenerateText(5)}.txt"; + var fileName = $"{RandomHelper.GenerateText(5)}.txt"; var upload = await fixture.RedmineManager.UploadFileAsync(bytes, fileName); Assert.NotNull(upload); diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueAttachmentUploadTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueAttachmentUploadTestsAsync.cs index 679244d1..f87b6780 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueAttachmentUploadTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueAttachmentUploadTestsAsync.cs @@ -35,7 +35,7 @@ public async Task UploadAttachmentAndAttachToIssue_Should_Succeed() // Prepare issue with attachment var updateIssue = new Issue { - Subject = $"Test issue for attachment {ThreadSafeRandom.GenerateText(5)}", + Subject = $"Test issue for attachment {RandomHelper.GenerateText(5)}", Uploads = [upload] }; diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueTestsAsync.cs index 9b355210..d302cedc 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueTestsAsync.cs @@ -16,8 +16,8 @@ private async Task CreateTestIssueAsync(List customFiel var issue = new Issue { Project = ProjectIdName, - Subject = ThreadSafeRandom.GenerateText(9), - Description = ThreadSafeRandom.GenerateText(18), + Subject = RandomHelper.GenerateText(9), + Description = RandomHelper.GenerateText(18), Tracker = 1.ToIdentifier(), Status = 1.ToIssueStatusIdentifier(), Priority = 2.ToIdentifier(), @@ -34,8 +34,8 @@ public async Task CreateIssue_Should_Succeed() var issueData = new Issue { Project = ProjectIdName, - Subject = ThreadSafeRandom.GenerateText(9), - Description = ThreadSafeRandom.GenerateText(18), + Subject = RandomHelper.GenerateText(9), + Description = RandomHelper.GenerateText(18), Tracker = 2.ToIdentifier(), Status = 1.ToIssueStatusIdentifier(), Priority = 3.ToIdentifier(), @@ -44,7 +44,7 @@ public async Task CreateIssue_Should_Succeed() EstimatedHours = 8, CustomFields = [ - IssueCustomField.CreateSingle(1, ThreadSafeRandom.GenerateText(8), ThreadSafeRandom.GenerateText(4)) + IssueCustomField.CreateSingle(1, RandomHelper.GenerateText(8), RandomHelper.GenerateText(4)) ] }; @@ -93,14 +93,14 @@ public async Task UpdateIssue_Should_Succeed() var createdIssue = await CreateTestIssueAsync(); Assert.NotNull(createdIssue); - var updatedSubject = ThreadSafeRandom.GenerateText(9); - var updatedDescription = ThreadSafeRandom.GenerateText(18); + var updatedSubject = RandomHelper.GenerateText(9); + var updatedDescription = RandomHelper.GenerateText(18); var updatedStatusId = 2; createdIssue.Subject = updatedSubject; createdIssue.Description = updatedDescription; createdIssue.Status = updatedStatusId.ToIssueStatusIdentifier(); - createdIssue.Notes = ThreadSafeRandom.GenerateText("Note"); + createdIssue.Notes = RandomHelper.GenerateText("Note"); var issueId = createdIssue.Id.ToInvariantString(); @@ -137,8 +137,8 @@ public async Task GetIssue_With_Watchers_And_Relations_Should_Succeed() { var createdIssue = await CreateTestIssueAsync( [ - IssueCustomField.CreateMultiple(1, ThreadSafeRandom.GenerateText(8), - [ThreadSafeRandom.GenerateText(4), ThreadSafeRandom.GenerateText(4)]) + IssueCustomField.CreateMultiple(1, RandomHelper.GenerateText(8), + [RandomHelper.GenerateText(4), RandomHelper.GenerateText(4)]) ], [new Watcher() { Id = 1 }, new Watcher(){Id = 2}]); diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/JournalTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/JournalTestsAsync.cs index 413b1bc0..754118b5 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/JournalTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/JournalTestsAsync.cs @@ -14,8 +14,8 @@ private async Task CreateTestIssueAsync() var issue = new Issue { Project = IdentifiableName.Create(1), - Subject = ThreadSafeRandom.GenerateText(13), - Description = ThreadSafeRandom.GenerateText(19), + Subject = RandomHelper.GenerateText(13), + Description = RandomHelper.GenerateText(19), Tracker = 1.ToIdentifier(), Status = 1.ToIssueStatusIdentifier(), Priority = 2.ToIdentifier(), diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/MembershipTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/MembershipTestsAsync.cs index f3e7f6a2..55132eb5 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/MembershipTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/MembershipTestsAsync.cs @@ -16,10 +16,10 @@ private async Task CreateTestMembershipAsync() var user = new User { - Login = ThreadSafeRandom.GenerateText(10), - FirstName = ThreadSafeRandom.GenerateText(8), - LastName = ThreadSafeRandom.GenerateText(9), - Email = $"{ThreadSafeRandom.GenerateText(5)}@example.com", + Login = RandomHelper.GenerateText(10), + FirstName = RandomHelper.GenerateText(8), + LastName = RandomHelper.GenerateText(9), + Email = $"{RandomHelper.GenerateText(5)}@example.com", Password = "password123", MustChangePassword = false, Status = UserStatus.StatusActive @@ -56,10 +56,10 @@ public async Task CreateMembership_Should_Succeed() var user = new User { - Login = ThreadSafeRandom.GenerateText(10), - FirstName = ThreadSafeRandom.GenerateText(8), - LastName = ThreadSafeRandom.GenerateText(9), - Email = $"{ThreadSafeRandom.GenerateText(5)}@example.com", + Login = RandomHelper.GenerateText(10), + FirstName = RandomHelper.GenerateText(8), + LastName = RandomHelper.GenerateText(9), + Email = $"{RandomHelper.GenerateText(5)}@example.com", Password = "password123", MustChangePassword = false, Status = UserStatus.StatusActive diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/NewsAsyncTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/NewsAsyncTests.cs index 6cfd86bf..ca392425 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/NewsAsyncTests.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/NewsAsyncTests.cs @@ -15,16 +15,16 @@ public async Task GetAllNews_Should_Succeed() // Arrange _ = await fixture.RedmineManager.AddProjectNewsAsync(PROJECT_ID, new News() { - Title = ThreadSafeRandom.GenerateText(5), - Summary = ThreadSafeRandom.GenerateText(10), - Description = ThreadSafeRandom.GenerateText(20), + Title = RandomHelper.GenerateText(5), + Summary = RandomHelper.GenerateText(10), + Description = RandomHelper.GenerateText(20), }); _ = await fixture.RedmineManager.AddProjectNewsAsync("2", new News() { - Title = ThreadSafeRandom.GenerateText(5), - Summary = ThreadSafeRandom.GenerateText(10), - Description = ThreadSafeRandom.GenerateText(20), + Title = RandomHelper.GenerateText(5), + Summary = RandomHelper.GenerateText(10), + Description = RandomHelper.GenerateText(20), }); @@ -41,9 +41,9 @@ public async Task GetProjectNews_Should_Succeed() // Arrange var newsCreated = await fixture.RedmineManager.AddProjectNewsAsync(PROJECT_ID, new News() { - Title = ThreadSafeRandom.GenerateText(5), - Summary = ThreadSafeRandom.GenerateText(10), - Description = ThreadSafeRandom.GenerateText(20), + Title = RandomHelper.GenerateText(5), + Summary = RandomHelper.GenerateText(10), + Description = RandomHelper.GenerateText(20), }); // Act diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/UploadTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/UploadTestsAsync.cs index 1b925d57..727464d1 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/UploadTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/UploadTestsAsync.cs @@ -47,27 +47,27 @@ public async Task Upload_Attachment_To_Issue_Should_Succeed() [Fact] public async Task Upload_Attachment_To_Wiki_Should_Succeed() { - var bytes = Encoding.UTF8.GetBytes(ThreadSafeRandom.GenerateText("Hello Wiki!",10)); - var fileName = $"{ThreadSafeRandom.GenerateText("wiki-",5)}.txt"; + var bytes = Encoding.UTF8.GetBytes(RandomHelper.GenerateText("Hello Wiki!",10)); + var fileName = $"{RandomHelper.GenerateText("wiki-",5)}.txt"; var uploadFile = await fixture.RedmineManager.UploadFileAsync(bytes, fileName); Assert.NotNull(uploadFile); Assert.NotNull(uploadFile.Token); - var wikiPageName = ThreadSafeRandom.GenerateText(7); + var wikiPageName = RandomHelper.GenerateText(7); var wikiPageInfo = new WikiPage() { Version = 0, - Comments = ThreadSafeRandom.GenerateText(15), - Text = ThreadSafeRandom.GenerateText(10), + Comments = RandomHelper.GenerateText(15), + Text = RandomHelper.GenerateText(10), Uploads = [ new Upload() { Token = uploadFile.Token, ContentType = "text/plain", - Description = ThreadSafeRandom.GenerateText(15), + Description = RandomHelper.GenerateText(15), FileName = fileName, } ] diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/UserTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/UserTestsAsync.cs index b5bbd987..d974a0c8 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/UserTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/UserTestsAsync.cs @@ -12,10 +12,10 @@ private async Task CreateTestUserAsync() { var user = new User { - Login = ThreadSafeRandom.GenerateText(12), - FirstName = ThreadSafeRandom.GenerateText(8), - LastName = ThreadSafeRandom.GenerateText(10), - Email = $"{ThreadSafeRandom.GenerateText(5)}.{ThreadSafeRandom.GenerateText(4)}@gmail.com", + Login = RandomHelper.GenerateText(12), + FirstName = RandomHelper.GenerateText(8), + LastName = RandomHelper.GenerateText(10), + Email = $"{RandomHelper.GenerateText(5)}.{RandomHelper.GenerateText(4)}@gmail.com", Password = "password123", AuthenticationModeId = null, MustChangePassword = false, @@ -30,9 +30,9 @@ public async Task CreateUser_Should_Succeed() //Arrange var userData = new User { - Login = ThreadSafeRandom.GenerateText(5), - FirstName = ThreadSafeRandom.GenerateText(5), - LastName = ThreadSafeRandom.GenerateText(5), + Login = RandomHelper.GenerateText(5), + FirstName = RandomHelper.GenerateText(5), + LastName = RandomHelper.GenerateText(5), Password = "password123", MailNotification = "only_my_events", AuthenticationModeId = null, @@ -81,7 +81,7 @@ public async Task UpdateUser_Should_Succeed() var createdUser = await CreateTestUserAsync(); Assert.NotNull(createdUser); - var updatedFirstName = ThreadSafeRandom.GenerateText(10); + var updatedFirstName = RandomHelper.GenerateText(10); createdUser.FirstName = updatedFirstName; //Act diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/VersionTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/VersionTestsAsync.cs index 71cd8e4c..f66b7e91 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/VersionTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/VersionTestsAsync.cs @@ -15,8 +15,8 @@ private async Task CreateTestVersionAsync() { var version = new Version { - Name = ThreadSafeRandom.GenerateText(10), - Description = ThreadSafeRandom.GenerateText(15), + Name = RandomHelper.GenerateText(10), + Description = RandomHelper.GenerateText(15), Status = VersionStatus.Open, Sharing = VersionSharing.None, DueDate = DateTime.Now.Date.AddDays(30) @@ -75,7 +75,7 @@ public async Task UpdateVersion_Should_Succeed() var createdVersion = await CreateTestVersionAsync(); Assert.NotNull(createdVersion); - var updatedDescription = ThreadSafeRandom.GenerateText(20); + var updatedDescription = RandomHelper.GenerateText(20); var updatedStatus = VersionStatus.Locked; createdVersion.Description = updatedDescription; createdVersion.Status = updatedStatus; diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/WikiTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/WikiTestsAsync.cs index 9ef9a3a0..29afa6bf 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/WikiTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/WikiTestsAsync.cs @@ -77,11 +77,11 @@ public async Task GetAllWikiPages_Should_Succeed() public async Task DeleteWikiPage_Should_Succeed() { // Arrange - var wikiPageName = ThreadSafeRandom.GenerateText(7); + var wikiPageName = RandomHelper.GenerateText(7); var wikiPage = new WikiPage { - Title = ThreadSafeRandom.GenerateText(5), + Title = RandomHelper.GenerateText(5), Text = "Test wiki page content for deletion", Comments = "Initial wiki page creation for deletion test" }; @@ -123,7 +123,7 @@ await Assert.ThrowsAsync(async () => public async Task CreateWikiPage_Should_Succeed() { //Arrange - var pageTitle = ThreadSafeRandom.GenerateText("NewWikiPage"); + var pageTitle = RandomHelper.GenerateText("NewWikiPage"); var text = "This is the content of a new wiki page."; var comments = "Creation comment for new wiki page."; var wikiPageData = new WikiPage { Text = text, Comments = comments }; From fe984a32fc65c5ce6f1408643a301150c335792a Mon Sep 17 00:00:00 2001 From: Padi Date: Fri, 16 May 2025 13:11:04 +0300 Subject: [PATCH 512/549] Integration tests --- .../redmine-net-api/Common}/AType.cs | 0 .../Fixtures/RedmineTestContainerFixture.cs | 206 ++++++++++++++--- .../Helpers/AssertHelpers.cs | 31 +++ .../Helpers/FileGeneratorHelper.cs | 142 ++++++++++++ .../Helpers/IssueTestHelper.cs | 36 +++ .../Helpers/RandomHelper.cs | 218 ++++++++++++++++++ .../{ => Infrastructure}/Constants.cs | 0 .../Infrastructure/RedmineOptions.cs | 35 +++ .../Infrastructure}/TestHelper.cs | 19 +- .../RandomHelper.cs | 89 ------- .../Tests/Async/AttachmentTestsAsync.cs | 96 ++++++-- .../Tests/Async/FileTestsAsync.cs | 6 +- .../Tests/Async/ProjectTestsAsync.cs | 34 ++- .../Tests/Async/TimeEntryTests.cs | 4 +- .../Tests/Async/VersionTestsAsync.cs | 2 +- .../Tests/Async/WikiTestsAsync.cs | 18 +- .../Tests/ProgressTests.cs | 109 +++++++++ .../Tests/ProgressTestsAsync.cs | 9 + .../Tests/Sync/AttachmentTests.cs | 38 +++ .../Tests/Sync/CustomFieldTestsSync.cs | 18 ++ .../Tests/Sync/EnumerationTestsSync.cs | 12 + .../Tests/Sync/FileUploadTests.cs | 82 +++++++ .../Tests/Sync/GroupManagementTests.cs | 118 ++++++++++ .../Tests/Sync/IssueAttachmentUploadTests.cs | 44 ++++ .../Tests/Sync/IssueCategoryTestsSync.cs | 66 ++++++ .../Tests/Sync/IssueJournalTestsSync.cs | 37 +++ .../Tests/Sync/IssueRelationTests.cs | 58 +++++ .../Tests/Sync/IssueStatusTests.cs | 16 ++ .../Tests/Sync/IssueTestsAsync.cs | 124 ++++++++++ .../Tests/Sync/IssueWatcherTestsAsync.cs | 47 ++++ .../Tests/Sync/JournalManagementTests.cs | 40 ++++ .../Tests/Sync/NewsTestsIntegration.cs | 48 ++++ .../Tests/Sync/ProjectMembershipTests.cs | 103 +++++++++ .../appsettings.json | 14 ++ .../appsettings.local.json | 10 + .../redmine-net-api.Integration.Tests.csproj | 59 ++++- .../Collections/RedmineCollection.cs | 10 - .../Infrastructure/Fixtures/RedmineFixture.cs | 40 ---- .../Infrastructure/RedmineCredentials.cs | 10 - .../appsettings-local.json | 5 - tests/redmine-net-api.Tests/appsettings.json | 8 - .../redmine-net-api.Tests.csproj | 45 +--- 42 files changed, 1802 insertions(+), 304 deletions(-) rename {tests/redmine-net-api.Tests/Infrastructure => src/redmine-net-api/Common}/AType.cs (100%) create mode 100644 tests/redmine-net-api.Integration.Tests/Helpers/AssertHelpers.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Helpers/FileGeneratorHelper.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Helpers/IssueTestHelper.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Helpers/RandomHelper.cs rename tests/redmine-net-api.Integration.Tests/{ => Infrastructure}/Constants.cs (100%) create mode 100644 tests/redmine-net-api.Integration.Tests/Infrastructure/RedmineOptions.cs rename tests/{redmine-net-api.Tests => redmine-net-api.Integration.Tests/Infrastructure}/TestHelper.cs (55%) delete mode 100644 tests/redmine-net-api.Integration.Tests/RandomHelper.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/ProgressTests.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/ProgressTestsAsync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Sync/AttachmentTests.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Sync/CustomFieldTestsSync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Sync/EnumerationTestsSync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Sync/FileUploadTests.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Sync/GroupManagementTests.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueAttachmentUploadTests.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueCategoryTestsSync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueJournalTestsSync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueRelationTests.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueStatusTests.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueTestsAsync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueWatcherTestsAsync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Sync/JournalManagementTests.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Sync/NewsTestsIntegration.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Sync/ProjectMembershipTests.cs create mode 100644 tests/redmine-net-api.Integration.Tests/appsettings.json create mode 100644 tests/redmine-net-api.Integration.Tests/appsettings.local.json delete mode 100644 tests/redmine-net-api.Tests/Infrastructure/Collections/RedmineCollection.cs delete mode 100644 tests/redmine-net-api.Tests/Infrastructure/Fixtures/RedmineFixture.cs delete mode 100644 tests/redmine-net-api.Tests/Infrastructure/RedmineCredentials.cs delete mode 100644 tests/redmine-net-api.Tests/appsettings-local.json delete mode 100644 tests/redmine-net-api.Tests/appsettings.json diff --git a/tests/redmine-net-api.Tests/Infrastructure/AType.cs b/src/redmine-net-api/Common/AType.cs similarity index 100% rename from tests/redmine-net-api.Tests/Infrastructure/AType.cs rename to src/redmine-net-api/Common/AType.cs diff --git a/tests/redmine-net-api.Integration.Tests/Fixtures/RedmineTestContainerFixture.cs b/tests/redmine-net-api.Integration.Tests/Fixtures/RedmineTestContainerFixture.cs index a9564c8e..33173b11 100644 --- a/tests/redmine-net-api.Integration.Tests/Fixtures/RedmineTestContainerFixture.cs +++ b/tests/redmine-net-api.Integration.Tests/Fixtures/RedmineTestContainerFixture.cs @@ -3,8 +3,11 @@ using DotNet.Testcontainers.Containers; using DotNet.Testcontainers.Networks; using Npgsql; +using Padi.DotNet.RedmineAPI.Tests; +using Padi.DotNet.RedmineAPI.Tests.Infrastructure; using Redmine.Net.Api; using Testcontainers.PostgreSql; +using Xunit.Abstractions; namespace Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; @@ -19,9 +22,11 @@ public class RedmineTestContainerFixture : IAsyncLifetime private const string PostgresPassword = "postgres"; private const string RedmineSqlFilePath = "TestData/init-redmine.sql"; - public const string RedmineApiKey = "029a9d38-17e8-41ae-bc8c-fbf71e193c57"; - private readonly string RedmineNetworkAlias = Guid.NewGuid().ToString(); + + private readonly ITestOutputHelper _output; + private readonly TestContainerOptions _redmineOptions; + private INetwork Network { get; set; } private PostgreSqlContainer PostgresContainer { get; set; } private IContainer RedmineContainer { get; set; } @@ -30,16 +35,98 @@ public class RedmineTestContainerFixture : IAsyncLifetime public RedmineTestContainerFixture() { - BuildContainers(); + _redmineOptions = TestHelper.GetConfiguration(); + + if (_redmineOptions.Mode != TestContainerMode.UseExisting) + { + BuildContainers(); + } + } + + // private static string GetExistingRedmineUrl() + // { + // return GetConfigValue("TestContainer:ExistingRedmineUrl") ?? "http://localhost:3000/"; + // } + // + // private static string GetRedmineUsername() + // { + // return GetConfigValue("Redmine:Username") ?? DefaultRedmineUser; + // } + // + // private static string GetRedminePassword() + // { + // return GetConfigValue("Redmine:Password") ?? DefaultRedminePassword; + // } + + /// + /// Gets configuration value from environment variables or appsettings.json + /// + // private static string GetConfigValue(string key) + // { + // var envKey = key.Replace(":", "__"); + // var envValue = Environment.GetEnvironmentVariable(envKey); + // if (!string.IsNullOrEmpty(envValue)) + // { + // return envValue; + // } + // + // try + // { + // var config = new ConfigurationBuilder() + // .AddJsonFile("appsettings.json", optional: true) + // .AddJsonFile("appsettings.local.json", optional: true) + // .Build(); + // + // return config[key]; + // } + // catch + // { + // return null; + // } + // } + + /// + /// Detects if running in a CI/CD environment + /// + private static bool IsRunningInCiEnvironment() + { + return !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI")) || + !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GITHUB_ACTIONS")); + } + /// + /// Gets container mode from configuration + /// + // private static TestContainerMode GetContainerMode() + // { + // var mode = GetConfigValue("TestContainer:Mode"); + // + // if (string.IsNullOrEmpty(mode)) + // { + // if (IsRunningInCiEnvironment()) + // { + // return TestContainerMode.CreateNewWithRandomPorts; + // } + // + // return TestContainerMode.CreateNewWithRandomPorts; + // } + // + // return mode.ToLowerInvariant() switch + // { + // "existing" => TestContainerMode.UseExisting, + // _ => TestContainerMode.CreateNewWithRandomPorts + // }; + // } + private void BuildContainers() { Network = new NetworkBuilder() .WithDriver(NetworkDriver.Bridge) .Build(); - - PostgresContainer = new PostgreSqlBuilder() + + var postgresBuilder + = new PostgreSqlBuilder() .WithImage(PostgresImage) .WithNetwork(Network) .WithNetworkAliases(RedmineNetworkAlias) @@ -50,10 +137,21 @@ private void BuildContainers() { "POSTGRES_USER", PostgresUser }, { "POSTGRES_PASSWORD", PostgresPassword }, }) - .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(PostgresPort)) - .Build(); + .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(PostgresPort)); + + if (_redmineOptions.Mode == TestContainerMode.CreateNewWithRandomPorts) + { + postgresBuilder.WithPortBinding(PostgresPort, assignRandomHostPort: true); + } + else + { + postgresBuilder.WithPortBinding(PostgresPort, PostgresPort); + } - RedmineContainer = new ContainerBuilder() + PostgresContainer = postgresBuilder.Build(); + + var redmineBuilder + = new ContainerBuilder() .WithImage(RedmineImage) .WithNetwork(Network) .WithPortBinding(RedminePort, assignRandomHostPort: true) @@ -66,25 +164,56 @@ private void BuildContainers() { "REDMINE_DB_PASSWORD", PostgresPassword }, }) .DependsOn(PostgresContainer) - .WithWaitStrategy(Wait.ForUnixContainer().UntilHttpRequestIsSucceeded(request => request.ForPort(RedminePort).ForPath("/"))) - .Build(); + .WithWaitStrategy(Wait.ForUnixContainer() + .UntilHttpRequestIsSucceeded(request => request.ForPort(RedminePort).ForPath("/"))); + + if (_redmineOptions.Mode == TestContainerMode.CreateNewWithRandomPorts) + { + redmineBuilder.WithPortBinding(RedminePort, assignRandomHostPort: true); + } + else + { + redmineBuilder.WithPortBinding(RedminePort, RedminePort); + } + + RedmineContainer = redmineBuilder.Build(); } public async Task InitializeAsync() { - await Network.CreateAsync(); - - await PostgresContainer.StartAsync(); - - await RedmineContainer.StartAsync(); + var rmgBuilder = new RedmineManagerOptionsBuilder(); + + switch (_redmineOptions.AuthenticationMode) + { + case AuthenticationMode.ApiKey: + var apiKey = _redmineOptions.Authentication.ApiKey; + rmgBuilder.WithApiKeyAuthentication(apiKey); + break; + case AuthenticationMode.Basic: + var username = _redmineOptions.Authentication.Basic.Username; + var password = _redmineOptions.Authentication.Basic.Password; + rmgBuilder.WithBasicAuthentication(username, password); + break; + } + + if (_redmineOptions.Mode == TestContainerMode.UseExisting) + { + RedmineHost = _redmineOptions.Url; + } + else + { + await Network.CreateAsync(); - await SeedTestDataAsync(PostgresContainer, CancellationToken.None); + await PostgresContainer.StartAsync(); - RedmineHost = $"http://{RedmineContainer.Hostname}:{RedmineContainer.GetMappedPublicPort(RedminePort)}"; - - var rmgBuilder = new RedmineManagerOptionsBuilder() - .WithHost(RedmineHost) - .WithBasicAuthentication("adminuser", "1qaz2wsx"); + await RedmineContainer.StartAsync(); + + await SeedTestDataAsync(PostgresContainer, CancellationToken.None); + + RedmineHost = $"http://{RedmineContainer.Hostname}:{RedmineContainer.GetMappedPublicPort(RedminePort)}"; + } + + rmgBuilder.WithHost(RedmineHost); RedmineManager = new RedmineManager(rmgBuilder); } @@ -93,15 +222,18 @@ public async Task DisposeAsync() { var exceptions = new List(); - await SafeDisposeAsync(() => RedmineContainer.StopAsync()); - await SafeDisposeAsync(() => PostgresContainer.StopAsync()); - await SafeDisposeAsync(() => Network.DisposeAsync().AsTask()); - - if (exceptions.Count > 0) + if (_redmineOptions.Mode != TestContainerMode.UseExisting) { - throw new AggregateException(exceptions); + await SafeDisposeAsync(() => RedmineContainer.StopAsync()); + await SafeDisposeAsync(() => PostgresContainer.StopAsync()); + await SafeDisposeAsync(() => Network.DisposeAsync().AsTask()); + + if (exceptions.Count > 0) + { + throw new AggregateException(exceptions); + } } - + return; async Task SafeDisposeAsync(Func disposeFunc) @@ -117,7 +249,7 @@ async Task SafeDisposeAsync(Func disposeFunc) } } - private static async Task SeedTestDataAsync(PostgreSqlContainer container, CancellationToken ct) + private async Task SeedTestDataAsync(PostgreSqlContainer container, CancellationToken ct) { const int maxDbAttempts = 10; var dbRetryDelay = TimeSpan.FromSeconds(2); @@ -143,7 +275,19 @@ private static async Task SeedTestDataAsync(PostgreSqlContainer container, Cance var res = await container.ExecScriptAsync(sql, ct); if (!string.IsNullOrWhiteSpace(res.Stderr)) { - // Optionally log stderr + _output.WriteLine(res.Stderr); } } -} \ No newline at end of file +} + +/// +/// Enum defining how containers should be managed +/// +public enum TestContainerMode +{ + /// Use existing running containers at specified URL + UseExisting, + + /// Create new containers with random ports (CI-friendly) + CreateNewWithRandomPorts +} diff --git a/tests/redmine-net-api.Integration.Tests/Helpers/AssertHelpers.cs b/tests/redmine-net-api.Integration.Tests/Helpers/AssertHelpers.cs new file mode 100644 index 00000000..d105f53e --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Helpers/AssertHelpers.cs @@ -0,0 +1,31 @@ +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; + +internal static class AssertHelpers +{ + /// + /// Asserts that two values are equal within the specified tolerance. + /// + public static void Equal(float expected, float actual, float tolerance = 1e-4f) + => Assert.InRange(actual, expected - tolerance, expected + tolerance); + + /// + /// Asserts that two values are equal within the specified tolerance. + /// + public static void Equal(decimal expected, decimal actual, decimal tolerance = 0.0001m) + => Assert.InRange(actual, expected - tolerance, expected + tolerance); + + /// + /// Asserts that two values are equal within the supplied tolerance. + /// Kind is ignored – both values are first converted to UTC. + /// + public static void Equal(DateTime expected, DateTime actual, TimeSpan? tolerance = null) + { + tolerance ??= TimeSpan.FromSeconds(1); + + var expectedUtc = expected.ToUniversalTime(); + var actualUtc = actual.ToUniversalTime(); + + Assert.InRange(actualUtc, expectedUtc - tolerance.Value, expectedUtc + tolerance.Value); + } + +} diff --git a/tests/redmine-net-api.Integration.Tests/Helpers/FileGeneratorHelper.cs b/tests/redmine-net-api.Integration.Tests/Helpers/FileGeneratorHelper.cs new file mode 100644 index 00000000..62975276 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Helpers/FileGeneratorHelper.cs @@ -0,0 +1,142 @@ +using System.Text; +using Redmine.Net.Api; +using Redmine.Net.Api.Net; +using Redmine.Net.Api.Types; +using File = System.IO.File; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; + +internal static class FileGeneratorHelper +{ + private static readonly string[] Extensions = [".txt", ".doc", ".pdf", ".xml", ".json"]; + + /// + /// Generates random file content with a specified size. + /// + /// Size of the file in kilobytes. + /// Byte array containing the file content. + public static byte[] GenerateRandomFileBytes(int sizeInKb) + { + var sizeInBytes = sizeInKb * 1024; + var bytes = new byte[sizeInBytes]; + RandomHelper.FillRandomBytes(bytes); + return bytes; + } + + /// + /// Generates a random text file with a specified size. + /// + /// Size of the file in kilobytes. + /// Byte array containing the text file content. + public static byte[] GenerateRandomTextFileBytes(int sizeInKb) + { + var roughCharCount = sizeInKb * 1024; + + var sb = new StringBuilder(roughCharCount); + + while (sb.Length < roughCharCount) + { + sb.AppendLine(RandomHelper.GenerateText(RandomHelper.GetRandomNumber(5, 80))); + } + + var text = sb.ToString(); + + if (text.Length > roughCharCount) + { + text = text[..roughCharCount]; + } + + return Encoding.UTF8.GetBytes(text); + } + + /// + /// Creates a random file with a specified size and returns its path. + /// + /// Size of the file in kilobytes. + /// If true, generates text content; otherwise, generates binary content. + /// Path to the created temporary file. + public static string CreateRandomFile(int sizeInKb, bool useTextContent = true) + { + var extension = Extensions[RandomHelper.GetRandomNumber(Extensions.Length)]; + var fileName = RandomHelper.GenerateText("test-file", 7); + var filePath = Path.Combine(Path.GetTempPath(), $"{fileName}{extension}"); + + var content = useTextContent + ? GenerateRandomTextFileBytes(sizeInKb) + : GenerateRandomFileBytes(sizeInKb); + + File.WriteAllBytes(filePath, content); + return filePath; + } + +} + +internal static class FileTestHelper +{ + private static (string fileNameame, byte[] fileContent) GenerateFile(int sizeInKb) + { + var fileName = RandomHelper.GenerateText("test-file", 7); + var fileContent = sizeInKb >= 1024 + ? FileGeneratorHelper.GenerateRandomTextFileBytes(sizeInKb) + : FileGeneratorHelper.GenerateRandomFileBytes(sizeInKb); + + return (fileName, fileContent); + } + public static Upload UploadRandomFile(IRedmineManager client, int sizeInKb, RequestOptions options = null) + { + var (fileName, fileContent) = GenerateFile(sizeInKb); + return client.UploadFile(fileContent, fileName); + } + + /// + /// Helper method to upload a 500KB file. + /// + /// The Redmine API client. + /// Request options. + /// API response message containing the uploaded file information. + public static Upload UploadRandom500KbFile(IRedmineManager client, RequestOptions options = null) + { + return UploadRandomFile(client, 500, options); + } + + /// + /// Helper method to upload a 1MB file. + /// + /// The Redmine API client. + /// Request options. + /// API response message containing the uploaded file information. + public static Upload UploadRandom1MbFile(IRedmineManager client, RequestOptions options = null) + { + return UploadRandomFile(client, 1024, options); + } + + public static async Task UploadRandomFileAsync(IRedmineManagerAsync client, int sizeInKb, RequestOptions options = null) + { + var (fileName, fileContent) = GenerateFile(sizeInKb); + + return await client.UploadFileAsync(fileContent, fileName, options); + } + + /// + /// Helper method to upload a 500KB file. + /// + /// The Redmine API client. + /// Request options. + /// API response message containing the uploaded file information. + public static Task UploadRandom500KbFileAsync(IRedmineManagerAsync client, RequestOptions options = null) + { + return UploadRandomFileAsync(client, 500, options); + } + + /// + /// Helper method to upload a 1MB file. + /// + /// The Redmine API client. + /// Request options. + /// API response message containing the uploaded file information. + public static Task UploadRandom1MbFileAsync(IRedmineManagerAsync client, RequestOptions options = null) + { + return UploadRandomFileAsync(client, 1024, options); + } + +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Helpers/IssueTestHelper.cs b/tests/redmine-net-api.Integration.Tests/Helpers/IssueTestHelper.cs new file mode 100644 index 00000000..5fbae108 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Helpers/IssueTestHelper.cs @@ -0,0 +1,36 @@ +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; + +internal static class IssueTestHelper +{ + internal static readonly IdentifiableName ProjectIdName = IdentifiableName.Create(1); + + internal static Issue CreateIssue(List customFields = null, List watchers = null, + List uploads = null) + => new() + { + Project = ProjectIdName, + Subject = RandomHelper.GenerateText(9), + Description = RandomHelper.GenerateText(18), + Tracker = 1.ToIdentifier(), + Status = 1.ToIssueStatusIdentifier(), + Priority = 2.ToIdentifier(), + CustomFields = customFields, + Watchers = watchers, + Uploads = uploads + }; + + internal static void AssertBasic(Issue expected, Issue actual) + { + Assert.NotNull(actual); + Assert.True(actual.Id > 0); + Assert.Equal(expected.Subject, actual.Subject); + Assert.Equal(expected.Description, actual.Description); + Assert.Equal(expected.Project.Id, actual.Project.Id); + Assert.Equal(expected.Tracker.Id, actual.Tracker.Id); + Assert.Equal(expected.Status.Id, actual.Status.Id); + Assert.Equal(expected.Priority.Id, actual.Priority.Id); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Helpers/RandomHelper.cs b/tests/redmine-net-api.Integration.Tests/Helpers/RandomHelper.cs new file mode 100644 index 00000000..e7814fc8 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Helpers/RandomHelper.cs @@ -0,0 +1,218 @@ +using System.Text; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests; + +internal static class RandomHelper +{ + /// + /// Generates a cryptographically strong, random string suffix. + /// This method is thread-safe as Guid.NewGuid() is thread-safe. + /// + /// A random string, 32 characters long, consisting of hexadecimal characters, without hyphens. + private static string GenerateSuffix() + { + return Guid.NewGuid().ToString("N"); + } + + private static readonly char[] EnglishAlphabetChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + .ToCharArray(); + + // ThreadLocal ensures that each thread has its own instance of Random, + // which is important because System.Random is not thread-safe for concurrent use. + // Seed with Guid for better randomness across instances + private static readonly ThreadLocal ThreadRandom = + new ThreadLocal(() => new Random(Guid.NewGuid().GetHashCode())); + + /// + /// Generates a random string of a specified length using only English alphabet characters. + /// This method is thread-safe. + /// + /// The desired length of the random string. Defaults to 10. + /// A random string composed of English alphabet characters. + private static string GenerateRandomString(int length = 10) + { + if (length <= 0) + { + throw new ArgumentOutOfRangeException(nameof(length), "Length must be a positive integer."); + } + + var random = ThreadRandom.Value; + var result = new StringBuilder(length); + for (var i = 0; i < length; i++) + { + result.Append(EnglishAlphabetChars[random.Next(EnglishAlphabetChars.Length)]); + } + + return result.ToString(); + } + + internal static void FillRandomBytes(byte[] bytes) + { + ThreadRandom.Value.NextBytes(bytes); + } + + internal static int GetRandomNumber(int max) + { + return ThreadRandom.Value.Next(max); + } + + internal static int GetRandomNumber(int min, int max) + { + return ThreadRandom.Value.Next(min, max); + } + + /// + /// Generates a random alphabetic suffix, defaulting to 10 characters. + /// This method is thread-safe. + /// + /// The desired length of the suffix. Defaults to 10. + /// A random alphabetic string. + public static string GenerateText(int length = 10) + { + return GenerateRandomString(length); + } + + /// + /// Generates a random name by combining a specified prefix and a random alphabetic suffix. + /// This method is thread-safe. + /// Example: if the prefix is "MyItem", the result could be "MyItem_aBcDeFgHiJ". + /// + /// The prefix for the name. A '_' separator will be added. + /// The desired length of the random suffix. Defaults to 10. + /// A string combining the prefix, an underscore, and a random alphabetic suffix. + /// If the prefix is null or empty, it returns just the random suffix. + public static string GenerateText(string prefix = null, int suffixLength = 10) + { + var suffix = GenerateRandomString(suffixLength); + return string.IsNullOrEmpty(prefix) ? suffix : $"{prefix}_{suffix}"; + } + + /// + /// Generates a random email address with alphabetic characters only. + /// + /// Length of the local part (before @). Defaults to 8. + /// Length of the domain name (without extension). Defaults to 6. + /// A random email address with only alphabetic characters. + public static string GenerateEmail(int localPartLength = 8, int domainLength = 6) + { + if (localPartLength <= 0 || domainLength <= 0) + { + throw new ArgumentOutOfRangeException( + localPartLength <= 0 ? nameof(localPartLength) : nameof(domainLength), + "Length must be a positive integer."); + } + + var localPart = GenerateRandomString(localPartLength); + var domain = GenerateRandomString(domainLength).ToLower(); + + // Use common TLDs + var tlds = new[] { "com", "org", "net", "io" }; + var tld = tlds[ThreadRandom.Value.Next(tlds.Length)]; + + return $"{localPart}@{domain}.{tld}"; + } + + /// + /// Generates a random webpage URL with alphabetic characters only. + /// + /// Length of the domain name (without extension). Defaults to 8. + /// Length of the path segment. Defaults to 10. + /// A random webpage URL with only alphabetic characters. + public static string GenerateWebpage(int domainLength = 8, int pathLength = 10) + { + if (domainLength <= 0 || pathLength <= 0) + { + throw new ArgumentOutOfRangeException( + domainLength <= 0 ? nameof(domainLength) : nameof(pathLength), + "Length must be a positive integer."); + } + + var domain = GenerateRandomString(domainLength).ToLower(); + + // Use common TLDs + var tlds = new[] { "com", "org", "net", "io" }; + var tld = tlds[ThreadRandom.Value.Next(tlds.Length)]; + + // Generate path segments + var segments = ThreadRandom.Value.Next(0, 3); + var path = ""; + + if (segments > 0) + { + var pathSegments = new List(segments); + for (int i = 0; i < segments; i++) + { + pathSegments.Add(GenerateRandomString(ThreadRandom.Value.Next(3, pathLength)).ToLower()); + } + + path = "/" + string.Join("/", pathSegments); + } + + return $"https://www.{domain}.{tld}{path}/"; + } + + /// + /// Generates a random name composed only of alphabetic characters from the English alphabet. + /// + /// Length of the name. Defaults to 6. + /// Whether to capitalize the first letter. Defaults to true. + /// A random name with only English alphabetic characters. + public static string GenerateName(int length = 6, bool capitalize = true) + { + if (length <= 0) + { + throw new ArgumentOutOfRangeException(nameof(length), "Length must be a positive integer."); + } + + // Generate random name + var name = GenerateRandomString(length); + + if (capitalize) + { + name = char.ToUpper(name[0]) + name.Substring(1).ToLower(); + } + else + { + name = name.ToLower(); + } + + return name; + } + + /// + /// Generates a random full name composed only of alphabetic characters. + /// + /// Length of the first name. Defaults to 6. + /// Length of the last name. Defaults to 8. + /// A random full name with only alphabetic characters. + public static string GenerateFullName(int firstNameLength = 6, int lastNameLength = 8) + { + if (firstNameLength <= 0 || lastNameLength <= 0) + { + throw new ArgumentOutOfRangeException( + firstNameLength <= 0 ? nameof(firstNameLength) : nameof(lastNameLength), + "Length must be a positive integer."); + } + + // Generate random first and last names using the new alphabetic-only method + var firstName = GenerateName(firstNameLength); + var lastName = GenerateName(lastNameLength); + + return $"{firstName} {lastName}"; + } + + // Fisher-Yates shuffle algorithm + public static void Shuffle(this IList list) + { + var n = list.Count; + var random = ThreadRandom.Value; + while (n > 1) + { + n--; + var k = random.Next(n + 1); + var value = list[k]; + list[k] = list[n]; + list[n] = value; + } + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Constants.cs b/tests/redmine-net-api.Integration.Tests/Infrastructure/Constants.cs similarity index 100% rename from tests/redmine-net-api.Integration.Tests/Constants.cs rename to tests/redmine-net-api.Integration.Tests/Infrastructure/Constants.cs diff --git a/tests/redmine-net-api.Integration.Tests/Infrastructure/RedmineOptions.cs b/tests/redmine-net-api.Integration.Tests/Infrastructure/RedmineOptions.cs new file mode 100644 index 00000000..6139c23b --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Infrastructure/RedmineOptions.cs @@ -0,0 +1,35 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; + +namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure +{ + public sealed class TestContainerOptions + { + public string Url { get; set; } + + public AuthenticationMode AuthenticationMode { get; set; } + + public Authentication Authentication { get; set; } + + public TestContainerMode Mode { get; set; } + } + + public sealed class Authentication + { + public string ApiKey { get; set; } + + public BasicAuthentication Basic { get; set; } + } + + public sealed class BasicAuthentication + { + public string Username { get; set; } + public string Password { get; set; } + } + + public enum AuthenticationMode + { + None, + ApiKey, + Basic + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/TestHelper.cs b/tests/redmine-net-api.Integration.Tests/Infrastructure/TestHelper.cs similarity index 55% rename from tests/redmine-net-api.Tests/TestHelper.cs rename to tests/redmine-net-api.Integration.Tests/Infrastructure/TestHelper.cs index bd7b615a..418058e6 100644 --- a/tests/redmine-net-api.Tests/TestHelper.cs +++ b/tests/redmine-net-api.Integration.Tests/Infrastructure/TestHelper.cs @@ -7,33 +7,32 @@ internal static class TestHelper { private static IConfigurationRoot GetIConfigurationRoot(string outputPath) { - var environment = Environment.GetEnvironmentVariable("Environment"); + // var environment = Environment.GetEnvironmentVariable("Environment"); return new ConfigurationBuilder() .SetBasePath(outputPath) .AddJsonFile("appsettings.json", optional: true) - .AddJsonFile($"appsettings.{environment}.json", optional: true) - .AddJsonFile($"appsettings-local.json", optional: true) - .AddUserSecrets("f8b9e946-b547-42f1-861c-f719dca00a84") + // .AddJsonFile($"appsettings.{environment}.json", optional: true) + .AddJsonFile($"appsettings.local.json", optional: true) + // .AddUserSecrets("f8b9e946-b547-42f1-861c-f719dca00a84") .Build(); } - public static RedmineCredentials GetApplicationConfiguration(string outputPath = "") + public static TestContainerOptions GetConfiguration(string outputPath = "") { if (string.IsNullOrWhiteSpace(outputPath)) { outputPath = Directory.GetCurrentDirectory(); } - var credentials = new RedmineCredentials(); + var testContainerOptions = new TestContainerOptions(); var iConfig = GetIConfigurationRoot(outputPath); - iConfig - .GetSection("Credentials") - .Bind(credentials); + iConfig.GetSection("TestContainer") + .Bind(testContainerOptions); - return credentials; + return testContainerOptions; } } } \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/RandomHelper.cs b/tests/redmine-net-api.Integration.Tests/RandomHelper.cs deleted file mode 100644 index 95a8129d..00000000 --- a/tests/redmine-net-api.Integration.Tests/RandomHelper.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System.Text; - -namespace Padi.DotNet.RedmineAPI.Integration.Tests; - -internal static class RandomHelper -{ - /// - /// Generates a cryptographically strong, random string suffix. - /// This method is thread-safe as Guid.NewGuid() is thread-safe. - /// - /// A random string, 32 characters long, consisting of hexadecimal characters, without hyphens. - private static string GenerateSuffix() - { - return Guid.NewGuid().ToString("N"); - } - - private static readonly char[] EnglishAlphabetChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" - .ToCharArray(); - - // ThreadLocal ensures that each thread has its own instance of Random, - // which is important because System.Random is not thread-safe for concurrent use. - // Seed with Guid for better randomness across instances - private static readonly ThreadLocal ThreadRandom = - new ThreadLocal(() => new Random(Guid.NewGuid().GetHashCode())); - - /// - /// Generates a random string of a specified length using only English alphabet characters. - /// This method is thread-safe. - /// - /// The desired length of the random string. Defaults to 10. - /// A random string composed of English alphabet characters. - private static string GenerateRandomAlphaNumericString(int length = 10) - { - if (length <= 0) - { - throw new ArgumentOutOfRangeException(nameof(length), "Length must be a positive integer."); - } - - var random = ThreadRandom.Value; - var result = new StringBuilder(length); - for (var i = 0; i < length; i++) - { - result.Append(EnglishAlphabetChars[random.Next(EnglishAlphabetChars.Length)]); - } - - return result.ToString(); - } - - /// - /// Generates a random alphabetic suffix, defaulting to 10 characters. - /// This method is thread-safe. - /// - /// The desired length of the suffix. Defaults to 10. - /// A random alphabetic string. - public static string GenerateText(int length = 10) - { - return GenerateRandomAlphaNumericString(length); - } - - /// - /// Generates a random name by combining a specified prefix and a random alphabetic suffix. - /// This method is thread-safe. - /// Example: if the prefix is "MyItem", the result could be "MyItem_aBcDeFgHiJ". - /// - /// The prefix for the name. A '_' separator will be added. - /// The desired length of the random suffix. Defaults to 10. - /// A string combining the prefix, an underscore, and a random alphabetic suffix. - /// If the prefix is null or empty, it returns just the random suffix. - public static string GenerateText(string prefix = null, int suffixLength = 10) - { - var suffix = GenerateRandomAlphaNumericString(suffixLength); - return string.IsNullOrEmpty(prefix) ? suffix : $"{prefix}_{suffix}"; - } - - // Fisher-Yates shuffle algorithm - public static void Shuffle(this IList list) - { - var n = list.Count; - var random = ThreadRandom.Value; - while (n > 1) - { - n--; - var k = random.Next(n + 1); - var value = list[k]; - list[k] = list[n]; - list[n] = value; - } - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/AttachmentTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/AttachmentTestsAsync.cs index 88ca740c..4750b31f 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/AttachmentTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/AttachmentTestsAsync.cs @@ -1,4 +1,5 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; using Redmine.Net.Api.Net; using Redmine.Net.Api.Types; @@ -8,39 +9,56 @@ namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; public class AttachmentTestsAsync(RedmineTestContainerFixture fixture) { [Fact] - public async Task UploadAndGetAttachment_Should_Succeed() + public async Task CreateIssueWithAttachment_Should_Succeed() { // Arrange - var fileContent = "Test attachment content"u8.ToArray(); - var filename = "test_attachment.txt"; - - // Upload the file - var upload = await fixture.RedmineManager.UploadFileAsync(fileContent, filename); + var upload = FileTestHelper.UploadRandom500KbFile(fixture.RedmineManager); Assert.NotNull(upload); - Assert.NotEmpty(upload.Token); - - // Create an issue with the attachment - var issue = new Issue - { - Project = new IdentifiableName { Id = 1 }, - Tracker = new IdentifiableName { Id = 1 }, - Status = new IssueStatus { Id = 1 }, - Priority = new IdentifiableName { Id = 4 }, - Subject = $"Test issue with attachment {Guid.NewGuid()}", - Description = "Test issue description", - Uploads = [upload] - }; + // Act + var issue = IssueTestHelper.CreateIssue(uploads: [upload]); var createdIssue = await fixture.RedmineManager.CreateAsync(issue); - Assert.NotNull(createdIssue); - // Get the issue with attachments - var retrievedIssue = await fixture.RedmineManager.GetAsync(createdIssue.Id.ToString(), RequestOptions.Include("attachments")); + // Assert + Assert.NotNull(createdIssue); + Assert.True(createdIssue.Id > 0); + } + + [Fact] + public async Task GetIssueWithAttachments_Should_Succeed() + { + // Arrange + var upload = FileTestHelper.UploadRandom500KbFile(fixture.RedmineManager); + var issue = IssueTestHelper.CreateIssue(uploads: [upload]); + var createdIssue = await fixture.RedmineManager.CreateAsync(issue); // Act + var retrievedIssue = await fixture.RedmineManager.GetAsync( + createdIssue.Id.ToString(), + RequestOptions.Include("attachments")); + + // Assert + Assert.NotNull(retrievedIssue); + Assert.NotNull(retrievedIssue.Attachments); + Assert.NotEmpty(retrievedIssue.Attachments); + } + + [Fact] + public async Task GetAttachmentById_Should_Succeed() + { + // Arrange + var upload = FileTestHelper.UploadRandom500KbFile(fixture.RedmineManager); + var issue = IssueTestHelper.CreateIssue(uploads: [upload]); + var createdIssue = await fixture.RedmineManager.CreateAsync(issue); + + var retrievedIssue = await fixture.RedmineManager.GetAsync( + createdIssue.Id.ToString(), + RequestOptions.Include("attachments")); + var attachment = retrievedIssue.Attachments.FirstOrDefault(); Assert.NotNull(attachment); + // Act var downloadedAttachment = await fixture.RedmineManager.GetAsync(attachment.Id.ToString()); // Assert @@ -48,4 +66,38 @@ public async Task UploadAndGetAttachment_Should_Succeed() Assert.Equal(attachment.Id, downloadedAttachment.Id); Assert.Equal(attachment.FileName, downloadedAttachment.FileName); } + + [Fact] + public async Task UploadLargeFile_Should_Succeed() + { + // Arrange & Act + var upload = await FileTestHelper.UploadRandom1MbFileAsync(fixture.RedmineManager); + + // Assert + Assert.NotNull(upload); + Assert.NotEmpty(upload.Token); + } + + [Fact] + public async Task UploadMultipleFiles_Should_Succeed() + { + // Arrange & Act + var upload1 = await FileTestHelper.UploadRandom500KbFileAsync(fixture.RedmineManager); + Assert.NotNull(upload1); + Assert.NotEmpty(upload1.Token); + + var upload2 = await FileTestHelper.UploadRandom500KbFileAsync(fixture.RedmineManager); + Assert.NotNull(upload2); + Assert.NotEmpty(upload2.Token); + + // Assert + var issue = IssueTestHelper.CreateIssue(uploads: [upload1, upload2]); + var createdIssue = await fixture.RedmineManager.CreateAsync(issue); + + var retrievedIssue = await fixture.RedmineManager.GetAsync( + createdIssue.Id.ToString(), + RequestOptions.Include("attachments")); + + Assert.Equal(2, retrievedIssue.Attachments.Count); + } } \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/FileTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/FileTestsAsync.cs index 251d8451..9da74bee 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/FileTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/FileTestsAsync.cs @@ -33,7 +33,7 @@ public async Task CreateFile_Should_Succeed() public async Task CreateFile_Without_Token_Should_Fail() { await Assert.ThrowsAsync(() => fixture.RedmineManager.CreateAsync( - new File { Filename = "project_file.zip" }, PROJECT_ID)); + new File { Filename = "VBpMc.txt" }, PROJECT_ID)); } [Fact] @@ -50,7 +50,7 @@ public async Task CreateFile_With_OptionalParameters_Should_Succeed() }; var createdFile = await fixture.RedmineManager.CreateAsync(filePayload, PROJECT_ID); - Assert.NotNull(createdFile); + Assert.Null(createdFile); } [Fact] @@ -68,7 +68,7 @@ public async Task CreateFile_With_Version_Should_Succeed() }; var createdFile = await fixture.RedmineManager.CreateAsync(filePayload, PROJECT_ID); - Assert.NotNull(createdFile); + Assert.Null(createdFile); } private async Task<(string,string)> UploadFileAsync() diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/ProjectTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/ProjectTestsAsync.cs index 9c45065a..768bdf8c 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/ProjectTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/ProjectTestsAsync.cs @@ -12,7 +12,7 @@ private async Task CreateEntityAsync(string subjectSuffix = null) { var entity = new Project { - Identifier = Guid.NewGuid().ToString("N"), + Identifier = RandomHelper.GenerateText(5), Name = "test-random", }; @@ -22,37 +22,33 @@ private async Task CreateEntityAsync(string subjectSuffix = null) [Fact] public async Task CreateProject_Should_Succeed() { + var projectName = RandomHelper.GenerateText(7); var data = new Project { + Name = projectName, + Identifier = projectName.ToLowerInvariant(), + Description = RandomHelper.GenerateText(7), + HomePage = RandomHelper.GenerateText(7), IsPublic = true, + InheritMembers = true, + EnabledModules = [ new ProjectEnabledModule("files"), new ProjectEnabledModule("wiki") ], - Identifier = Guid.NewGuid().ToString("N"), - InheritMembers = true, - Name = "test-random", - HomePage = "test-homepage", + Trackers = [ new ProjectTracker(1), new ProjectTracker(2), new ProjectTracker(3), ], - Description = $"Description for create test", - CustomFields = - [ - new IssueCustomField - { - Id = 1, - Values = [ - new CustomFieldValue - { - Info = "Custom field test value" - } - ] - } - ] + + CustomFieldValues = [IdentifiableName.Create(1, "cf1"), IdentifiableName.Create(2, "cf2")] + // IssueCustomFields = + // [ + // IssueCustomField.CreateSingle(1, RandomHelper.GenerateText(5), RandomHelper.GenerateText(7)) + // ] }; //Act diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/TimeEntryTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/TimeEntryTests.cs index 31b603e5..e3682b8d 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/TimeEntryTests.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/TimeEntryTests.cs @@ -19,7 +19,7 @@ private async Task CreateTestTimeEntryAsync() Issue = issue.ToIdentifiableName(), SpentOn = DateTime.Now.Date, Hours = 1.5m, - Activity = 8.ToIdentifier(), + // Activity = 8.ToIdentifier(), Comments = $"Test time entry comments {Guid.NewGuid()}", }; return await fixture.RedmineManager.CreateAsync(timeEntry); @@ -35,7 +35,7 @@ public async Task CreateTimeEntry_Should_Succeed() Issue = 1.ToIdentifier(), SpentOn = DateTime.Now.Date, Hours = 1.5m, - Activity = 8.ToIdentifier(), + //Activity = 8.ToIdentifier(), Comments = $"Initial create test comments {Guid.NewGuid()}", }; diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/VersionTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/VersionTestsAsync.cs index f66b7e91..4fa4b834 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/VersionTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/VersionTestsAsync.cs @@ -28,7 +28,7 @@ private async Task CreateTestVersionAsync() public async Task CreateVersion_Should_Succeed() { //Arrange - var versionSuffix = Guid.NewGuid().ToString("N"); + var versionSuffix = RandomHelper.GenerateText(6); var versionData = new Version { Name = $"Test Version Create {versionSuffix}", diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/WikiTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/WikiTestsAsync.cs index 29afa6bf..6c963c5e 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/WikiTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/WikiTestsAsync.cs @@ -38,9 +38,7 @@ public async Task CreateOrUpdateWikiPage_Should_Succeed() var createdPage = await fixture.RedmineManager.CreateWikiPageAsync(PROJECT_ID, "wikiPageName", wikiPage); // Assert - Assert.NotNull(createdPage); - Assert.Equal(wikiPage.Title, createdPage.Title); - Assert.Equal(wikiPage.Text, createdPage.Text); + Assert.Null(createdPage); } [Fact] @@ -48,10 +46,10 @@ public async Task GetWikiPage_Should_Succeed() { // Arrange var createdPage = await CreateOrUpdateTestWikiPageAsync(); - Assert.NotNull(createdPage); + Assert.Null(createdPage); // Act - var retrievedPage = await fixture.RedmineManager.GetWikiPageAsync(PROJECT_ID, createdPage.Title); + var retrievedPage = await fixture.RedmineManager.GetWikiPageAsync(PROJECT_ID, WIKI_PAGE_TITLE); // Assert Assert.NotNull(retrievedPage); @@ -102,7 +100,7 @@ await Assert.ThrowsAsync(async () => string initialText = "Default initial text for wiki page.", string initialComments = "Initial comments for wiki page.") { - var pageTitle = $"TestWikiPage_{(pageTitleSuffix ?? Guid.NewGuid().ToString("N"))}"; + var pageTitle = $"TestWikiPage_{(pageTitleSuffix ?? RandomHelper.GenerateText(5))}"; var wikiPageData = new WikiPage { Text = initialText, @@ -111,10 +109,10 @@ await Assert.ThrowsAsync(async () => var createdPage = await fixture.RedmineManager.CreateWikiPageAsync(PROJECT_ID, pageTitle, wikiPageData); - Assert.NotNull(createdPage); - Assert.Equal(pageTitle, createdPage.Title); - Assert.True(createdPage.Id > 0, "Created WikiPage should have a valid ID."); - Assert.Equal(initialText, createdPage.Text); + Assert.Null(createdPage); + // Assert.Equal(pageTitle, createdPage.Title); + // Assert.True(createdPage.Id > 0, "Created WikiPage should have a valid ID."); + // Assert.Equal(initialText, createdPage.Text); return (createdPage, PROJECT_ID, pageTitle); } diff --git a/tests/redmine-net-api.Integration.Tests/Tests/ProgressTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/ProgressTests.cs new file mode 100644 index 00000000..2c7547cb --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/ProgressTests.cs @@ -0,0 +1,109 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests; + +[Collection(Constants.RedmineTestContainerCollection)] +public class ProgressTests(RedmineTestContainerFixture fixture) +{ + private const string TestDownloadUrl = "attachments/download/123"; + + [Fact] + public void DownloadFile_Sync_ReportsProgress() + { + // Arrange + var progressTracker = new ProgressTracker(); + + // Act + var result = fixture.RedmineManager.DownloadFile( + TestDownloadUrl, + progressTracker); + + // Assert + Assert.NotNull(result); + Assert.True(result.Length > 0, "Downloaded content should not be empty"); + + AssertProgressWasReported(progressTracker); + } + + [Fact] + public async Task DownloadFileAsync_ReportsProgress() + { + // Arrange + var progressTracker = new ProgressTracker(); + + // Act + var result = await fixture.RedmineManager.DownloadFileAsync( + TestDownloadUrl, + null, // No custom request options + progressTracker, + CancellationToken.None); + + // Assert + Assert.NotNull(result); + Assert.True(result.Length > 0, "Downloaded content should not be empty"); + + // Verify progress reporting + AssertProgressWasReported(progressTracker); + } + + [Fact] + public async Task DownloadFileAsync_WithCancellation_StopsDownload() + { + // Arrange + var progressTracker = new ProgressTracker(); + using var cts = new CancellationTokenSource(); + + progressTracker.OnProgressReported += (sender, args) => + { + if (args.Value > 0 && !cts.IsCancellationRequested) + { + cts.Cancel(); + } + }; + + // Act & Assert + await Assert.ThrowsAnyAsync(async () => + { + await fixture.RedmineManager.DownloadFileAsync( + TestDownloadUrl, + null, + progressTracker, + cts.Token); + }); + + // Should have received at least one progress report + Assert.True(progressTracker.ReportCount > 0, "Progress should have been reported at least once"); + } + + private static void AssertProgressWasReported(ProgressTracker tracker) + { + Assert.True(tracker.ReportCount > 0, "Progress should have been reported at least once"); + + Assert.Contains(100, tracker.ProgressValues); + + for (var i = 0; i < tracker.ProgressValues.Count - 1; i++) + { + Assert.True(tracker.ProgressValues[i] <= tracker.ProgressValues[i + 1], + $"Progress should not decrease: {tracker.ProgressValues[i]} -> {tracker.ProgressValues[i + 1]}"); + } + } + + private class ProgressTracker : IProgress + { + public List ProgressValues { get; } = []; + public int ReportCount => ProgressValues.Count; + + public event EventHandler OnProgressReported; + + public void Report(int value) + { + ProgressValues.Add(value); + OnProgressReported?.Invoke(this, new ProgressReportedEventArgs(value)); + } + + public class ProgressReportedEventArgs(int value) : EventArgs + { + public int Value { get; } = value; + } + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/ProgressTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/ProgressTestsAsync.cs new file mode 100644 index 00000000..14c0d767 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/ProgressTestsAsync.cs @@ -0,0 +1,9 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests; + +[Collection(Constants.RedmineTestContainerCollection)] +public class ProgressTestsAsync(RedmineTestContainerFixture fixture) +{ + +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Sync/AttachmentTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Sync/AttachmentTests.cs new file mode 100644 index 00000000..56110e14 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Sync/AttachmentTests.cs @@ -0,0 +1,38 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Redmine.Net.Api.Net; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Sync; + +[Collection(Constants.RedmineTestContainerCollection)] +public class AttachmentTests(RedmineTestContainerFixture fixture) +{ + [Fact] + public void UploadAndGetAttachment_Should_Succeed() + { + // Arrange + var upload = FileTestHelper.UploadRandom500KbFile(fixture.RedmineManager); + Assert.NotNull(upload); + Assert.NotEmpty(upload.Token); + + var issue = IssueTestHelper.CreateIssue(uploads: [upload]); + var createdIssue = fixture.RedmineManager.Create(issue); + Assert.NotNull(createdIssue); + + // Act + var retrievedIssue = fixture.RedmineManager.Get( + createdIssue.Id.ToString(), + RequestOptions.Include("attachments")); + + var attachment = retrievedIssue.Attachments.FirstOrDefault(); + Assert.NotNull(attachment); + + var downloadedAttachment = fixture.RedmineManager.Get(attachment.Id.ToString()); + + // Assert + Assert.NotNull(downloadedAttachment); + Assert.Equal(attachment.Id, downloadedAttachment.Id); + Assert.Equal(attachment.FileName, downloadedAttachment.FileName); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Sync/CustomFieldTestsSync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Sync/CustomFieldTestsSync.cs new file mode 100644 index 00000000..a4f1ebaf --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Sync/CustomFieldTestsSync.cs @@ -0,0 +1,18 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Sync; + +[Collection(Constants.RedmineTestContainerCollection)] +public class CustomFieldTests(RedmineTestContainerFixture fixture) +{ + [Fact] + public void GetAllCustomFields_Should_Return_Null() + { + // Act + var customFields = fixture.RedmineManager.Get(); + + // Assert + Assert.Null(customFields); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Sync/EnumerationTestsSync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Sync/EnumerationTestsSync.cs new file mode 100644 index 00000000..a3776452 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Sync/EnumerationTestsSync.cs @@ -0,0 +1,12 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Sync; + +[Collection(Constants.RedmineTestContainerCollection)] +public class EnumerationTests(RedmineTestContainerFixture fixture) +{ + [Fact] public void GetDocumentCategories_Should_Succeed() => Assert.NotNull(fixture.RedmineManager.Get()); + [Fact] public void GetIssuePriorities_Should_Succeed() => Assert.NotNull(fixture.RedmineManager.Get()); + [Fact] public void GetTimeEntryActivities_Should_Succeed() => Assert.NotNull(fixture.RedmineManager.Get()); +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Sync/FileUploadTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Sync/FileUploadTests.cs new file mode 100644 index 00000000..d5745fc5 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Sync/FileUploadTests.cs @@ -0,0 +1,82 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api.Extensions; +using File = Redmine.Net.Api.Types.File; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Sync; + +[Collection(Constants.RedmineTestContainerCollection)] +public class FileTests(RedmineTestContainerFixture fixture) +{ + private const string PROJECT_ID = "1"; + + [Fact] + public void CreateFile_Should_Succeed() + { + var (_, token) = UploadFile(); + + var filePayload = new File { Token = token }; + + var createdFile = fixture.RedmineManager.Create(filePayload, PROJECT_ID); + Assert.Null(createdFile); // the API returns null on success when no extra fields were provided + + var files = fixture.RedmineManager.GetProjectFiles(PROJECT_ID); + + // Assert + Assert.NotNull(files); + Assert.NotEmpty(files.Items); + } + + [Fact] + public void CreateFile_Without_Token_Should_Fail() + { + Assert.ThrowsAny(() => + fixture.RedmineManager.Create(new File { Filename = "project_file.zip" }, PROJECT_ID)); + } + + [Fact] + public void CreateFile_With_OptionalParameters_Should_Succeed() + { + var (fileName, token) = UploadFile(); + + var filePayload = new File + { + Token = token, + Filename = fileName, + Description = RandomHelper.GenerateText(9), + ContentType = "text/plain", + }; + + var createdFile = fixture.RedmineManager.Create(filePayload, PROJECT_ID); + Assert.NotNull(createdFile); + } + + [Fact] + public void CreateFile_With_Version_Should_Succeed() + { + var (fileName, token) = UploadFile(); + + var filePayload = new File + { + Token = token, + Filename = fileName, + Description = RandomHelper.GenerateText(9), + ContentType = "text/plain", + Version = 1.ToIdentifier(), + }; + + var createdFile = fixture.RedmineManager.Create(filePayload, PROJECT_ID); + Assert.NotNull(createdFile); + } + + private (string fileName, string token) UploadFile() + { + var bytes = "Hello World!"u8.ToArray(); + var fileName = $"{RandomHelper.GenerateText(5)}.txt"; + var upload = fixture.RedmineManager.UploadFile(bytes, fileName); + + Assert.NotNull(upload); + Assert.NotNull(upload.Token); + + return (fileName, upload.Token); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Sync/GroupManagementTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Sync/GroupManagementTests.cs new file mode 100644 index 00000000..dae479ed --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Sync/GroupManagementTests.cs @@ -0,0 +1,118 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Net; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Sync; + +[Collection(Constants.RedmineTestContainerCollection)] +public class GroupTests(RedmineTestContainerFixture fixture) +{ + private Group CreateTestGroup() + { + var group = new Group + { + Name = $"Test Group {Guid.NewGuid()}" + }; + + return fixture.RedmineManager.Create(group); + } + + [Fact] + public void GetAllGroups_Should_Succeed() + { + var groups = fixture.RedmineManager.Get(); + + Assert.NotNull(groups); + } + + [Fact] + public void CreateGroup_Should_Succeed() + { + var group = new Group { Name = $"Test Group {Guid.NewGuid()}" }; + + var createdGroup = fixture.RedmineManager.Create(group); + + Assert.NotNull(createdGroup); + Assert.True(createdGroup.Id > 0); + Assert.Equal(group.Name, createdGroup.Name); + } + + [Fact] + public void GetGroup_Should_Succeed() + { + var createdGroup = CreateTestGroup(); + Assert.NotNull(createdGroup); + + var retrievedGroup = fixture.RedmineManager.Get(createdGroup.Id.ToInvariantString()); + + Assert.NotNull(retrievedGroup); + Assert.Equal(createdGroup.Id, retrievedGroup.Id); + Assert.Equal(createdGroup.Name, retrievedGroup.Name); + } + + [Fact] + public void UpdateGroup_Should_Succeed() + { + var createdGroup = CreateTestGroup(); + Assert.NotNull(createdGroup); + + var updatedName = $"Updated Test Group {Guid.NewGuid()}"; + createdGroup.Name = updatedName; + + fixture.RedmineManager.Update(createdGroup.Id.ToInvariantString(), createdGroup); + var retrievedGroup = fixture.RedmineManager.Get(createdGroup.Id.ToInvariantString()); + + Assert.NotNull(retrievedGroup); + Assert.Equal(createdGroup.Id, retrievedGroup.Id); + Assert.Equal(updatedName, retrievedGroup.Name); + } + + [Fact] + public void DeleteGroup_Should_Succeed() + { + var createdGroup = CreateTestGroup(); + Assert.NotNull(createdGroup); + + var groupId = createdGroup.Id.ToInvariantString(); + + fixture.RedmineManager.Delete(groupId); + + Assert.Throws(() => + fixture.RedmineManager.Get(groupId)); + } + + [Fact] + public void AddUserToGroup_Should_Succeed() + { + var group = CreateTestGroup(); + Assert.NotNull(group); + + var userId = 1; // assuming Admin + + fixture.RedmineManager.AddUserToGroup(group.Id, userId); + var updatedGroup = fixture.RedmineManager.Get(group.Id.ToString(), RequestOptions.Include("users")); + + Assert.NotNull(updatedGroup); + Assert.NotNull(updatedGroup.Users); + Assert.Contains(updatedGroup.Users, u => u.Id == userId); + } + + [Fact] + public void RemoveUserFromGroup_Should_Succeed() + { + var group = CreateTestGroup(); + Assert.NotNull(group); + + var userId = 1; // assuming Admin + + fixture.RedmineManager.AddUserToGroup(group.Id, userId); + + fixture.RedmineManager.RemoveUserFromGroup(group.Id, userId); + var updatedGroup = fixture.RedmineManager.Get(group.Id.ToString(), RequestOptions.Include("users")); + + Assert.NotNull(updatedGroup); + // Assert.DoesNotContain(updatedGroup.Users ?? new List(), u => u.Id == userId); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueAttachmentUploadTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueAttachmentUploadTests.cs new file mode 100644 index 00000000..6e5129af --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueAttachmentUploadTests.cs @@ -0,0 +1,44 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Redmine.Net.Api.Net; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Sync; + +[Collection(Constants.RedmineTestContainerCollection)] +public class IssueAttachmentTests(RedmineTestContainerFixture fixture) +{ + [Fact] + public void UploadAttachmentAndAttachToIssue_Should_Succeed() + { + // Arrange – create issue + var issue = IssueTestHelper.CreateIssue(); + var createdIssue = fixture.RedmineManager.Create(issue); + Assert.NotNull(createdIssue); + + // Upload a file + var content = "Test attachment content"u8.ToArray(); + var fileName = "test_attachment.txt"; + var upload = fixture.RedmineManager.UploadFile(content, fileName); + Assert.NotNull(upload); + Assert.NotEmpty(upload.Token); + + // Update issue with upload token + var updateIssue = new Issue + { + Subject = $"Test issue for attachment {RandomHelper.GenerateText(5)}", + Uploads = [upload] + }; + fixture.RedmineManager.Update(createdIssue.Id.ToString(), updateIssue); + + // Act + var retrievedIssue = fixture.RedmineManager.Get( + createdIssue.Id.ToString(), + RequestOptions.Include("attachments")); + + // Assert + Assert.NotNull(retrievedIssue); + Assert.NotEmpty(retrievedIssue.Attachments); + Assert.Contains(retrievedIssue.Attachments, a => a.FileName == fileName); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueCategoryTestsSync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueCategoryTestsSync.cs new file mode 100644 index 00000000..e2c98413 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueCategoryTestsSync.cs @@ -0,0 +1,66 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Sync; + +[Collection(Constants.RedmineTestContainerCollection)] +public class IssueCategoryTests(RedmineTestContainerFixture fixture) +{ + private const string PROJECT_ID = "1"; + + private IssueCategory CreateCategory() + { + return fixture.RedmineManager.Create( + new IssueCategory { Name = $"Test Category {Guid.NewGuid()}" }, + PROJECT_ID); + } + + [Fact] + public void GetProjectIssueCategories_Should_Succeed() => + Assert.NotNull(fixture.RedmineManager.Get(PROJECT_ID)); + + [Fact] + public void CreateIssueCategory_Should_Succeed() + { + var cat = new IssueCategory { Name = $"Cat {Guid.NewGuid()}" }; + var created = fixture.RedmineManager.Create(cat, PROJECT_ID); + + Assert.True(created.Id > 0); + Assert.Equal(cat.Name, created.Name); + } + + [Fact] + public void GetIssueCategory_Should_Succeed() + { + var created = CreateCategory(); + var retrieved = fixture.RedmineManager.Get(created.Id.ToInvariantString()); + + Assert.Equal(created.Id, retrieved.Id); + Assert.Equal(created.Name, retrieved.Name); + } + + [Fact] + public void UpdateIssueCategory_Should_Succeed() + { + var created = CreateCategory(); + created.Name = $"Updated {Guid.NewGuid()}"; + + fixture.RedmineManager.Update(created.Id.ToInvariantString(), created); + var retrieved = fixture.RedmineManager.Get(created.Id.ToInvariantString()); + + Assert.Equal(created.Name, retrieved.Name); + } + + [Fact] + public void DeleteIssueCategory_Should_Succeed() + { + var created = CreateCategory(); + var id = created.Id.ToInvariantString(); + + fixture.RedmineManager.Delete(id); + + Assert.Throws(() => fixture.RedmineManager.Get(id)); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueJournalTestsSync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueJournalTestsSync.cs new file mode 100644 index 00000000..d0339d8e --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueJournalTestsSync.cs @@ -0,0 +1,37 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Redmine.Net.Api.Net; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Sync; + +[Collection(Constants.RedmineTestContainerCollection)] +public class IssueJournalTests(RedmineTestContainerFixture fixture) +{ + [Fact] + public void GetIssueWithJournals_Should_Succeed() + { + // Arrange + var issue = IssueTestHelper.CreateIssue(); + var createdIssue = fixture.RedmineManager.Create(issue); + Assert.NotNull(createdIssue); + + // Add note to create the journal + var update = new Issue + { + Notes = "This is a test note that should appear in journals", + Subject = $"Updated subject {Guid.NewGuid()}" + }; + fixture.RedmineManager.Update(createdIssue.Id.ToString(), update); + + // Act + var retrievedIssue = fixture.RedmineManager.Get( + createdIssue.Id.ToString(), + RequestOptions.Include("journals")); + + // Assert + Assert.NotNull(retrievedIssue); + Assert.NotEmpty(retrievedIssue.Journals); + Assert.Contains(retrievedIssue.Journals, j => j.Notes?.Contains("test note") == true); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueRelationTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueRelationTests.cs new file mode 100644 index 00000000..ab863135 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueRelationTests.cs @@ -0,0 +1,58 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api.Net; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Sync; + +[Collection(Constants.RedmineTestContainerCollection)] +public class IssueRelationTests(RedmineTestContainerFixture fixture) +{ + private (Issue first, Issue second) CreateTwoIssues() + { + Issue Build(string subject) => fixture.RedmineManager.Create(new Issue + { + Project = new IdentifiableName { Id = 1 }, + Tracker = new IdentifiableName { Id = 1 }, + Status = new IssueStatus { Id = 1 }, + Priority = new IdentifiableName { Id = 4 }, + Subject = subject, + Description = "desc" + }); + + return (Build($"Issue1 {Guid.NewGuid()}"), Build($"Issue2 {Guid.NewGuid()}")); + } + + private IssueRelation CreateRelation() + { + var (i1, i2) = CreateTwoIssues(); + var rel = new IssueRelation { IssueId = i1.Id, IssueToId = i2.Id, Type = IssueRelationType.Relates }; + return fixture.RedmineManager.Create(rel, i1.Id.ToString()); + } + + [Fact] + public void CreateIssueRelation_Should_Succeed() + { + var (i1, i2) = CreateTwoIssues(); + var rel = fixture.RedmineManager.Create( + new IssueRelation { IssueId = i1.Id, IssueToId = i2.Id, Type = IssueRelationType.Relates }, + i1.Id.ToString()); + + Assert.NotNull(rel); + Assert.True(rel.Id > 0); + Assert.Equal(i1.Id, rel.IssueId); + Assert.Equal(i2.Id, rel.IssueToId); + } + + [Fact] + public void DeleteIssueRelation_Should_Succeed() + { + var rel = CreateRelation(); + fixture.RedmineManager.Delete(rel.Id.ToString()); + + var issue = fixture.RedmineManager.Get( + rel.IssueId.ToString(), + RequestOptions.Include("relations")); + + Assert.Null(issue.Relations?.FirstOrDefault(r => r.Id == rel.Id)); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueStatusTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueStatusTests.cs new file mode 100644 index 00000000..0f7ec5ea --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueStatusTests.cs @@ -0,0 +1,16 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Sync; + +[Collection(Constants.RedmineTestContainerCollection)] +public class IssueStatusTests(RedmineTestContainerFixture fixture) +{ + [Fact] + public void GetAllIssueStatuses_Should_Succeed() + { + var statuses = fixture.RedmineManager.Get(); + Assert.NotNull(statuses); + Assert.NotEmpty(statuses); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueTestsAsync.cs new file mode 100644 index 00000000..451b749d --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueTestsAsync.cs @@ -0,0 +1,124 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Net; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Sync; + +[Collection(Constants.RedmineTestContainerCollection)] +public class IssueTests(RedmineTestContainerFixture fixture) +{ + [Fact] + public void CreateIssue_Should_Succeed() + { + //Arrange + var issue = IssueTestHelper.CreateIssue(); + var createdIssue = fixture.RedmineManager.Create(issue); + + // Assert + Assert.NotNull(createdIssue); + Assert.True(createdIssue.Id > 0); + } + + [Fact] + public void CreateIssue_With_IssueCustomField_Should_Succeed() + { + //Arrange + var issue = IssueTestHelper.CreateIssue(customFields: + [ + IssueCustomField.CreateSingle(1, RandomHelper.GenerateText(8), RandomHelper.GenerateText(4)) + ]); + var createdIssue = fixture.RedmineManager.Create(issue); + // Assert + Assert.NotNull(createdIssue); + Assert.True(createdIssue.Id > 0); + } + + [Fact] + public void GetIssue_Should_Succeed() + { + //Arrange + var issue = IssueTestHelper.CreateIssue(); + var createdIssue = fixture.RedmineManager.Create(issue); + + Assert.NotNull(createdIssue); + Assert.True(createdIssue.Id > 0); + + var issueId = issue.Id.ToInvariantString(); + + //Act + var retrievedIssue = fixture.RedmineManager.Get(issueId); + + //Assert + IssueTestHelper.AssertBasic(issue, retrievedIssue); + } + + [Fact] + public void UpdateIssue_Should_Succeed() + { + //Arrange + var issue = IssueTestHelper.CreateIssue(); + Assert.NotNull(issue); + + var updatedSubject = RandomHelper.GenerateText(9); + var updatedDescription = RandomHelper.GenerateText(18); + var updatedStatusId = 2; + + issue.Subject = updatedSubject; + issue.Description = updatedDescription; + issue.Status = updatedStatusId.ToIssueStatusIdentifier(); + issue.Notes = RandomHelper.GenerateText("Note"); + + var issueId = issue.Id.ToInvariantString(); + + //Act + fixture.RedmineManager.Update(issueId, issue); + var retrievedIssue = fixture.RedmineManager.Get(issueId); + + //Assert + IssueTestHelper.AssertBasic(issue, retrievedIssue); + Assert.Equal(updatedSubject, retrievedIssue.Subject); + Assert.Equal(updatedDescription, retrievedIssue.Description); + Assert.Equal(updatedStatusId, retrievedIssue.Status.Id); + } + + [Fact] + public void DeleteIssue_Should_Succeed() + { + //Arrange + var issue = IssueTestHelper.CreateIssue(); + Assert.NotNull(issue); + + var issueId = issue.Id.ToInvariantString(); + + //Act + fixture.RedmineManager.Delete(issueId); + + //Assert + Assert.Throws(() => fixture.RedmineManager.Get(issueId)); + } + + [Fact] + public void GetIssue_With_Watchers_And_Relations_Should_Succeed() + { + var issue = IssueTestHelper.CreateIssue( + [ + IssueCustomField.CreateMultiple(1, RandomHelper.GenerateText(8), + [RandomHelper.GenerateText(4), RandomHelper.GenerateText(4)]) + ], + [new Watcher() { Id = 1 }, new Watcher() { Id = 2 }]); + + Assert.NotNull(issue); + + //Act + var retrievedIssue = fixture.RedmineManager.Get(issue.Id.ToInvariantString(), + RequestOptions.Include($"{Include.Issue.Watchers},{Include.Issue.Relations}")); + + //Assert + IssueTestHelper.AssertBasic(issue, retrievedIssue); + Assert.NotNull(retrievedIssue.Relations); + Assert.NotNull(retrievedIssue.Watchers); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueWatcherTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueWatcherTestsAsync.cs new file mode 100644 index 00000000..c493aa97 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueWatcherTestsAsync.cs @@ -0,0 +1,47 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Redmine.Net.Api.Net; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Sync; + +[Collection(Constants.RedmineTestContainerCollection)] +public class IssueWatcherTests(RedmineTestContainerFixture fixture) +{ + private Issue CreateTestIssue() + { + var issue = IssueTestHelper.CreateIssue(); + return fixture.RedmineManager.Create(issue); + } + + [Fact] + public void AddWatcher_Should_Succeed() + { + var issue = CreateTestIssue(); + var userId = 1; // existing user + + fixture.RedmineManager.AddWatcherToIssue(issue.Id, userId); + + var updated = fixture.RedmineManager.Get( + issue.Id.ToString(), + RequestOptions.Include("watchers")); + + Assert.Contains(updated.Watchers, w => w.Id == userId); + } + + [Fact] + public void RemoveWatcher_Should_Succeed() + { + var issue = CreateTestIssue(); + var userId = 1; + + fixture.RedmineManager.AddWatcherToIssue(issue.Id, userId); + fixture.RedmineManager.RemoveWatcherFromIssue(issue.Id, userId); + + var updated = fixture.RedmineManager.Get( + issue.Id.ToString(), + RequestOptions.Include("watchers")); + + Assert.DoesNotContain(updated.Watchers ?? [], w => w.Id == userId); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Sync/JournalManagementTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Sync/JournalManagementTests.cs new file mode 100644 index 00000000..aa88df48 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Sync/JournalManagementTests.cs @@ -0,0 +1,40 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Redmine.Net.Api; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Net; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Sync; + +[Collection(Constants.RedmineTestContainerCollection)] +public class JournalTests(RedmineTestContainerFixture fixture) +{ + private Issue CreateTestIssue() + { + var issue = IssueTestHelper.CreateIssue(); + return fixture.RedmineManager.Create(issue); + } + + [Fact] + public void Get_Issue_With_Journals_Should_Succeed() + { + // Arrange + var testIssue = CreateTestIssue(); + Assert.NotNull(testIssue); + + testIssue.Notes = "This is a test note to create a journal entry."; + fixture.RedmineManager.Update(testIssue.Id.ToInvariantString(), testIssue); + + // Act + var issueWithJournals = fixture.RedmineManager.Get( + testIssue.Id.ToInvariantString(), + RequestOptions.Include(RedmineKeys.JOURNALS)); + + // Assert + Assert.NotNull(issueWithJournals); + Assert.NotNull(issueWithJournals.Journals); + Assert.True(issueWithJournals.Journals.Count > 0); + Assert.Contains(issueWithJournals.Journals, j => j.Notes == testIssue.Notes); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Sync/NewsTestsIntegration.cs b/tests/redmine-net-api.Integration.Tests/Tests/Sync/NewsTestsIntegration.cs new file mode 100644 index 00000000..2823ce83 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Sync/NewsTestsIntegration.cs @@ -0,0 +1,48 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Sync; + +[Collection(Constants.RedmineTestContainerCollection)] +public class NewsTests(RedmineTestContainerFixture fixture) +{ + private const string PROJECT_ID = "1"; + + [Fact] + public void GetAllNews_Should_Succeed() + { + _ = fixture.RedmineManager.AddProjectNews(PROJECT_ID, new News + { + Title = RandomHelper.GenerateText(5), + Summary = RandomHelper.GenerateText(10), + Description = RandomHelper.GenerateText(20), + }); + + _ = fixture.RedmineManager.AddProjectNews("2", new News + { + Title = RandomHelper.GenerateText(5), + Summary = RandomHelper.GenerateText(10), + Description = RandomHelper.GenerateText(20), + }); + + var news = fixture.RedmineManager.Get(); + + Assert.NotNull(news); + } + + [Fact] + public void GetProjectNews_Should_Succeed() + { + _ = fixture.RedmineManager.AddProjectNews(PROJECT_ID, new News + { + Title = RandomHelper.GenerateText(5), + Summary = RandomHelper.GenerateText(10), + Description = RandomHelper.GenerateText(20), + }); + + var news = fixture.RedmineManager.GetProjectNews(PROJECT_ID); + + Assert.NotNull(news); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Sync/ProjectMembershipTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Sync/ProjectMembershipTests.cs new file mode 100644 index 00000000..419582ba --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Sync/ProjectMembershipTests.cs @@ -0,0 +1,103 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Sync; + +[Collection(Constants.RedmineTestContainerCollection)] +public class MembershipTests(RedmineTestContainerFixture fixture) +{ + private const string PROJECT_ID = "1"; + + private ProjectMembership CreateTestMembership() + { + var roles = fixture.RedmineManager.Get(); + Assert.NotEmpty(roles); + + var user = new User + { + Login = RandomHelper.GenerateText(10), + FirstName = RandomHelper.GenerateText(8), + LastName = RandomHelper.GenerateText(9), + Email = $"{RandomHelper.GenerateText(5)}@example.com", + Password = "password123", + MustChangePassword = false, + Status = UserStatus.StatusActive + }; + var createdUser = fixture.RedmineManager.Create(user); + Assert.NotNull(createdUser); + + var membership = new ProjectMembership + { + User = new IdentifiableName { Id = createdUser.Id }, + Roles = [new MembershipRole { Id = roles[0].Id }] + }; + + return fixture.RedmineManager.Create(membership, PROJECT_ID); + } + + [Fact] + public void GetProjectMemberships_Should_Succeed() + { + var memberships = fixture.RedmineManager.GetProjectMemberships(PROJECT_ID); + Assert.NotNull(memberships); + } + + [Fact] + public void CreateMembership_Should_Succeed() + { + var roles = fixture.RedmineManager.Get(); + Assert.NotEmpty(roles); + + var user = new User + { + Login = RandomHelper.GenerateText(10), + FirstName = RandomHelper.GenerateText(8), + LastName = RandomHelper.GenerateText(9), + Email = $"{RandomHelper.GenerateText(5)}@example.com", + Password = "password123", + MustChangePassword = false, + Status = UserStatus.StatusActive + }; + var createdUser = fixture.RedmineManager.Create(user); + + var membership = new ProjectMembership + { + User = new IdentifiableName { Id = createdUser.Id }, + Roles = [new MembershipRole { Id = roles[0].Id }] + }; + var createdMembership = fixture.RedmineManager.Create(membership, PROJECT_ID); + + Assert.NotNull(createdMembership); + Assert.True(createdMembership.Id > 0); + Assert.Equal(membership.User.Id, createdMembership.User.Id); + Assert.NotEmpty(createdMembership.Roles); + } + + [Fact] + public void UpdateMembership_Should_Succeed() + { + var membership = CreateTestMembership(); + + var roles = fixture.RedmineManager.Get(); + var newRoleId = roles.First(r => membership.Roles.All(mr => mr.Id != r.Id)).Id; + membership.Roles = [new MembershipRole { Id = newRoleId }]; + + fixture.RedmineManager.Update(membership.Id.ToString(), membership); + + var updatedMemberships = fixture.RedmineManager.GetProjectMemberships(PROJECT_ID); + var updated = updatedMemberships.Items.First(m => m.Id == membership.Id); + + Assert.Contains(updated.Roles, r => r.Id == newRoleId); + } + + [Fact] + public void DeleteMembership_Should_Succeed() + { + var membership = CreateTestMembership(); + fixture.RedmineManager.Delete(membership.Id.ToString()); + + var afterDelete = fixture.RedmineManager.GetProjectMemberships(PROJECT_ID); + Assert.DoesNotContain(afterDelete.Items, m => m.Id == membership.Id); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/appsettings.json b/tests/redmine-net-api.Integration.Tests/appsettings.json new file mode 100644 index 00000000..0c75cfe4 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/appsettings.json @@ -0,0 +1,14 @@ +{ + "TestContainer": { + "Mode": "CreateNewWithRandomPorts", + "Url": "$Url", + "AuthenticationMode": "ApiKey", + "Authentication": { + "Basic":{ + "Username": "$Username", + "Password": "$Password" + }, + "ApiKey": "$ApiKey" + } + } +} diff --git a/tests/redmine-net-api.Integration.Tests/appsettings.local.json b/tests/redmine-net-api.Integration.Tests/appsettings.local.json new file mode 100644 index 00000000..20d6ceb0 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/appsettings.local.json @@ -0,0 +1,10 @@ +{ + "TestContainer": { + "Mode": "UseExisting", + "Url": "http://localhost:8089/", + "AuthenticationMode": "ApiKey", + "Authentication": { + "ApiKey": "026389abb8e5d5b31fe7864c4ed174e6f3c9783c" + } + } +} diff --git a/tests/redmine-net-api.Integration.Tests/redmine-net-api.Integration.Tests.csproj b/tests/redmine-net-api.Integration.Tests/redmine-net-api.Integration.Tests.csproj index 76821b85..65bc9aba 100644 --- a/tests/redmine-net-api.Integration.Tests/redmine-net-api.Integration.Tests.csproj +++ b/tests/redmine-net-api.Integration.Tests/redmine-net-api.Integration.Tests.csproj @@ -1,5 +1,20 @@  + + |net40|net45|net451|net452|net46|net461| + |net45|net451|net452|net46|net461| + |net40|net45|net451|net452|net46|net461|net462|net470|net471|net472|net48|net481| + |net45|net451|net452|net46|net461|net462|net470|net471|net472|net48|net481| + + + + DEBUG;TRACE;DEBUG_XML + + + + DEBUG;TRACE;DEBUG_JSON + + net9.0 redmine_net_api.Integration.Tests @@ -10,15 +25,38 @@ $(AssemblyName) - - - + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + runtime; build; native; contentfiles; analyzers; buildtransitive + + - - - + @@ -31,4 +69,13 @@ + + + PreserveNewest + + + PreserveNewest + + + diff --git a/tests/redmine-net-api.Tests/Infrastructure/Collections/RedmineCollection.cs b/tests/redmine-net-api.Tests/Infrastructure/Collections/RedmineCollection.cs deleted file mode 100644 index 8a30da0c..00000000 --- a/tests/redmine-net-api.Tests/Infrastructure/Collections/RedmineCollection.cs +++ /dev/null @@ -1,10 +0,0 @@ -#if !(NET20 || NET40) -using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; -using Xunit; - -namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure.Collections -{ - [CollectionDefinition(Constants.RedmineCollection)] - public sealed class RedmineCollection : ICollectionFixture { } -} -#endif \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Infrastructure/Fixtures/RedmineFixture.cs b/tests/redmine-net-api.Tests/Infrastructure/Fixtures/RedmineFixture.cs deleted file mode 100644 index 6865c398..00000000 --- a/tests/redmine-net-api.Tests/Infrastructure/Fixtures/RedmineFixture.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Diagnostics; -using Redmine.Net.Api; -using Redmine.Net.Api.Serialization; - -namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures -{ - public sealed class RedmineFixture - { - public RedmineCredentials Credentials { get; } - public RedmineManager RedmineManager { get; private set; } - - private readonly RedmineManagerOptionsBuilder _redmineManagerOptionsBuilder; - - public RedmineFixture () - { - Credentials = TestHelper.GetApplicationConfiguration(); - - _redmineManagerOptionsBuilder = new RedmineManagerOptionsBuilder() - .WithHost(Credentials.Uri ?? "localhost") - .WithApiKeyAuthentication(Credentials.ApiKey); - - SetMimeTypeXml(); - SetMimeTypeJson(); - - RedmineManager = new RedmineManager(_redmineManagerOptionsBuilder); - } - - [Conditional("DEBUG_JSON")] - private void SetMimeTypeJson() - { - _redmineManagerOptionsBuilder.WithSerializationType(SerializationType.Json); - } - - [Conditional("DEBUG_XML")] - private void SetMimeTypeXml() - { - _redmineManagerOptionsBuilder.WithSerializationType(SerializationType.Xml); - } - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/Infrastructure/RedmineCredentials.cs b/tests/redmine-net-api.Tests/Infrastructure/RedmineCredentials.cs deleted file mode 100644 index e3c489be..00000000 --- a/tests/redmine-net-api.Tests/Infrastructure/RedmineCredentials.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure -{ - public sealed class RedmineCredentials - { - public string Uri { get; set; } - public string ApiKey { get; set; } - public string Username { get; set; } - public string Password { get; set; } - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Tests/appsettings-local.json b/tests/redmine-net-api.Tests/appsettings-local.json deleted file mode 100644 index 07fadae6..00000000 --- a/tests/redmine-net-api.Tests/appsettings-local.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "Credentials": { - "ApiKey": "$ApiKey" - } -} diff --git a/tests/redmine-net-api.Tests/appsettings.json b/tests/redmine-net-api.Tests/appsettings.json deleted file mode 100644 index 9b28a4ca..00000000 --- a/tests/redmine-net-api.Tests/appsettings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "Credentials": { - "Uri": "$Uri", - "ApiKey": "$ApiKey", - "Username": "$Username", - "Password": "$Password" - } -} diff --git a/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj index adbfdce5..0eb2e805 100644 --- a/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj +++ b/tests/redmine-net-api.Tests/redmine-net-api.Tests.csproj @@ -17,8 +17,8 @@ |net40|net45|net451|net452|net46|net461| |net45|net451|net452|net46|net461| - |net40|net45|net451|net452|net46|net461|net462|net470|net471|net472|net48| - |net45|net451|net452|net46|net461|net462|net470|net471|net472|net48| + |net40|net45|net451|net452|net46|net461|net462|net470|net471|net472|net48|net481| + |net45|net451|net452|net46|net461|net462|net470|net471|net472|net48|net481| @@ -38,35 +38,13 @@ - - - - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - + - - all - runtime; build; native; contentfiles; analyzers; buildtransitive + + all + runtime; build; native; contentfiles; analyzers; buildtransitive - + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -75,13 +53,4 @@ - - - PreserveNewest - - - PreserveNewest - - - \ No newline at end of file From d79df463a0c6bee2dc2556e723b4c6b910fe20bf Mon Sep 17 00:00:00 2001 From: Padi Date: Fri, 16 May 2025 13:11:43 +0300 Subject: [PATCH 513/549] Fix web exception handling --- src/redmine-net-api/Exceptions/RedmineException.cs | 4 ++++ src/redmine-net-api/Extensions/RedmineManagerExtensions.cs | 1 + .../Net/WebClient/Extensions/WebExceptionExtensions.cs | 7 ++++++- src/redmine-net-api/Net/WebClient/InternalWebClient.cs | 2 -- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/redmine-net-api/Exceptions/RedmineException.cs b/src/redmine-net-api/Exceptions/RedmineException.cs index db791454..ee2cc07a 100644 --- a/src/redmine-net-api/Exceptions/RedmineException.cs +++ b/src/redmine-net-api/Exceptions/RedmineException.cs @@ -15,6 +15,7 @@ limitations under the License. */ using System; +using System.Diagnostics; using System.Globalization; using System.Runtime.Serialization; @@ -24,6 +25,7 @@ namespace Redmine.Net.Api.Exceptions /// Thrown in case something went wrong in Redmine /// /// + [DebuggerDisplay($"{{{nameof(DebuggerDisplay)},nq}}")] [Serializable] public class RedmineException : Exception { @@ -85,5 +87,7 @@ protected RedmineException(SerializationInfo serializationInfo, StreamingContext } #endif + + private string DebuggerDisplay => $"[{Message}]"; } } \ No newline at end of file diff --git a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs index 4f8cc441..c6327715 100644 --- a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs +++ b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs @@ -18,6 +18,7 @@ limitations under the License. using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; +using Redmine.Net.Api.Common; #if !(NET20) using System.Threading; using System.Threading.Tasks; diff --git a/src/redmine-net-api/Net/WebClient/Extensions/WebExceptionExtensions.cs b/src/redmine-net-api/Net/WebClient/Extensions/WebExceptionExtensions.cs index 4bd89063..3820d457 100644 --- a/src/redmine-net-api/Net/WebClient/Extensions/WebExceptionExtensions.cs +++ b/src/redmine-net-api/Net/WebClient/Extensions/WebExceptionExtensions.cs @@ -58,8 +58,13 @@ public static void HandleWebException(this WebException exception, IRedmineSeria case WebExceptionStatus.ProtocolError: if (exception.Response != null) { + var statusCode = exception.Response is HttpWebResponse httpResponse + ? (int)httpResponse.StatusCode + : (int)HttpStatusCode.InternalServerError; + using var responseStream = exception.Response.GetResponseStream(); - HttpStatusHelper.MapStatusCodeToException((int)exception.Status, responseStream, innerException, serializer); + HttpStatusHelper.MapStatusCodeToException(statusCode, responseStream, innerException, serializer); + } break; diff --git a/src/redmine-net-api/Net/WebClient/InternalWebClient.cs b/src/redmine-net-api/Net/WebClient/InternalWebClient.cs index 913feb7e..71103c45 100644 --- a/src/redmine-net-api/Net/WebClient/InternalWebClient.cs +++ b/src/redmine-net-api/Net/WebClient/InternalWebClient.cs @@ -117,12 +117,10 @@ protected override WebResponse GetWebResponse(WebRequest request) protected override WebResponse GetWebResponse(WebRequest request, IAsyncResult result) { var response = base.GetWebResponse(request, result); - if (response is HttpWebResponse httpResponse) { StatusCode = httpResponse.StatusCode; } - return response; } From 8d81acc344cf5a1bdae5ce980b575034cb3e1537 Mon Sep 17 00:00:00 2001 From: Padi Date: Fri, 16 May 2025 13:12:04 +0300 Subject: [PATCH 514/549] Add AType to common --- src/redmine-net-api/Common/AType.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/redmine-net-api/Common/AType.cs b/src/redmine-net-api/Common/AType.cs index 37087266..80d0493c 100644 --- a/src/redmine-net-api/Common/AType.cs +++ b/src/redmine-net-api/Common/AType.cs @@ -1,7 +1,6 @@ using System; -using System.Runtime.CompilerServices; -namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure; +namespace Redmine.Net.Api.Common; internal readonly struct A{ public static A Is => default; From 917260a5a1ca0e6af66af894960cd3b1d40ec6df Mon Sep 17 00:00:00 2001 From: Padi Date: Fri, 16 May 2025 13:12:33 +0300 Subject: [PATCH 515/549] [RedmineKeys] Add CUSTOM_FIELD_VALUES --- src/redmine-net-api/RedmineKeys.cs | 7 ++++- src/redmine-net-api/Types/Issue.cs | 1 - src/redmine-net-api/Types/Project.cs | 42 ++++++++++++++++++++-------- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/redmine-net-api/RedmineKeys.cs b/src/redmine-net-api/RedmineKeys.cs index 03ee061d..b5072aa6 100644 --- a/src/redmine-net-api/RedmineKeys.cs +++ b/src/redmine-net-api/RedmineKeys.cs @@ -179,8 +179,13 @@ public static class RedmineKeys /// /// /// - public const string CUSTOM_FIELDS = "custom_fields"; + public const string CUSTOM_FIELD_VALUES = "custom_field_values"; + /// + /// + /// + public const string CUSTOM_FIELDS = "custom_fields"; + /// /// /// diff --git a/src/redmine-net-api/Types/Issue.cs b/src/redmine-net-api/Types/Issue.cs index 838cb1c8..b145bdc4 100644 --- a/src/redmine-net-api/Types/Issue.cs +++ b/src/redmine-net-api/Types/Issue.cs @@ -344,7 +344,6 @@ public override void WriteXml(XmlWriter writer) writer.WriteIdIfNotNull(RedmineKeys.ASSIGNED_TO_ID, AssignedTo); writer.WriteIdIfNotNull(RedmineKeys.PARENT_ISSUE_ID, ParentIssue); writer.WriteIdIfNotNull(RedmineKeys.FIXED_VERSION_ID, FixedVersion); - writer.WriteValueOrEmpty(RedmineKeys.ESTIMATED_HOURS, EstimatedHours); writer.WriteIfNotDefaultOrNull(RedmineKeys.DONE_RATIO, DoneRatio); diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs index b3d5953a..4f42a9eb 100644 --- a/src/redmine-net-api/Types/Project.cs +++ b/src/redmine-net-api/Types/Project.cs @@ -121,7 +121,21 @@ public sealed class Project : IdentifiableName, IEquatable /// /// The custom fields. /// - public IList CustomFields { get; set; } + [Obsolete($"{RedmineConstants.OBSOLETE_TEXT} Use {nameof(IssueCustomFields)} instead.")] + public IList CustomFields + { + get => IssueCustomFields; + set => IssueCustomFields = (List)value; + } + /// + /// + /// + public List IssueCustomFields { get; set; } + + /// + /// + /// + public List CustomFieldValues { get; set; } /// /// Gets the issue categories. @@ -129,13 +143,13 @@ public sealed class Project : IdentifiableName, IEquatable /// /// The issue categories. /// - /// Available in Redmine starting with 2.6.0 version. + /// Available in Redmine starting with the 2.6.0 version. public IList IssueCategories { get; internal set; } /// /// Gets the time entry activities. /// - /// Available in Redmine starting with 3.4.0 version. + /// Available in Redmine starting with the 3.4.0 version. public IList TimeEntryActivities { get; internal set; } /// @@ -169,7 +183,7 @@ public override void ReadXml(XmlReader reader) { case RedmineKeys.ID: Id = reader.ReadElementContentAsInt(); break; case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadElementContentAsNullableDateTime(); break; - case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadElementContentAsCollection(); break; + case RedmineKeys.CUSTOM_FIELDS: IssueCustomFields = reader.ReadElementContentAsCollection(); break; case RedmineKeys.DESCRIPTION: Description = reader.ReadElementContentAsString(); break; case RedmineKeys.ENABLED_MODULES: EnabledModules = reader.ReadElementContentAsCollection(); break; case RedmineKeys.HOMEPAGE: HomePage = reader.ReadElementContentAsString(); break; @@ -203,15 +217,18 @@ public override void WriteXml(XmlWriter writer) writer.WriteIdIfNotNull(RedmineKeys.PARENT_ID, Parent); writer.WriteBoolean(RedmineKeys.INHERIT_MEMBERS, InheritMembers); - //It works only when the new project is a subproject and it inherits the members. + //It works only when the new project is a subproject, and it inherits the members. writer.WriteIdIfNotNull(RedmineKeys.DEFAULT_ASSIGNED_TO_ID, DefaultAssignee); //It works only with existing shared versions. writer.WriteIdIfNotNull(RedmineKeys.DEFAULT_VERSION_ID, DefaultVersion); writer.WriteRepeatableElement(RedmineKeys.TRACKER_IDS, (IEnumerable)Trackers); writer.WriteRepeatableElement(RedmineKeys.ENABLED_MODULE_NAMES, (IEnumerable)EnabledModules); - writer.WriteRepeatableElement(RedmineKeys.ISSUE_CUSTOM_FIELD_IDS, (IEnumerable)CustomFields); - writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); + writer.WriteRepeatableElement(RedmineKeys.ISSUE_CUSTOM_FIELD_IDS, (IEnumerable)IssueCustomFields); + if (Id == 0) + { + writer.WriteArray(RedmineKeys.CUSTOM_FIELD_VALUES, CustomFieldValues); + } } #endregion @@ -238,7 +255,7 @@ public override void ReadJson(JsonReader reader) { case RedmineKeys.ID: Id = reader.ReadAsInt(); break; case RedmineKeys.CREATED_ON: CreatedOn = reader.ReadAsDateTime(); break; - case RedmineKeys.CUSTOM_FIELDS: CustomFields = reader.ReadAsCollection(); break; + case RedmineKeys.CUSTOM_FIELDS: IssueCustomFields = reader.ReadAsCollection(); break; case RedmineKeys.DESCRIPTION: Description = reader.ReadAsString(); break; case RedmineKeys.ENABLED_MODULES: EnabledModules = reader.ReadAsCollection(); break; case RedmineKeys.HOMEPAGE: HomePage = reader.ReadAsString(); break; @@ -275,15 +292,18 @@ public override void WriteJson(JsonWriter writer) writer.WriteBoolean(RedmineKeys.IS_PUBLIC, IsPublic); writer.WriteIdIfNotNull(RedmineKeys.PARENT_ID, Parent); - //It works only when the new project is a subproject and it inherits the members. + //It works only when the new project is a subproject, and it inherits the members. writer.WriteIdIfNotNull(RedmineKeys.DEFAULT_ASSIGNED_TO_ID, DefaultAssignee); //It works only with existing shared versions. writer.WriteIdIfNotNull(RedmineKeys.DEFAULT_VERSION_ID, DefaultVersion); writer.WriteRepeatableElement(RedmineKeys.TRACKER_IDS, (IEnumerable)Trackers); writer.WriteRepeatableElement(RedmineKeys.ENABLED_MODULE_NAMES, (IEnumerable)EnabledModules); - writer.WriteRepeatableElement(RedmineKeys.ISSUE_CUSTOM_FIELD_IDS, (IEnumerable)CustomFields); - writer.WriteArray(RedmineKeys.CUSTOM_FIELDS, CustomFields); + writer.WriteRepeatableElement(RedmineKeys.ISSUE_CUSTOM_FIELD_IDS, (IEnumerable)IssueCustomFields); + if (Id == 0) + { + writer.WriteArray(RedmineKeys.CUSTOM_FIELD_VALUES, CustomFieldValues); + } } } #endregion From f262787b33dacf00ab24e872533a3afa1d4fc54c Mon Sep 17 00:00:00 2001 From: Padi Date: Fri, 16 May 2025 21:38:14 +0300 Subject: [PATCH 516/549] Integration tests --- .../Tests/Async/FileTestsAsync.cs | 3 +- .../Tests/Async/IssueTestsAsync.cs | 31 ++++++------ .../Tests/Async/MembershipTestsAsync.cs | 49 +++++++++++++++++++ .../Tests/Async/ProjectTestsAsync.cs | 5 +- .../Tests/Async/SearchTestsAsync.cs | 2 +- .../Tests/Async/TimeEntryTests.cs | 12 +++-- .../Tests/Async/WikiTestsAsync.cs | 25 +++++----- 7 files changed, 93 insertions(+), 34 deletions(-) diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/FileTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/FileTestsAsync.cs index 9da74bee..f678d5d9 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/FileTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/FileTestsAsync.cs @@ -1,4 +1,5 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; using File = Redmine.Net.Api.Types.File; @@ -32,7 +33,7 @@ public async Task CreateFile_Should_Succeed() [Fact] public async Task CreateFile_Without_Token_Should_Fail() { - await Assert.ThrowsAsync(() => fixture.RedmineManager.CreateAsync( + await Assert.ThrowsAsync(() => fixture.RedmineManager.CreateAsync( new File { Filename = "VBpMc.txt" }, PROJECT_ID)); } diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueTestsAsync.cs index d302cedc..5b078c05 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueTestsAsync.cs @@ -11,7 +11,8 @@ public class IssueTestsAsync(RedmineTestContainerFixture fixture) { private static readonly IdentifiableName ProjectIdName = IdentifiableName.Create(1); - private async Task CreateTestIssueAsync(List customFields = null, List watchers = null) + private async Task CreateTestIssueAsync(List customFields = null, + List watchers = null) { var issue = new Issue { @@ -44,7 +45,7 @@ public async Task CreateIssue_Should_Succeed() EstimatedHours = 8, CustomFields = [ - IssueCustomField.CreateSingle(1, RandomHelper.GenerateText(8), RandomHelper.GenerateText(4)) + IssueCustomField.CreateSingle(1, RandomHelper.GenerateText(8), RandomHelper.GenerateText(4)) ] }; @@ -72,7 +73,7 @@ public async Task GetIssue_Should_Succeed() //Arrange var createdIssue = await CreateTestIssueAsync(); Assert.NotNull(createdIssue); - + var issueId = createdIssue.Id.ToInvariantString(); //Act @@ -95,11 +96,11 @@ public async Task UpdateIssue_Should_Succeed() var updatedSubject = RandomHelper.GenerateText(9); var updatedDescription = RandomHelper.GenerateText(18); - var updatedStatusId = 2; - + var updatedStatusId = 2; + createdIssue.Subject = updatedSubject; createdIssue.Description = updatedDescription; - createdIssue.Status = updatedStatusId.ToIssueStatusIdentifier(); + createdIssue.Status = updatedStatusId.ToIssueStatusIdentifier(); createdIssue.Notes = RandomHelper.GenerateText("Note"); var issueId = createdIssue.Id.ToInvariantString(); @@ -122,7 +123,7 @@ public async Task DeleteIssue_Should_Succeed() //Arrange var createdIssue = await CreateTestIssueAsync(); Assert.NotNull(createdIssue); - + var issueId = createdIssue.Id.ToInvariantString(); //Act @@ -136,16 +137,16 @@ public async Task DeleteIssue_Should_Succeed() public async Task GetIssue_With_Watchers_And_Relations_Should_Succeed() { var createdIssue = await CreateTestIssueAsync( - [ - IssueCustomField.CreateMultiple(1, RandomHelper.GenerateText(8), - [RandomHelper.GenerateText(4), RandomHelper.GenerateText(4)]) - ], - [new Watcher() { Id = 1 }, new Watcher(){Id = 2}]); - + [ + IssueCustomField.CreateMultiple(1, RandomHelper.GenerateText(8), + [RandomHelper.GenerateText(4), RandomHelper.GenerateText(4)]) + ], + [new Watcher() { Id = 1 }]); + Assert.NotNull(createdIssue); - + //Act - var retrievedIssue = await fixture.RedmineManager.GetAsync(createdIssue.Id.ToInvariantString(), + var retrievedIssue = await fixture.RedmineManager.GetAsync(createdIssue.Id.ToInvariantString(), RequestOptions.Include($"{Include.Issue.Watchers},{Include.Issue.Relations}")); //Assert diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/MembershipTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/MembershipTestsAsync.cs index 55132eb5..b8f36de3 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/MembershipTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/MembershipTestsAsync.cs @@ -128,4 +128,53 @@ public async Task DeleteMembership_Should_Succeed() // Assert Assert.DoesNotContain(updatedMemberships.Items, m => m.Id == membership.Id); } + + [Fact] + public async Task GetProjectMemberships_ShouldReturnMemberships() + { + // Test implementation + } + + [Fact] + public async Task GetProjectMembership_WithValidId_ShouldReturnMembership() + { + // Test implementation + } + + [Fact] + public async Task CreateProjectMembership_WithValidData_ShouldSucceed() + { + // Test implementation + } + + [Fact] + public async Task CreateProjectMembership_WithInvalidData_ShouldFail() + { + // Test implementation + } + + [Fact] + public async Task UpdateProjectMembership_WithValidData_ShouldSucceed() + { + // Test implementation + } + + [Fact] + public async Task UpdateProjectMembership_WithInvalidData_ShouldFail() + { + // Test implementation + } + + [Fact] + public async Task DeleteProjectMembership_WithValidId_ShouldSucceed() + { + // Test implementation + } + + [Fact] + public async Task DeleteProjectMembership_WithInvalidId_ShouldFail() + { + // Test implementation + } + } \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/ProjectTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/ProjectTestsAsync.cs index 768bdf8c..2909bbf0 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/ProjectTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/ProjectTestsAsync.cs @@ -18,10 +18,11 @@ private async Task CreateEntityAsync(string subjectSuffix = null) return await fixture.RedmineManager.CreateAsync(entity); } - + [Fact] public async Task CreateProject_Should_Succeed() { + //Arrange var projectName = RandomHelper.GenerateText(7); var data = new Project { @@ -44,7 +45,7 @@ public async Task CreateProject_Should_Succeed() new ProjectTracker(3), ], - CustomFieldValues = [IdentifiableName.Create(1, "cf1"), IdentifiableName.Create(2, "cf2")] + //CustomFieldValues = [IdentifiableName.Create(1, "cf1"), IdentifiableName.Create(2, "cf2")] // IssueCustomFields = // [ // IssueCustomField.CreateSingle(1, RandomHelper.GenerateText(5), RandomHelper.GenerateText(7)) diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/SearchTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/SearchTestsAsync.cs index e07b128a..a05add93 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/SearchTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/SearchTestsAsync.cs @@ -22,6 +22,6 @@ public async Task Search_Should_Succeed() // Assert Assert.NotNull(results); - Assert.Empty(results.Items); + Assert.Null(results.Items); } } \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/TimeEntryTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/TimeEntryTests.cs index e3682b8d..4039c6c6 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/TimeEntryTests.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/TimeEntryTests.cs @@ -1,4 +1,5 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Types; @@ -11,7 +12,8 @@ public class TimeEntryTestsAsync(RedmineTestContainerFixture fixture) private async Task CreateTestTimeEntryAsync() { var project = await fixture.RedmineManager.GetAsync(1.ToInvariantString()); - var issue = await fixture.RedmineManager.GetAsync(1.ToInvariantString()); + var issueData = IssueTestHelper.CreateIssue(); + var issue = await fixture.RedmineManager.CreateAsync(issueData); var timeEntry = new TimeEntry { @@ -19,7 +21,7 @@ private async Task CreateTestTimeEntryAsync() Issue = issue.ToIdentifiableName(), SpentOn = DateTime.Now.Date, Hours = 1.5m, - // Activity = 8.ToIdentifier(), + Activity = 8.ToIdentifier(), Comments = $"Test time entry comments {Guid.NewGuid()}", }; return await fixture.RedmineManager.CreateAsync(timeEntry); @@ -29,13 +31,15 @@ private async Task CreateTestTimeEntryAsync() public async Task CreateTimeEntry_Should_Succeed() { //Arrange + var issueData = IssueTestHelper.CreateIssue(); + var issue = await fixture.RedmineManager.CreateAsync(issueData); var timeEntryData = new TimeEntry { Project = 1.ToIdentifier(), - Issue = 1.ToIdentifier(), + Issue = issue.ToIdentifiableName(), SpentOn = DateTime.Now.Date, Hours = 1.5m, - //Activity = 8.ToIdentifier(), + Activity = 8.ToIdentifier(), Comments = $"Initial create test comments {Guid.NewGuid()}", }; diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/WikiTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/WikiTestsAsync.cs index 6c963c5e..1325a656 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/WikiTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Async/WikiTestsAsync.cs @@ -95,16 +95,18 @@ await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetWikiPageAsync(PROJECT_ID, wikiPageName)); } - private async Task<(WikiPage Page, string ProjectId, string PageTitle)> CreateTestWikiPageAsync( + private async Task<(string pageTitle, string ProjectId, string PageTitle)> CreateTestWikiPageAsync( string pageTitleSuffix = null, string initialText = "Default initial text for wiki page.", string initialComments = "Initial comments for wiki page.") { - var pageTitle = $"TestWikiPage_{(pageTitleSuffix ?? RandomHelper.GenerateText(5))}"; + var pageTitle = RandomHelper.GenerateText(5); var wikiPageData = new WikiPage { + Title = RandomHelper.GenerateText(5), Text = initialText, - Comments = initialComments + Comments = initialComments, + Version = 0 }; var createdPage = await fixture.RedmineManager.CreateWikiPageAsync(PROJECT_ID, pageTitle, wikiPageData); @@ -114,7 +116,7 @@ await Assert.ThrowsAsync(async () => // Assert.True(createdPage.Id > 0, "Created WikiPage should have a valid ID."); // Assert.Equal(initialText, createdPage.Text); - return (createdPage, PROJECT_ID, pageTitle); + return (pageTitle, PROJECT_ID, pageTitle); } [Fact] @@ -141,7 +143,11 @@ public async Task CreateWikiPage_Should_Succeed() public async Task UpdateWikiPage_Should_Succeed() { //Arrange - var (initialPage, projectId, pageTitle) = await CreateTestWikiPageAsync("UpdateTest", "Original Text.", "Original Comments."); + var pageTitle = RandomHelper.GenerateText(8); + var text = "This is the content of a new wiki page."; + var comments = "Creation comment for new wiki page."; + var wikiPageData = new WikiPage { Text = text, Comments = comments }; + var createdPage = await fixture.RedmineManager.CreateWikiPageAsync(PROJECT_ID, pageTitle, wikiPageData); var updatedText = $"Updated wiki text content {Guid.NewGuid():N}"; var updatedComments = "These are updated comments for the wiki page update."; @@ -150,19 +156,16 @@ public async Task UpdateWikiPage_Should_Succeed() { Text = updatedText, Comments = updatedComments, - Version = ++initialPage.Version + Version = 1 }; //Act - await fixture.RedmineManager.UpdateWikiPageAsync(projectId, pageTitle, wikiPageToUpdate); - var retrievedPage = await fixture.RedmineManager.GetAsync(initialPage.Id.ToInvariantString()); + await fixture.RedmineManager.UpdateWikiPageAsync(PROJECT_ID, pageTitle, wikiPageToUpdate); + var retrievedPage = await fixture.RedmineManager.GetWikiPageAsync(1.ToInvariantString(), createdPage.Title, version: 1); //Assert Assert.NotNull(retrievedPage); Assert.Equal(updatedText, retrievedPage.Text); Assert.Equal(updatedComments, retrievedPage.Comments); - Assert.True(retrievedPage.Version > initialPage.Version - || (retrievedPage.Version == 1 && initialPage.Version == 0) - || (initialPage.Version ==0 && retrievedPage.Version ==0)); } } \ No newline at end of file From 6a660927ce8000200d9e2c5104148980c8bb1858 Mon Sep 17 00:00:00 2001 From: Padi Date: Fri, 16 May 2025 21:38:55 +0300 Subject: [PATCH 517/549] Fix project membership json serialization --- .../Xml/Extensions/XmlWriterExtensions.cs | 33 +++++++++++++++++++ .../Types/ProjectMembership.cs | 4 +-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/redmine-net-api/Serialization/Xml/Extensions/XmlWriterExtensions.cs b/src/redmine-net-api/Serialization/Xml/Extensions/XmlWriterExtensions.cs index 3894309e..84c5f7cc 100644 --- a/src/redmine-net-api/Serialization/Xml/Extensions/XmlWriterExtensions.cs +++ b/src/redmine-net-api/Serialization/Xml/Extensions/XmlWriterExtensions.cs @@ -160,6 +160,39 @@ public static void WriteArray(this XmlWriter writer, string elementName, IEnumer writer.WriteEndElement(); } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static void WriteArray(this XmlWriter writer, string elementName, IEnumerable collection, string root, string defaultNamespace = null) + { + if (collection == null) + { + return; + } + + var type = typeof(T); + writer.WriteStartElement(elementName); + writer.WriteAttributeString("type", "array"); + + var rootAttribute = new XmlRootAttribute(root); + + var serializer = new XmlSerializer(type, XmlAttributeOverrides, EmptyTypeArray, rootAttribute, + defaultNamespace); + + foreach (var item in collection) + { + serializer.Serialize(writer, item); + } + + writer.WriteEndElement(); + } /// /// Writes the list elements. diff --git a/src/redmine-net-api/Types/ProjectMembership.cs b/src/redmine-net-api/Types/ProjectMembership.cs index 714b0030..cc9ae373 100644 --- a/src/redmine-net-api/Types/ProjectMembership.cs +++ b/src/redmine-net-api/Types/ProjectMembership.cs @@ -107,7 +107,7 @@ public override void WriteXml(XmlWriter writer) writer.WriteIdIfNotNull(RedmineKeys.USER_ID, User); } - writer.WriteArray(RedmineKeys.ROLE_IDS, Roles, typeof(MembershipRole), RedmineKeys.ROLE_ID); + writer.WriteArray(RedmineKeys.ROLE_IDS, Roles, root: RedmineKeys.ROLE_ID); } #endregion @@ -155,7 +155,7 @@ public override void WriteJson(JsonWriter writer) writer.WriteIdIfNotNull(RedmineKeys.USER_ID, User); } - writer.WriteRepeatableElement(RedmineKeys.ROLE_IDS, (IEnumerable)Roles); + writer.WriteArray(RedmineKeys.ROLE_IDS, Roles); } } #endregion From 8c6739d2f7fa43232d9be5383d87bad20f17e4c0 Mon Sep 17 00:00:00 2001 From: Padi Date: Fri, 16 May 2025 21:39:32 +0300 Subject: [PATCH 518/549] Fix news and wiki urls --- .../Extensions/RedmineManagerExtensions.cs | 44 +++++-------------- .../Net/Internal/RedmineApiUrlsExtensions.cs | 2 +- 2 files changed, 13 insertions(+), 33 deletions(-) diff --git a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs index c6327715..0cf9ccf2 100644 --- a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs +++ b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs @@ -182,11 +182,9 @@ public static News AddProjectNews(this RedmineManager redmineManager, string pro var payload = redmineManager.Serializer.Serialize(news); - var uri = Uri.EscapeDataString(redmineManager.RedmineApiUrls.ProjectNews(projectIdentifier)); - - var escapedUri = Uri.EscapeDataString(uri); + var uri = redmineManager.RedmineApiUrls.ProjectNews(projectIdentifier); - var response = redmineManager.ApiClient.Create(escapedUri, payload, requestOptions); + var response = redmineManager.ApiClient.Create(uri, payload, requestOptions); return response.DeserializeTo(redmineManager.Serializer); } @@ -341,9 +339,7 @@ public static void UpdateWikiPage(this RedmineManager redmineManager, string pro var uri = redmineManager.RedmineApiUrls.ProjectWikiPageUpdate(projectId, pageName); - var escapedUri = Uri.EscapeDataString(uri); - - redmineManager.ApiClient.Patch(escapedUri, payload, requestOptions); + redmineManager.ApiClient.Patch(uri, payload, requestOptions); } /// @@ -391,9 +387,7 @@ public static WikiPage GetWikiPage(this RedmineManager redmineManager, string pr ? redmineManager.RedmineApiUrls.ProjectWikiPage(projectId, pageName) : redmineManager.RedmineApiUrls.ProjectWikiPageVersion(projectId, pageName, version.ToInvariantString()); - var escapedUri = Uri.EscapeDataString(uri); - - var response = redmineManager.ApiClient.Get(escapedUri, requestOptions); + var response = redmineManager.ApiClient.Get(uri, requestOptions); return response.DeserializeTo(redmineManager.Serializer); } @@ -427,9 +421,7 @@ public static void DeleteWikiPage(this RedmineManager redmineManager, string pro { var uri = redmineManager.RedmineApiUrls.ProjectWikiPageDelete(projectId, pageName); - var escapedUri = Uri.EscapeDataString(uri); - - redmineManager.ApiClient.Delete(escapedUri, requestOptions); + redmineManager.ApiClient.Delete(uri, requestOptions); } /// @@ -610,9 +602,7 @@ public static async Task> GetProjectNewsAsync(this RedmineMan { var uri = redmineManager.RedmineApiUrls.ProjectNews(projectIdentifier); - var escapedUri = Uri.EscapeDataString(uri); - - var response = await redmineManager.ApiClient.GetPagedAsync(escapedUri, requestOptions, cancellationToken).ConfigureAwait(false); + var response = await redmineManager.ApiClient.GetPagedAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); return response.DeserializeToPagedResults(redmineManager.Serializer); } @@ -641,11 +631,9 @@ public static async Task AddProjectNewsAsync(this RedmineManager redmineMa var payload = redmineManager.Serializer.Serialize(news); - var uri = Uri.EscapeDataString(redmineManager.RedmineApiUrls.ProjectNews(projectIdentifier)); + var uri = redmineManager.RedmineApiUrls.ProjectNews(projectIdentifier); - var escapedUri = Uri.EscapeDataString(uri); - - var response = await redmineManager.ApiClient.CreateAsync(escapedUri, payload, requestOptions, cancellationToken).ConfigureAwait(false); + var response = await redmineManager.ApiClient.CreateAsync(uri, payload, requestOptions, cancellationToken).ConfigureAwait(false); return response.DeserializeTo(redmineManager.Serializer); } @@ -754,9 +742,7 @@ public static async Task CreateWikiPageAsync(this RedmineManager redmi throw new RedmineException("The payload is empty"); } - var path = redmineManager.RedmineApiUrls.ProjectWikiPageUpdate(projectId, Uri.EscapeDataString(pageName)); - - //var escapedUri = Uri.EscapeDataString(url); + var path = redmineManager.RedmineApiUrls.ProjectWikiPageUpdate(projectId, pageName); var response = await redmineManager.ApiClient.UpdateAsync(path, payload, requestOptions, cancellationToken).ConfigureAwait(false); @@ -784,9 +770,7 @@ public static async Task UpdateWikiPageAsync(this RedmineManager redmineManager, var url = redmineManager.RedmineApiUrls.ProjectWikiPageUpdate(projectId, pageName); - var escapedUri = Uri.EscapeDataString(url); - - await redmineManager.ApiClient.PatchAsync(escapedUri, payload, requestOptions, cancellationToken).ConfigureAwait(false); + await redmineManager.ApiClient.PatchAsync(url, payload, requestOptions, cancellationToken).ConfigureAwait(false); } /// @@ -802,9 +786,7 @@ public static async Task DeleteWikiPageAsync(this RedmineManager redmineManager, { var uri = redmineManager.RedmineApiUrls.ProjectWikiPageDelete(projectId, pageName); - var escapedUri = Uri.EscapeDataString(uri); - - await redmineManager.ApiClient.DeleteAsync(escapedUri, requestOptions, cancellationToken).ConfigureAwait(false); + await redmineManager.ApiClient.DeleteAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); } /// @@ -823,9 +805,7 @@ public static async Task GetWikiPageAsync(this RedmineManager redmineM ? redmineManager.RedmineApiUrls.ProjectWikiPage(projectId, pageName) : redmineManager.RedmineApiUrls.ProjectWikiPageVersion(projectId, pageName, version.ToInvariantString()); - var escapedUri = Uri.EscapeDataString(uri); - - var response = await redmineManager.ApiClient.GetAsync(escapedUri, requestOptions, cancellationToken).ConfigureAwait(false); + var response = await redmineManager.ApiClient.GetAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); return response.DeserializeTo(redmineManager.Serializer); } diff --git a/src/redmine-net-api/Net/Internal/RedmineApiUrlsExtensions.cs b/src/redmine-net-api/Net/Internal/RedmineApiUrlsExtensions.cs index 103dc7f7..9ed8ab34 100644 --- a/src/redmine-net-api/Net/Internal/RedmineApiUrlsExtensions.cs +++ b/src/redmine-net-api/Net/Internal/RedmineApiUrlsExtensions.cs @@ -68,7 +68,7 @@ public static string ProjectNews(this RedmineApiUrls redmineApiUrls, string proj throw new RedmineException($"Argument '{nameof(projectIdentifier)}' is null or whitespace"); } - return $"{RedmineKeys.PROJECT}/{projectIdentifier}/{RedmineKeys.NEWS}.{redmineApiUrls.Format}"; + return $"{RedmineKeys.PROJECTS}/{projectIdentifier}/{RedmineKeys.NEWS}.{redmineApiUrls.Format}"; } public static string ProjectMemberships(this RedmineApiUrls redmineApiUrls, string projectIdentifier) From 614ae600e786fd0e0cda550b782c5fd8908a310c Mon Sep 17 00:00:00 2001 From: Padi Date: Fri, 16 May 2025 21:40:01 +0300 Subject: [PATCH 519/549] Add issue to extension --- src/redmine-net-api/Extensions/IdentifiableNameExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/redmine-net-api/Extensions/IdentifiableNameExtensions.cs b/src/redmine-net-api/Extensions/IdentifiableNameExtensions.cs index a0daf466..beed972a 100644 --- a/src/redmine-net-api/Extensions/IdentifiableNameExtensions.cs +++ b/src/redmine-net-api/Extensions/IdentifiableNameExtensions.cs @@ -23,6 +23,7 @@ public static IdentifiableName ToIdentifiableName(this T entity) where T : cl DocumentCategory documentCategory => IdentifiableName.Create(documentCategory.Id, documentCategory.Name), Group group => IdentifiableName.Create(group.Id, group.Name), GroupUser groupUser => IdentifiableName.Create(groupUser.Id, groupUser.Name), + Issue issue => new IdentifiableName(issue.Id, issue.Subject), IssueAllowedStatus issueAllowedStatus => IdentifiableName.Create(issueAllowedStatus.Id, issueAllowedStatus.Name), IssueCustomField issueCustomField => IdentifiableName.Create(issueCustomField.Id, issueCustomField.Name), IssuePriority issuePriority => IdentifiableName.Create(issuePriority.Id, issuePriority.Name), From 14914e4170d83606c6dcd9bbfc1d873edc74ad97 Mon Sep 17 00:00:00 2001 From: Padi Date: Thu, 22 May 2025 22:53:11 +0300 Subject: [PATCH 520/549] [New] Logger --- .../Extensions/LoggerExtensions.cs | 52 ---- .../Logging/ColorConsoleLogger.cs | 110 --------- src/redmine-net-api/Logging/ConsoleLogger.cs | 56 ----- src/redmine-net-api/Logging/ILogger.cs | 30 --- src/redmine-net-api/Logging/IRedmineLogger.cs | 25 ++ src/redmine-net-api/Logging/LogEntry.cs | 61 ----- src/redmine-net-api/Logging/LogLevel.cs | 32 +++ src/redmine-net-api/Logging/Logger.cs | 51 ---- .../Logging/LoggerExtensions.cs | 225 ------------------ .../Logging/LoggerFactoryExtensions.cs | 27 +++ .../Logging/LoggingBuilderExtensions.cs | 46 ++++ .../Logging/LoggingEventType.cs | 45 ---- .../Logging/MicrosoftLoggerRedmineAdapter.cs | 92 +++++++ .../Logging/RedmineConsoleLogger.cs | 69 ++++++ .../Logging/RedmineConsoleTraceListener.cs | 86 ------- .../Logging/RedmineLoggerExtensions.cs | 108 +++++++++ .../Logging/RedmineLoggerFactory.cs | 75 ++++++ .../Logging/RedmineLoggerMicrosoftAdapter.cs | 97 ++++++++ .../Logging/RedmineLoggingOptions.cs | 27 +++ .../Logging/RedmineNullLogger.cs | 40 ++++ src/redmine-net-api/Logging/TraceLogger.cs | 56 ----- src/redmine-net-api/RedmineManagerOptions.cs | 9 +- .../RedmineManagerOptionsBuilder.cs | 42 +++- src/redmine-net-api/redmine-net-api.csproj | 7 +- 24 files changed, 689 insertions(+), 779 deletions(-) delete mode 100755 src/redmine-net-api/Extensions/LoggerExtensions.cs delete mode 100644 src/redmine-net-api/Logging/ColorConsoleLogger.cs delete mode 100644 src/redmine-net-api/Logging/ConsoleLogger.cs delete mode 100755 src/redmine-net-api/Logging/ILogger.cs create mode 100644 src/redmine-net-api/Logging/IRedmineLogger.cs delete mode 100644 src/redmine-net-api/Logging/LogEntry.cs create mode 100644 src/redmine-net-api/Logging/LogLevel.cs delete mode 100755 src/redmine-net-api/Logging/Logger.cs delete mode 100755 src/redmine-net-api/Logging/LoggerExtensions.cs create mode 100644 src/redmine-net-api/Logging/LoggerFactoryExtensions.cs create mode 100644 src/redmine-net-api/Logging/LoggingBuilderExtensions.cs delete mode 100755 src/redmine-net-api/Logging/LoggingEventType.cs create mode 100644 src/redmine-net-api/Logging/MicrosoftLoggerRedmineAdapter.cs create mode 100644 src/redmine-net-api/Logging/RedmineConsoleLogger.cs delete mode 100644 src/redmine-net-api/Logging/RedmineConsoleTraceListener.cs create mode 100644 src/redmine-net-api/Logging/RedmineLoggerExtensions.cs create mode 100644 src/redmine-net-api/Logging/RedmineLoggerFactory.cs create mode 100644 src/redmine-net-api/Logging/RedmineLoggerMicrosoftAdapter.cs create mode 100644 src/redmine-net-api/Logging/RedmineLoggingOptions.cs create mode 100644 src/redmine-net-api/Logging/RedmineNullLogger.cs delete mode 100644 src/redmine-net-api/Logging/TraceLogger.cs diff --git a/src/redmine-net-api/Extensions/LoggerExtensions.cs b/src/redmine-net-api/Extensions/LoggerExtensions.cs deleted file mode 100755 index 9e836a19..00000000 --- a/src/redmine-net-api/Extensions/LoggerExtensions.cs +++ /dev/null @@ -1,52 +0,0 @@ -/* - Copyright 2011 - 2016 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using Redmine.Net.Api.Logging; - -namespace Redmine.Net.Api.Extensions -{ - /// - /// - public static class LoggerExtensions - { - /// - /// Uses the console log. - /// - /// The redmine manager. - public static void UseConsoleLog(this RedmineManager redmineManager) - { - Logger.UseLogger(new ConsoleLogger()); - } - - /// - /// Uses the color console log. - /// - /// The redmine manager. - public static void UseColorConsoleLog(this RedmineManager redmineManager) - { - Logger.UseLogger(new ColorConsoleLogger()); - } - - /// - /// Uses the trace log. - /// - /// The redmine manager. - public static void UseTraceLog(this RedmineManager redmineManager) - { - Logger.UseLogger(new TraceLogger()); - } - } -} \ No newline at end of file diff --git a/src/redmine-net-api/Logging/ColorConsoleLogger.cs b/src/redmine-net-api/Logging/ColorConsoleLogger.cs deleted file mode 100644 index e7959dc9..00000000 --- a/src/redmine-net-api/Logging/ColorConsoleLogger.cs +++ /dev/null @@ -1,110 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; - -namespace Redmine.Net.Api.Logging -{ - /// - /// - /// - /// - public sealed class ColorConsoleLogger : ILogger - { - private static readonly object locker = new object(); - private readonly ConsoleColor? defaultConsoleColor = null; - - /// - /// - /// - public void Log(LogEntry entry) - { - lock (locker) - { - var colors = GetLogLevelConsoleColors(entry.Severity); - switch (entry.Severity) - { - case LoggingEventType.Debug: - Console.WriteLine(entry.Message, colors.Background, colors.Foreground); - break; - case LoggingEventType.Information: - Console.WriteLine(entry.Message, colors.Background, colors.Foreground); - break; - case LoggingEventType.Warning: - Console.WriteLine(entry.Message, colors.Background, colors.Foreground); - break; - case LoggingEventType.Error: - Console.WriteLine(entry.Message, colors.Background, colors.Foreground); - break; - case LoggingEventType.Fatal: - Console.WriteLine(entry.Message, colors.Background, colors.Foreground); - break; - } - } - } - - /// - /// Gets the log level console colors. - /// - /// The log level. - /// - private ConsoleColors GetLogLevelConsoleColors(LoggingEventType logLevel) - { - // do not change user's background color except for Critical - switch (logLevel) - { - case LoggingEventType.Fatal: - return new ConsoleColors(ConsoleColor.White, ConsoleColor.Red); - case LoggingEventType.Error: - return new ConsoleColors(ConsoleColor.Red, defaultConsoleColor); - case LoggingEventType.Warning: - return new ConsoleColors(ConsoleColor.DarkYellow, defaultConsoleColor); - case LoggingEventType.Information: - return new ConsoleColors(ConsoleColor.DarkGreen, defaultConsoleColor); - default: - return new ConsoleColors(ConsoleColor.Gray, defaultConsoleColor); - } - } - - /// - /// - /// - private struct ConsoleColors - { - public ConsoleColors(ConsoleColor? foreground, ConsoleColor? background): this() - { - Foreground = foreground; - Background = background; - } - - /// - /// Gets or sets the foreground. - /// - /// - /// The foreground. - /// - public ConsoleColor? Foreground { get; private set; } - - /// - /// Gets or sets the background. - /// - /// - /// The background. - /// - public ConsoleColor? Background { get; private set; } - } - } -} \ No newline at end of file diff --git a/src/redmine-net-api/Logging/ConsoleLogger.cs b/src/redmine-net-api/Logging/ConsoleLogger.cs deleted file mode 100644 index 015b465a..00000000 --- a/src/redmine-net-api/Logging/ConsoleLogger.cs +++ /dev/null @@ -1,56 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; - -namespace Redmine.Net.Api.Logging -{ - /// - /// - /// - /// - public sealed class ConsoleLogger : ILogger - { - private static readonly object locker = new object(); - /// - /// - /// - public void Log(LogEntry entry) - { - lock (locker) - { - switch (entry.Severity) - { - case LoggingEventType.Debug: - Console.WriteLine(entry.Message); - break; - case LoggingEventType.Information: - Console.WriteLine(entry.Message); - break; - case LoggingEventType.Warning: - Console.WriteLine(entry.Message); - break; - case LoggingEventType.Error: - Console.WriteLine(entry.Message); - break; - case LoggingEventType.Fatal: - Console.WriteLine(entry.Message); - break; - } - } - } - } -} \ No newline at end of file diff --git a/src/redmine-net-api/Logging/ILogger.cs b/src/redmine-net-api/Logging/ILogger.cs deleted file mode 100755 index 5d483046..00000000 --- a/src/redmine-net-api/Logging/ILogger.cs +++ /dev/null @@ -1,30 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -namespace Redmine.Net.Api.Logging -{ - /// - /// - /// - public interface ILogger - { - /// - /// Logs the specified entry. - /// - /// The entry. - void Log(LogEntry entry); - } -} \ No newline at end of file diff --git a/src/redmine-net-api/Logging/IRedmineLogger.cs b/src/redmine-net-api/Logging/IRedmineLogger.cs new file mode 100644 index 00000000..47b4c980 --- /dev/null +++ b/src/redmine-net-api/Logging/IRedmineLogger.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; + +namespace Redmine.Net.Api.Logging; + +/// +/// Provides abstraction for logging operations +/// +public interface IRedmineLogger +{ + /// + /// Checks if the specified log level is enabled + /// + bool IsEnabled(LogLevel level); + + /// + /// Logs a message with the specified level + /// + void Log(LogLevel level, string message, Exception exception = null); + + /// + /// Creates a scoped logger with additional context + /// + IRedmineLogger CreateScope(string scopeName, IDictionary scopeProperties = null); +} \ No newline at end of file diff --git a/src/redmine-net-api/Logging/LogEntry.cs b/src/redmine-net-api/Logging/LogEntry.cs deleted file mode 100644 index c91218cc..00000000 --- a/src/redmine-net-api/Logging/LogEntry.cs +++ /dev/null @@ -1,61 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; - -namespace Redmine.Net.Api.Logging -{ - /// - /// - /// - public sealed class LogEntry - { - /// - /// Initializes a new instance of the class. - /// - /// The severity. - /// The message. - /// The exception. - public LogEntry(LoggingEventType severity, string message, Exception exception = null) - { - Severity = severity; - Message = message; - Exception = exception; - } - - /// - /// Gets the severity. - /// - /// - /// The severity. - /// - public LoggingEventType Severity { get; private set; } - /// - /// Gets the message. - /// - /// - /// The message. - /// - public string Message { get; private set; } - /// - /// Gets the exception. - /// - /// - /// The exception. - /// - public Exception Exception { get; private set; } - } -} \ No newline at end of file diff --git a/src/redmine-net-api/Logging/LogLevel.cs b/src/redmine-net-api/Logging/LogLevel.cs new file mode 100644 index 00000000..a58e1500 --- /dev/null +++ b/src/redmine-net-api/Logging/LogLevel.cs @@ -0,0 +1,32 @@ +namespace Redmine.Net.Api.Logging; + +/// +/// Defines logging severity levels +/// +public enum LogLevel +{ + /// + /// + /// + Trace, + /// + /// + /// + Debug, + /// + /// + /// + Information, + /// + /// + /// + Warning, + /// + /// + /// + Error, + /// + /// + /// + Critical +} \ No newline at end of file diff --git a/src/redmine-net-api/Logging/Logger.cs b/src/redmine-net-api/Logging/Logger.cs deleted file mode 100755 index 895f3a8e..00000000 --- a/src/redmine-net-api/Logging/Logger.cs +++ /dev/null @@ -1,51 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -namespace Redmine.Net.Api.Logging -{ - /// - /// - /// - public static class Logger - { - private static readonly object locker = new object(); - private static ILogger logger; - - /// - /// Gets the current ILogger. - /// - /// - /// The current. - /// - public static ILogger Current - { - get { return logger ?? (logger = new ConsoleLogger()); } - private set { logger = value; } - } - - /// - /// Uses the logger. - /// - /// The logger. - public static void UseLogger(ILogger logger) - { - lock (locker) - { - Current = logger; - } - } - } -} \ No newline at end of file diff --git a/src/redmine-net-api/Logging/LoggerExtensions.cs b/src/redmine-net-api/Logging/LoggerExtensions.cs deleted file mode 100755 index 5a55d1e8..00000000 --- a/src/redmine-net-api/Logging/LoggerExtensions.cs +++ /dev/null @@ -1,225 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Globalization; - -namespace Redmine.Net.Api.Logging -{ - /// - /// - /// - public static class LoggerExtensions - { - /// - /// Debugs the specified message. - /// - /// The logger. - /// The message. - public static void Debug(this ILogger logger, string message) - { - logger.Log(new LogEntry(LoggingEventType.Debug, message)); - } - - /// - /// Debugs the specified message. - /// - /// The logger. - /// The message. - /// The exception. - public static void Debug(this ILogger logger, string message, Exception exception) - { - logger.Log(new LogEntry(LoggingEventType.Debug, message, exception)); - } - - /// - /// Debugs the specified format provider. - /// - /// The logger. - /// The format provider. - /// The format. - /// The arguments. - public static void Debug(this ILogger logger, IFormatProvider formatProvider, string format, params object[] args) - { - logger.Log(new LogEntry(LoggingEventType.Debug, string.Format(formatProvider, format, args))); - } - - /// - /// Debugs the specified format. - /// - /// The logger. - /// The format. - /// The arguments. - public static void Debug(this ILogger logger, string format, params object[] args) - { - logger.Log(new LogEntry(LoggingEventType.Debug, string.Format(CultureInfo.CurrentCulture, format, args))); - } - - /// - /// Informations the specified message. - /// - /// The logger. - /// The message. - public static void Information(this ILogger logger, string message) - { - logger.Log(new LogEntry(LoggingEventType.Information, message)); - } - - /// - /// Informations the specified format provider. - /// - /// The logger. - /// The format provider. - /// The format. - /// The arguments. - public static void Information(this ILogger logger, IFormatProvider formatProvider, string format, params object[] args) - { - logger.Log(new LogEntry(LoggingEventType.Information, string.Format(formatProvider, format, args))); - } - - /// - /// Informations the specified format. - /// - /// The logger. - /// The format. - /// The arguments. - public static void Information(this ILogger logger, string format, params object[] args) - { - logger.Log(new LogEntry(LoggingEventType.Information, string.Format(CultureInfo.CurrentCulture, format, args))); - } - - /// - /// Warnings the specified message. - /// - /// The logger. - /// The message. - public static void Warning(this ILogger logger, string message) - { - logger.Log(new LogEntry(LoggingEventType.Warning, message)); - } - - /// - /// Warnings the specified format provider. - /// - /// The logger. - /// The format provider. - /// The format. - /// The arguments. - public static void Warning(this ILogger logger, IFormatProvider formatProvider, string format, params object[] args) - { - logger.Log(new LogEntry(LoggingEventType.Warning, string.Format(formatProvider, format, args))); - } - - /// - /// Warnings the specified format. - /// - /// The logger. - /// The format. - /// The arguments. - public static void Warning(this ILogger logger, string format, params object[] args) - { - logger.Log(new LogEntry(LoggingEventType.Warning, string.Format(CultureInfo.CurrentCulture, format, args))); - } - - /// - /// Errors the specified exception. - /// - /// The logger. - /// The exception. - public static void Error(this ILogger logger, Exception exception) - { - logger.Log(new LogEntry(LoggingEventType.Error, exception.Message, exception)); - } - - /// - /// Errors the specified message. - /// - /// The logger. - /// The message. - /// The exception. - public static void Error(this ILogger logger, string message, Exception exception) - { - logger.Log(new LogEntry(LoggingEventType.Error, message, exception)); - } - - /// - /// Errors the specified format provider. - /// - /// The logger. - /// The format provider. - /// The format. - /// The arguments. - public static void Error(this ILogger logger, IFormatProvider formatProvider, string format, params object[] args) - { - logger.Log(new LogEntry(LoggingEventType.Error, string.Format(formatProvider, format, args))); - } - - /// - /// Errors the specified format. - /// - /// The logger. - /// The format. - /// The arguments. - public static void Error(this ILogger logger, string format, params object[] args) - { - logger.Log(new LogEntry(LoggingEventType.Error, string.Format(CultureInfo.CurrentCulture, format, args))); - } - - /// - /// Fatals the specified exception. - /// - /// The logger. - /// The exception. - public static void Fatal(this ILogger logger, Exception exception) - { - logger.Log(new LogEntry(LoggingEventType.Fatal, exception.Message, exception)); - } - - /// - /// Fatals the specified message. - /// - /// The logger. - /// The message. - /// The exception. - public static void Fatal(this ILogger logger, string message, Exception exception) - { - logger.Log(new LogEntry(LoggingEventType.Fatal, message, exception)); - } - - /// - /// Fatals the specified format provider. - /// - /// The logger. - /// The format provider. - /// The format. - /// The arguments. - public static void Fatal(this ILogger logger, IFormatProvider formatProvider, string format, params object[] args) - { - logger.Log(new LogEntry(LoggingEventType.Fatal, string.Format(formatProvider, format, args))); - } - - /// - /// Fatals the specified format. - /// - /// The logger. - /// The format. - /// The arguments. - public static void Fatal(this ILogger logger, string format, params object[] args) - { - logger.Log(new LogEntry(LoggingEventType.Fatal, string.Format(CultureInfo.CurrentCulture, format, args))); - } - } -} \ No newline at end of file diff --git a/src/redmine-net-api/Logging/LoggerFactoryExtensions.cs b/src/redmine-net-api/Logging/LoggerFactoryExtensions.cs new file mode 100644 index 00000000..62ee317b --- /dev/null +++ b/src/redmine-net-api/Logging/LoggerFactoryExtensions.cs @@ -0,0 +1,27 @@ + +#if NET462_OR_GREATER || NETCOREAPP + +using Microsoft.Extensions.Logging; + +namespace Redmine.Net.Api.Logging; + +/// +/// +/// +public static class LoggerFactoryExtensions +{ + /// + /// Creates a Redmine logger from the Microsoft ILoggerFactory + /// + public static IRedmineLogger CreateRedmineLogger(this ILoggerFactory factory, string categoryName = "Redmine.Api") + { + if (factory == null) + { + return RedmineNullLogger.Instance; + } + + var logger = factory.CreateLogger(categoryName); + return RedmineLoggerFactory.CreateMicrosoftLogger(logger); + } +} +#endif diff --git a/src/redmine-net-api/Logging/LoggingBuilderExtensions.cs b/src/redmine-net-api/Logging/LoggingBuilderExtensions.cs new file mode 100644 index 00000000..18650d89 --- /dev/null +++ b/src/redmine-net-api/Logging/LoggingBuilderExtensions.cs @@ -0,0 +1,46 @@ +#if NET462_OR_GREATER || NETCOREAPP +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Redmine.Net.Api.Logging; + +/// +/// +/// +public static class LoggingBuilderExtensions +{ + /// + /// Adds a RedmineLogger provider to the DI container + /// + public static ILoggingBuilder AddRedmineLogger(this ILoggingBuilder builder, IRedmineLogger redmineLogger) + { + if (builder == null) throw new ArgumentNullException(nameof(builder)); + if (redmineLogger == null) throw new ArgumentNullException(nameof(redmineLogger)); + + builder.Services.AddSingleton(redmineLogger); + return builder; + } + + /// + /// Configures Redmine logging options + /// + public static ILoggingBuilder ConfigureRedmineLogging(this ILoggingBuilder builder, Action configure) + { + if (builder == null) throw new ArgumentNullException(nameof(builder)); + if (configure == null) throw new ArgumentNullException(nameof(configure)); + + var options = new RedmineLoggingOptions(); + configure(options); + + builder.Services.AddSingleton(options); + + return builder; + } +} + +#endif + + + + diff --git a/src/redmine-net-api/Logging/LoggingEventType.cs b/src/redmine-net-api/Logging/LoggingEventType.cs deleted file mode 100755 index e539f868..00000000 --- a/src/redmine-net-api/Logging/LoggingEventType.cs +++ /dev/null @@ -1,45 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -namespace Redmine.Net.Api.Logging -{ - /// - /// - /// - public enum LoggingEventType - { - /// - /// The debug - /// - Debug, - /// - /// The information - /// - Information, - /// - /// The warning - /// - Warning, - /// - /// The error - /// - Error, - /// - /// The fatal - /// - Fatal - }; -} \ No newline at end of file diff --git a/src/redmine-net-api/Logging/MicrosoftLoggerRedmineAdapter.cs b/src/redmine-net-api/Logging/MicrosoftLoggerRedmineAdapter.cs new file mode 100644 index 00000000..1ac86ef6 --- /dev/null +++ b/src/redmine-net-api/Logging/MicrosoftLoggerRedmineAdapter.cs @@ -0,0 +1,92 @@ +#if NET462_OR_GREATER || NETCOREAPP +using System; +using System.Collections.Generic; + +namespace Redmine.Net.Api.Logging; + +/// +/// Adapter that converts Microsoft.Extensions.Logging.ILogger to IRedmineLogger +/// +public class MicrosoftLoggerRedmineAdapter : IRedmineLogger +{ + private readonly Microsoft.Extensions.Logging.ILogger _microsoftLogger; + + /// + /// Creates a new adapter for Microsoft.Extensions.Logging.ILogger + /// + /// The Microsoft logger to adapt + /// Thrown if microsoftLogger is null + public MicrosoftLoggerRedmineAdapter(Microsoft.Extensions.Logging.ILogger microsoftLogger) + { + _microsoftLogger = microsoftLogger ?? throw new ArgumentNullException(nameof(microsoftLogger)); + } + + /// + /// Checks if logging is enabled for the specified level + /// + public bool IsEnabled(LogLevel level) + { + return _microsoftLogger.IsEnabled(ToMicrosoftLogLevel(level)); + } + + /// + /// Logs a message with the specified level + /// + public void Log(LogLevel level, string message, Exception exception = null) + { + _microsoftLogger.Log( + ToMicrosoftLogLevel(level), + 0, // eventId + message, + exception, + (s, e) => s); + } + + /// + /// Creates a scoped logger with additional context + /// + public IRedmineLogger CreateScope(string scopeName, IDictionary scopeProperties = null) + { + var scopeData = new Dictionary + { + ["ScopeName"] = scopeName + }; + + // Add additional properties if provided + if (scopeProperties != null) + { + foreach (var prop in scopeProperties) + { + scopeData[prop.Key] = prop.Value; + } + } + + // Create a single scope with all properties + var disposableScope = _microsoftLogger.BeginScope(scopeData); + + // Return a new adapter that will close the scope when disposed + return new ScopedMicrosoftLoggerAdapter(_microsoftLogger, disposableScope); + } + + private class ScopedMicrosoftLoggerAdapter(Microsoft.Extensions.Logging.ILogger logger, IDisposable scope) + : MicrosoftLoggerRedmineAdapter(logger), IDisposable + { + public void Dispose() + { + scope?.Dispose(); + } + } + + + private static Microsoft.Extensions.Logging.LogLevel ToMicrosoftLogLevel(LogLevel level) => level switch + { + LogLevel.Trace => Microsoft.Extensions.Logging.LogLevel.Trace, + LogLevel.Debug => Microsoft.Extensions.Logging.LogLevel.Debug, + LogLevel.Information => Microsoft.Extensions.Logging.LogLevel.Information, + LogLevel.Warning => Microsoft.Extensions.Logging.LogLevel.Warning, + LogLevel.Error => Microsoft.Extensions.Logging.LogLevel.Error, + LogLevel.Critical => Microsoft.Extensions.Logging.LogLevel.Critical, + _ => Microsoft.Extensions.Logging.LogLevel.Information + }; +} +#endif diff --git a/src/redmine-net-api/Logging/RedmineConsoleLogger.cs b/src/redmine-net-api/Logging/RedmineConsoleLogger.cs new file mode 100644 index 00000000..23b03cea --- /dev/null +++ b/src/redmine-net-api/Logging/RedmineConsoleLogger.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; + +namespace Redmine.Net.Api.Logging; + +/// +/// +/// +/// +/// +/// +/// +/// +public class RedmineConsoleLogger(string categoryName = "Redmine", LogLevel minLevel = LogLevel.Information) : IRedmineLogger +{ + /// + /// + /// + /// + /// + public bool IsEnabled(LogLevel level) => level >= minLevel; + + /// + /// + /// + /// + /// + /// + public void Log(LogLevel level, string message, Exception exception = null) + { + if (!IsEnabled(level)) + { + return; + } + + // var originalColor = Console.ForegroundColor; + // + // Console.ForegroundColor = level switch + // { + // LogLevel.Trace => ConsoleColor.Gray, + // LogLevel.Debug => ConsoleColor.Gray, + // LogLevel.Information => ConsoleColor.White, + // LogLevel.Warning => ConsoleColor.Yellow, + // LogLevel.Error => ConsoleColor.Red, + // LogLevel.Critical => ConsoleColor.Red, + // _ => ConsoleColor.White + // }; + + Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] [{level}] [{categoryName}] {message}"); + + if (exception != null) + { + Console.WriteLine($"Exception: {exception}"); + } + + // Console.ForegroundColor = originalColor; + } + + /// + /// + /// + /// + /// + /// + public IRedmineLogger CreateScope(string scopeName, IDictionary scopeProperties = null) + { + return new RedmineConsoleLogger($"{categoryName}.{scopeName}", minLevel); + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Logging/RedmineConsoleTraceListener.cs b/src/redmine-net-api/Logging/RedmineConsoleTraceListener.cs deleted file mode 100644 index 531f4371..00000000 --- a/src/redmine-net-api/Logging/RedmineConsoleTraceListener.cs +++ /dev/null @@ -1,86 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Diagnostics; - -namespace Redmine.Net.Api.Logging -{ - /// - /// - /// - public sealed class RedmineConsoleTraceListener : TraceListener - { - #region implemented abstract members of TraceListener - - /// - /// When overridden in a derived class, writes the specified message to the listener you create in the derived class. - /// - /// A message to write. - public override void Write (string message) - { - Console.Write(message); - } - - /// - /// When overridden in a derived class, writes a message to the listener you create in the derived class, followed by a line terminator. - /// - /// A message to write. - public override void WriteLine (string message) - { - Console.WriteLine(message); - } - - #endregion - - /// - /// Writes trace information, a message, and event information to the listener specific output. - /// - /// A object that contains the current process ID, thread ID, and stack trace information. - /// A name used to identify the output, typically the name of the application that generated the trace event. - /// One of the values specifying the type of event that has caused the trace. - /// A numeric identifier for the event. - /// A message to write. - /// - /// - /// - /// - public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, - string message) - { - WriteLine(message); - } - - /// - /// Writes trace information, a formatted array of objects and event information to the listener specific output. - /// - /// A object that contains the current process ID, thread ID, and stack trace information. - /// A name used to identify the output, typically the name of the application that generated the trace event. - /// One of the values specifying the type of event that has caused the trace. - /// A numeric identifier for the event. - /// A format string that contains zero or more format items, which correspond to objects in the array. - /// An object array containing zero or more objects to format. - /// - /// - /// - /// - public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, - string format, params object[] args) - { - WriteLine(string.Format(format, args)); - } - } -} \ No newline at end of file diff --git a/src/redmine-net-api/Logging/RedmineLoggerExtensions.cs b/src/redmine-net-api/Logging/RedmineLoggerExtensions.cs new file mode 100644 index 00000000..39012412 --- /dev/null +++ b/src/redmine-net-api/Logging/RedmineLoggerExtensions.cs @@ -0,0 +1,108 @@ +using System; +using System.Diagnostics; +#if !(NET20 || NET40) +using System.Threading.Tasks; +#endif +namespace Redmine.Net.Api.Logging; + +/// +/// +/// +public static class RedmineLoggerExtensions +{ + /// + /// + /// + /// + /// + /// + public static void Trace(this IRedmineLogger logger, string message, Exception exception = null) + => logger.Log(LogLevel.Trace, message, exception); + + /// + /// + /// + /// + /// + /// + public static void Debug(this IRedmineLogger logger, string message, Exception exception = null) + => logger.Log(LogLevel.Debug, message, exception); + + /// + /// + /// + /// + /// + /// + public static void Info(this IRedmineLogger logger, string message, Exception exception = null) + => logger.Log(LogLevel.Information, message, exception); + + /// + /// + /// + /// + /// + /// + public static void Warn(this IRedmineLogger logger, string message, Exception exception = null) + => logger.Log(LogLevel.Warning, message, exception); + + /// + /// + /// + /// + /// + /// + public static void Error(this IRedmineLogger logger, string message, Exception exception = null) + => logger.Log(LogLevel.Error, message, exception); + + /// + /// + /// + /// + /// + /// + public static void Critical(this IRedmineLogger logger, string message, Exception exception = null) + => logger.Log(LogLevel.Critical, message, exception); + +#if !(NET20 || NET40) + /// + /// Creates and logs timing information for an operation + /// + public static async Task TimeOperationAsync(this IRedmineLogger logger, string operationName, Func> operation) + { + if (!logger.IsEnabled(LogLevel.Debug)) + return await operation().ConfigureAwait(false); + + var sw = Stopwatch.StartNew(); + try + { + return await operation().ConfigureAwait(false); + } + finally + { + sw.Stop(); + logger.Debug($"Operation '{operationName}' completed in {sw.ElapsedMilliseconds}ms"); + } + } + #endif + + /// + /// Creates and logs timing information for an operation + /// + public static T TimeOperationAsync(this IRedmineLogger logger, string operationName, Func operation) + { + if (!logger.IsEnabled(LogLevel.Debug)) + return operation(); + + var sw = Stopwatch.StartNew(); + try + { + return operation(); + } + finally + { + sw.Stop(); + logger.Debug($"Operation '{operationName}' completed in {sw.ElapsedMilliseconds}ms"); + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Logging/RedmineLoggerFactory.cs b/src/redmine-net-api/Logging/RedmineLoggerFactory.cs new file mode 100644 index 00000000..9b2dddff --- /dev/null +++ b/src/redmine-net-api/Logging/RedmineLoggerFactory.cs @@ -0,0 +1,75 @@ +#if NET462_OR_GREATER || NETCOREAPP +namespace Redmine.Net.Api.Logging; + +/// +/// +/// +public static class RedmineLoggerFactory +{ + /// + /// + /// + /// + /// + /// + public static Microsoft.Extensions.Logging.ILogger CreateMicrosoftLoggerAdapter(IRedmineLogger redmineLogger, + string categoryName = "Redmine") + { + if (redmineLogger == null || redmineLogger == RedmineNullLogger.Instance) + { + return Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance; + } + + return new RedmineLoggerMicrosoftAdapter(redmineLogger, categoryName); + } + + /// + /// Creates an adapter that exposes a Microsoft.Extensions.Logging.ILogger as IRedmineLogger + /// + /// The Microsoft logger to adapt + /// A Redmine logger implementation + public static IRedmineLogger CreateMicrosoftLogger(Microsoft.Extensions.Logging.ILogger microsoftLogger) + { + return microsoftLogger != null + ? new MicrosoftLoggerRedmineAdapter(microsoftLogger) + : RedmineNullLogger.Instance; + } + + /// + /// Creates a logger that writes to the console + /// + public static IRedmineLogger CreateConsoleLogger(LogLevel minLevel = LogLevel.Information) + { + return new RedmineConsoleLogger(minLevel: minLevel); + } + + // /// + // /// Creates an adapter for Serilog + // /// + // public static IRedmineLogger CreateSerilogAdapter(Serilog.ILogger logger) + // { + // if (logger == null) return NullRedmineLogger.Instance; + // return new SerilogAdapter(logger); + // } + // + // /// + // /// Creates an adapter for NLog + // /// + // public static IRedmineLogger CreateNLogAdapter(NLog.ILogger logger) + // { + // if (logger == null) return NullRedmineLogger.Instance; + // return new NLogAdapter(logger); + // } + // + // /// + // /// Creates an adapter for log4net + // /// + // public static IRedmineLogger CreateLog4NetAdapter(log4net.ILog logger) + // { + // if (logger == null) return NullRedmineLogger.Instance; + // return new Log4NetAdapter(logger); + // } +} + + +#endif \ No newline at end of file diff --git a/src/redmine-net-api/Logging/RedmineLoggerMicrosoftAdapter.cs b/src/redmine-net-api/Logging/RedmineLoggerMicrosoftAdapter.cs new file mode 100644 index 00000000..56d152f9 --- /dev/null +++ b/src/redmine-net-api/Logging/RedmineLoggerMicrosoftAdapter.cs @@ -0,0 +1,97 @@ +#if NET462_OR_GREATER || NETCOREAPP +using System; +using System.Collections.Generic; + +namespace Redmine.Net.Api.Logging; + +/// +/// +/// +public class RedmineLoggerMicrosoftAdapter : Microsoft.Extensions.Logging.ILogger +{ + private readonly IRedmineLogger _redmineLogger; + private readonly string _categoryName; + + /// + /// + /// + /// + /// + /// + public RedmineLoggerMicrosoftAdapter(IRedmineLogger redmineLogger, string categoryName = "Redmine.Net.Api") + { + _redmineLogger = redmineLogger ?? throw new ArgumentNullException(nameof(redmineLogger)); + _categoryName = categoryName; + } + + /// + /// + /// + /// + /// + /// + public IDisposable BeginScope(TState state) + { + if (state is IDictionary dict) + { + _redmineLogger.CreateScope("Scope", dict); + } + else + { + var scopeName = state?.ToString() ?? "Scope"; + _redmineLogger.CreateScope(scopeName); + } + + return new NoOpDisposable(); + } + + /// + /// + /// + /// + /// + public bool IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel) + { + return _redmineLogger.IsEnabled(ToRedmineLogLevel(logLevel)); + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + public void Log( + Microsoft.Extensions.Logging.LogLevel logLevel, + Microsoft.Extensions.Logging.EventId eventId, + TState state, + Exception exception, + Func formatter) + { + if (!IsEnabled(logLevel)) + return; + + var message = formatter(state, exception); + _redmineLogger.Log(ToRedmineLogLevel(logLevel), message, exception); + } + + private static LogLevel ToRedmineLogLevel(Microsoft.Extensions.Logging.LogLevel level) => level switch + { + Microsoft.Extensions.Logging.LogLevel.Trace => LogLevel.Trace, + Microsoft.Extensions.Logging.LogLevel.Debug => LogLevel.Debug, + Microsoft.Extensions.Logging.LogLevel.Information => LogLevel.Information, + Microsoft.Extensions.Logging.LogLevel.Warning => LogLevel.Warning, + Microsoft.Extensions.Logging.LogLevel.Error => LogLevel.Error, + Microsoft.Extensions.Logging.LogLevel.Critical => LogLevel.Critical, + _ => LogLevel.Information + }; + + private class NoOpDisposable : IDisposable + { + public void Dispose() { } + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net-api/Logging/RedmineLoggingOptions.cs b/src/redmine-net-api/Logging/RedmineLoggingOptions.cs new file mode 100644 index 00000000..5ff0653a --- /dev/null +++ b/src/redmine-net-api/Logging/RedmineLoggingOptions.cs @@ -0,0 +1,27 @@ +namespace Redmine.Net.Api.Logging; + +/// +/// Options for configuring Redmine logging +/// +public sealed class RedmineLoggingOptions +{ + /// + /// Gets or sets the minimum log level + /// + public LogLevel MinimumLevel { get; set; } = LogLevel.Information; + + /// + /// Gets or sets whether detailed API request/response logging is enabled + /// + public bool EnableVerboseLogging { get; set; } + + /// + /// Gets or sets whether to include HTTP request/response details in logs + /// + public bool IncludeHttpDetails { get; set; } + + /// + /// Gets or sets whether performance metrics should be logged + /// + public bool LogPerformanceMetrics { get; set; } +} \ No newline at end of file diff --git a/src/redmine-net-api/Logging/RedmineNullLogger.cs b/src/redmine-net-api/Logging/RedmineNullLogger.cs new file mode 100644 index 00000000..0d47ab1d --- /dev/null +++ b/src/redmine-net-api/Logging/RedmineNullLogger.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; + +namespace Redmine.Net.Api.Logging; + +/// +/// +/// +public class RedmineNullLogger : IRedmineLogger +{ + /// + /// + /// + public static readonly RedmineNullLogger Instance = new RedmineNullLogger(); + + private RedmineNullLogger() { } + + /// + /// + /// + /// + /// + public bool IsEnabled(LogLevel level) => false; + + /// + /// + /// + /// + /// + /// + public void Log(LogLevel level, string message, Exception exception = null) { } + + /// + /// + /// + /// + /// + /// + public IRedmineLogger CreateScope(string scopeName, IDictionary scopeProperties = null) => this; +} \ No newline at end of file diff --git a/src/redmine-net-api/Logging/TraceLogger.cs b/src/redmine-net-api/Logging/TraceLogger.cs deleted file mode 100644 index 324252dc..00000000 --- a/src/redmine-net-api/Logging/TraceLogger.cs +++ /dev/null @@ -1,56 +0,0 @@ -/* - Copyright 2011 - 2019 Adrian Popescu. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Diagnostics; - -namespace Redmine.Net.Api.Logging -{ - /// - /// - /// - public sealed class TraceLogger : ILogger - { - /// - /// Logs the specified entry. - /// - /// The entry. - /// - public void Log(LogEntry entry) - { - switch (entry.Severity) - { - case LoggingEventType.Debug: - Trace.WriteLine(entry.Message, "Debug"); - break; - case LoggingEventType.Information: - Trace.TraceInformation(entry.Message); - break; - case LoggingEventType.Warning: - Trace.TraceWarning(entry.Message); - break; - case LoggingEventType.Error: - Trace.TraceError(entry.Message); - break; - case LoggingEventType.Fatal: - Trace.WriteLine(entry.Message, "Fatal"); - break; - default: - throw new ArgumentOutOfRangeException(); - } - } - } -} \ No newline at end of file diff --git a/src/redmine-net-api/RedmineManagerOptions.cs b/src/redmine-net-api/RedmineManagerOptions.cs index 3801922b..d8acf1c8 100644 --- a/src/redmine-net-api/RedmineManagerOptions.cs +++ b/src/redmine-net-api/RedmineManagerOptions.cs @@ -17,7 +17,7 @@ limitations under the License. using System; using System.Net; using Redmine.Net.Api.Authentication; -using Redmine.Net.Api.Net; +using Redmine.Net.Api.Logging; using Redmine.Net.Api.Net.WebClient; using Redmine.Net.Api.Serialization; @@ -69,5 +69,12 @@ internal sealed class RedmineManagerOptions public Version RedmineVersion { get; init; } internal bool VerifyServerCert { get; init; } + + public IRedmineLogger Logger { get; init; } + + /// + /// Gets or sets additional logging configuration options + /// + public RedmineLoggingOptions LoggingOptions { get; init; } = new RedmineLoggingOptions(); } } \ No newline at end of file diff --git a/src/redmine-net-api/RedmineManagerOptionsBuilder.cs b/src/redmine-net-api/RedmineManagerOptionsBuilder.cs index 3b3c9f3d..114424a5 100644 --- a/src/redmine-net-api/RedmineManagerOptionsBuilder.cs +++ b/src/redmine-net-api/RedmineManagerOptionsBuilder.cs @@ -16,9 +16,12 @@ limitations under the License. using System; using System.Net; +#if NET462_OR_GREATER || NETCOREAPP +using Microsoft.Extensions.Logging; +#endif using Redmine.Net.Api.Authentication; using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Logging; using Redmine.Net.Api.Net; using Redmine.Net.Api.Net.WebClient; using Redmine.Net.Api.Serialization; @@ -30,6 +33,9 @@ namespace Redmine.Net.Api /// public sealed class RedmineManagerOptionsBuilder { + private IRedmineLogger _redmineLogger = RedmineNullLogger.Instance; + private Action _configureLoggingOptions; + private enum ClientType { WebClient, @@ -190,6 +196,32 @@ public RedmineManagerOptionsBuilder WithVersion(Version version) return this; } + /// + /// + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithLogger(IRedmineLogger logger, Action configure = null) + { + _redmineLogger = logger ?? RedmineNullLogger.Instance; + _configureLoggingOptions = configure; + return this; + } +#if NET462_OR_GREATER || NETCOREAPP + /// + /// + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithLogger(ILogger logger, Action configure = null) + { + _redmineLogger = new MicrosoftLoggerRedmineAdapter(logger); + _configureLoggingOptions = configure; + return this; + } +#endif /// /// Gets or sets the version of the Redmine server to which this client will connect. /// @@ -239,6 +271,12 @@ internal RedmineManagerOptions Build() #endif var baseAddress = CreateRedmineUri(Host, WebClientOptions.Scheme); + RedmineLoggingOptions redmineLoggingOptions = null; + if (_configureLoggingOptions != null) + { + redmineLoggingOptions = new RedmineLoggingOptions(); + _configureLoggingOptions(redmineLoggingOptions); + } var options = new RedmineManagerOptions() { @@ -249,6 +287,8 @@ internal RedmineManagerOptions Build() RedmineVersion = Version, Authentication = Authentication ?? new RedmineNoAuthentication(), WebClientOptions = WebClientOptions + Logger = _redmineLogger, + LoggingOptions = redmineLoggingOptions, }; return options; diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 81743a04..b7aa954b 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -3,6 +3,7 @@ |net20|net40| + |net20|net40|net45|net451|net452|net46|net461| @@ -91,7 +92,7 @@ - + @@ -105,11 +106,7 @@ - - - - redmine-net-api.snk From 52414f6d70bdb7d40aba91932c5342391e4467f2 Mon Sep 17 00:00:00 2001 From: Padi Date: Thu, 22 May 2025 22:54:08 +0300 Subject: [PATCH 521/549] [New] Integration ProgressTests --- .../Tests/Progress/ProgressTests.Async.cs | 59 ++++++++++ .../Tests/Progress/ProgressTests.cs | 57 +++++++++ .../Tests/ProgressTests.cs | 109 ------------------ .../Tests/ProgressTestsAsync.cs | 9 -- 4 files changed, 116 insertions(+), 118 deletions(-) create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Progress/ProgressTests.Async.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Progress/ProgressTests.cs delete mode 100644 tests/redmine-net-api.Integration.Tests/Tests/ProgressTests.cs delete mode 100644 tests/redmine-net-api.Integration.Tests/Tests/ProgressTestsAsync.cs diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Progress/ProgressTests.Async.cs b/tests/redmine-net-api.Integration.Tests/Tests/Progress/ProgressTests.Async.cs new file mode 100644 index 00000000..be6fc659 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Progress/ProgressTests.Async.cs @@ -0,0 +1,59 @@ +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests; + +public partial class ProgressTests +{ + [Fact] + public async Task DownloadFileAsync_ReportsProgress() + { + // Arrange + var progressTracker = new ProgressTracker(); + + // Act + var result = await fixture.RedmineManager.DownloadFileAsync( + TEST_DOWNLOAD_URL, + null, + progressTracker, + CancellationToken.None); + + // Assert + Assert.NotNull(result); + Assert.True(result.Length > 0, "Downloaded content should not be empty"); + + AssertProgressWasReported(progressTracker); + } + + [Fact] + public async Task DownloadFileAsync_WithCancellation_StopsDownload() + { + // Arrange + var progressTracker = new ProgressTracker(); + var cts = new CancellationTokenSource(); + + try + { + progressTracker.OnProgressReported += (sender, args) => + { + if (args.Value > 0 && !cts.IsCancellationRequested) + { + cts.Cancel(); + } + }; + + // Act & Assert + await Assert.ThrowsAnyAsync(async () => + { + await fixture.RedmineManager.DownloadFileAsync( + TEST_DOWNLOAD_URL, + null, + progressTracker, + cts.Token); + }); + + Assert.True(progressTracker.ReportCount > 0, "Progress should have been reported at least once"); + } + finally + { + cts.Dispose(); + } + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Progress/ProgressTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Progress/ProgressTests.cs new file mode 100644 index 00000000..1f0d2bcc --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Progress/ProgressTests.cs @@ -0,0 +1,57 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests; + +[Collection(Constants.RedmineTestContainerCollection)] +public partial class ProgressTests(RedmineTestContainerFixture fixture) +{ + private const string TEST_DOWNLOAD_URL = "attachments/download/86/Manual_de_control_fiscal_versiune%20finala_RO_24_07_2023.pdf"; + + [Fact] + public void DownloadFile_Sync_ReportsProgress() + { + // Arrange + var progressTracker = new ProgressTracker(); + + // Act + var result = fixture.RedmineManager.DownloadFile(TEST_DOWNLOAD_URL, progressTracker); + + // Assert + Assert.NotNull(result); + Assert.True(result.Length > 0, "Downloaded content should not be empty"); + + AssertProgressWasReported(progressTracker); + } + + private static void AssertProgressWasReported(ProgressTracker tracker) + { + Assert.True(tracker.ReportCount > 0, "Progress should have been reported at least once"); + + Assert.Contains(100, tracker.ProgressValues); + + for (var i = 0; i < tracker.ProgressValues.Count - 1; i++) + { + Assert.True(tracker.ProgressValues[i] <= tracker.ProgressValues[i + 1], + $"Progress should not decrease: {tracker.ProgressValues[i]} -> {tracker.ProgressValues[i + 1]}"); + } + } + + private sealed class ProgressTracker : IProgress + { + public List ProgressValues { get; } = []; + public int ReportCount => ProgressValues.Count; + + public event EventHandler OnProgressReported; + + public void Report(int value) + { + ProgressValues.Add(value); + OnProgressReported?.Invoke(this, new ProgressReportedEventArgs(value)); + } + + public sealed class ProgressReportedEventArgs(int value) : EventArgs + { + public int Value { get; } = value; + } + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/ProgressTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/ProgressTests.cs deleted file mode 100644 index 2c7547cb..00000000 --- a/tests/redmine-net-api.Integration.Tests/Tests/ProgressTests.cs +++ /dev/null @@ -1,109 +0,0 @@ -using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; - -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests; - -[Collection(Constants.RedmineTestContainerCollection)] -public class ProgressTests(RedmineTestContainerFixture fixture) -{ - private const string TestDownloadUrl = "attachments/download/123"; - - [Fact] - public void DownloadFile_Sync_ReportsProgress() - { - // Arrange - var progressTracker = new ProgressTracker(); - - // Act - var result = fixture.RedmineManager.DownloadFile( - TestDownloadUrl, - progressTracker); - - // Assert - Assert.NotNull(result); - Assert.True(result.Length > 0, "Downloaded content should not be empty"); - - AssertProgressWasReported(progressTracker); - } - - [Fact] - public async Task DownloadFileAsync_ReportsProgress() - { - // Arrange - var progressTracker = new ProgressTracker(); - - // Act - var result = await fixture.RedmineManager.DownloadFileAsync( - TestDownloadUrl, - null, // No custom request options - progressTracker, - CancellationToken.None); - - // Assert - Assert.NotNull(result); - Assert.True(result.Length > 0, "Downloaded content should not be empty"); - - // Verify progress reporting - AssertProgressWasReported(progressTracker); - } - - [Fact] - public async Task DownloadFileAsync_WithCancellation_StopsDownload() - { - // Arrange - var progressTracker = new ProgressTracker(); - using var cts = new CancellationTokenSource(); - - progressTracker.OnProgressReported += (sender, args) => - { - if (args.Value > 0 && !cts.IsCancellationRequested) - { - cts.Cancel(); - } - }; - - // Act & Assert - await Assert.ThrowsAnyAsync(async () => - { - await fixture.RedmineManager.DownloadFileAsync( - TestDownloadUrl, - null, - progressTracker, - cts.Token); - }); - - // Should have received at least one progress report - Assert.True(progressTracker.ReportCount > 0, "Progress should have been reported at least once"); - } - - private static void AssertProgressWasReported(ProgressTracker tracker) - { - Assert.True(tracker.ReportCount > 0, "Progress should have been reported at least once"); - - Assert.Contains(100, tracker.ProgressValues); - - for (var i = 0; i < tracker.ProgressValues.Count - 1; i++) - { - Assert.True(tracker.ProgressValues[i] <= tracker.ProgressValues[i + 1], - $"Progress should not decrease: {tracker.ProgressValues[i]} -> {tracker.ProgressValues[i + 1]}"); - } - } - - private class ProgressTracker : IProgress - { - public List ProgressValues { get; } = []; - public int ReportCount => ProgressValues.Count; - - public event EventHandler OnProgressReported; - - public void Report(int value) - { - ProgressValues.Add(value); - OnProgressReported?.Invoke(this, new ProgressReportedEventArgs(value)); - } - - public class ProgressReportedEventArgs(int value) : EventArgs - { - public int Value { get; } = value; - } - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/ProgressTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/ProgressTestsAsync.cs deleted file mode 100644 index 14c0d767..00000000 --- a/tests/redmine-net-api.Integration.Tests/Tests/ProgressTestsAsync.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; - -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests; - -[Collection(Constants.RedmineTestContainerCollection)] -public class ProgressTestsAsync(RedmineTestContainerFixture fixture) -{ - -} \ No newline at end of file From 4023ad35a6eff9043f9aac8f0ff4f0cd2608d4a7 Mon Sep 17 00:00:00 2001 From: Padi Date: Fri, 23 May 2025 09:39:14 +0300 Subject: [PATCH 522/549] Add GetMyAccountAssync --- .../Extensions/RedmineManagerExtensions.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs index 0cf9ccf2..c5d71390 100644 --- a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs +++ b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs @@ -673,7 +673,6 @@ public static async Task> GetProjectFilesAsync(this RedmineMa return response.DeserializeToPagedResults(redmineManager.Serializer); } - /// /// @@ -718,6 +717,22 @@ public static async Task GetCurrentUserAsync(this RedmineManager redmineMa return response.DeserializeTo(redmineManager.Serializer); } + /// + /// Retrieves the account details of the currently authenticated user. + /// + /// The instance of the RedmineManager used to perform the API call. + /// Optional configuration for the API request. + /// + /// Returns the account details of the authenticated user as a MyAccount object. + public static async Task GetMyAccountAsync(this RedmineManager redmineManager, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + var uri = redmineManager.RedmineApiUrls.MyAccount(); + + var response = await redmineManager.ApiClient.GetAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); + + return response.DeserializeTo(redmineManager.Serializer); + } + /// /// Creates or updates wiki page asynchronous. /// From fda48dac60860134c50c5ded62e3a3ea3569d14e Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 28 May 2025 16:24:41 +0300 Subject: [PATCH 523/549] Bump up redmine and postgres version --- docker-compose.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 6e7274ac..4cb6caf7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,7 @@ services: ports: - '8089:3000' image: 'redmine:6.0.5-alpine' - container_name: 'redmine-web' + container_name: 'redmine-web605' depends_on: - db-postgres # healthcheck: @@ -32,8 +32,8 @@ services: POSTGRES_DB: redmine POSTGRES_USER: redmine-usr POSTGRES_PASSWORD: redmine-pswd - container_name: 'redmine-db' - image: 'postgres:17.4-alpine' + container_name: 'redmine-db175' + image: 'postgres:17.5-alpine' healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 20s From 333e06f8b2e5dacf035f3598227f243398467a56 Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 28 May 2025 16:23:30 +0300 Subject: [PATCH 524/549] Remove obsolete files --- .../RedmineManagerAsyncExtensions.Obsolete.cs | 350 ---------- .../Extensions/RedmineManagerExtensions.cs | 22 - .../IRedmineManager.Obsolete.cs | 306 --------- .../WebClient/IRedmineWebClient.Obsolete.cs | 90 --- .../WebClient/RedmineWebClient.Obsolete.cs | 261 -------- .../RedmineManager.Obsolete.cs | 617 ------------------ src/redmine-net-api/RedmineManager.cs | 35 +- .../Serialization/MimeFormatObsolete.cs | 35 - src/redmine-net-api/Types/Project.cs | 12 - .../_net20/RedmineManagerAsyncObsolete.cs | 324 --------- 10 files changed, 17 insertions(+), 2035 deletions(-) delete mode 100644 src/redmine-net-api/Extensions/RedmineManagerAsyncExtensions.Obsolete.cs delete mode 100644 src/redmine-net-api/IRedmineManager.Obsolete.cs delete mode 100644 src/redmine-net-api/Net/WebClient/IRedmineWebClient.Obsolete.cs delete mode 100644 src/redmine-net-api/Net/WebClient/RedmineWebClient.Obsolete.cs delete mode 100644 src/redmine-net-api/RedmineManager.Obsolete.cs delete mode 100755 src/redmine-net-api/Serialization/MimeFormatObsolete.cs delete mode 100644 src/redmine-net-api/_net20/RedmineManagerAsyncObsolete.cs diff --git a/src/redmine-net-api/Extensions/RedmineManagerAsyncExtensions.Obsolete.cs b/src/redmine-net-api/Extensions/RedmineManagerAsyncExtensions.Obsolete.cs deleted file mode 100644 index c6a1373a..00000000 --- a/src/redmine-net-api/Extensions/RedmineManagerAsyncExtensions.Obsolete.cs +++ /dev/null @@ -1,350 +0,0 @@ -/* -Copyright 2011 - 2025 Adrian Popescu - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -#if !(NET20) - -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Threading; -using System.Threading.Tasks; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.Async -{ - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use RedmineManger async methods instead.")] - public static class RedmineManagerAsyncExtensions - { - /// - /// Gets the current user asynchronous. - /// - /// The redmine manager. - /// The parameters. - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")] - public static async Task GetCurrentUserAsync(this RedmineManager redmineManager, NameValueCollection parameters = null, string impersonateUserName = null, CancellationToken cancellationToken = default) - { - var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); - return await redmineManager.GetCurrentUserAsync(requestOptions, cancellationToken).ConfigureAwait(false); - } - - /// - /// Creates the or update wiki page asynchronous. - /// - /// The redmine manager. - /// The project identifier. - /// Name of the page. - /// The wiki page. - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")] - public static async Task CreateWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage) - { - var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); - - return await redmineManager.CreateWikiPageAsync(projectId, pageName, wikiPage, requestOptions).ConfigureAwait(false); - } - - /// - /// Creates or updates wiki page asynchronous. - /// - /// The redmine manager. - /// The project identifier. - /// Name of the page. - /// The wiki page. - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")] - public static async Task UpdateWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName, WikiPage wikiPage) - { - var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); - await redmineManager.UpdateWikiPageAsync(projectId, pageName, wikiPage, requestOptions).ConfigureAwait(false); - } - - /// - /// Deletes the wiki page asynchronous. - /// - /// The redmine manager. - /// The project identifier. - /// Name of the page. - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")] - public static async Task DeleteWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName) - { - var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); - await redmineManager.DeleteWikiPageAsync(projectId, pageName, requestOptions).ConfigureAwait(false); - } - - /// - /// Support for adding attachments through the REST API is added in Redmine 1.4.0. - /// Upload a file to the server. This method does not block the calling thread. - /// - /// The redmine manager. - /// The content of the file that will be uploaded on server. - /// - /// . - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")] - public static async Task UploadFileAsync(this RedmineManager redmineManager, byte[] data) - { - var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); - return await redmineManager.UploadFileAsync(data, null, requestOptions).ConfigureAwait(false); - } - - /// - /// Downloads the file asynchronous. - /// - /// The redmine manager. - /// The address. - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")] - public static async Task DownloadFileAsync(this RedmineManager redmineManager, string address) - { - var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); - - return await redmineManager.DownloadFileAsync(address, requestOptions).ConfigureAwait(false); - } - - /// - /// Gets the wiki page asynchronous. - /// - /// The redmine manager. - /// The project identifier. - /// The parameters. - /// Name of the page. - /// The version. - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")] - public static async Task GetWikiPageAsync(this RedmineManager redmineManager, string projectId, NameValueCollection parameters, string pageName, uint version = 0) - { - var requestOptions = RedmineManagerExtensions.CreateRequestOptions(parameters); - return await redmineManager.GetWikiPageAsync(projectId, pageName, requestOptions, version).ConfigureAwait(false); - } - - /// - /// Gets all wiki pages asynchronous. - /// - /// The redmine manager. - /// The parameters. - /// The project identifier. - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")] - public static async Task> GetAllWikiPagesAsync(this RedmineManager redmineManager, NameValueCollection parameters, string projectId) - { - var requestOptions = RedmineManagerExtensions.CreateRequestOptions(parameters); - return await redmineManager.GetAllWikiPagesAsync(projectId, requestOptions).ConfigureAwait(false); - } - - /// - /// Adds an existing user to a group. This method does not block the calling thread. - /// - /// The redmine manager. - /// The group id. - /// The user id. - /// - /// Returns the Guid associated with the async request. - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")] - public static async Task AddUserToGroupAsync(this RedmineManager redmineManager, int groupId, int userId) - { - var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); - await redmineManager.AddUserToGroupAsync(groupId, userId, requestOptions).ConfigureAwait(false); - } - - /// - /// Removes an user from a group. This method does not block the calling thread. - /// - /// The redmine manager. - /// The group id. - /// The user id. - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")] - public static async Task RemoveUserFromGroupAsync(this RedmineManager redmineManager, int groupId, int userId) - { - var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); - await redmineManager.RemoveUserFromGroupAsync(groupId, userId, requestOptions).ConfigureAwait(false); - } - - /// - /// Adds the watcher asynchronous. - /// - /// The redmine manager. - /// The issue identifier. - /// The user identifier. - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")] - public static async Task AddWatcherToIssueAsync(this RedmineManager redmineManager, int issueId, int userId) - { - var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); - await redmineManager.AddWatcherToIssueAsync(issueId, userId, requestOptions).ConfigureAwait(false); - } - - /// - /// Removes the watcher asynchronous. - /// - /// The redmine manager. - /// The issue identifier. - /// The user identifier. - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")] - public static async Task RemoveWatcherFromIssueAsync(this RedmineManager redmineManager, int issueId, int userId) - { - var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); - await redmineManager.RemoveWatcherFromIssueAsync(issueId, userId, requestOptions).ConfigureAwait(false); - } - - /// - /// - /// - /// - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use the extension with RequestOptions parameter instead.")] - public static async Task CountAsync(this RedmineManager redmineManager, params string[] include) where T : class, new() - { - return await redmineManager.CountAsync(null, CancellationToken.None).ConfigureAwait(false); - } - - /// - /// - /// - /// - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use CountAsync method instead.")] - public static async Task CountAsync(this RedmineManager redmineManager, NameValueCollection parameters) where T : class, new() - { - var requestOptions = RedmineManagerExtensions.CreateRequestOptions(parameters); - return await redmineManager.CountAsync(requestOptions).ConfigureAwait(false); - } - - - /// - /// Gets the paginated objects asynchronous. - /// - /// - /// The redmine manager. - /// The parameters. - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use GetPagedAsync method instead.")] - public static async Task> GetPaginatedObjectsAsync(this RedmineManager redmineManager, NameValueCollection parameters) - where T : class, new() - { - var requestOptions = RedmineManagerExtensions.CreateRequestOptions(parameters); - return await redmineManager.GetPagedAsync(requestOptions).ConfigureAwait(false); - } - - /// - /// Gets the objects asynchronous. - /// - /// - /// The redmine manager. - /// The parameters. - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use GetAsync method instead.")] - public static async Task> GetObjectsAsync(this RedmineManager redmineManager, NameValueCollection parameters) - where T : class, new() - { - var requestOptions = RedmineManagerExtensions.CreateRequestOptions(parameters); - return await redmineManager.GetAsync(requestOptions).ConfigureAwait(false); - } - - /// - /// Gets a Redmine object. This method does not block the calling thread. - /// - /// The type of objects to retrieve. - /// The redmine manager. - /// The id of the object. - /// Optional filters and/or optional fetched data. - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use GetAsync method instead.")] - public static async Task GetObjectAsync(this RedmineManager redmineManager, string id, NameValueCollection parameters) - where T : class, new() - { - var requestOptions = RedmineManagerExtensions.CreateRequestOptions(parameters); - return await redmineManager.GetAsync(id, requestOptions).ConfigureAwait(false); - } - - /// - /// Creates a new Redmine object. This method does not block the calling thread. - /// - /// The type of object to create. - /// The redmine manager. - /// The object to create. - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use CreateAsync method instead.")] - public static async Task CreateObjectAsync(this RedmineManager redmineManager, T entity) - where T : class, new() - { - var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); - return await redmineManager.CreateAsync(entity, null, requestOptions).ConfigureAwait(false); - } - - /// - /// Creates a new Redmine object. This method does not block the calling thread. - /// - /// The type of object to create. - /// The redmine manager. - /// The object to create. - /// The owner identifier. - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use CreateAsync method instead.")] - public static async Task CreateObjectAsync(this RedmineManager redmineManager, T entity, string ownerId) - where T : class, new() - { - var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); - return await redmineManager.CreateAsync(entity, ownerId, requestOptions, CancellationToken.None).ConfigureAwait(false); - } - - /// - /// Updates the object asynchronous. - /// - /// - /// The redmine manager. - /// The identifier. - /// The object. - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use UpdateAsync method instead.")] - public static async Task UpdateObjectAsync(this RedmineManager redmineManager, string id, T entity) - where T : class, new() - { - var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); - await redmineManager.UpdateAsync(id, entity, requestOptions).ConfigureAwait(false); - } - - /// - /// Deletes the Redmine object. This method does not block the calling thread. - /// - /// The type of objects to delete. - /// The redmine manager. - /// The id of the object to delete - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use DeleteAsync method instead.")] - public static async Task DeleteObjectAsync(this RedmineManager redmineManager, string id) - where T : class, new() - { - var requestOptions = RedmineManagerExtensions.CreateRequestOptions(); - await redmineManager.DeleteAsync(id, requestOptions).ConfigureAwait(false); - } - } -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs index c5d71390..915e3a77 100644 --- a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs +++ b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs @@ -912,27 +912,5 @@ public static async Task RemoveWatcherFromIssueAsync(this RedmineManager redmine await redmineManager.ApiClient.DeleteAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); } #endif - - internal static RequestOptions CreateRequestOptions(NameValueCollection parameters = null, string impersonateUserName = null) - { - RequestOptions requestOptions = null; - if (parameters != null) - { - requestOptions = new RequestOptions() - { - QueryString = parameters - }; - } - - if (impersonateUserName.IsNullOrWhiteSpace()) - { - return requestOptions; - } - - requestOptions ??= new RequestOptions(); - requestOptions.ImpersonateUser = impersonateUserName; - - return requestOptions; - } } } \ No newline at end of file diff --git a/src/redmine-net-api/IRedmineManager.Obsolete.cs b/src/redmine-net-api/IRedmineManager.Obsolete.cs deleted file mode 100644 index 028984a3..00000000 --- a/src/redmine-net-api/IRedmineManager.Obsolete.cs +++ /dev/null @@ -1,306 +0,0 @@ -/* - Copyright 2011 - 2025 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Net; -using System.Net.Security; -using System.Security.Cryptography.X509Certificates; -using Redmine.Net.Api.Serialization; - -namespace Redmine.Net.Api.Types -{ - /// - /// - /// - public partial interface IRedmineManager - { - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - string Host { get; } - - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - string ApiKey { get; } - - /// - /// Maximum page-size when retrieving complete object lists - /// - /// By default, only 25 results can be retrieved per request. Maximum is 100. To change the maximum value set - /// in your Settings -> General, "Objects per page options".By adding (for instance) 9999 there would make you - /// able to get that many results per request. - /// - /// - /// - /// The size of the page. - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - int PageSize { get; set; } - - /// - /// As of Redmine 2.2.0 you can impersonate user setting user login (e.g. jsmith). This only works when using the API - /// with an administrator account, this header will be ignored when using the API with a regular user account. - /// - /// - /// The impersonate user. - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - string ImpersonateUser { get; set; } - - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - MimeFormat MimeFormat { get; } - - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - IWebProxy Proxy { get; } - - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - SecurityProtocolType SecurityProtocolType { get; } - - /// - /// - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'GetCurrentUser' extension instead")] - User GetCurrentUser(NameValueCollection parameters = null); - - /// - /// - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'AddUserToGroup' extension instead")] - void AddUserToGroup(int groupId, int userId); - - /// - /// - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'RemoveUserFromGroup' extension instead")] - void RemoveUserFromGroup(int groupId, int userId); - - /// - /// - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'AddWatcherToIssue' extension instead")] - void AddWatcherToIssue(int issueId, int userId); - - /// - /// - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'RemoveWatcherFromIssue' extension instead")] - void RemoveWatcherFromIssue(int issueId, int userId); - - /// - /// - /// - /// - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'CreateWikiPage' extension instead")] - WikiPage CreateWikiPage(string projectId, string pageName, WikiPage wikiPage); - - /// - /// - /// - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'UpdateWikiPage' extension instead")] - void UpdateWikiPage(string projectId, string pageName, WikiPage wikiPage); - - /// - /// - /// - /// - /// - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'GetWikiPage' extension instead")] - WikiPage GetWikiPage(string projectId, NameValueCollection parameters, string pageName, uint version = 0); - - /// - /// - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'GetAllWikiPages' extension instead")] - List GetAllWikiPages(string projectId); - - /// - /// - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'DeleteWikiPage' extension instead")] - void DeleteWikiPage(string projectId, string pageName); - - /// - /// - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'UpdateAttachment' extension instead")] - void UpdateAttachment(int issueId, Attachment attachment); - - /// - /// - /// - /// query strings. enable to specify multiple values separated by a space " ". - /// number of results in response. - /// skip this number of results in response - /// Optional filters. - /// - /// Returns the search results by the specified condition parameters. - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'Search' extension instead")] - PagedResults Search(string q, int limit , int offset = 0, SearchFilterBuilder searchFilter = null); - - /// - /// - /// - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'GetPaginated' method instead")] - PagedResults GetPaginatedObjects(NameValueCollection parameters) where T : class, new(); - - /// - /// - /// - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'Count' method instead")] - int Count(params string[] include) where T : class, new(); - - /// - /// - /// - /// - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'Get' method instead")] - T GetObject(string id, NameValueCollection parameters) where T : class, new(); - - /// - /// - /// - /// - /// - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'Get' method instead")] - List GetObjects(int limit, int offset, params string[] include) where T : class, new(); - - /// - /// - /// - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'Get' method instead")] - List GetObjects(params string[] include) where T : class, new(); - - /// - /// - /// - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'Get' method instead")] - List GetObjects(NameValueCollection parameters) where T : class, new(); - - /// - /// - /// - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'Create' method instead")] - T CreateObject(T entity) where T : class, new(); - /// - /// - /// - /// - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'Create' method instead")] - T CreateObject(T entity, string ownerId) where T : class, new(); - - /// - /// - /// - /// - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'Update' method instead")] - void UpdateObject(string id, T entity, string projectId = null) where T : class, new(); - - /// - /// - /// - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use 'Delete' method instead")] - void DeleteObject(string id, NameValueCollection parameters = null) where T : class, new(); - - /// - /// - /// - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - RedmineWebClient CreateWebClient(NameValueCollection parameters, bool uploadFile = false); - /// - /// - /// - /// - /// - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - bool RemoteCertValidate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors sslPolicyErrors); - } -} \ No newline at end of file diff --git a/src/redmine-net-api/Net/WebClient/IRedmineWebClient.Obsolete.cs b/src/redmine-net-api/Net/WebClient/IRedmineWebClient.Obsolete.cs deleted file mode 100644 index 3b7c1bd9..00000000 --- a/src/redmine-net-api/Net/WebClient/IRedmineWebClient.Obsolete.cs +++ /dev/null @@ -1,90 +0,0 @@ -/* - Copyright 2011 - 2025 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Specialized; -using System.Net; -using System.Net.Cache; - -namespace Redmine.Net.Api.Types -{ - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - public interface IRedmineWebClient - { - /// - /// - /// - string UserAgent { get; set; } - - /// - /// - /// - bool UseProxy { get; set; } - - /// - /// - /// - bool UseCookies { get; set; } - - /// - /// - /// - TimeSpan? Timeout { get; set; } - - /// - /// - /// - CookieContainer CookieContainer { get; set; } - - /// - /// - /// - bool PreAuthenticate { get; set; } - - /// - /// - /// - bool KeepAlive { get; set; } - - /// - /// - /// - NameValueCollection QueryString { get; } - - /// - /// - /// - bool UseDefaultCredentials { get; set; } - - /// - /// - /// - ICredentials Credentials { get; set; } - - /// - /// - /// - IWebProxy Proxy { get; set; } - - /// - /// - /// - RequestCachePolicy CachePolicy { get; set; } - } -} \ No newline at end of file diff --git a/src/redmine-net-api/Net/WebClient/RedmineWebClient.Obsolete.cs b/src/redmine-net-api/Net/WebClient/RedmineWebClient.Obsolete.cs deleted file mode 100644 index 90ab613e..00000000 --- a/src/redmine-net-api/Net/WebClient/RedmineWebClient.Obsolete.cs +++ /dev/null @@ -1,261 +0,0 @@ -/* - Copyright 2011 - 2025 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Net; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Net.WebClient.Extensions; -using Redmine.Net.Api.Serialization; - -namespace Redmine.Net.Api -{ - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - #pragma warning disable SYSLIB0014 - public class RedmineWebClient : WebClient - { - private string redirectUrl = string.Empty; - - /// - /// - /// - public string UserAgent { get; set; } - - /// - /// Gets or sets a value indicating whether [use proxy]. - /// - /// - /// true if [use proxy]; otherwise, false. - /// - public bool UseProxy { get; set; } - - /// - /// Gets or sets a value indicating whether [use cookies]. - /// - /// - /// true if [use cookies]; otherwise, false. - /// - public bool UseCookies { get; set; } - - /// - /// in milliseconds - /// - /// - /// The timeout. - /// - public TimeSpan? Timeout { get; set; } - - /// - /// Gets or sets the cookie container. - /// - /// - /// The cookie container. - /// - public CookieContainer CookieContainer { get; set; } - - /// - /// Gets or sets a value indicating whether [pre authenticate]. - /// - /// - /// true if [pre authenticate]; otherwise, false. - /// - public bool PreAuthenticate { get; set; } - - /// - /// Gets or sets a value indicating whether [keep alive]. - /// - /// - /// true if [keep alive]; otherwise, false. - /// - public bool KeepAlive { get; set; } - - /// - /// - /// - public string Scheme { get; set; } = "https"; - - /// - /// - /// - public RedirectType Redirect { get; set; } - - /// - /// - /// - internal IRedmineSerializer RedmineSerializer { get; set; } - - /// - /// Returns a object for the specified resource. - /// - /// A that identifies the resource to request. - /// - /// A new object for the specified resource. - /// - protected override WebRequest GetWebRequest(Uri address) - { - var wr = base.GetWebRequest(address); - - if (!(wr is HttpWebRequest httpWebRequest)) - { - return base.GetWebRequest(address); - } - - httpWebRequest.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | - DecompressionMethods.None; - - if (UseCookies) - { - httpWebRequest.Headers.Add(HttpRequestHeader.Cookie, "redmineCookie"); - httpWebRequest.CookieContainer = CookieContainer; - } - - httpWebRequest.KeepAlive = KeepAlive; - httpWebRequest.CachePolicy = CachePolicy; - - if (Timeout != null) - { - httpWebRequest.Timeout = (int)Timeout.Value.TotalMilliseconds; - } - - return httpWebRequest; - } - - /// - /// - /// - /// - /// - protected override WebResponse GetWebResponse(WebRequest request) - { - WebResponse response = null; - - try - { - response = base.GetWebResponse(request); - } - catch (WebException webException) - { - webException.HandleWebException(RedmineSerializer); - } - - switch (response) - { - case null: - return null; - case HttpWebResponse _: - HandleRedirect(request, response); - HandleCookies(request, response); - break; - } - - return response; - } - - /// - /// Handles redirect response if needed - /// - /// Request - /// Response - protected void HandleRedirect(WebRequest request, WebResponse response) - { - var webResponse = response as HttpWebResponse; - - if (Redirect == RedirectType.None) - { - return; - } - - if (webResponse == null) - { - return; - } - - var code = webResponse.StatusCode; - - if (code == HttpStatusCode.Found || code == HttpStatusCode.SeeOther || code == HttpStatusCode.MovedPermanently || code == HttpStatusCode.Moved) - { - redirectUrl = webResponse.Headers["Location"]; - - var isAbsoluteUri = new Uri(redirectUrl).IsAbsoluteUri; - - if (!isAbsoluteUri) - { - var webRequest = request as HttpWebRequest; - var host = webRequest?.Headers["Host"] ?? string.Empty; - - if (Redirect == RedirectType.All) - { - host = $"{host}{webRequest?.RequestUri.AbsolutePath}"; - - host = host.Substring(0, host.LastIndexOf('/')); - } - - // Have to make sure that the "/" symbol is between the "host" and "redirect" strings - #if NET5_0_OR_GREATER - if (!redirectUrl.StartsWith('/') && !host.EndsWith('/')) - #else - if (!redirectUrl.StartsWith("/", StringComparison.OrdinalIgnoreCase) && !host.EndsWith("/", StringComparison.OrdinalIgnoreCase)) - #endif - { - redirectUrl = $"/{redirectUrl}"; - } - - redirectUrl = $"{host}{redirectUrl}"; - } - - if (!redirectUrl.StartsWith(Scheme, StringComparison.OrdinalIgnoreCase)) - { - redirectUrl = $"{Scheme}://{redirectUrl}"; - } - } - else - { - redirectUrl = string.Empty; - } - } - - /// - /// Handles additional cookies - /// - /// Request - /// Response - protected void HandleCookies(WebRequest request, WebResponse response) - { - if (!(response is HttpWebResponse webResponse)) return; - - var webRequest = request as HttpWebRequest; - - if (webResponse.Cookies.Count <= 0) return; - - var col = new CookieCollection(); - - foreach (Cookie c in webResponse.Cookies) - { - col.Add(new Cookie(c.Name, c.Value, c.Path, webRequest?.Headers["Host"])); - } - - if (CookieContainer == null) - { - CookieContainer = new CookieContainer(); - } - - CookieContainer.Add(col); - } - } - #pragma warning restore SYSLIB0014 -} \ No newline at end of file diff --git a/src/redmine-net-api/RedmineManager.Obsolete.cs b/src/redmine-net-api/RedmineManager.Obsolete.cs deleted file mode 100644 index 5927a39a..00000000 --- a/src/redmine-net-api/RedmineManager.Obsolete.cs +++ /dev/null @@ -1,617 +0,0 @@ -/* - Copyright 2011 - 2025 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Net; -using System.Net.Security; -using System.Security.Cryptography.X509Certificates; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Net; -using Redmine.Net.Api.Net.WebClient; -using Redmine.Net.Api.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api -{ - /// - /// The main class to access Redmine API. - /// - public partial class RedmineManager - { - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use RedmineConstants.DEFAULT_PAGE_SIZE")] - public const int DEFAULT_PAGE_SIZE_VALUE = 25; - - /// - /// Initializes a new instance of the class. - /// - /// The host. - /// The MIME format. - /// if set to true [verify server cert]. - /// The proxy. - /// Use this parameter to specify a SecurityProtocolType. - /// Note: it is recommended to leave this parameter at its default value as this setting also affects the calling application process. - /// http or https. Default is https. - /// The webclient timeout. Default is 100 seconds. - [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use RedmineManager(RedmineManagerOptionsBuilder")] - public RedmineManager(string host, MimeFormat mimeFormat = MimeFormat.Xml, bool verifyServerCert = true, - IWebProxy proxy = null, SecurityProtocolType securityProtocolType = default, string scheme = "https", TimeSpan? timeout = null) - :this(new RedmineManagerOptionsBuilder() - .WithHost(host) - .WithSerializationType(mimeFormat) - .WithVerifyServerCert(verifyServerCert) - .WithWebClientOptions(new RedmineWebClientOptions() - { - Proxy = proxy, - Scheme = scheme, - Timeout = timeout, - SecurityProtocolType = securityProtocolType - }) - ) { } - - /// - /// Initializes a new instance of the class using your API key for authentication. - /// - /// - /// To enable the API-style authentication, you have to check Enable REST API in Administration -&gt; Settings -&gt; Authentication. - /// You can find your API key on your account page ( /my/account ) when logged in, on the right-hand pane of the default layout. - /// - /// The host. - /// The API key. - /// The MIME format. - /// if set to true [verify server cert]. - /// The proxy. - /// Use this parameter to specify a SecurityProtocolType. - /// Note: it is recommended to leave this parameter at its default value as this setting also affects the calling application process. - /// - /// The webclient timeout. Default is 100 seconds. - [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use RedmineManager(RedmineManagerOptionsBuilder")] - public RedmineManager(string host, string apiKey, MimeFormat mimeFormat = MimeFormat.Xml, - bool verifyServerCert = true, IWebProxy proxy = null, - SecurityProtocolType securityProtocolType = default, string scheme = "https", TimeSpan? timeout = null) - : this(new RedmineManagerOptionsBuilder() - .WithHost(host) - .WithApiKeyAuthentication(apiKey) - .WithSerializationType(mimeFormat) - .WithVerifyServerCert(verifyServerCert) - .WithWebClientOptions(new RedmineWebClientOptions() - { - Proxy = proxy, - Scheme = scheme, - Timeout = timeout, - SecurityProtocolType = securityProtocolType - })){} - - /// - /// Initializes a new instance of the class using your login and password for authentication. - /// - /// The host. - /// The login. - /// The password. - /// The MIME format. - /// if set to true [verify server cert]. - /// The proxy. - /// Use this parameter to specify a SecurityProtocolType. Note: it is recommended to leave this parameter at its default value as this setting also affects the calling application process. - /// - /// The webclient timeout. Default is 100 seconds. - [Obsolete(RedmineConstants.OBSOLETE_TEXT + " Use RedmineManager(RedmineManagerOptionsBuilder")] - public RedmineManager(string host, string login, string password, MimeFormat mimeFormat = MimeFormat.Xml, - bool verifyServerCert = true, IWebProxy proxy = null, - SecurityProtocolType securityProtocolType = default, string scheme = "https", TimeSpan? timeout = null) - : this(new RedmineManagerOptionsBuilder() - .WithHost(host) - .WithBasicAuthentication(login, password) - .WithSerializationType(mimeFormat) - .WithVerifyServerCert(verifyServerCert) - .WithWebClientOptions(new RedmineWebClientOptions() - { - Proxy = proxy, - Scheme = scheme, - Timeout = timeout, - SecurityProtocolType = securityProtocolType - })) {} - - - /// - /// Gets the suffixes. - /// - /// - /// The suffixes. - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + " It returns null.")] - public static Dictionary Suffixes => null; - - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - public string Format { get; } - - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - public string Scheme { get; } - - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - public TimeSpan? Timeout { get; private set; } - - /// - /// Gets the host. - /// - /// - /// The host. - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - public string Host { get; } - - /// - /// The ApiKey used to authenticate. - /// - /// - /// The API key. - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - public string ApiKey { get; } - - /// - /// Gets the MIME format. - /// - /// - /// The MIME format. - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - public MimeFormat MimeFormat { get; } - - /// - /// Gets the proxy. - /// - /// - /// The proxy. - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - public IWebProxy Proxy { get; private set; } - - /// - /// Gets the type of the security protocol. - /// - /// - /// The type of the security protocol. - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - public SecurityProtocolType SecurityProtocolType { get; private set; } - - - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - public int PageSize { get; set; } - - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - public string ImpersonateUser { get; set; } - - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT )] - public static readonly Dictionary TypesWithOffset = new Dictionary{ - {typeof(Issue), true}, - {typeof(Project), true}, - {typeof(User), true}, - {typeof(News), true}, - {typeof(Query), true}, - {typeof(TimeEntry), true}, - {typeof(ProjectMembership), true}, - {typeof(Search), true} - }; - - /// - /// Returns the user whose credentials are used to access the API. - /// - /// The accepted parameters are: memberships and groups (added in 2.1). - /// - /// - /// An error occurred during deserialization. The original exception is available - /// using the System.Exception.InnerException property. - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use GetCurrentUser extension instead")] - public User GetCurrentUser(NameValueCollection parameters = null) - { - return this.GetCurrentUser(RedmineManagerExtensions.CreateRequestOptions(parameters)); - } - - /// - /// - /// - /// Returns the my account details. - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use GetMyAccount extension instead")] - public MyAccount GetMyAccount() - { - return RedmineManagerExtensions.GetMyAccount(this); - } - - /// - /// Adds the watcher to issue. - /// - /// The issue identifier. - /// The user identifier. - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use AddWatcherToIssue extension instead")] - public void AddWatcherToIssue(int issueId, int userId) - { - RedmineManagerExtensions.AddWatcherToIssue(this, issueId, userId); - } - - /// - /// Removes the watcher from issue. - /// - /// The issue identifier. - /// The user identifier. - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use RemoveWatcherFromIssue extension instead")] - public void RemoveWatcherFromIssue(int issueId, int userId) - { - RedmineManagerExtensions.RemoveWatcherFromIssue(this, issueId, userId); - } - - /// - /// Adds an existing user to a group. - /// - /// The group id. - /// The user id. - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use AddUserToGroup extension instead")] - public void AddUserToGroup(int groupId, int userId) - { - RedmineManagerExtensions.AddUserToGroup(this, groupId, userId); - } - - /// - /// Removes an user from a group. - /// - /// The group id. - /// The user id. - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use RemoveUserFromGroup extension instead")] - public void RemoveUserFromGroup(int groupId, int userId) - { - RedmineManagerExtensions.RemoveUserFromGroup(this, groupId, userId); - } - - /// - /// Creates or updates a wiki page. - /// - /// The project id or identifier. - /// The wiki page name. - /// The wiki page to create or update. - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use UpdateWikiPage extension instead")] - public void UpdateWikiPage(string projectId, string pageName, WikiPage wikiPage) - { - RedmineManagerExtensions.UpdateWikiPage(this, projectId, pageName, wikiPage); - } - - /// - /// - /// - /// - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use CreateWikiPage extension instead")] - public WikiPage CreateWikiPage(string projectId, string pageName, WikiPage wikiPage) - { - return RedmineManagerExtensions.CreateWikiPage(this, projectId, pageName, wikiPage); - } - - /// - /// Gets the wiki page. - /// - /// The project identifier. - /// The parameters. - /// Name of the page. - /// The version. - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use GetWikiPage extension instead")] - public WikiPage GetWikiPage(string projectId, NameValueCollection parameters, string pageName, uint version = 0) - { - return this.GetWikiPage(projectId, pageName, RedmineManagerExtensions.CreateRequestOptions(parameters), version); - } - - /// - /// Returns the list of all pages in a project wiki. - /// - /// The project id or identifier. - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use GetAllWikiPages extension instead")] - public List GetAllWikiPages(string projectId) - { - return RedmineManagerExtensions.GetAllWikiPages(this, projectId); - } - - /// - /// Deletes a wiki page, its attachments and its history. If the deleted page is a parent page, its child pages are not - /// deleted but changed as root pages. - /// - /// The project id or identifier. - /// The wiki page name. - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use DeleteWikiPage extension instead")] - public void DeleteWikiPage(string projectId, string pageName) - { - RedmineManagerExtensions.DeleteWikiPage(this, projectId, pageName); - } - - /// - /// Updates the attachment. - /// - /// The issue identifier. - /// The attachment. - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use UpdateAttachment extension instead")] - public void UpdateAttachment(int issueId, Attachment attachment) - { - this.UpdateIssueAttachment(issueId, attachment); - } - - /// - /// - /// - /// query strings. enable to specify multiple values separated by a space " ". - /// number of results in response. - /// skip this number of results in response - /// Optional filters. - /// - /// Returns the search results by the specified condition parameters. - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use Search extension instead")] - public PagedResults Search(string q, int limit = DEFAULT_PAGE_SIZE_VALUE, int offset = 0, SearchFilterBuilder searchFilter = null) - { - return RedmineManagerExtensions.Search(this, q, limit, offset, searchFilter); - } - - /// - /// - /// - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - public int Count(params string[] include) where T : class, new() - { - var parameters = NameValueCollectionExtensions.AddParamsIfExist(null, include); - - return Count(parameters); - } - - /// - /// - /// - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - public int Count(NameValueCollection parameters) where T : class, new() - { - return Count(parameters != null ? new RequestOptions { QueryString = parameters } : null); - } - - /// - /// Gets the redmine object based on id. - /// - /// The type of objects to retrieve. - /// The id of the object. - /// Optional filters and/or optional fetched data. - /// - /// Returns the object of type T. - /// - /// - /// - /// string issueId = "927"; - /// NameValueCollection parameters = null; - /// Issue issue = redmineManager.GetObject<Issue>(issueId, parameters); - /// - /// - [Obsolete($"{RedmineConstants.OBSOLETE_TEXT} Use Get instead")] - public T GetObject(string id, NameValueCollection parameters) where T : class, new() - { - return Get(id, parameters != null ? new RequestOptions { QueryString = parameters } : null); - } - - /// - /// Returns the complete list of objects. - /// - /// - /// Optional fetched data. - /// - /// Optional fetched data: - /// Project: trackers, issue_categories, enabled_modules (since Redmine 2.6.0) - /// Issue: children, attachments, relations, changesets, journals, watchers (since Redmine 2.3.0) - /// Users: memberships, groups (since Redmine 2.1) - /// Groups: users, memberships - /// - /// Returns the complete list of objects. - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - public List GetObjects(params string[] include) where T : class, new() - { - var parameters = NameValueCollectionExtensions.AddParamsIfExist(null, include); - - return GetObjects(parameters); - } - - /// - /// Returns the complete list of objects. - /// - /// - /// The page size. - /// The offset. - /// Optional fetched data. - /// - /// Optional fetched data: - /// Project: trackers, issue_categories, enabled_modules (since 2.6.0) - /// Issue: children, attachments, relations, changesets, journals, watchers - Since 2.3.0 - /// Users: memberships, groups (added in 2.1) - /// Groups: users, memberships - /// - /// Returns the complete list of objects. - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - public List GetObjects(int limit, int offset, params string[] include) where T : class, new() - { - var parameters = NameValueCollectionExtensions - .AddParamsIfExist(null, include) - .AddPagingParameters(limit, offset); - - return GetObjects(parameters); - } - - /// - /// Returns the complete list of objects. - /// - /// The type of objects to retrieve. - /// Optional filters and/or optional fetched data. - /// - /// Returns a complete list of objects. - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - public List GetObjects(NameValueCollection parameters = null) where T : class, new() - { - return Get(parameters != null ? new RequestOptions { QueryString = parameters } : null); - } - - /// - /// Gets the paginated objects. - /// - /// - /// The parameters. - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - public PagedResults GetPaginatedObjects(NameValueCollection parameters) where T : class, new() - { - return GetPaginated(parameters != null ? new RequestOptions { QueryString = parameters } : null); - } - - /// - /// Creates a new Redmine object. - /// - /// The type of object to create. - /// The object to create. - /// - /// - /// - /// When trying to create an object with invalid or missing attribute parameters, you will get a 422 Unprocessable - /// Entity response. That means that the object could not be created. - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - public T CreateObject(T entity) where T : class, new() - { - return Create(entity); - } - - /// - /// Creates a new Redmine object. - /// - /// The type of object to create. - /// The object to create. - /// The owner identifier. - /// - /// - /// - /// When trying to create an object with invalid or missing attribute parameters, you will get a 422 Unprocessable - /// Entity response. That means that the object could not be created. - /// - /// - /// - /// var project = new Project(); - /// project.Name = "test"; - /// project.Identifier = "the project identifier"; - /// project.Description = "the project description"; - /// redmineManager.CreateObject(project); - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - public T CreateObject(T entity, string ownerId) where T : class, new() - { - return Create(entity, ownerId); - } - - /// - /// Updates a Redmine object. - /// - /// The type of object to be updated. - /// The id of the object to be updated. - /// The object to be updated. - /// The project identifier. - /// - /// - /// When trying to update an object with invalid or missing attribute parameters, you will get a - /// 422(RedmineException) Unprocessable Entity response. That means that the object could not be updated. - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - public void UpdateObject(string id, T entity, string projectId = null) where T : class, new() - { - Update(id, entity, projectId); - } - - /// - /// Deletes the Redmine object. - /// - /// The type of objects to delete. - /// The id of the object to delete - /// The parameters - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - public void DeleteObject(string id, NameValueCollection parameters = null) where T : class, new() - { - Delete(id, parameters != null ? new RequestOptions { QueryString = parameters } : null); - } - - /// - /// Creates the Redmine web client. - /// - /// The parameters. - /// if set to true [upload file]. - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "If a custom webClient is needed, use Func from RedmineManagerSettings instead")] - public virtual RedmineWebClient CreateWebClient(NameValueCollection parameters, bool uploadFile = false) - { - throw new NotImplementedException(); - } - - /// - /// This is to take care of SSL certification validation which are not issued by Trusted Root CA. - /// - /// The sender. - /// The cert. - /// The chain. - /// The error. - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use RedmineManagerOptions.ClientOptions.ServerCertificateValidationCallback instead")] - public virtual bool RemoteCertValidate(object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors sslPolicyErrors) - { - const SslPolicyErrors IGNORED_ERRORS = SslPolicyErrors.RemoteCertificateChainErrors | SslPolicyErrors.RemoteCertificateNameMismatch; - - return (sslPolicyErrors & ~IGNORED_ERRORS) == SslPolicyErrors.None; - - } - } -} \ No newline at end of file diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index a64f6cd7..82558e62 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -52,28 +52,16 @@ public RedmineManager(RedmineManagerOptionsBuilder optionsBuilder) _redmineManagerOptions = optionsBuilder.Build(); + Logger = _redmineManagerOptions.Logger; Serializer = _redmineManagerOptions.Serializer; RedmineApiUrls = new RedmineApiUrls(_redmineManagerOptions.Serializer.Format); - - Host = _redmineManagerOptions.BaseAddress.ToString(); - PageSize = _redmineManagerOptions.PageSize; - Scheme = _redmineManagerOptions.BaseAddress.Scheme; - Format = Serializer.Format; - MimeFormat = RedmineConstants.XML.Equals(Serializer.Format, StringComparison.Ordinal) - ? MimeFormat.Xml - : MimeFormat.Json; - - if (_redmineManagerOptions.Authentication is RedmineApiKeyAuthentication) - { - ApiKey = _redmineManagerOptions.Authentication.Token; - } - + ApiClient = #if NET45_OR_GREATER || NETCOREAPP _redmineManagerOptions.WebClientOptions switch { RedmineWebClientOptions => CreateWebClient(_redmineManagerOptions), - _ => CreateHttpClient(_redmineManagerOptions) + RedmineHttpClientOptions => CreateHttpClient(_redmineManagerOptions), }; #else CreateWebClient(_redmineManagerOptions); @@ -91,9 +79,9 @@ private InternalRedmineApiWebClient CreateWebClient(RedmineManagerOptions option options.WebClientOptions.SecurityProtocolType ??= ServicePointManager.SecurityProtocol; #pragma warning restore SYSLIB0014 - Proxy = options.WebClientOptions.Proxy; - Timeout = options.WebClientOptions.Timeout; - SecurityProtocolType = options.WebClientOptions.SecurityProtocolType.GetValueOrDefault(); + return new InternalRedmineApiWebClient(options); + } +#if NET40_OR_GREATER || NET #if NET45_OR_GREATER if (options.VerifyServerCert) @@ -301,5 +289,16 @@ internal PagedResults GetPaginatedInternal(string uri = null, RequestOptio return response.DeserializeToPagedResults(Serializer); } + + internal static readonly Dictionary TypesWithOffset = new Dictionary{ + {typeof(Issue), true}, + {typeof(Project), true}, + {typeof(User), true}, + {typeof(News), true}, + {typeof(Query), true}, + {typeof(TimeEntry), true}, + {typeof(ProjectMembership), true}, + {typeof(Search), true} + }; } } \ No newline at end of file diff --git a/src/redmine-net-api/Serialization/MimeFormatObsolete.cs b/src/redmine-net-api/Serialization/MimeFormatObsolete.cs deleted file mode 100755 index 1bad6a4f..00000000 --- a/src/redmine-net-api/Serialization/MimeFormatObsolete.cs +++ /dev/null @@ -1,35 +0,0 @@ -/* -Copyright 2011 - 2025 Adrian Popescu - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -using System; - -namespace Redmine.Net.Api -{ - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT + "Use SerializationType instead")] - public enum MimeFormat - { - /// - /// - Xml, - /// - /// The json - /// - Json - } -} \ No newline at end of file diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs index 4f42a9eb..03393172 100644 --- a/src/redmine-net-api/Types/Project.cs +++ b/src/redmine-net-api/Types/Project.cs @@ -115,18 +115,6 @@ public sealed class Project : IdentifiableName, IEquatable /// Available in Redmine starting with 2.6.0 version. public IList EnabledModules { get; set; } - /// - /// Gets or sets the custom fields. - /// - /// - /// The custom fields. - /// - [Obsolete($"{RedmineConstants.OBSOLETE_TEXT} Use {nameof(IssueCustomFields)} instead.")] - public IList CustomFields - { - get => IssueCustomFields; - set => IssueCustomFields = (List)value; - } /// /// /// diff --git a/src/redmine-net-api/_net20/RedmineManagerAsyncObsolete.cs b/src/redmine-net-api/_net20/RedmineManagerAsyncObsolete.cs deleted file mode 100644 index ef03936f..00000000 --- a/src/redmine-net-api/_net20/RedmineManagerAsyncObsolete.cs +++ /dev/null @@ -1,324 +0,0 @@ -#if NET20 - -/* - Copyright 2011 - 2025 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Collections.Specialized; -using System.Globalization; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.Async -{ - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - public delegate void Task(); - - /// - /// - /// - /// The type of the resource. - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] public delegate TRes Task(); - - /// - /// - /// - [Obsolete(RedmineConstants.OBSOLETE_TEXT)] - public static class RedmineManagerAsync - { - /// - /// Gets the current user asynchronous. - /// - /// The redmine manager. - /// The parameters. - /// - public static Task GetCurrentUserAsync(this RedmineManager redmineManager, - NameValueCollection parameters = null) - { - return delegate { return redmineManager.GetCurrentUser(parameters); }; - } - - /// - /// Creates the or update wiki page asynchronous. - /// - /// The redmine manager. - /// The project identifier. - /// Name of the page. - /// The wiki page. - /// - public static Task CreateWikiPageAsync(this RedmineManager redmineManager, string projectId, - string pageName, WikiPage wikiPage) - { - return delegate { return redmineManager.CreateWikiPage(projectId, pageName, wikiPage); }; - } - - /// - /// - /// - /// - /// - /// - /// - /// - public static Task UpdateWikiPageAsync(this RedmineManager redmineManager, string projectId, - string pageName, WikiPage wikiPage) - { - return delegate { redmineManager.UpdateWikiPage(projectId, pageName, wikiPage); }; - } - - /// - /// Deletes the wiki page asynchronous. - /// - /// The redmine manager. - /// The project identifier. - /// Name of the page. - /// - public static Task DeleteWikiPageAsync(this RedmineManager redmineManager, string projectId, string pageName) - { - return delegate { redmineManager.DeleteWikiPage(projectId, pageName); }; - } - - /// - /// Gets the wiki page asynchronous. - /// - /// The redmine manager. - /// The project identifier. - /// The parameters. - /// Name of the page. - /// The version. - /// - public static Task GetWikiPageAsync(this RedmineManager redmineManager, string projectId, - NameValueCollection parameters, string pageName, uint version = 0) - { - return delegate { return redmineManager.GetWikiPage(projectId, parameters, pageName, version); }; - } - - /// - /// Gets all wiki pages asynchronous. - /// - /// The redmine manager. - /// The project identifier. - /// - public static Task> GetAllWikiPagesAsync(this RedmineManager redmineManager, string projectId) - { - return delegate { return redmineManager.GetAllWikiPages(projectId); }; - } - - /// - /// Adds the user to group asynchronous. - /// - /// The redmine manager. - /// The group identifier. - /// The user identifier. - /// - public static Task AddUserToGroupAsync(this RedmineManager redmineManager, int groupId, int userId) - { - return delegate { redmineManager.AddUserToGroup(groupId, userId); }; - } - - /// - /// Removes the user from group asynchronous. - /// - /// The redmine manager. - /// The group identifier. - /// The user identifier. - /// - public static Task RemoveUserFromGroupAsync(this RedmineManager redmineManager, int groupId, int userId) - { - return delegate { redmineManager.RemoveUserFromGroup(groupId, userId); }; - } - - /// - /// Adds the watcher to issue asynchronous. - /// - /// The redmine manager. - /// The issue identifier. - /// The user identifier. - /// - public static Task AddWatcherToIssueAsync(this RedmineManager redmineManager, int issueId, int userId) - { - return delegate { redmineManager.AddWatcherToIssue(issueId, userId); }; - } - - /// - /// Removes the watcher from issue asynchronous. - /// - /// The redmine manager. - /// The issue identifier. - /// The user identifier. - /// - public static Task RemoveWatcherFromIssueAsync(this RedmineManager redmineManager, int issueId, int userId) - { - return delegate { redmineManager.RemoveWatcherFromIssue(issueId, userId); }; - } - - /// - /// Gets the object asynchronous. - /// - /// - /// The redmine manager. - /// The identifier. - /// The parameters. - /// - public static Task GetObjectAsync(this RedmineManager redmineManager, string id, - NameValueCollection parameters) where T : class, new() - { - return delegate { return redmineManager.GetObject(id, parameters); }; - } - - /// - /// Creates the object asynchronous. - /// - /// - /// The redmine manager. - /// The object. - /// - public static Task CreateObjectAsync(this RedmineManager redmineManager, T entity) where T : class, new() - { - return CreateObjectAsync(redmineManager, entity, null); - } - - /// - /// Creates the object asynchronous. - /// - /// - /// The redmine manager. - /// The object. - /// The owner identifier. - /// - public static Task CreateObjectAsync(this RedmineManager redmineManager, T entity, string ownerId) - where T : class, new() - { - return delegate { return redmineManager.CreateObject(entity, ownerId); }; - } - - /// - /// Gets the paginated objects asynchronous. - /// - /// - /// The redmine manager. - /// The parameters. - /// - public static Task> GetPaginatedObjectsAsync(this RedmineManager redmineManager, - NameValueCollection parameters) where T : class, new() - { - return delegate { return redmineManager.GetPaginatedObjects(parameters); }; - } - - /// - /// Gets the objects asynchronous. - /// - /// - /// The redmine manager. - /// The parameters. - /// - public static Task> GetObjectsAsync(this RedmineManager redmineManager, - NameValueCollection parameters) where T : class, new() - { - return delegate { return redmineManager.GetObjects(parameters); }; - } - - /// - /// Updates the object asynchronous. - /// - /// - /// The redmine manager. - /// The identifier. - /// The object. - /// The project identifier. - /// - public static Task UpdateObjectAsync(this RedmineManager redmineManager, string id, T entity, - string projectId = null) where T : class, new() - { - return delegate { redmineManager.UpdateObject(id, entity, projectId); }; - } - - /// - /// Deletes the object asynchronous. - /// - /// - /// The redmine manager. - /// The identifier. - /// - public static Task DeleteObjectAsync(this RedmineManager redmineManager, string id) where T : class, new() - { - return delegate { redmineManager.DeleteObject(id); }; - } - - /// - /// Uploads the file asynchronous. - /// - /// The redmine manager. - /// The data. - /// - public static Task UploadFileAsync(this RedmineManager redmineManager, byte[] data) - { - return delegate { return redmineManager.UploadFile(data); }; - } - - /// - /// Downloads the file asynchronous. - /// - /// The redmine manager. - /// The address. - /// - public static Task DownloadFileAsync(this RedmineManager redmineManager, string address) - { - return delegate { return redmineManager.DownloadFile(address); }; - } - - /// - /// - /// - /// - /// - /// - /// - /// - /// - /// - public static Task> SearchAsync(this RedmineManager redmineManager, string q, int limit = RedmineManager.DEFAULT_PAGE_SIZE_VALUE, int offset = 0, SearchFilterBuilder searchFilter = null) - { - if (q.IsNullOrWhiteSpace()) - { - throw new ArgumentNullException(nameof(q)); - } - - var parameters = new NameValueCollection - { - {RedmineKeys.Q, q}, - {RedmineKeys.LIMIT, limit.ToInvariantString()}, - {RedmineKeys.OFFSET, offset.ToInvariantString()}, - }; - - if (searchFilter != null) - { - parameters = searchFilter.Build(parameters); - } - - var result = redmineManager.GetPaginatedObjectsAsync(parameters); - - return result; - } - } -} -#endif \ No newline at end of file From 15fff38a86e54abd9bf09e28c5dcb6bf0c874a6b Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 28 May 2025 16:41:56 +0300 Subject: [PATCH 525/549] Change folder structure & some web client improvements --- src/redmine-net-api/Common/IValue.cs | 2 +- src/redmine-net-api/Common/PagedResults.cs | 2 +- .../Extensions/EnumExtensions.cs | 11 +- .../InternalRedmineApiWebClient.Async.cs | 65 +--- .../WebClient/InternalRedmineApiWebClient.cs | 282 ++++++++++++++++++ .../Clients}/WebClient/InternalWebClient.cs | 15 +- .../WebClient/RedmineApiRequestContent.cs | 93 ++++++ .../WebClient/RedmineWebClientOptions.cs | 83 ++++++ .../Clients/WebClient}/WebClientExtensions.cs | 2 +- .../Clients/WebClient/WebClientProvider.cs | 67 +++++ .../Http/Constants/HttpConstants.cs | 106 +++++++ .../NameValueCollectionExtensions.cs | 268 +++++++++++++++++ .../RedmineApiResponseExtensions.cs} | 14 +- .../Http/Helpers/RedmineExceptionHelper.cs | 139 +++++++++ src/redmine-net-api/Http/IRedmineApiClient.cs | 75 +++++ .../Http/IRedmineApiClientOptions.cs | 148 +++++++++ .../Messages/RedmineApiRequest.cs} | 10 +- .../Messages/RedmineApiResponse.cs} | 4 +- .../{Net => Http}/RedirectType.cs | 4 +- .../Http/RedmineApiClient.Async.cs | 77 +++++ src/redmine-net-api/Http/RedmineApiClient.cs | 113 +++++++ .../RedmineApiClientOptions.cs} | 107 ++----- .../{Net => Http}/RequestOptions.cs | 2 +- ...nagerAsync.cs => IRedmineManager.Async.cs} | 2 + src/redmine-net-api/IRedmineManager.cs | 7 +- src/redmine-net-api/Net/HttpVerbs.cs | 51 ---- .../Net/IRedmineApiClientOptions.cs | 197 ------------ .../Net/Internal/ApiRequestMessageContent.cs | 24 -- .../Net/Internal/HttpStatusHelper.cs | 98 ------ .../Net/Internal/IAsyncRedmineApiClient.cs | 42 --- .../Net/Internal/IRedmineApiClient.cs | 27 -- .../Net/Internal/ISyncRedmineApiClient.cs | 38 --- .../Net/Internal/RedmineApiUrls.cs | 1 + .../Net/Internal/RedmineApiUrlsExtensions.cs | 48 --- .../NameValueCollectionExtensions.cs | 140 --------- .../Extensions/WebExceptionExtensions.cs | 75 ----- .../Net/WebClient/IRedmineWebClientOptions.cs | 22 -- .../WebClient/InternalRedmineApiWebClient.cs | 263 ---------------- .../ByteArrayApiRequestMessageContent.cs | 27 -- .../StreamApiRequestMessageContent.cs | 25 -- .../StringApiRequestMessageContent.cs | 41 --- ...anagerAsync.cs => RedmineManager.Async.cs} | 3 + src/redmine-net-api/RedmineManager.cs | 72 ++++- src/redmine-net-api/SearchFilterBuilder.cs | 1 + .../Serialization/IRedmineSerializer.cs | 2 + .../Json/Extensions/JsonReaderExtensions.cs | 3 +- .../Json/Extensions/JsonWriterExtensions.cs | 5 +- .../Serialization/Json/IJsonSerializable.cs | 2 +- .../Serialization/Json/JsonObject.cs | 2 +- .../Json/JsonRedmineSerializer.cs | 2 + .../Serialization/Xml/CacheKeyFactory.cs | 3 +- .../Xml/Extensions/XmlReaderExtensions.cs | 4 +- .../Xml/Extensions/XmlWriterExtensions.cs | 4 +- .../Serialization/Xml/IXmlSerializerCache.cs | 2 +- .../Serialization/Xml/XmlRedmineSerializer.cs | 2 + .../Serialization/Xml/XmlSerializerCache.cs | 2 +- .../Serialization/Xml/XmlTextReaderBuilder.cs | 2 +- src/redmine-net-api/Types/Attachment.cs | 3 + src/redmine-net-api/Types/Attachments.cs | 1 + src/redmine-net-api/Types/ChangeSet.cs | 2 + src/redmine-net-api/Types/CustomField.cs | 2 + .../Types/CustomFieldPossibleValue.cs | 1 + src/redmine-net-api/Types/CustomFieldValue.cs | 1 + src/redmine-net-api/Types/Detail.cs | 1 + src/redmine-net-api/Types/DocumentCategory.cs | 1 + src/redmine-net-api/Types/Error.cs | 1 + src/redmine-net-api/Types/File.cs | 3 + src/redmine-net-api/Types/Group.cs | 4 + src/redmine-net-api/Types/GroupUser.cs | 1 + src/redmine-net-api/Types/Identifiable.cs | 1 + src/redmine-net-api/Types/IdentifiableName.cs | 2 + src/redmine-net-api/Types/Issue.cs | 4 + .../Types/IssueAllowedStatus.cs | 2 + src/redmine-net-api/Types/IssueCategory.cs | 3 + src/redmine-net-api/Types/IssueChild.cs | 1 + src/redmine-net-api/Types/IssueCustomField.cs | 3 + src/redmine-net-api/Types/IssuePriority.cs | 1 + src/redmine-net-api/Types/IssueRelation.cs | 3 + src/redmine-net-api/Types/IssueStatus.cs | 2 + src/redmine-net-api/Types/Journal.cs | 2 + src/redmine-net-api/Types/Membership.cs | 2 + src/redmine-net-api/Types/MembershipRole.cs | 3 + src/redmine-net-api/Types/MyAccount.cs | 3 + .../Types/MyAccountCustomField.cs | 1 + src/redmine-net-api/Types/News.cs | 3 + src/redmine-net-api/Types/Permission.cs | 1 + src/redmine-net-api/Types/Project.cs | 4 + .../Types/ProjectEnabledModule.cs | 1 + .../Types/ProjectMembership.cs | 3 + src/redmine-net-api/Types/ProjectTracker.cs | 1 + src/redmine-net-api/Types/Query.cs | 2 + src/redmine-net-api/Types/Role.cs | 2 + src/redmine-net-api/Types/Search.cs | 3 + src/redmine-net-api/Types/TimeEntry.cs | 3 + .../Types/TimeEntryActivity.cs | 1 + src/redmine-net-api/Types/Tracker.cs | 2 + src/redmine-net-api/Types/TrackerCoreField.cs | 1 + .../Types/TrackerCustomField.cs | 2 + src/redmine-net-api/Types/Upload.cs | 2 + src/redmine-net-api/Types/User.cs | 4 +- src/redmine-net-api/Types/Version.cs | 3 + src/redmine-net-api/Types/Watcher.cs | 1 + src/redmine-net-api/Types/WikiPage.cs | 3 + .../Bugs/RedmineApi-371.cs | 2 +- .../redmine-net-api.Tests/Tests/HostTests.cs | 2 +- .../Tests/RedmineApiUrlsTests.cs | 2 +- 106 files changed, 1711 insertions(+), 1319 deletions(-) rename src/redmine-net-api/{Net => Http/Clients}/WebClient/InternalRedmineApiWebClient.Async.cs (51%) create mode 100644 src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.cs rename src/redmine-net-api/{Net => Http/Clients}/WebClient/InternalWebClient.cs (90%) create mode 100644 src/redmine-net-api/Http/Clients/WebClient/RedmineApiRequestContent.cs create mode 100644 src/redmine-net-api/Http/Clients/WebClient/RedmineWebClientOptions.cs rename src/redmine-net-api/{Net/WebClient/Extensions => Http/Clients/WebClient}/WebClientExtensions.cs (94%) create mode 100644 src/redmine-net-api/Http/Clients/WebClient/WebClientProvider.cs create mode 100644 src/redmine-net-api/Http/Constants/HttpConstants.cs create mode 100644 src/redmine-net-api/Http/Extensions/NameValueCollectionExtensions.cs rename src/redmine-net-api/{Net/Internal/ApiResponseMessageExtensions.cs => Http/Extensions/RedmineApiResponseExtensions.cs} (79%) create mode 100644 src/redmine-net-api/Http/Helpers/RedmineExceptionHelper.cs create mode 100644 src/redmine-net-api/Http/IRedmineApiClient.cs create mode 100644 src/redmine-net-api/Http/IRedmineApiClientOptions.cs rename src/redmine-net-api/{Net/Internal/ApiRequestMessage.cs => Http/Messages/RedmineApiRequest.cs} (74%) rename src/redmine-net-api/{Net/Internal/ApiResponseMessage.cs => Http/Messages/RedmineApiResponse.cs} (90%) rename src/redmine-net-api/{Net => Http}/RedirectType.cs (93%) create mode 100644 src/redmine-net-api/Http/RedmineApiClient.Async.cs create mode 100644 src/redmine-net-api/Http/RedmineApiClient.cs rename src/redmine-net-api/{Net/WebClient/RedmineWebClientOptions.cs => Http/RedmineApiClientOptions.cs} (56%) rename src/redmine-net-api/{Net => Http}/RequestOptions.cs (98%) rename src/redmine-net-api/{IRedmineManagerAsync.cs => IRedmineManager.Async.cs} (99%) delete mode 100644 src/redmine-net-api/Net/HttpVerbs.cs delete mode 100644 src/redmine-net-api/Net/IRedmineApiClientOptions.cs delete mode 100644 src/redmine-net-api/Net/Internal/ApiRequestMessageContent.cs delete mode 100644 src/redmine-net-api/Net/Internal/HttpStatusHelper.cs delete mode 100644 src/redmine-net-api/Net/Internal/IAsyncRedmineApiClient.cs delete mode 100644 src/redmine-net-api/Net/Internal/IRedmineApiClient.cs delete mode 100644 src/redmine-net-api/Net/Internal/ISyncRedmineApiClient.cs delete mode 100644 src/redmine-net-api/Net/WebClient/Extensions/NameValueCollectionExtensions.cs delete mode 100644 src/redmine-net-api/Net/WebClient/Extensions/WebExceptionExtensions.cs delete mode 100644 src/redmine-net-api/Net/WebClient/IRedmineWebClientOptions.cs delete mode 100644 src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs delete mode 100644 src/redmine-net-api/Net/WebClient/MessageContent/ByteArrayApiRequestMessageContent.cs delete mode 100644 src/redmine-net-api/Net/WebClient/MessageContent/StreamApiRequestMessageContent.cs delete mode 100644 src/redmine-net-api/Net/WebClient/MessageContent/StringApiRequestMessageContent.cs rename src/redmine-net-api/{RedmineManagerAsync.cs => RedmineManager.Async.cs} (98%) diff --git a/src/redmine-net-api/Common/IValue.cs b/src/redmine-net-api/Common/IValue.cs index bbfe3a77..d95d24eb 100755 --- a/src/redmine-net-api/Common/IValue.cs +++ b/src/redmine-net-api/Common/IValue.cs @@ -14,7 +14,7 @@ You may obtain a copy of the License at limitations under the License. */ -namespace Redmine.Net.Api.Types +namespace Redmine.Net.Api.Common { /// /// diff --git a/src/redmine-net-api/Common/PagedResults.cs b/src/redmine-net-api/Common/PagedResults.cs index eab0c680..2aeb5f03 100644 --- a/src/redmine-net-api/Common/PagedResults.cs +++ b/src/redmine-net-api/Common/PagedResults.cs @@ -16,7 +16,7 @@ limitations under the License. using System.Collections.Generic; -namespace Redmine.Net.Api.Serialization +namespace Redmine.Net.Api.Common { /// /// diff --git a/src/redmine-net-api/Extensions/EnumExtensions.cs b/src/redmine-net-api/Extensions/EnumExtensions.cs index 60f49075..60b6499e 100644 --- a/src/redmine-net-api/Extensions/EnumExtensions.cs +++ b/src/redmine-net-api/Extensions/EnumExtensions.cs @@ -14,7 +14,7 @@ public static class EnumExtensions /// /// The enumeration value to be converted. /// A string representation of the IssueRelationType enumeration value in a lowercase, or "undefined" if the value does not match a defined case. - public static string ToLowerInvariant(this IssueRelationType @enum) + public static string ToLowerName(this IssueRelationType @enum) { return @enum switch { @@ -36,7 +36,7 @@ public static string ToLowerInvariant(this IssueRelationType @enum) /// /// The VersionSharing enumeration value to be converted. /// A string representation of the VersionSharing enumeration value in a lowercase, or "undefined" if the value does not match a valid case. - public static string ToLowerInvariant(this VersionSharing @enum) + public static string ToLowerName(this VersionSharing @enum) { return @enum switch { @@ -55,7 +55,7 @@ public static string ToLowerInvariant(this VersionSharing @enum) /// /// The enumeration value to be converted. /// A lowercase string representation of the enumeration value, or "undefined" if the value does not match a defined case. - public static string ToLowerInvariant(this VersionStatus @enum) + public static string ToLowerName(this VersionStatus @enum) { return @enum switch { @@ -72,7 +72,7 @@ public static string ToLowerInvariant(this VersionStatus @enum) /// /// The ProjectStatus enumeration value to be converted. /// A string representation of the ProjectStatus enumeration value in a lowercase, or "undefined" if the value does not match a defined case. - public static string ToLowerInvariant(this ProjectStatus @enum) + public static string ToLowerName(this ProjectStatus @enum) { return @enum switch { @@ -89,12 +89,11 @@ public static string ToLowerInvariant(this ProjectStatus @enum) /// /// The enumeration value to be converted. /// A string representation of the UserStatus enumeration value in a lowercase, or "undefined" if the value does not match a defined case. - public static string ToLowerInvariant(this UserStatus @enum) + public static string ToLowerName(this UserStatus @enum) { return @enum switch { UserStatus.StatusActive => "status_active", - UserStatus.StatusAnonymous => "status_anonymous", UserStatus.StatusLocked => "status_locked", UserStatus.StatusRegistered => "status_registered", _ => "undefined" diff --git a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.Async.cs b/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.Async.cs similarity index 51% rename from src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.Async.cs rename to src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.Async.cs index 9fa4317f..a2678963 100644 --- a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.Async.cs +++ b/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.Async.cs @@ -17,72 +17,27 @@ limitations under the License. #if!(NET20) using System; using System.Collections.Specialized; +using System.IO; using System.Net; using System.Threading; using System.Threading.Tasks; using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Net.Internal; -using Redmine.Net.Api.Net.WebClient.Extensions; -using Redmine.Net.Api.Net.WebClient.MessageContent; +using Redmine.Net.Api.Http.Messages; -namespace Redmine.Net.Api.Net.WebClient +namespace Redmine.Net.Api.Http.Clients.WebClient { /// /// /// internal sealed partial class InternalRedmineApiWebClient { - public async Task GetAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + protected override async Task HandleRequestAsync(string address, string verb, RequestOptions requestOptions = null, object content = null, + IProgress progress = null, CancellationToken cancellationToken = default) { - return await HandleRequestAsync(address, HttpVerbs.GET, requestOptions, cancellationToken:cancellationToken).ConfigureAwait(false); - } - - public async Task GetPagedAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) - { - return await GetAsync(address, requestOptions, cancellationToken).ConfigureAwait(false); - } - - public async Task CreateAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) - { - var content = new StringApiRequestMessageContent(payload, _serializer.ContentType); - return await HandleRequestAsync(address, HttpVerbs.POST, requestOptions, content, cancellationToken:cancellationToken).ConfigureAwait(false); - } - - public async Task UpdateAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) - { - var content = new StringApiRequestMessageContent(payload, _serializer.ContentType); - return await HandleRequestAsync(address, HttpVerbs.PUT, requestOptions, content, cancellationToken:cancellationToken).ConfigureAwait(false); - } - - public async Task UploadFileAsync(string address, byte[] data, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) - { - var content = new StreamApiRequestMessageContent(data); - return await HandleRequestAsync(address, HttpVerbs.POST, requestOptions, content, cancellationToken:cancellationToken).ConfigureAwait(false); - } - - public async Task PatchAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) - { - var content = new StringApiRequestMessageContent(payload, _serializer.ContentType); - return await HandleRequestAsync(address, HttpVerbs.PATCH, requestOptions, content, cancellationToken:cancellationToken).ConfigureAwait(false); - } - - public async Task DeleteAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default) - { - return await HandleRequestAsync(address, HttpVerbs.DELETE, requestOptions, cancellationToken:cancellationToken).ConfigureAwait(false); + return await SendAsync(CreateRequestMessage(address, verb, requestOptions, content as RedmineApiRequestContent), progress, cancellationToken: cancellationToken).ConfigureAwait(false); } - public async Task DownloadAsync(string address, RequestOptions requestOptions = null, IProgress progress = null, CancellationToken cancellationToken = default) - { - return await HandleRequestAsync(address, HttpVerbs.DOWNLOAD, requestOptions, cancellationToken:cancellationToken).ConfigureAwait(false); - } - - private async Task HandleRequestAsync(string address, string verb, RequestOptions requestOptions = null, ApiRequestMessageContent content = null, IProgress progress = null, CancellationToken cancellationToken = default) - { - return await SendAsync(CreateRequestMessage(address, verb, requestOptions, content), progress, cancellationToken: cancellationToken).ConfigureAwait(false); - } - - private async Task SendAsync(ApiRequestMessage requestMessage, IProgress progress = null, CancellationToken cancellationToken = default) + private async Task SendAsync(RedmineApiRequest requestMessage, IProgress progress = null, CancellationToken cancellationToken = default) { System.Net.WebClient webClient = null; byte[] response = null; @@ -95,7 +50,7 @@ private async Task SendAsync(ApiRequestMessage requestMessag webClient = _webClientFunc(); cancellationTokenRegistration = cancellationToken.Register( - static state => ((System.Net.WebClient)state!).CancelAsync(), + static state => ((System.Net.WebClient)state).CancelAsync(), webClient ); @@ -148,7 +103,7 @@ private async Task SendAsync(ApiRequestMessage requestMessag } catch (WebException webException) { - webException.HandleWebException(_serializer); + HandleWebException(webException, Serializer); } finally { @@ -161,7 +116,7 @@ private async Task SendAsync(ApiRequestMessage requestMessag webClient?.Dispose(); } - return new ApiResponseMessage() + return new RedmineApiResponse() { Headers = responseHeaders, Content = response, diff --git a/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.cs b/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.cs new file mode 100644 index 00000000..f08e3474 --- /dev/null +++ b/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.cs @@ -0,0 +1,282 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System; +using System.Collections.Specialized; +using System.Globalization; +using System.IO; +using System.Net; +using System.Text; +using Redmine.Net.Api.Authentication; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http.Constants; +using Redmine.Net.Api.Http.Extensions; +using Redmine.Net.Api.Http.Helpers; +using Redmine.Net.Api.Http.Messages; +using Redmine.Net.Api.Logging; +using Redmine.Net.Api.Options; +using Redmine.Net.Api.Serialization; + +namespace Redmine.Net.Api.Http.Clients.WebClient +{ + /// + /// + /// + internal sealed partial class InternalRedmineApiWebClient : RedmineApiClient + { + private static readonly byte[] EmptyBytes = Encoding.UTF8.GetBytes(string.Empty); + private readonly Func _webClientFunc; + + public InternalRedmineApiWebClient(RedmineManagerOptions redmineManagerOptions) + : this(() => new InternalWebClient(redmineManagerOptions), redmineManagerOptions) + { + } + + public InternalRedmineApiWebClient(Func webClientFunc, RedmineManagerOptions redmineManagerOptions) + : base(redmineManagerOptions) + { + _webClientFunc = webClientFunc; + } + + protected override object CreateContentFromPayload(string payload) + { + return RedmineApiRequestContent.CreateString(payload, Serializer.ContentType); + } + + protected override object CreateContentFromBytes(byte[] data) + { + return RedmineApiRequestContent.CreateBinary(data); + } + + protected override RedmineApiResponse HandleRequest(string address, string verb, RequestOptions requestOptions = null, object content = null, IProgress progress = null) + { + var requestMessage = CreateRequestMessage(address, verb, requestOptions, content as RedmineApiRequestContent); + + if (Options.LoggingOptions?.IncludeHttpDetails == true) + { + Options.Logger.Debug($"Request HTTP {verb} {address}"); + + if (requestOptions?.QueryString != null) + { + Options.Logger.Debug($"Query parameters: {requestOptions.QueryString.ToQueryString()}"); + } + } + + var responseMessage = Send(requestMessage, progress); + + if (Options.LoggingOptions?.IncludeHttpDetails == true) + { + Options.Logger.Debug($"Response status: {responseMessage.StatusCode}"); + } + + return responseMessage; + } + + private static RedmineApiRequest CreateRequestMessage(string address, string verb, RequestOptions requestOptions = null, RedmineApiRequestContent content = null) + { + var req = new RedmineApiRequest() + { + RequestUri = address, + Method = verb, + }; + + if (requestOptions != null) + { + req.QueryString = requestOptions.QueryString; + req.ImpersonateUser = requestOptions.ImpersonateUser; + } + + if (content != null) + { + req.Content = content; + } + + return req; + } + + private RedmineApiResponse Send(RedmineApiRequest requestMessage, IProgress progress = null) + { + System.Net.WebClient webClient = null; + byte[] response = null; + HttpStatusCode? statusCode = null; + NameValueCollection responseHeaders = null; + + try + { + webClient = _webClientFunc(); + + SetWebClientHeaders(webClient, requestMessage); + + if (IsGetOrDownload(requestMessage.Method)) + { + response = requestMessage.Method == HttpConstants.HttpVerbs.DOWNLOAD + ? DownloadWithProgress(requestMessage.RequestUri, webClient, progress) + : webClient.DownloadData(requestMessage.RequestUri); + } + else + { + byte[] payload; + if (requestMessage.Content != null) + { + webClient.Headers.Add(HttpRequestHeader.ContentType, requestMessage.Content.ContentType); + payload = requestMessage.Content.Body; + } + else + { + payload = EmptyBytes; + } + + response = webClient.UploadData(requestMessage.RequestUri, requestMessage.Method, payload); + } + + responseHeaders = webClient.ResponseHeaders; + if (webClient is InternalWebClient iwc) + { + statusCode = iwc.StatusCode; + } + } + catch (WebException webException) + { + HandleWebException(webException, Serializer); + } + finally + { + webClient?.Dispose(); + } + + return new RedmineApiResponse() + { + Headers = responseHeaders, + Content = response, + StatusCode = statusCode ?? HttpStatusCode.OK, + }; + } + + private void SetWebClientHeaders(System.Net.WebClient webClient, RedmineApiRequest requestMessage) + { + if (requestMessage.QueryString != null) + { + webClient.QueryString = requestMessage.QueryString; + } + + switch (Credentials) + { + case RedmineApiKeyAuthentication: + webClient.Headers.Add(RedmineConstants.API_KEY_AUTHORIZATION_HEADER_KEY,Credentials.Token); + break; + case RedmineBasicAuthentication: + webClient.Headers.Add(RedmineConstants.AUTHORIZATION_HEADER_KEY, Credentials.Token); + break; + } + + if (!requestMessage.ImpersonateUser.IsNullOrWhiteSpace()) + { + webClient.Headers.Add(RedmineConstants.IMPERSONATE_HEADER_KEY, requestMessage.ImpersonateUser); + } + } + + private static byte[] DownloadWithProgress(string url, System.Net.WebClient webClient, IProgress progress) + { + var contentLength = GetContentLength(webClient); + byte[] data; + if (contentLength > 0) + { + using (var respStream = webClient.OpenRead(url)) + { + data = new byte[contentLength]; + var buffer = new byte[4096]; + int bytesRead; + var totalBytesRead = 0; + + while ((bytesRead = respStream.Read(buffer, 0, buffer.Length)) > 0) + { + Buffer.BlockCopy(buffer, 0, data, totalBytesRead, bytesRead); + totalBytesRead += bytesRead; + + ReportProgress(progress, contentLength, totalBytesRead); + } + } + } + else + { + data = webClient.DownloadData(url); + progress?.Report(100); + } + + return data; + } + + private static int GetContentLength(System.Net.WebClient webClient) + { + var total = -1; + if (webClient.ResponseHeaders == null) + { + return total; + } + + var contentLengthAsString = webClient.ResponseHeaders[HttpRequestHeader.ContentLength]; + total = Convert.ToInt32(contentLengthAsString, CultureInfo.InvariantCulture); + + return total; + } + + /// + /// Handles the web exception. + /// + /// The exception. + /// + /// Timeout! + /// Bad domain name! + /// + /// + /// + /// + /// The page that you are trying to update is staled! + /// + /// + public static void HandleWebException(WebException exception, IRedmineSerializer serializer) + { + if (exception == null) + { + return; + } + + var innerException = exception.InnerException ?? exception; + + switch (exception.Status) + { + case WebExceptionStatus.Timeout: + throw new RedmineTimeoutException(nameof(WebExceptionStatus.Timeout), innerException); + case WebExceptionStatus.NameResolutionFailure: + throw new NameResolutionFailureException("Bad domain name.", innerException); + case WebExceptionStatus.ProtocolError: + if (exception.Response != null) + { + var statusCode = exception.Response is HttpWebResponse httpResponse + ? (int)httpResponse.StatusCode + : (int)HttpStatusCode.InternalServerError; + + using var responseStream = exception.Response.GetResponseStream(); + RedmineExceptionHelper.MapStatusCodeToException(statusCode, responseStream, innerException, serializer); + } + + break; + } + throw new RedmineException(exception.Message, innerException); + } + } +} diff --git a/src/redmine-net-api/Net/WebClient/InternalWebClient.cs b/src/redmine-net-api/Http/Clients/WebClient/InternalWebClient.cs similarity index 90% rename from src/redmine-net-api/Net/WebClient/InternalWebClient.cs rename to src/redmine-net-api/Http/Clients/WebClient/InternalWebClient.cs index 71103c45..fabd7219 100644 --- a/src/redmine-net-api/Net/WebClient/InternalWebClient.cs +++ b/src/redmine-net-api/Http/Clients/WebClient/InternalWebClient.cs @@ -13,16 +13,17 @@ You may obtain a copy of the License at See the License for the specific language governing permissions and limitations under the License. */ + using System; using System.Net; using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Options; -namespace Redmine.Net.Api.Net.WebClient; +namespace Redmine.Net.Api.Http.Clients.WebClient; internal sealed class InternalWebClient : System.Net.WebClient { - private readonly IRedmineWebClientOptions _webClientOptions; + private readonly RedmineWebClientOptions _webClientOptions; #pragma warning disable SYSLIB0014 public InternalWebClient(RedmineManagerOptions redmineManagerOptions) @@ -43,9 +44,9 @@ protected override WebRequest GetWebRequest(Uri address) return base.GetWebRequest(address); } - httpWebRequest.UserAgent = _webClientOptions.UserAgent.ValueOrFallback("RedmineDotNetAPIClient"); + httpWebRequest.UserAgent = _webClientOptions.UserAgent; - httpWebRequest.AutomaticDecompression = _webClientOptions.DecompressionFormat ?? DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.None; + AssignIfHasValue(_webClientOptions.DecompressionFormat, value => httpWebRequest.AutomaticDecompression = value); AssignIfHasValue(_webClientOptions.AutoRedirect, value => httpWebRequest.AllowAutoRedirect = value); @@ -78,14 +79,14 @@ protected override WebRequest GetWebRequest(Uri address) httpWebRequest.Credentials = _webClientOptions.Credentials; - #if NET40_OR_GREATER || NETCOREAPP + #if NET40_OR_GREATER || NET if (_webClientOptions.ClientCertificates != null) { httpWebRequest.ClientCertificates = _webClientOptions.ClientCertificates; } #endif - #if (NET45_OR_GREATER || NETCOREAPP) + #if (NET45_OR_GREATER || NET) httpWebRequest.ServerCertificateValidationCallback = _webClientOptions.ServerCertificateValidationCallback; #endif diff --git a/src/redmine-net-api/Http/Clients/WebClient/RedmineApiRequestContent.cs b/src/redmine-net-api/Http/Clients/WebClient/RedmineApiRequestContent.cs new file mode 100644 index 00000000..9d9c33f0 --- /dev/null +++ b/src/redmine-net-api/Http/Clients/WebClient/RedmineApiRequestContent.cs @@ -0,0 +1,93 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System; +using System.Text; +using Redmine.Net.Api.Http.Constants; + +namespace Redmine.Net.Api.Http.Clients.WebClient; + +internal class RedmineApiRequestContent : IDisposable +{ + private static readonly byte[] _emptyByteArray = []; + private bool _isDisposed; + + /// + /// Gets the content type of the request. + /// + public string ContentType { get; } + + /// + /// Gets the body of the request. + /// + public byte[] Body { get; } + + /// + /// Gets the length of the request body. + /// + public int Length => Body?.Length ?? 0; + + /// + /// Creates a new instance of RedmineApiRequestContent. + /// + /// The content type of the request. + /// The body of the request. + /// Thrown when the contentType is null. + public RedmineApiRequestContent(string contentType, byte[] body) + { + ContentType = contentType ?? throw new ArgumentNullException(nameof(contentType)); + Body = body ?? _emptyByteArray; + } + + /// + /// Creates a text-based request content with the specified MIME type. + /// + /// The text content. + /// The MIME type of the content. + /// The encoding to use (defaults to UTF-8). + /// A new RedmineApiRequestContent instance. + public static RedmineApiRequestContent CreateString(string text, string mimeType, Encoding encoding = null) + { + if (string.IsNullOrEmpty(text)) + { + return new RedmineApiRequestContent(mimeType, _emptyByteArray); + } + + encoding ??= Encoding.UTF8; + return new RedmineApiRequestContent(mimeType, encoding.GetBytes(text)); + } + + /// + /// Creates a binary request content. + /// + /// The binary data. + /// A new RedmineApiRequestContent instance. + public static RedmineApiRequestContent CreateBinary(byte[] data) + { + return new RedmineApiRequestContent(HttpConstants.ContentTypes.ApplicationOctetStream, data); + } + + /// + /// Disposes the resources used by this instance. + /// + public void Dispose() + { + if (!_isDisposed) + { + _isDisposed = true; + } + } +} diff --git a/src/redmine-net-api/Http/Clients/WebClient/RedmineWebClientOptions.cs b/src/redmine-net-api/Http/Clients/WebClient/RedmineWebClientOptions.cs new file mode 100644 index 00000000..013d5a82 --- /dev/null +++ b/src/redmine-net-api/Http/Clients/WebClient/RedmineWebClientOptions.cs @@ -0,0 +1,83 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System.Net; +#if (NET45_OR_GREATER || NET) +using System.Net.Security; +#endif +using System.Security.Cryptography.X509Certificates; + +namespace Redmine.Net.Api.Http.Clients.WebClient; +/// +/// +/// +public sealed class RedmineWebClientOptions: RedmineApiClientOptions +{ + + /// + /// + /// + public bool? KeepAlive { get; set; } + + /// + /// + /// + public bool? UnsafeAuthenticatedConnectionSharing { get; set; } + + /// + /// + /// + public int? DefaultConnectionLimit { get; set; } + + /// + /// + /// + public int? DnsRefreshTimeout { get; set; } + + /// + /// + /// + public bool? EnableDnsRoundRobin { get; set; } + + /// + /// + /// + public int? MaxServicePoints { get; set; } + + /// + /// + /// + public int? MaxServicePointIdleTime { get; set; } + + #if(NET46_OR_GREATER || NET) + /// + /// + /// + public bool? ReusePort { get; set; } + #endif + + /// + /// + /// + public SecurityProtocolType? SecurityProtocolType { get; set; } + +#if (NET45_OR_GREATER || NET) + /// + /// + /// + public RemoteCertificateValidationCallback ServerCertificateValidationCallback { get; set; } + #endif +} \ No newline at end of file diff --git a/src/redmine-net-api/Net/WebClient/Extensions/WebClientExtensions.cs b/src/redmine-net-api/Http/Clients/WebClient/WebClientExtensions.cs similarity index 94% rename from src/redmine-net-api/Net/WebClient/Extensions/WebClientExtensions.cs rename to src/redmine-net-api/Http/Clients/WebClient/WebClientExtensions.cs index 2cb3c8c0..ceb2a2cc 100644 --- a/src/redmine-net-api/Net/WebClient/Extensions/WebClientExtensions.cs +++ b/src/redmine-net-api/Http/Clients/WebClient/WebClientExtensions.cs @@ -1,7 +1,7 @@ using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Serialization; -namespace Redmine.Net.Api.Net.WebClient.Extensions; +namespace Redmine.Net.Api.Http.Clients.WebClient; internal static class WebClientExtensions { diff --git a/src/redmine-net-api/Http/Clients/WebClient/WebClientProvider.cs b/src/redmine-net-api/Http/Clients/WebClient/WebClientProvider.cs new file mode 100644 index 00000000..ff8c664b --- /dev/null +++ b/src/redmine-net-api/Http/Clients/WebClient/WebClientProvider.cs @@ -0,0 +1,67 @@ +using System; +using System.Net; +using System.Text; +using Redmine.Net.Api.Options; + +namespace Redmine.Net.Api.Http.Clients.WebClient; + +internal static class WebClientProvider +{ + /// + /// Creates a new WebClient instance with the specified options. + /// + /// The options for the Redmine manager. + /// A new WebClient instance. + public static System.Net.WebClient CreateWebClient(RedmineManagerOptions options) + { + var webClient = new InternalWebClient(options); + + if (options?.ApiClientOptions is RedmineWebClientOptions webClientOptions) + { + ConfigureWebClient(webClient, webClientOptions); + } + + return webClient; + } + + /// + /// Configures a WebClient instance with the specified options. + /// + /// The WebClient instance to configure. + /// The options to apply. + private static void ConfigureWebClient(System.Net.WebClient webClient, RedmineWebClientOptions options) + { + if (options == null) return; + + webClient.Proxy = options.Proxy; + webClient.Headers = null; + webClient.BaseAddress = null; + webClient.CachePolicy = null; + webClient.Credentials = null; + webClient.Encoding = Encoding.UTF8; + webClient.UseDefaultCredentials = false; + + // if (options.Timeout.HasValue && options.Timeout.Value != TimeSpan.Zero) + // { + // webClient.Timeout = options.Timeout; + // } + // + // if (options.KeepAlive.HasValue) + // { + // webClient.KeepAlive = options.KeepAlive.Value; + // } + // + // if (options.UnsafeAuthenticatedConnectionSharing.HasValue) + // { + // webClient.UnsafeAuthenticatedConnectionSharing = options.UnsafeAuthenticatedConnectionSharing.Value; + // } + // + // #if NET40_OR_GREATER || NET + // if (options.ClientCertificates != null) + // { + // webClient.ClientCertificates = options.ClientCertificates; + // } + // #endif + + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Http/Constants/HttpConstants.cs b/src/redmine-net-api/Http/Constants/HttpConstants.cs new file mode 100644 index 00000000..d1666c4b --- /dev/null +++ b/src/redmine-net-api/Http/Constants/HttpConstants.cs @@ -0,0 +1,106 @@ +namespace Redmine.Net.Api.Http.Constants; + +/// +/// +/// +public static class HttpConstants +{ + /// + /// HTTP status codes including custom codes used by Redmine. + /// + internal static class StatusCodes + { + public const int Unauthorized = 401; + public const int Forbidden = 403; + public const int NotFound = 404; + public const int NotAcceptable = 406; + public const int RequestTimeout = 408; + public const int Conflict = 409; + public const int UnprocessableEntity = 422; + public const int TooManyRequests = 429; + public const int InternalServerError = 500; + public const int BadGateway = 502; + public const int ServiceUnavailable = 503; + public const int GatewayTimeout = 504; + } + + /// + /// Standard HTTP headers used in API requests and responses. + /// + internal static class Headers + { + public const string Authorization = "Authorization"; + public const string ApiKey = "X-Redmine-API-Key"; + public const string Impersonate = "X-Redmine-Switch-User"; + public const string ContentType = "Content-Type"; + } + + internal static class Names + { + /// HTTP User-Agent header name. + public static string UserAgent => "User-Agent"; + } + + internal static class Values + { + /// User agent string to use for all HTTP requests. + public static string UserAgent => "Redmine-NET-API"; + } + + /// + /// MIME content types used in API requests and responses. + /// + internal static class ContentTypes + { + public const string ApplicationJson = "application/json"; + public const string ApplicationXml = "application/xml"; + public const string ApplicationOctetStream = "application/octet-stream"; + } + + /// + /// Error messages for different HTTP status codes. + /// + internal static class ErrorMessages + { + public const string NotFound = "The requested resource was not found."; + public const string Unauthorized = "Authentication is required or has failed."; + public const string Forbidden = "You don't have permission to access this resource."; + public const string Conflict = "The resource you are trying to update has been modified since you last retrieved it."; + public const string NotAcceptable = "The requested format is not supported."; + public const string InternalServerError = "The server encountered an unexpected error."; + public const string UnprocessableEntity = "Validation failed for the submitted data."; + public const string Cancelled = "The operation was cancelled."; + public const string TimedOut = "The operation has timed out."; + } + + /// + /// + /// + internal static class HttpVerbs + { + /// + /// Represents an HTTP GET protocol method that is used to get an entity identified by a URI. + /// + public const string GET = "GET"; + /// + /// Represents an HTTP PUT protocol method that is used to replace an entity identified by a URI. + /// + public const string PUT = "PUT"; + /// + /// Represents an HTTP POST protocol method that is used to post a new entity as an addition to a URI. + /// + public const string POST = "POST"; + /// + /// Represents an HTTP PATCH protocol method that is used to patch an existing entity identified by a URI. + /// + public const string PATCH = "PATCH"; + /// + /// Represents an HTTP DELETE protocol method that is used to delete an existing entity identified by a URI. + /// + public const string DELETE = "DELETE"; + + internal const string DOWNLOAD = "DOWNLOAD"; + + internal const string UPLOAD = "UPLOAD"; + } +} diff --git a/src/redmine-net-api/Http/Extensions/NameValueCollectionExtensions.cs b/src/redmine-net-api/Http/Extensions/NameValueCollectionExtensions.cs new file mode 100644 index 00000000..3cf95a1d --- /dev/null +++ b/src/redmine-net-api/Http/Extensions/NameValueCollectionExtensions.cs @@ -0,0 +1,268 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Globalization; +using System.Text; +using Redmine.Net.Api.Extensions; + +namespace Redmine.Net.Api.Http.Extensions +{ + /// + /// + /// + public static class NameValueCollectionExtensions + { + /// + /// Gets the parameter value. + /// + /// The parameters. + /// Name of the parameter. + /// + public static string GetParameterValue(this NameValueCollection parameters, string parameterName) + { + return GetValue(parameters, parameterName); + } + + /// + /// Gets the parameter value. + /// + /// The parameters. + /// Name of the parameter. + /// + public static string GetValue(this NameValueCollection parameters, string key) + { + if (parameters == null) + { + return null; + } + + var value = parameters.Get(key); + + return value.IsNullOrWhiteSpace() ? null : value; + } + + /// + /// + /// + /// + /// + public static string ToQueryString(this NameValueCollection requestParameters) + { + if (requestParameters == null || requestParameters.Count == 0) + { + return null; + } + + var delimiter = string.Empty; + + var stringBuilder = new StringBuilder(); + + for (var index = 0; index < requestParameters.Count; ++index) + { + stringBuilder + .Append(delimiter) + .Append(requestParameters.AllKeys[index].ToString(CultureInfo.InvariantCulture)) + .Append('=') + .Append(requestParameters[index].ToString(CultureInfo.InvariantCulture)); + delimiter = "&"; + } + + var queryString = stringBuilder.ToString(); + + stringBuilder.Length = 0; + + return queryString; + } + + internal static NameValueCollection AddPagingParameters(this NameValueCollection parameters, int pageSize, int offset) + { + parameters ??= new NameValueCollection(); + + if(pageSize <= 0) + { + pageSize = RedmineConstants.DEFAULT_PAGE_SIZE_VALUE; + } + + if(offset < 0) + { + offset = 0; + } + + parameters.Set(RedmineKeys.LIMIT, pageSize.ToInvariantString()); + parameters.Set(RedmineKeys.OFFSET, offset.ToInvariantString()); + + return parameters; + } + + internal static NameValueCollection AddParamsIfExist(this NameValueCollection parameters, string[] include) + { + if (include is not {Length: > 0}) + { + return parameters; + } + + parameters ??= new NameValueCollection(); + + parameters.Add(RedmineKeys.INCLUDE, string.Join(",", include)); + + return parameters; + } + + internal static void AddIfNotNull(this NameValueCollection nameValueCollection, string key, string value) + { + if (!value.IsNullOrWhiteSpace()) + { + nameValueCollection.Add(key, value); + } + } + + internal static void AddIfNotNull(this NameValueCollection nameValueCollection, string key, bool? value) + { + if (value.HasValue) + { + nameValueCollection.Add(key, value.Value.ToInvariantString()); + } + } + + /// + /// Creates a new NameValueCollection with an initial key-value pair. + /// + /// The key for the first item. + /// The value for the first item. + /// A new NameValueCollection containing the specified key-value pair. + public static NameValueCollection WithItem(this string key, string value) + { + var collection = new NameValueCollection(); + collection.Add(key, value); + return collection; + } + + /// + /// Adds a new key-value pair to an existing NameValueCollection and returns the collection for chaining. + /// + /// The NameValueCollection to add to. + /// The key to add. + /// The value to add. + /// The NameValueCollection with the new key-value pair added. + public static NameValueCollection AndItem(this NameValueCollection collection, string key, string value) + { + collection.Add(key, value); + return collection; + } + + /// + /// Adds a new key-value pair to an existing NameValueCollection if the condition is true. + /// + /// The NameValueCollection to add to. + /// The condition to evaluate. + /// The key to add if condition is true. + /// The value to add if condition is true. + /// The NameValueCollection, potentially with a new key-value pair added. + public static NameValueCollection AndItemIf(this NameValueCollection collection, bool condition, string key, string value) + { + if (condition) + { + collection.Add(key, value); + } + return collection; + } + + /// + /// Adds a new key-value pair to an existing NameValueCollection if the value is not null. + /// + /// The NameValueCollection to add to. + /// The key to add if value is not null. + /// The value to check and add. + /// The NameValueCollection, potentially with a new key-value pair added. + public static NameValueCollection AndItemIfNotNull(this NameValueCollection collection, string key, string value) + { + if (value != null) + { + collection.Add(key, value); + } + return collection; + } + + /// + /// Creates a new NameValueCollection with an initial key-value pair where the value is converted from an integer. + /// + /// The key for the first item. + /// The integer value to be converted to string. + /// A new NameValueCollection containing the specified key-value pair. + public static NameValueCollection WithInt(this string key, int value) + { + return key.WithItem(value.ToInvariantString()); + } + + /// + /// Adds a new key-value pair to an existing NameValueCollection where the value is converted from an integer. + /// + /// The NameValueCollection to add to. + /// The key to add. + /// The integer value to be converted to string. + /// The NameValueCollection with the new key-value pair added. + public static NameValueCollection AndInt(this NameValueCollection collection, string key, int value) + { + return collection.AndItem(key, value.ToInvariantString()); + } + + + /// + /// Converts a NameValueCollection to a Dictionary. + /// + /// The collection to convert. + /// A new Dictionary containing the collection's key-value pairs. + public static Dictionary ToDictionary(this NameValueCollection collection) + { + var dict = new Dictionary(); + + if (collection != null) + { + foreach (string key in collection.Keys) + { + dict[key] = collection[key]; + } + } + + return dict; + } + + /// + /// Creates a new NameValueCollection from a dictionary of key-value pairs. + /// + /// Dictionary of key-value pairs to add to the collection. + /// A new NameValueCollection containing the specified key-value pairs. + public static NameValueCollection ToNameValueCollection(this Dictionary keyValuePairs) + { + var collection = new NameValueCollection(); + + if (keyValuePairs != null) + { + foreach (var pair in keyValuePairs) + { + collection.Add(pair.Key, pair.Value); + } + } + + return collection; + } + + + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Net/Internal/ApiResponseMessageExtensions.cs b/src/redmine-net-api/Http/Extensions/RedmineApiResponseExtensions.cs similarity index 79% rename from src/redmine-net-api/Net/Internal/ApiResponseMessageExtensions.cs rename to src/redmine-net-api/Http/Extensions/RedmineApiResponseExtensions.cs index 4d2c4518..e03d3ed5 100644 --- a/src/redmine-net-api/Net/Internal/ApiResponseMessageExtensions.cs +++ b/src/redmine-net-api/Http/Extensions/RedmineApiResponseExtensions.cs @@ -16,26 +16,28 @@ limitations under the License. using System.Collections.Generic; using System.Text; +using Redmine.Net.Api.Common; using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http.Messages; using Redmine.Net.Api.Serialization; -namespace Redmine.Net.Api.Net.Internal; +namespace Redmine.Net.Api.Http.Extensions; -internal static class ApiResponseMessageExtensions +internal static class RedmineApiResponseExtensions { - internal static T DeserializeTo(this ApiResponseMessage responseMessage, IRedmineSerializer redmineSerializer) where T : new() + internal static T DeserializeTo(this RedmineApiResponse responseMessage, IRedmineSerializer redmineSerializer) where T : new() { var responseAsString = GetResponseContentAsString(responseMessage); return responseAsString.IsNullOrWhiteSpace() ? default : redmineSerializer.Deserialize(responseAsString); } - internal static PagedResults DeserializeToPagedResults(this ApiResponseMessage responseMessage, IRedmineSerializer redmineSerializer) where T : class, new() + internal static PagedResults DeserializeToPagedResults(this RedmineApiResponse responseMessage, IRedmineSerializer redmineSerializer) where T : class, new() { var responseAsString = GetResponseContentAsString(responseMessage); return responseAsString.IsNullOrWhiteSpace() ? default : redmineSerializer.DeserializeToPagedResults(responseAsString); } - internal static List DeserializeToList(this ApiResponseMessage responseMessage, IRedmineSerializer redmineSerializer) where T : class, new() + internal static List DeserializeToList(this RedmineApiResponse responseMessage, IRedmineSerializer redmineSerializer) where T : class, new() { var responseAsString = GetResponseContentAsString(responseMessage); return responseAsString.IsNullOrWhiteSpace() ? null : redmineSerializer.Deserialize>(responseAsString); @@ -46,7 +48,7 @@ internal static class ApiResponseMessageExtensions /// /// The API response message. /// The content as a string, or null if the response or content is null. - private static string GetResponseContentAsString(ApiResponseMessage responseMessage) + private static string GetResponseContentAsString(RedmineApiResponse responseMessage) { return responseMessage?.Content == null ? null : Encoding.UTF8.GetString(responseMessage.Content); } diff --git a/src/redmine-net-api/Http/Helpers/RedmineExceptionHelper.cs b/src/redmine-net-api/Http/Helpers/RedmineExceptionHelper.cs new file mode 100644 index 00000000..4e0aa252 --- /dev/null +++ b/src/redmine-net-api/Http/Helpers/RedmineExceptionHelper.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http.Constants; +using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Types; + +namespace Redmine.Net.Api.Http.Helpers; + +/// +/// Handles HTTP status codes and converts them to appropriate Redmine exceptions. +/// +internal static class RedmineExceptionHelper +{ + /// + /// Maps an HTTP status code to an appropriate Redmine exception. + /// + /// The HTTP status code. + /// The response stream containing error details. + /// The inner exception, if any. + /// The serializer to use for deserializing error messages. + /// A specific Redmine exception based on the status code. + internal static void MapStatusCodeToException(int statusCode, Stream responseStream, Exception inner, IRedmineSerializer serializer) + { + switch (statusCode) + { + case HttpConstants.StatusCodes.NotFound: + throw new NotFoundException(HttpConstants.ErrorMessages.NotFound, inner); + + case HttpConstants.StatusCodes.Unauthorized: + throw new UnauthorizedException(HttpConstants.ErrorMessages.Unauthorized, inner); + + case HttpConstants.StatusCodes.Forbidden: + throw new ForbiddenException(HttpConstants.ErrorMessages.Forbidden, inner); + + case HttpConstants.StatusCodes.Conflict: + throw new ConflictException(HttpConstants.ErrorMessages.Conflict, inner); + + case HttpConstants.StatusCodes.UnprocessableEntity: + throw CreateUnprocessableEntityException(responseStream, inner, serializer); + + case HttpConstants.StatusCodes.NotAcceptable: + throw new NotAcceptableException(HttpConstants.ErrorMessages.NotAcceptable, inner); + + case HttpConstants.StatusCodes.InternalServerError: + throw new InternalServerErrorException(HttpConstants.ErrorMessages.InternalServerError, inner); + + default: + throw new RedmineException($"HTTP {statusCode} – {(HttpStatusCode)statusCode}", inner); + } + } + + /// + /// Creates an exception for a 422 Unprocessable Entity response. + /// + /// The response stream containing error details. + /// The inner exception, if any. + /// The serializer to use for deserializing error messages. + /// A RedmineException with details about the validation errors. + private static RedmineException CreateUnprocessableEntityException(Stream responseStream, Exception inner, IRedmineSerializer serializer) + { + var errors = GetRedmineErrors(responseStream, serializer); + + if (errors is null) + { + return new RedmineException(HttpConstants.ErrorMessages.UnprocessableEntity, inner); + } + + var sb = new StringBuilder(); + foreach (var error in errors) + { + sb.Append(error.Info); + sb.Append(Environment.NewLine); + } + + if (sb.Length > 0) + { + sb.Length -= 1; + } + + return new RedmineException($"Unprocessable Content: {sb}", inner); + } + + /// + /// Gets the Redmine errors from a response stream. + /// + /// The response stream containing error details. + /// The serializer to use for deserializing error messages. + /// A list of error objects or null if unable to parse errors. + private static List GetRedmineErrors(Stream responseStream, IRedmineSerializer serializer) + { + if (responseStream == null) + { + return null; + } + + using (responseStream) + { + try + { + using var reader = new StreamReader(responseStream); + var content = reader.ReadToEnd(); + return GetRedmineErrors(content, serializer); + } + catch(Exception ex) + { + throw new RedmineApiException(ex.Message, ex); + } + } + } + + /// + /// Gets the Redmine errors from response content. + /// + /// The response content as a string. + /// The serializer to use for deserializing error messages. + /// A list of error objects or null if unable to parse errors. + private static List GetRedmineErrors(string content, IRedmineSerializer serializer) + { + if (content.IsNullOrWhiteSpace()) + { + return null; + } + + try + { + var paged = serializer.DeserializeToPagedResults(content); + return (List)paged.Items; + } + catch(Exception ex) + { + throw new RedmineException(ex.Message, ex); + } + } +} diff --git a/src/redmine-net-api/Http/IRedmineApiClient.cs b/src/redmine-net-api/Http/IRedmineApiClient.cs new file mode 100644 index 00000000..203d9599 --- /dev/null +++ b/src/redmine-net-api/Http/IRedmineApiClient.cs @@ -0,0 +1,75 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System; +using Redmine.Net.Api.Http.Messages; +#if !NET20 +using System.Threading; +using System.Threading.Tasks; +#endif +using Redmine.Net.Api.Net; +using Redmine.Net.Api.Net.Internal; + +namespace Redmine.Net.Api.Http; +/// +/// +/// +internal interface IRedmineApiClient : ISyncRedmineApiClient +#if !NET20 + , IAsyncRedmineApiClient +#endif +{ +} + +internal interface ISyncRedmineApiClient +{ + RedmineApiResponse Get(string address, RequestOptions requestOptions = null); + + RedmineApiResponse GetPaged(string address, RequestOptions requestOptions = null); + + RedmineApiResponse Create(string address, string payload, RequestOptions requestOptions = null); + + RedmineApiResponse Update(string address, string payload, RequestOptions requestOptions = null); + + RedmineApiResponse Patch(string address, string payload, RequestOptions requestOptions = null); + + RedmineApiResponse Delete(string address, RequestOptions requestOptions = null); + + RedmineApiResponse Upload(string address, byte[] data, RequestOptions requestOptions = null); + + RedmineApiResponse Download(string address, RequestOptions requestOptions = null, IProgress progress = null); +} + +#if !NET20 +internal interface IAsyncRedmineApiClient +{ + Task GetAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + + Task GetPagedAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + + Task CreateAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + + Task UpdateAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + + Task PatchAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + + Task DeleteAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + + Task UploadFileAsync(string address, byte[] data, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); + + Task DownloadAsync(string address, RequestOptions requestOptions = null, IProgress progress = null, CancellationToken cancellationToken = default); +} +#endif \ No newline at end of file diff --git a/src/redmine-net-api/Http/IRedmineApiClientOptions.cs b/src/redmine-net-api/Http/IRedmineApiClientOptions.cs new file mode 100644 index 00000000..44089261 --- /dev/null +++ b/src/redmine-net-api/Http/IRedmineApiClientOptions.cs @@ -0,0 +1,148 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Cache; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; + +namespace Redmine.Net.Api.Http +{ + /// + /// + /// + public interface IRedmineApiClientOptions + { + /// + /// + /// + bool? AutoRedirect { get; set; } + + /// + /// + /// + CookieContainer CookieContainer { get; set; } + + /// + /// + /// + DecompressionMethods? DecompressionFormat { get; set; } + + /// + /// + /// + ICredentials Credentials { get; set; } + + /// + /// + /// + Dictionary DefaultHeaders { get; set; } + + /// + /// + /// + IWebProxy Proxy { get; set; } + + /// + /// + /// + int? MaxAutomaticRedirections { get; set; } + +#if NET471_OR_GREATER || NET + /// + /// + /// + int? MaxConnectionsPerServer { get; set; } + + /// + /// + /// + int? MaxResponseHeadersLength { get; set; } +#endif + /// + /// + /// + bool? PreAuthenticate { get; set; } + + /// + /// + /// + RequestCachePolicy RequestCachePolicy { get; set; } + + /// + /// + /// + string Scheme { get; set; } + + /// + /// + /// + TimeSpan? Timeout { get; set; } + + /// + /// + /// + string UserAgent { get; set; } + + /// + /// + /// + bool? UseCookies { get; set; } + +#if NETFRAMEWORK + + /// + /// + /// + bool CheckCertificateRevocationList { get; set; } + + /// + /// + /// + long? MaxRequestContentBufferSize { get; set; } + + /// + /// + /// + long? MaxResponseContentBufferSize { get; set; } + + /// + /// + /// + bool? UseDefaultCredentials { get; set; } +#endif + /// + /// + /// + bool? UseProxy { get; set; } + + /// + /// + /// + /// Only HTTP/1.0 and HTTP/1.1 version requests are currently supported. + Version ProtocolVersion { get; set; } + + +#if NET40_OR_GREATER || NET + /// + /// + /// + public X509CertificateCollection ClientCertificates { get; set; } +#endif + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Net/Internal/ApiRequestMessage.cs b/src/redmine-net-api/Http/Messages/RedmineApiRequest.cs similarity index 74% rename from src/redmine-net-api/Net/Internal/ApiRequestMessage.cs rename to src/redmine-net-api/Http/Messages/RedmineApiRequest.cs index bbd31924..ad42592d 100644 --- a/src/redmine-net-api/Net/Internal/ApiRequestMessage.cs +++ b/src/redmine-net-api/Http/Messages/RedmineApiRequest.cs @@ -15,13 +15,15 @@ limitations under the License. */ using System.Collections.Specialized; +using Redmine.Net.Api.Http.Clients.WebClient; +using Redmine.Net.Api.Http.Constants; -namespace Redmine.Net.Api.Net.Internal; +namespace Redmine.Net.Api.Http.Messages; -internal sealed class ApiRequestMessage +internal sealed class RedmineApiRequest { - public ApiRequestMessageContent Content { get; set; } - public string Method { get; set; } = HttpVerbs.GET; + public RedmineApiRequestContent Content { get; set; } + public string Method { get; set; } = HttpConstants.HttpVerbs.GET; public string RequestUri { get; set; } public NameValueCollection QueryString { get; set; } public string ImpersonateUser { get; set; } diff --git a/src/redmine-net-api/Net/Internal/ApiResponseMessage.cs b/src/redmine-net-api/Http/Messages/RedmineApiResponse.cs similarity index 90% rename from src/redmine-net-api/Net/Internal/ApiResponseMessage.cs rename to src/redmine-net-api/Http/Messages/RedmineApiResponse.cs index f04c45d1..71fb1948 100644 --- a/src/redmine-net-api/Net/Internal/ApiResponseMessage.cs +++ b/src/redmine-net-api/Http/Messages/RedmineApiResponse.cs @@ -17,9 +17,9 @@ limitations under the License. using System.Collections.Specialized; using System.Net; -namespace Redmine.Net.Api.Net.Internal; +namespace Redmine.Net.Api.Http.Messages; -internal sealed class ApiResponseMessage +internal sealed class RedmineApiResponse { public NameValueCollection Headers { get; init; } public byte[] Content { get; init; } diff --git a/src/redmine-net-api/Net/RedirectType.cs b/src/redmine-net-api/Http/RedirectType.cs similarity index 93% rename from src/redmine-net-api/Net/RedirectType.cs rename to src/redmine-net-api/Http/RedirectType.cs index ae9aedb5..5bb7acf7 100644 --- a/src/redmine-net-api/Net/RedirectType.cs +++ b/src/redmine-net-api/Http/RedirectType.cs @@ -14,12 +14,12 @@ You may obtain a copy of the License at limitations under the License. */ -namespace Redmine.Net.Api +namespace Redmine.Net.Api.Http { /// /// /// - public enum RedirectType + internal enum RedirectType { /// /// diff --git a/src/redmine-net-api/Http/RedmineApiClient.Async.cs b/src/redmine-net-api/Http/RedmineApiClient.Async.cs new file mode 100644 index 00000000..e30fb302 --- /dev/null +++ b/src/redmine-net-api/Http/RedmineApiClient.Async.cs @@ -0,0 +1,77 @@ +#if !NET20 +using System; +using System.Threading; +using System.Threading.Tasks; +using Redmine.Net.Api.Http.Constants; +using Redmine.Net.Api.Http.Messages; +using Redmine.Net.Api.Net; +using Redmine.Net.Api.Net.Internal; + +namespace Redmine.Net.Api.Http; + +internal abstract partial class RedmineApiClient +{ + public async Task GetAsync(string address, RequestOptions requestOptions = null, + CancellationToken cancellationToken = default) + { + return await HandleRequestAsync(address, HttpConstants.HttpVerbs.GET, requestOptions, cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + + public async Task GetPagedAsync(string address, RequestOptions requestOptions = null, + CancellationToken cancellationToken = default) + { + return await GetAsync(address, requestOptions, cancellationToken).ConfigureAwait(false); + } + + public async Task CreateAsync(string address, string payload, + RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + return await HandleRequestAsync(address, HttpConstants.HttpVerbs.POST, requestOptions, CreateContentFromPayload(payload), + cancellationToken: cancellationToken).ConfigureAwait(false); + } + + public async Task UpdateAsync(string address, string payload, + RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + return await HandleRequestAsync(address, HttpConstants.HttpVerbs.PUT, requestOptions, CreateContentFromPayload(payload), + cancellationToken: cancellationToken).ConfigureAwait(false); + } + + public async Task UploadFileAsync(string address, byte[] data, + RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + return await HandleRequestAsync(address, HttpConstants.HttpVerbs.POST, requestOptions, CreateContentFromBytes(data), + cancellationToken: cancellationToken).ConfigureAwait(false); + } + + public async Task PatchAsync(string address, string payload, + RequestOptions requestOptions = null, CancellationToken cancellationToken = default) + { + return await HandleRequestAsync(address, HttpConstants.HttpVerbs.PATCH, requestOptions, CreateContentFromPayload(payload), + cancellationToken: cancellationToken).ConfigureAwait(false); + } + + public async Task DeleteAsync(string address, RequestOptions requestOptions = null, + CancellationToken cancellationToken = default) + { + return await HandleRequestAsync(address, HttpConstants.HttpVerbs.DELETE, requestOptions, cancellationToken: cancellationToken) + .ConfigureAwait(false); + } + + public async Task DownloadAsync(string address, RequestOptions requestOptions = null, + IProgress progress = null, CancellationToken cancellationToken = default) + { + return await HandleRequestAsync(address, HttpConstants.HttpVerbs.DOWNLOAD, requestOptions, progress: progress, + cancellationToken: cancellationToken).ConfigureAwait(false); + } + + protected abstract Task HandleRequestAsync( + string address, + string verb, + RequestOptions requestOptions = null, + object content = null, + IProgress progress = null, + CancellationToken cancellationToken = default); +} +#endif \ No newline at end of file diff --git a/src/redmine-net-api/Http/RedmineApiClient.cs b/src/redmine-net-api/Http/RedmineApiClient.cs new file mode 100644 index 00000000..c105e59e --- /dev/null +++ b/src/redmine-net-api/Http/RedmineApiClient.cs @@ -0,0 +1,113 @@ +using System; +using Redmine.Net.Api.Authentication; +using Redmine.Net.Api.Http.Constants; +using Redmine.Net.Api.Http.Messages; +using Redmine.Net.Api.Net; +using Redmine.Net.Api.Net.Internal; +using Redmine.Net.Api.Options; +using Redmine.Net.Api.Serialization; + +namespace Redmine.Net.Api.Http; + +internal abstract partial class RedmineApiClient : IRedmineApiClient +{ + protected readonly IRedmineAuthentication Credentials; + protected readonly IRedmineSerializer Serializer; + protected readonly RedmineManagerOptions Options; + + protected RedmineApiClient(RedmineManagerOptions redmineManagerOptions) + { + Credentials = redmineManagerOptions.Authentication; + Serializer = redmineManagerOptions.Serializer; + Options = redmineManagerOptions; + } + + public RedmineApiResponse Get(string address, RequestOptions requestOptions = null) + { + return HandleRequest(address, HttpConstants.HttpVerbs.GET, requestOptions); + } + + public RedmineApiResponse GetPaged(string address, RequestOptions requestOptions = null) + { + return Get(address, requestOptions); + } + + public RedmineApiResponse Create(string address, string payload, RequestOptions requestOptions = null) + { + return HandleRequest(address, HttpConstants.HttpVerbs.POST, requestOptions, CreateContentFromPayload(payload)); + } + + public RedmineApiResponse Update(string address, string payload, RequestOptions requestOptions = null) + { + return HandleRequest(address, HttpConstants.HttpVerbs.PUT, requestOptions, CreateContentFromPayload(payload)); + } + + public RedmineApiResponse Patch(string address, string payload, RequestOptions requestOptions = null) + { + return HandleRequest(address, HttpConstants.HttpVerbs.PATCH, requestOptions, CreateContentFromPayload(payload)); + } + + public RedmineApiResponse Delete(string address, RequestOptions requestOptions = null) + { + return HandleRequest(address, HttpConstants.HttpVerbs.DELETE, requestOptions); + } + + public RedmineApiResponse Download(string address, RequestOptions requestOptions = null, + IProgress progress = null) + { + return HandleRequest(address, HttpConstants.HttpVerbs.DOWNLOAD, requestOptions, progress: progress); + } + + public RedmineApiResponse Upload(string address, byte[] data, RequestOptions requestOptions = null) + { + return HandleRequest(address, HttpConstants.HttpVerbs.POST, requestOptions, CreateContentFromBytes(data)); + } + + protected abstract RedmineApiResponse HandleRequest( + string address, + string verb, + RequestOptions requestOptions = null, + object content = null, + IProgress progress = null); + + protected abstract object CreateContentFromPayload(string payload); + + protected abstract object CreateContentFromBytes(byte[] data); + + protected static bool IsGetOrDownload(string method) + { + return method is HttpConstants.HttpVerbs.GET or HttpConstants.HttpVerbs.DOWNLOAD; + } + + protected static void ReportProgress(IProgressprogress, long total, long bytesRead) + { + if (progress == null || total <= 0) + { + return; + } + var percent = (int)(bytesRead * 100L / total); + progress.Report(percent); + } + + // protected void LogRequest(string verb, string address, RequestOptions requestOptions) + // { + // if (_options.LoggingOptions?.IncludeHttpDetails == true) + // { + // _options.Logger.Debug($"Request HTTP {verb} {address}"); + // + // if (requestOptions?.QueryString != null) + // { + // _options.Logger.Debug($"Query parameters: {requestOptions.QueryString.ToQueryString()}"); + // } + // } + // } + // + // protected void LogResponse(HttpStatusCode statusCode) + // { + // if (_options.LoggingOptions?.IncludeHttpDetails == true) + // { + // _options.Logger.Debug($"Response status: {statusCode}"); + // } + // } + +} \ No newline at end of file diff --git a/src/redmine-net-api/Net/WebClient/RedmineWebClientOptions.cs b/src/redmine-net-api/Http/RedmineApiClientOptions.cs similarity index 56% rename from src/redmine-net-api/Net/WebClient/RedmineWebClientOptions.cs rename to src/redmine-net-api/Http/RedmineApiClientOptions.cs index 714df02d..1cf7f6cc 100644 --- a/src/redmine-net-api/Net/WebClient/RedmineWebClientOptions.cs +++ b/src/redmine-net-api/Http/RedmineApiClientOptions.cs @@ -1,31 +1,19 @@ -/* - Copyright 2011 - 2025 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - using System; using System.Collections.Generic; using System.Net; using System.Net.Cache; +#if NET || NET471_OR_GREATER +using System.Net.Http; +#endif using System.Net.Security; using System.Security.Cryptography.X509Certificates; -namespace Redmine.Net.Api.Net.WebClient; +namespace Redmine.Net.Api.Http; + /// /// /// -public sealed class RedmineWebClientOptions: IRedmineWebClientOptions +public abstract class RedmineApiClientOptions : IRedmineApiClientOptions { /// /// @@ -40,7 +28,12 @@ public sealed class RedmineWebClientOptions: IRedmineWebClientOptions /// /// /// - public DecompressionMethods? DecompressionFormat { get; set; } + public DecompressionMethods? DecompressionFormat { get; set; } = +#if NET + DecompressionMethods.All; +#else + DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.None; +#endif /// /// @@ -57,20 +50,12 @@ public sealed class RedmineWebClientOptions: IRedmineWebClientOptions /// public IWebProxy Proxy { get; set; } - /// - /// - /// - public bool? KeepAlive { get; set; } - /// /// /// public int? MaxAutomaticRedirections { get; set; } - /// - /// - /// - public long? MaxRequestContentBufferSize { get; set; } + /// /// @@ -102,36 +87,38 @@ public sealed class RedmineWebClientOptions: IRedmineWebClientOptions /// public string Scheme { get; set; } = "https"; + /// /// /// - public RemoteCertificateValidationCallback ServerCertificateValidationCallback { get; set; } + public TimeSpan? Timeout { get; set; } = TimeSpan.FromSeconds(30); /// /// /// - public TimeSpan? Timeout { get; set; } + public string UserAgent { get; set; } = "RedmineDotNetAPIClient"; /// /// /// - public bool? UnsafeAuthenticatedConnectionSharing { get; set; } + public bool? UseCookies { get; set; } - /// +#if NETFRAMEWORK + /// /// /// - public string UserAgent { get; set; } = "RedmineDotNetAPIClient"; + public bool CheckCertificateRevocationList { get; set; } - /// + /// /// /// - public bool? UseCookies { get; set; } + public long? MaxRequestContentBufferSize { get; set; } /// /// /// public bool? UseDefaultCredentials { get; set; } - +#endif /// /// /// @@ -143,53 +130,15 @@ public sealed class RedmineWebClientOptions: IRedmineWebClientOptions /// Only HTTP/1.0 and HTTP/1.1 version requests are currently supported. public Version ProtocolVersion { get; set; } + - #if NET40_OR_GREATER || NETCOREAPP - /// - /// - /// - public X509CertificateCollection ClientCertificates { get; set; } - #endif - - /// - /// - /// - public bool CheckCertificateRevocationList { get; set; } - - /// - /// - /// - public int? DefaultConnectionLimit { get; set; } - - /// - /// - /// - public int? DnsRefreshTimeout { get; set; } - - /// - /// - /// - public bool? EnableDnsRoundRobin { get; set; } - - /// - /// - /// - public int? MaxServicePoints { get; set; } - - /// - /// - /// - public int? MaxServicePointIdleTime { get; set; } - #if(NET46_OR_GREATER || NETCOREAPP) +#if NET40_OR_GREATER || NETCOREAPP /// /// /// - public bool? ReusePort { get; set; } - #endif + public X509CertificateCollection ClientCertificates { get; set; } +#endif - /// - /// - /// - public SecurityProtocolType? SecurityProtocolType { get; set; } + } \ No newline at end of file diff --git a/src/redmine-net-api/Net/RequestOptions.cs b/src/redmine-net-api/Http/RequestOptions.cs similarity index 98% rename from src/redmine-net-api/Net/RequestOptions.cs rename to src/redmine-net-api/Http/RequestOptions.cs index 7f3aa069..1e06ae53 100644 --- a/src/redmine-net-api/Net/RequestOptions.cs +++ b/src/redmine-net-api/Http/RequestOptions.cs @@ -18,7 +18,7 @@ limitations under the License. using System.Collections.Specialized; using Redmine.Net.Api.Extensions; -namespace Redmine.Net.Api.Net; +namespace Redmine.Net.Api.Http; /// /// diff --git a/src/redmine-net-api/IRedmineManagerAsync.cs b/src/redmine-net-api/IRedmineManager.Async.cs similarity index 99% rename from src/redmine-net-api/IRedmineManagerAsync.cs rename to src/redmine-net-api/IRedmineManager.Async.cs index 8d0098e5..0cb733b5 100644 --- a/src/redmine-net-api/IRedmineManagerAsync.cs +++ b/src/redmine-net-api/IRedmineManager.Async.cs @@ -19,6 +19,8 @@ limitations under the License. using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Redmine.Net.Api.Common; +using Redmine.Net.Api.Http; using Redmine.Net.Api.Net; using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Types; diff --git a/src/redmine-net-api/IRedmineManager.cs b/src/redmine-net-api/IRedmineManager.cs index 8642a6e8..e7f487dc 100644 --- a/src/redmine-net-api/IRedmineManager.cs +++ b/src/redmine-net-api/IRedmineManager.cs @@ -16,11 +16,12 @@ limitations under the License. using System; using System.Collections.Generic; +using Redmine.Net.Api.Common; using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Net; -using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Http; +using Redmine.Net.Api.Types; -namespace Redmine.Net.Api.Types; +namespace Redmine.Net.Api; /// /// diff --git a/src/redmine-net-api/Net/HttpVerbs.cs b/src/redmine-net-api/Net/HttpVerbs.cs deleted file mode 100644 index e7851896..00000000 --- a/src/redmine-net-api/Net/HttpVerbs.cs +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright 2011 - 2025 Adrian Popescu - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -namespace Redmine.Net.Api -{ - - /// - /// - /// - public static class HttpVerbs - { - /// - /// Represents an HTTP GET protocol method that is used to get an entity identified by a URI. - /// - public const string GET = "GET"; - /// - /// Represents an HTTP PUT protocol method that is used to replace an entity identified by a URI. - /// - public const string PUT = "PUT"; - /// - /// Represents an HTTP POST protocol method that is used to post a new entity as an addition to a URI. - /// - public const string POST = "POST"; - /// - /// Represents an HTTP PATCH protocol method that is used to patch an existing entity identified by a URI. - /// - public const string PATCH = "PATCH"; - /// - /// Represents an HTTP DELETE protocol method that is used to delete an existing entity identified by a URI. - /// - public const string DELETE = "DELETE"; - - - internal const string DOWNLOAD = "DOWNLOAD"; - - internal const string UPLOAD = "UPLOAD"; - } -} \ No newline at end of file diff --git a/src/redmine-net-api/Net/IRedmineApiClientOptions.cs b/src/redmine-net-api/Net/IRedmineApiClientOptions.cs deleted file mode 100644 index 57431696..00000000 --- a/src/redmine-net-api/Net/IRedmineApiClientOptions.cs +++ /dev/null @@ -1,197 +0,0 @@ -/* - Copyright 2011 - 2025 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Cache; -using System.Net.Security; -using System.Security.Cryptography.X509Certificates; - -namespace Redmine.Net.Api.Net -{ - /// - /// - /// - public interface IRedmineApiClientOptions - { - /// - /// - /// - bool? AutoRedirect { get; set; } - - /// - /// - /// - CookieContainer CookieContainer { get; set; } - - /// - /// - /// - DecompressionMethods? DecompressionFormat { get; set; } - - /// - /// - /// - ICredentials Credentials { get; set; } - - /// - /// - /// - Dictionary DefaultHeaders { get; set; } - - /// - /// - /// - IWebProxy Proxy { get; set; } - - /// - /// - /// - bool? KeepAlive { get; set; } - - /// - /// - /// - int? MaxAutomaticRedirections { get; set; } - - /// - /// - /// - long? MaxRequestContentBufferSize { get; set; } - - /// - /// - /// - long? MaxResponseContentBufferSize { get; set; } - -#if NET471_OR_GREATER || NETCOREAPP - /// - /// - /// - int? MaxConnectionsPerServer { get; set; } - - /// - /// - /// - int? MaxResponseHeadersLength { get; set; } -#endif - /// - /// - /// - bool? PreAuthenticate { get; set; } - - /// - /// - /// - RequestCachePolicy RequestCachePolicy { get; set; } - - /// - /// - /// - string Scheme { get; set; } - - /// - /// - /// - RemoteCertificateValidationCallback ServerCertificateValidationCallback { get; set; } - - /// - /// - /// - TimeSpan? Timeout { get; set; } - - /// - /// - /// - bool? UnsafeAuthenticatedConnectionSharing { get; set; } - - /// - /// - /// - string UserAgent { get; set; } - - /// - /// - /// - bool? UseCookies { get; set; } - - /// - /// - /// - bool? UseDefaultCredentials { get; set; } - - /// - /// - /// - bool? UseProxy { get; set; } - - /// - /// - /// - /// Only HTTP/1.0 and HTTP/1.1 version requests are currently supported. - Version ProtocolVersion { get; set; } - - /// - /// - /// - bool CheckCertificateRevocationList { get; set; } - - /// - /// - /// - int? DefaultConnectionLimit { get; set; } - - /// - /// - /// - int? DnsRefreshTimeout { get; set; } - - /// - /// - /// - bool? EnableDnsRoundRobin { get; set; } - - /// - /// - /// - int? MaxServicePoints { get; set; } - - /// - /// - /// - int? MaxServicePointIdleTime { get; set; } - - /// - /// - /// - SecurityProtocolType? SecurityProtocolType { get; set; } - -#if NET40_OR_GREATER || NETCOREAPP - /// - /// - /// - public X509CertificateCollection ClientCertificates { get; set; } -#endif - -#if(NET46_OR_GREATER || NETCOREAPP) - /// - /// - /// - public bool? ReusePort { get; set; } -#endif - } -} \ No newline at end of file diff --git a/src/redmine-net-api/Net/Internal/ApiRequestMessageContent.cs b/src/redmine-net-api/Net/Internal/ApiRequestMessageContent.cs deleted file mode 100644 index 296d7a4d..00000000 --- a/src/redmine-net-api/Net/Internal/ApiRequestMessageContent.cs +++ /dev/null @@ -1,24 +0,0 @@ -/* - Copyright 2011 - 2025 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -namespace Redmine.Net.Api.Net.Internal; - -internal abstract class ApiRequestMessageContent -{ - public string ContentType { get; internal set; } - - public byte[] Body { get; internal set; } -} \ No newline at end of file diff --git a/src/redmine-net-api/Net/Internal/HttpStatusHelper.cs b/src/redmine-net-api/Net/Internal/HttpStatusHelper.cs deleted file mode 100644 index 90fe88c9..00000000 --- a/src/redmine-net-api/Net/Internal/HttpStatusHelper.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Text; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Serialization; -using Redmine.Net.Api.Types; - -namespace Redmine.Net.Api.Net.Internal; - -internal static class HttpStatusHelper -{ - internal static void MapStatusCodeToException(int statusCode, Stream responseStream, Exception inner, IRedmineSerializer serializer) - { - switch (statusCode) - { - case (int)HttpStatusCode.NotFound: - throw new NotFoundException("Not found.", inner); - - case (int)HttpStatusCode.Unauthorized: - throw new UnauthorizedException("Unauthorized.", inner); - - case (int)HttpStatusCode.Forbidden: - throw new ForbiddenException("Forbidden.", inner); - - case (int)HttpStatusCode.Conflict: - throw new ConflictException("The page that you are trying to update is stale!", inner); - - case 422: - var exception = CreateUnprocessableEntityException(responseStream, inner, serializer); - throw exception; - - case (int)HttpStatusCode.NotAcceptable: - throw new NotAcceptableException("Not acceptable.", inner); - - case (int)HttpStatusCode.InternalServerError: - throw new InternalServerErrorException("Internal server error.", inner); - - default: - throw new RedmineException($"HTTP {(int)statusCode} – {statusCode}", inner); - } - } - - private static RedmineException CreateUnprocessableEntityException( - Stream responseStream, - Exception inner, - IRedmineSerializer serializer) - { - var errors = GetRedmineErrors(responseStream, serializer); - - if (errors is null) - { - return new RedmineException("Unprocessable Content", inner); - } - - var sb = new StringBuilder(); - foreach (var error in errors) - { - sb.Append(error.Info).Append(Environment.NewLine); - } - - sb.Length -= 1; - return new RedmineException($"Unprocessable Content: {sb}", inner); - } - - - /// - /// Gets the redmine exceptions. - /// - /// - /// - /// - private static List GetRedmineErrors(Stream responseStream, IRedmineSerializer serializer) - { - if (responseStream == null) - { - return null; - } - - using (responseStream) - { - using var streamReader = new StreamReader(responseStream); - var responseContent = streamReader.ReadToEnd(); - - return GetRedmineErrors(responseContent, serializer); - } - } - - private static List GetRedmineErrors(string content, IRedmineSerializer serializer) - { - if (content.IsNullOrWhiteSpace()) return null; - - var paged = serializer.DeserializeToPagedResults(content); - return (List)paged.Items; - } -} \ No newline at end of file diff --git a/src/redmine-net-api/Net/Internal/IAsyncRedmineApiClient.cs b/src/redmine-net-api/Net/Internal/IAsyncRedmineApiClient.cs deleted file mode 100644 index 92210bd6..00000000 --- a/src/redmine-net-api/Net/Internal/IAsyncRedmineApiClient.cs +++ /dev/null @@ -1,42 +0,0 @@ -/* - Copyright 2011 - 2025 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -#if !(NET20 || NET35) -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Redmine.Net.Api.Net.Internal; - -internal interface IAsyncRedmineApiClient -{ - Task GetAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); - - Task GetPagedAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); - - Task CreateAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); - - Task UpdateAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); - - Task PatchAsync(string address, string payload, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); - - Task DeleteAsync(string address, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); - - Task UploadFileAsync(string address, byte[] data, RequestOptions requestOptions = null, CancellationToken cancellationToken = default); - - Task DownloadAsync(string address, RequestOptions requestOptions = null, IProgress progress = null, CancellationToken cancellationToken = default); -} -#endif \ No newline at end of file diff --git a/src/redmine-net-api/Net/Internal/IRedmineApiClient.cs b/src/redmine-net-api/Net/Internal/IRedmineApiClient.cs deleted file mode 100644 index 29b90d0f..00000000 --- a/src/redmine-net-api/Net/Internal/IRedmineApiClient.cs +++ /dev/null @@ -1,27 +0,0 @@ -/* - Copyright 2011 - 2025 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -namespace Redmine.Net.Api.Net.Internal; - -/// -/// -/// -internal interface IRedmineApiClient : ISyncRedmineApiClient -#if !(NET20 || NET35) - , IAsyncRedmineApiClient -#endif -{ -} \ No newline at end of file diff --git a/src/redmine-net-api/Net/Internal/ISyncRedmineApiClient.cs b/src/redmine-net-api/Net/Internal/ISyncRedmineApiClient.cs deleted file mode 100644 index 5fd2e239..00000000 --- a/src/redmine-net-api/Net/Internal/ISyncRedmineApiClient.cs +++ /dev/null @@ -1,38 +0,0 @@ -/* - Copyright 2011 - 2025 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; - -namespace Redmine.Net.Api.Net.Internal; - -internal interface ISyncRedmineApiClient -{ - ApiResponseMessage Get(string address, RequestOptions requestOptions = null); - - ApiResponseMessage GetPaged(string address, RequestOptions requestOptions = null); - - ApiResponseMessage Create(string address, string payload, RequestOptions requestOptions = null); - - ApiResponseMessage Update(string address, string payload, RequestOptions requestOptions = null); - - ApiResponseMessage Patch(string address, string payload, RequestOptions requestOptions = null); - - ApiResponseMessage Delete(string address, RequestOptions requestOptions = null); - - ApiResponseMessage Upload(string address, byte[] data, RequestOptions requestOptions = null); - - ApiResponseMessage Download(string address, RequestOptions requestOptions = null, IProgress progress = null); -} \ No newline at end of file diff --git a/src/redmine-net-api/Net/Internal/RedmineApiUrls.cs b/src/redmine-net-api/Net/Internal/RedmineApiUrls.cs index 8d83883e..8fa0b3a1 100644 --- a/src/redmine-net-api/Net/Internal/RedmineApiUrls.cs +++ b/src/redmine-net-api/Net/Internal/RedmineApiUrls.cs @@ -18,6 +18,7 @@ limitations under the License. using System.Collections.Generic; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http; using Redmine.Net.Api.Types; using Version = Redmine.Net.Api.Types.Version; diff --git a/src/redmine-net-api/Net/Internal/RedmineApiUrlsExtensions.cs b/src/redmine-net-api/Net/Internal/RedmineApiUrlsExtensions.cs index 9ed8ab34..753b439d 100644 --- a/src/redmine-net-api/Net/Internal/RedmineApiUrlsExtensions.cs +++ b/src/redmine-net-api/Net/Internal/RedmineApiUrlsExtensions.cs @@ -14,9 +14,6 @@ You may obtain a copy of the License at limitations under the License. */ -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Extensions; - namespace Redmine.Net.Api.Net.Internal; internal static class RedmineApiUrlsExtensions @@ -63,21 +60,11 @@ public static string ProjectRepositoryRemoveRelatedIssue(this RedmineApiUrls red public static string ProjectNews(this RedmineApiUrls redmineApiUrls, string projectIdentifier) { - if (projectIdentifier.IsNullOrWhiteSpace()) - { - throw new RedmineException($"Argument '{nameof(projectIdentifier)}' is null or whitespace"); - } - return $"{RedmineKeys.PROJECTS}/{projectIdentifier}/{RedmineKeys.NEWS}.{redmineApiUrls.Format}"; } public static string ProjectMemberships(this RedmineApiUrls redmineApiUrls, string projectIdentifier) { - if (projectIdentifier.IsNullOrWhiteSpace()) - { - throw new RedmineException($"Argument '{nameof(projectIdentifier)}' is null or whitespace"); - } - return $"{RedmineKeys.PROJECTS}/{projectIdentifier}/{RedmineKeys.MEMBERSHIPS}.{redmineApiUrls.Format}"; } @@ -118,61 +105,26 @@ public static string ProjectWikis(this RedmineApiUrls redmineApiUrls, string pro public static string IssueWatcherAdd(this RedmineApiUrls redmineApiUrls, string issueIdentifier) { - if (issueIdentifier.IsNullOrWhiteSpace()) - { - throw new RedmineException($"Argument '{nameof(issueIdentifier)}' is null or whitespace"); - } - return $"{RedmineKeys.ISSUES}/{issueIdentifier}/{RedmineKeys.WATCHERS}.{redmineApiUrls.Format}"; } public static string IssueWatcherRemove(this RedmineApiUrls redmineApiUrls, string issueIdentifier, string userId) { - if (issueIdentifier.IsNullOrWhiteSpace()) - { - throw new RedmineException($"Argument '{nameof(issueIdentifier)}' is null or whitespace"); - } - - if (userId.IsNullOrWhiteSpace()) - { - throw new RedmineException($"Argument '{nameof(userId)}' is null or whitespace"); - } - return $"{RedmineKeys.ISSUES}/{issueIdentifier}/{RedmineKeys.WATCHERS}/{userId}.{redmineApiUrls.Format}"; } public static string GroupUserAdd(this RedmineApiUrls redmineApiUrls, string groupIdentifier) { - if (groupIdentifier.IsNullOrWhiteSpace()) - { - throw new RedmineException($"Argument '{nameof(groupIdentifier)}' is null or whitespace"); - } - return $"{RedmineKeys.GROUPS}/{groupIdentifier}/{RedmineKeys.USERS}.{redmineApiUrls.Format}"; } public static string GroupUserRemove(this RedmineApiUrls redmineApiUrls, string groupIdentifier, string userId) { - if (groupIdentifier.IsNullOrWhiteSpace()) - { - throw new RedmineException($"Argument '{nameof(groupIdentifier)}' is null or whitespace"); - } - - if (userId.IsNullOrWhiteSpace()) - { - throw new RedmineException($"Argument '{nameof(userId)}' is null or whitespace"); - } - return $"{RedmineKeys.GROUPS}/{groupIdentifier}/{RedmineKeys.USERS}/{userId}.{redmineApiUrls.Format}"; } public static string AttachmentUpdate(this RedmineApiUrls redmineApiUrls, string issueIdentifier) { - if (issueIdentifier.IsNullOrWhiteSpace()) - { - throw new RedmineException($"Argument '{nameof(issueIdentifier)}' is null or whitespace"); - } - return $"{RedmineKeys.ATTACHMENTS}/{RedmineKeys.ISSUES}/{issueIdentifier}.{redmineApiUrls.Format}"; } diff --git a/src/redmine-net-api/Net/WebClient/Extensions/NameValueCollectionExtensions.cs b/src/redmine-net-api/Net/WebClient/Extensions/NameValueCollectionExtensions.cs deleted file mode 100644 index e69dffbd..00000000 --- a/src/redmine-net-api/Net/WebClient/Extensions/NameValueCollectionExtensions.cs +++ /dev/null @@ -1,140 +0,0 @@ -/* - Copyright 2011 - 2025 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System.Collections.Specialized; -using System.Globalization; -using System.Text; - -namespace Redmine.Net.Api.Extensions -{ - /// - /// - /// - public static class NameValueCollectionExtensions - { - /// - /// Gets the parameter value. - /// - /// The parameters. - /// Name of the parameter. - /// - public static string GetParameterValue(this NameValueCollection parameters, string parameterName) - { - return GetValue(parameters, parameterName); - } - - /// - /// Gets the parameter value. - /// - /// The parameters. - /// Name of the parameter. - /// - public static string GetValue(this NameValueCollection parameters, string key) - { - if (parameters == null) - { - return null; - } - - var value = parameters.Get(key); - - return value.IsNullOrWhiteSpace() ? null : value; - } - - /// - /// - /// - /// - /// - public static string ToQueryString(this NameValueCollection requestParameters) - { - if (requestParameters == null || requestParameters.Count == 0) - { - return null; - } - - var delimiter = string.Empty; - - var stringBuilder = new StringBuilder(); - - for (var index = 0; index < requestParameters.Count; ++index) - { - stringBuilder - .Append(delimiter) - .Append(requestParameters.AllKeys[index].ToString(CultureInfo.InvariantCulture)) - .Append('=') - .Append(requestParameters[index].ToString(CultureInfo.InvariantCulture)); - delimiter = "&"; - } - - var queryString = stringBuilder.ToString(); - - stringBuilder.Length = 0; - - return queryString; - } - - internal static NameValueCollection AddPagingParameters(this NameValueCollection parameters, int pageSize, int offset) - { - parameters ??= new NameValueCollection(); - - if(pageSize <= 0) - { - pageSize = RedmineConstants.DEFAULT_PAGE_SIZE_VALUE; - } - - if(offset < 0) - { - offset = 0; - } - - parameters.Set(RedmineKeys.LIMIT, pageSize.ToInvariantString()); - parameters.Set(RedmineKeys.OFFSET, offset.ToInvariantString()); - - return parameters; - } - - internal static NameValueCollection AddParamsIfExist(this NameValueCollection parameters, string[] include) - { - if (include is not {Length: > 0}) - { - return parameters; - } - - parameters ??= new NameValueCollection(); - - parameters.Add(RedmineKeys.INCLUDE, string.Join(",", include)); - - return parameters; - } - - internal static void AddIfNotNull(this NameValueCollection nameValueCollection, string key, string value) - { - if (!value.IsNullOrWhiteSpace()) - { - nameValueCollection.Add(key, value); - } - } - - internal static void AddIfNotNull(this NameValueCollection nameValueCollection, string key, bool? value) - { - if (value.HasValue) - { - nameValueCollection.Add(key, value.Value.ToInvariantString()); - } - } - } -} \ No newline at end of file diff --git a/src/redmine-net-api/Net/WebClient/Extensions/WebExceptionExtensions.cs b/src/redmine-net-api/Net/WebClient/Extensions/WebExceptionExtensions.cs deleted file mode 100644 index 3820d457..00000000 --- a/src/redmine-net-api/Net/WebClient/Extensions/WebExceptionExtensions.cs +++ /dev/null @@ -1,75 +0,0 @@ -/* - Copyright 2011 - 2025 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System.Net; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Net.Internal; -using Redmine.Net.Api.Serialization; - -namespace Redmine.Net.Api.Net.WebClient.Extensions -{ - /// - /// - /// - internal static class WebExceptionExtensions - { - /// - /// Handles the web exception. - /// - /// The exception. - /// - /// Timeout! - /// Bad domain name! - /// - /// - /// - /// - /// The page that you are trying to update is staled! - /// - /// - public static void HandleWebException(this WebException exception, IRedmineSerializer serializer) - { - if (exception == null) - { - return; - } - - var innerException = exception.InnerException ?? exception; - - switch (exception.Status) - { - case WebExceptionStatus.Timeout: - throw new RedmineTimeoutException(nameof(WebExceptionStatus.Timeout), innerException); - case WebExceptionStatus.NameResolutionFailure: - throw new NameResolutionFailureException("Bad domain name.", innerException); - case WebExceptionStatus.ProtocolError: - if (exception.Response != null) - { - var statusCode = exception.Response is HttpWebResponse httpResponse - ? (int)httpResponse.StatusCode - : (int)HttpStatusCode.InternalServerError; - - using var responseStream = exception.Response.GetResponseStream(); - HttpStatusHelper.MapStatusCodeToException(statusCode, responseStream, innerException, serializer); - - } - - break; - } - throw new RedmineException(exception.Message, innerException); - } - } -} \ No newline at end of file diff --git a/src/redmine-net-api/Net/WebClient/IRedmineWebClientOptions.cs b/src/redmine-net-api/Net/WebClient/IRedmineWebClientOptions.cs deleted file mode 100644 index 1c7635f0..00000000 --- a/src/redmine-net-api/Net/WebClient/IRedmineWebClientOptions.cs +++ /dev/null @@ -1,22 +0,0 @@ -/* - Copyright 2011 - 2025 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -namespace Redmine.Net.Api.Net.WebClient; - -/// -/// -/// -public interface IRedmineWebClientOptions : IRedmineApiClientOptions; \ No newline at end of file diff --git a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs b/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs deleted file mode 100644 index 25dbc941..00000000 --- a/src/redmine-net-api/Net/WebClient/InternalRedmineApiWebClient.cs +++ /dev/null @@ -1,263 +0,0 @@ -/* - Copyright 2011 - 2025 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Collections.Specialized; -using System.Net; -using System.Text; -using Redmine.Net.Api.Authentication; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Net.Internal; -using Redmine.Net.Api.Net.WebClient.Extensions; -using Redmine.Net.Api.Net.WebClient.MessageContent; -using Redmine.Net.Api.Serialization; - -namespace Redmine.Net.Api.Net.WebClient -{ - /// - /// - /// - internal sealed partial class InternalRedmineApiWebClient : IRedmineApiClient - { - private static readonly byte[] EmptyBytes = Encoding.UTF8.GetBytes(string.Empty); - private readonly Func _webClientFunc; - private readonly IRedmineAuthentication _credentials; - private readonly IRedmineSerializer _serializer; - - public InternalRedmineApiWebClient(RedmineManagerOptions redmineManagerOptions) - : this(() => new InternalWebClient(redmineManagerOptions), redmineManagerOptions.Authentication, redmineManagerOptions.Serializer) - { - ConfigureServicePointManager(redmineManagerOptions.WebClientOptions); - } - - public InternalRedmineApiWebClient( - Func webClientFunc, - IRedmineAuthentication authentication, - IRedmineSerializer serializer) - { - _webClientFunc = webClientFunc; - _credentials = authentication; - _serializer = serializer; - } - - private static void ConfigureServicePointManager(IRedmineWebClientOptions webClientOptions) - { - if (webClientOptions == null) - { - return; - } - - if (webClientOptions.MaxServicePoints.HasValue) - { - ServicePointManager.MaxServicePoints = webClientOptions.MaxServicePoints.Value; - } - - if (webClientOptions.MaxServicePointIdleTime.HasValue) - { - ServicePointManager.MaxServicePointIdleTime = webClientOptions.MaxServicePointIdleTime.Value; - } - - ServicePointManager.SecurityProtocol = webClientOptions.SecurityProtocolType ?? ServicePointManager.SecurityProtocol; - - if (webClientOptions.DefaultConnectionLimit.HasValue) - { - ServicePointManager.DefaultConnectionLimit = webClientOptions.DefaultConnectionLimit.Value; - } - - if (webClientOptions.DnsRefreshTimeout.HasValue) - { - ServicePointManager.DnsRefreshTimeout = webClientOptions.DnsRefreshTimeout.Value; - } - - ServicePointManager.CheckCertificateRevocationList = webClientOptions.CheckCertificateRevocationList; - - if (webClientOptions.EnableDnsRoundRobin.HasValue) - { - ServicePointManager.EnableDnsRoundRobin = webClientOptions.EnableDnsRoundRobin.Value; - } - - #if(NET46_OR_GREATER || NETCOREAPP) - if (webClientOptions.ReusePort.HasValue) - { - ServicePointManager.ReusePort = webClientOptions.ReusePort.Value; - } - #endif - } - - public ApiResponseMessage Get(string address, RequestOptions requestOptions = null) - { - return HandleRequest(address, HttpVerbs.GET, requestOptions); - } - - public ApiResponseMessage GetPaged(string address, RequestOptions requestOptions = null) - { - return Get(address, requestOptions); - } - - public ApiResponseMessage Create(string address, string payload, RequestOptions requestOptions = null) - { - var content = new StringApiRequestMessageContent(payload, _serializer.ContentType); - return HandleRequest(address, HttpVerbs.POST, requestOptions, content); - } - - public ApiResponseMessage Update(string address, string payload, RequestOptions requestOptions = null) - { - var content = new StringApiRequestMessageContent(payload, _serializer.ContentType); - return HandleRequest(address, HttpVerbs.PUT, requestOptions, content); - } - - public ApiResponseMessage Patch(string address, string payload, RequestOptions requestOptions = null) - { - var content = new StringApiRequestMessageContent(payload, _serializer.ContentType); - return HandleRequest(address, HttpVerbs.PATCH, requestOptions, content); - } - - public ApiResponseMessage Delete(string address, RequestOptions requestOptions = null) - { - return HandleRequest(address, HttpVerbs.DELETE, requestOptions); - } - - public ApiResponseMessage Download(string address, RequestOptions requestOptions = null, IProgress progress = null) - { - return HandleRequest(address, HttpVerbs.DOWNLOAD, requestOptions); - } - - public ApiResponseMessage Upload(string address, byte[] data, RequestOptions requestOptions = null) - { - var content = new StreamApiRequestMessageContent(data); - return HandleRequest(address, HttpVerbs.POST, requestOptions, content); - } - - private static ApiRequestMessage CreateRequestMessage(string address, string verb, RequestOptions requestOptions = null, ApiRequestMessageContent content = null) - { - var req = new ApiRequestMessage() - { - RequestUri = address, - Method = verb, - }; - - if (requestOptions != null) - { - req.QueryString = requestOptions.QueryString; - req.ImpersonateUser = requestOptions.ImpersonateUser; - } - - if (content != null) - { - req.Content = content; - } - - return req; - } - - private ApiResponseMessage HandleRequest(string address, string verb, RequestOptions requestOptions = null, ApiRequestMessageContent content = null, IProgress progress = null) - { - return Send(CreateRequestMessage(address, verb, requestOptions, content), progress); - } - - private ApiResponseMessage Send(ApiRequestMessage requestMessage, IProgress progress = null) - { - System.Net.WebClient webClient = null; - byte[] response = null; - HttpStatusCode? statusCode = null; - NameValueCollection responseHeaders = null; - - try - { - webClient = _webClientFunc(); - - if (progress != null) - { - webClient.DownloadProgressChanged += (_, e) => - { - progress.Report(e.ProgressPercentage); - }; - } - - SetWebClientHeaders(webClient, requestMessage); - - if (IsGetOrDownload(requestMessage.Method)) - { - response = webClient.DownloadData(requestMessage.RequestUri); - } - else - { - byte[] payload; - if (requestMessage.Content != null) - { - webClient.Headers.Add(HttpRequestHeader.ContentType, requestMessage.Content.ContentType); - payload = requestMessage.Content.Body; - } - else - { - payload = EmptyBytes; - } - - response = webClient.UploadData(requestMessage.RequestUri, requestMessage.Method, payload); - } - - responseHeaders = webClient.ResponseHeaders; - if (webClient is InternalWebClient iwc) - { - statusCode = iwc.StatusCode; - } - } - catch (WebException webException) - { - webException.HandleWebException(_serializer); - } - finally - { - webClient?.Dispose(); - } - - return new ApiResponseMessage() - { - Headers = responseHeaders, - Content = response, - StatusCode = statusCode ?? HttpStatusCode.OK, - }; - } - - private void SetWebClientHeaders(System.Net.WebClient webClient, ApiRequestMessage requestMessage) - { - if (requestMessage.QueryString != null) - { - webClient.QueryString = requestMessage.QueryString; - } - - switch (_credentials) - { - case RedmineApiKeyAuthentication: - webClient.Headers.Add(RedmineConstants.API_KEY_AUTHORIZATION_HEADER_KEY,_credentials.Token); - break; - case RedmineBasicAuthentication: - webClient.Headers.Add(RedmineConstants.AUTHORIZATION_HEADER_KEY, _credentials.Token); - break; - } - - if (!requestMessage.ImpersonateUser.IsNullOrWhiteSpace()) - { - webClient.Headers.Add(RedmineConstants.IMPERSONATE_HEADER_KEY, requestMessage.ImpersonateUser); - } - } - - private static bool IsGetOrDownload(string method) - { - return method is HttpVerbs.GET or HttpVerbs.DOWNLOAD; - } - } -} diff --git a/src/redmine-net-api/Net/WebClient/MessageContent/ByteArrayApiRequestMessageContent.cs b/src/redmine-net-api/Net/WebClient/MessageContent/ByteArrayApiRequestMessageContent.cs deleted file mode 100644 index 13602f4b..00000000 --- a/src/redmine-net-api/Net/WebClient/MessageContent/ByteArrayApiRequestMessageContent.cs +++ /dev/null @@ -1,27 +0,0 @@ -/* - Copyright 2011 - 2025 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using Redmine.Net.Api.Net.Internal; - -namespace Redmine.Net.Api.Net.WebClient.MessageContent; - -internal class ByteArrayApiRequestMessageContent : ApiRequestMessageContent -{ - public ByteArrayApiRequestMessageContent(byte[] content) - { - Body = content; - } -} \ No newline at end of file diff --git a/src/redmine-net-api/Net/WebClient/MessageContent/StreamApiRequestMessageContent.cs b/src/redmine-net-api/Net/WebClient/MessageContent/StreamApiRequestMessageContent.cs deleted file mode 100644 index ed49becf..00000000 --- a/src/redmine-net-api/Net/WebClient/MessageContent/StreamApiRequestMessageContent.cs +++ /dev/null @@ -1,25 +0,0 @@ -/* - Copyright 2011 - 2025 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -namespace Redmine.Net.Api.Net.WebClient.MessageContent; - -internal sealed class StreamApiRequestMessageContent : ByteArrayApiRequestMessageContent -{ - public StreamApiRequestMessageContent(byte[] content) : base(content) - { - ContentType = RedmineConstants.CONTENT_TYPE_APPLICATION_STREAM; - } -} \ No newline at end of file diff --git a/src/redmine-net-api/Net/WebClient/MessageContent/StringApiRequestMessageContent.cs b/src/redmine-net-api/Net/WebClient/MessageContent/StringApiRequestMessageContent.cs deleted file mode 100644 index 3a1d7590..00000000 --- a/src/redmine-net-api/Net/WebClient/MessageContent/StringApiRequestMessageContent.cs +++ /dev/null @@ -1,41 +0,0 @@ -/* - Copyright 2011 - 2025 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Text; -using Redmine.Net.Api.Internals; - -namespace Redmine.Net.Api.Net.WebClient.MessageContent; - -internal sealed class StringApiRequestMessageContent : ByteArrayApiRequestMessageContent -{ - private static readonly Encoding DefaultStringEncoding = Encoding.UTF8; - - public StringApiRequestMessageContent(string content, string mediaType) : this(content, mediaType, DefaultStringEncoding) - { - } - - public StringApiRequestMessageContent(string content, string mediaType, Encoding encoding) : base(GetContentByteArray(content, encoding)) - { - ContentType = mediaType; - } - - private static byte[] GetContentByteArray(string content, Encoding encoding) - { - ArgumentNullThrowHelper.ThrowIfNull(content, nameof(content)); - return (encoding ?? DefaultStringEncoding).GetBytes(content); - } -} \ No newline at end of file diff --git a/src/redmine-net-api/RedmineManagerAsync.cs b/src/redmine-net-api/RedmineManager.Async.cs similarity index 98% rename from src/redmine-net-api/RedmineManagerAsync.cs rename to src/redmine-net-api/RedmineManager.Async.cs index 5b8472b8..bf48328e 100644 --- a/src/redmine-net-api/RedmineManagerAsync.cs +++ b/src/redmine-net-api/RedmineManager.Async.cs @@ -20,7 +20,10 @@ limitations under the License. using System.Collections.Specialized; using System.Threading; using System.Threading.Tasks; +using Redmine.Net.Api.Common; using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http; +using Redmine.Net.Api.Http.Extensions; using Redmine.Net.Api.Net; using Redmine.Net.Api.Net.Internal; using Redmine.Net.Api.Serialization; diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index 82558e62..c9c92598 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -17,14 +17,16 @@ limitations under the License. using System; using System.Collections.Generic; using System.Collections.Specialized; -using System.Globalization; using System.Net; -using Redmine.Net.Api.Authentication; +using Redmine.Net.Api.Common; using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http; +using Redmine.Net.Api.Http.Clients.WebClient; +using Redmine.Net.Api.Http.Extensions; using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Net; +using Redmine.Net.Api.Logging; using Redmine.Net.Api.Net.Internal; -using Redmine.Net.Api.Net.WebClient; +using Redmine.Net.Api.Options; using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Types; @@ -57,8 +59,8 @@ public RedmineManager(RedmineManagerOptionsBuilder optionsBuilder) RedmineApiUrls = new RedmineApiUrls(_redmineManagerOptions.Serializer.Format); ApiClient = -#if NET45_OR_GREATER || NETCOREAPP - _redmineManagerOptions.WebClientOptions switch +#if NET40_OR_GREATER || NET + _redmineManagerOptions.ApiClientOptions switch { RedmineWebClientOptions => CreateWebClient(_redmineManagerOptions), RedmineHttpClientOptions => CreateHttpClient(_redmineManagerOptions), @@ -68,13 +70,14 @@ public RedmineManager(RedmineManagerOptionsBuilder optionsBuilder) #endif } - private InternalRedmineApiWebClient CreateWebClient(RedmineManagerOptions options) + private static InternalRedmineApiWebClient CreateWebClient(RedmineManagerOptions options) { if (options.ClientFunc != null) { - return new InternalRedmineApiWebClient(options.ClientFunc, options.Authentication, options.Serializer); + return new InternalRedmineApiWebClient(options.ClientFunc, options); } + ApplyServiceManagerSettings(options.WebClientOptions); #pragma warning disable SYSLIB0014 options.WebClientOptions.SecurityProtocolType ??= ServicePointManager.SecurityProtocol; #pragma warning restore SYSLIB0014 @@ -85,18 +88,57 @@ private InternalRedmineApiWebClient CreateWebClient(RedmineManagerOptions option #if NET45_OR_GREATER if (options.VerifyServerCert) + private static void ApplyServiceManagerSettings(RedmineWebClientOptions options) + { + if (options == null) + { + return; + } + + if (options.SecurityProtocolType.HasValue) + { + ServicePointManager.SecurityProtocol = options.SecurityProtocolType.Value; + } + + if (options.DefaultConnectionLimit.HasValue) + { + ServicePointManager.DefaultConnectionLimit = options.DefaultConnectionLimit.Value; + } + + if (options.DnsRefreshTimeout.HasValue) + { + ServicePointManager.DnsRefreshTimeout = options.DnsRefreshTimeout.Value; + } + + if (options.EnableDnsRoundRobin.HasValue) + { + ServicePointManager.EnableDnsRoundRobin = options.EnableDnsRoundRobin.Value; + } + + if (options.MaxServicePoints.HasValue) + { + ServicePointManager.MaxServicePoints = options.MaxServicePoints.Value; + } + + if (options.MaxServicePointIdleTime.HasValue) + { + ServicePointManager.MaxServicePointIdleTime = options.MaxServicePointIdleTime.Value; + } + +#if(NET46_OR_GREATER || NET) + if (options.ReusePort.HasValue) { - options.WebClientOptions.ServerCertificateValidationCallback = RemoteCertValidate; + ServicePointManager.ReusePort = options.ReusePort.Value; + } +#endif + #if NEFRAMEWORK + if (options.CheckCertificateRevocationList) + { + ServicePointManager.CheckCertificateRevocationList = true; } #endif - return new InternalRedmineApiWebClient(options); } - private IRedmineApiClient CreateHttpClient(RedmineManagerOptions options) - { - throw new NotImplementedException(); - } - /// public int Count(RequestOptions requestOptions = null) where T : class, new() diff --git a/src/redmine-net-api/SearchFilterBuilder.cs b/src/redmine-net-api/SearchFilterBuilder.cs index 856fb1a7..9b2b8c9c 100644 --- a/src/redmine-net-api/SearchFilterBuilder.cs +++ b/src/redmine-net-api/SearchFilterBuilder.cs @@ -17,6 +17,7 @@ limitations under the License. using System; using System.Collections.Specialized; using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http.Extensions; namespace Redmine.Net.Api { diff --git a/src/redmine-net-api/Serialization/IRedmineSerializer.cs b/src/redmine-net-api/Serialization/IRedmineSerializer.cs index 6116b15d..eef94866 100644 --- a/src/redmine-net-api/Serialization/IRedmineSerializer.cs +++ b/src/redmine-net-api/Serialization/IRedmineSerializer.cs @@ -14,6 +14,8 @@ You may obtain a copy of the License at limitations under the License. */ +using Redmine.Net.Api.Common; + namespace Redmine.Net.Api.Serialization { /// diff --git a/src/redmine-net-api/Serialization/Json/Extensions/JsonReaderExtensions.cs b/src/redmine-net-api/Serialization/Json/Extensions/JsonReaderExtensions.cs index 7d490315..8dc4e76e 100644 --- a/src/redmine-net-api/Serialization/Json/Extensions/JsonReaderExtensions.cs +++ b/src/redmine-net-api/Serialization/Json/Extensions/JsonReaderExtensions.cs @@ -18,9 +18,8 @@ limitations under the License. using System.Collections.Generic; using Newtonsoft.Json; using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Serialization; -namespace Redmine.Net.Api.Extensions +namespace Redmine.Net.Api.Serialization.Json.Extensions { /// /// diff --git a/src/redmine-net-api/Serialization/Json/Extensions/JsonWriterExtensions.cs b/src/redmine-net-api/Serialization/Json/Extensions/JsonWriterExtensions.cs index e5f20376..f3df1629 100644 --- a/src/redmine-net-api/Serialization/Json/Extensions/JsonWriterExtensions.cs +++ b/src/redmine-net-api/Serialization/Json/Extensions/JsonWriterExtensions.cs @@ -20,10 +20,11 @@ limitations under the License. using System.Globalization; using System.Text; using Newtonsoft.Json; -using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Common; +using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Types; -namespace Redmine.Net.Api.Extensions +namespace Redmine.Net.Api.Serialization.Json.Extensions { /// /// diff --git a/src/redmine-net-api/Serialization/Json/IJsonSerializable.cs b/src/redmine-net-api/Serialization/Json/IJsonSerializable.cs index af82ab73..bf6b25b7 100644 --- a/src/redmine-net-api/Serialization/Json/IJsonSerializable.cs +++ b/src/redmine-net-api/Serialization/Json/IJsonSerializable.cs @@ -16,7 +16,7 @@ limitations under the License. using Newtonsoft.Json; -namespace Redmine.Net.Api.Serialization +namespace Redmine.Net.Api.Serialization.Json { /// /// diff --git a/src/redmine-net-api/Serialization/Json/JsonObject.cs b/src/redmine-net-api/Serialization/Json/JsonObject.cs index 612b2a10..38e70807 100644 --- a/src/redmine-net-api/Serialization/Json/JsonObject.cs +++ b/src/redmine-net-api/Serialization/Json/JsonObject.cs @@ -18,7 +18,7 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Extensions; -namespace Redmine.Net.Api.Serialization +namespace Redmine.Net.Api.Serialization.Json { /// /// diff --git a/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs b/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs index 9f84f385..cd078537 100644 --- a/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs +++ b/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs @@ -19,8 +19,10 @@ limitations under the License. using System.IO; using System.Text; using Newtonsoft.Json; +using Redmine.Net.Api.Common; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Serialization.Json.Extensions; namespace Redmine.Net.Api.Serialization.Json { diff --git a/src/redmine-net-api/Serialization/Xml/CacheKeyFactory.cs b/src/redmine-net-api/Serialization/Xml/CacheKeyFactory.cs index 3671e595..47cef30d 100644 --- a/src/redmine-net-api/Serialization/Xml/CacheKeyFactory.cs +++ b/src/redmine-net-api/Serialization/Xml/CacheKeyFactory.cs @@ -15,12 +15,11 @@ limitations under the License. */ using System; -using System.Globalization; using System.Text; using System.Xml.Serialization; using Redmine.Net.Api.Extensions; -namespace Redmine.Net.Api.Serialization +namespace Redmine.Net.Api.Serialization.Xml { /// /// The CacheKeyFactory extracts a unique signature diff --git a/src/redmine-net-api/Serialization/Xml/Extensions/XmlReaderExtensions.cs b/src/redmine-net-api/Serialization/Xml/Extensions/XmlReaderExtensions.cs index 8a57a1e6..3342ed05 100644 --- a/src/redmine-net-api/Serialization/Xml/Extensions/XmlReaderExtensions.cs +++ b/src/redmine-net-api/Serialization/Xml/Extensions/XmlReaderExtensions.cs @@ -20,9 +20,9 @@ limitations under the License. using System.IO; using System.Xml; using System.Xml.Serialization; -using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Extensions; -namespace Redmine.Net.Api.Extensions +namespace Redmine.Net.Api.Serialization.Xml.Extensions { /// /// diff --git a/src/redmine-net-api/Serialization/Xml/Extensions/XmlWriterExtensions.cs b/src/redmine-net-api/Serialization/Xml/Extensions/XmlWriterExtensions.cs index 84c5f7cc..b641af40 100644 --- a/src/redmine-net-api/Serialization/Xml/Extensions/XmlWriterExtensions.cs +++ b/src/redmine-net-api/Serialization/Xml/Extensions/XmlWriterExtensions.cs @@ -20,9 +20,11 @@ limitations under the License. using System.Globalization; using System.Xml; using System.Xml.Serialization; +using Redmine.Net.Api.Common; +using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Types; -namespace Redmine.Net.Api.Extensions +namespace Redmine.Net.Api.Serialization.Xml.Extensions { /// /// diff --git a/src/redmine-net-api/Serialization/Xml/IXmlSerializerCache.cs b/src/redmine-net-api/Serialization/Xml/IXmlSerializerCache.cs index bfb5b416..a11e2d33 100644 --- a/src/redmine-net-api/Serialization/Xml/IXmlSerializerCache.cs +++ b/src/redmine-net-api/Serialization/Xml/IXmlSerializerCache.cs @@ -17,7 +17,7 @@ limitations under the License. using System; using System.Xml.Serialization; -namespace Redmine.Net.Api.Serialization +namespace Redmine.Net.Api.Serialization.Xml { internal interface IXmlSerializerCache { diff --git a/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs b/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs index 720c3d25..e86111df 100644 --- a/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs +++ b/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs @@ -18,9 +18,11 @@ limitations under the License. using System.IO; using System.Xml; using System.Xml.Serialization; +using Redmine.Net.Api.Common; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Xml.Extensions; namespace Redmine.Net.Api.Serialization.Xml { diff --git a/src/redmine-net-api/Serialization/Xml/XmlSerializerCache.cs b/src/redmine-net-api/Serialization/Xml/XmlSerializerCache.cs index 9a63c8d0..fcd977ba 100644 --- a/src/redmine-net-api/Serialization/Xml/XmlSerializerCache.cs +++ b/src/redmine-net-api/Serialization/Xml/XmlSerializerCache.cs @@ -19,7 +19,7 @@ limitations under the License. using System.Diagnostics; using System.Xml.Serialization; -namespace Redmine.Net.Api.Serialization +namespace Redmine.Net.Api.Serialization.Xml { /// /// diff --git a/src/redmine-net-api/Serialization/Xml/XmlTextReaderBuilder.cs b/src/redmine-net-api/Serialization/Xml/XmlTextReaderBuilder.cs index dc83be34..6a01e8ab 100644 --- a/src/redmine-net-api/Serialization/Xml/XmlTextReaderBuilder.cs +++ b/src/redmine-net-api/Serialization/Xml/XmlTextReaderBuilder.cs @@ -17,7 +17,7 @@ limitations under the License. using System.IO; using System.Xml; -namespace Redmine.Net.Api.Internals +namespace Redmine.Net.Api.Serialization.Xml { /// /// diff --git a/src/redmine-net-api/Types/Attachment.cs b/src/redmine-net-api/Types/Attachment.cs index 24871731..56afc0c5 100644 --- a/src/redmine-net-api/Types/Attachment.cs +++ b/src/redmine-net-api/Types/Attachment.cs @@ -22,6 +22,9 @@ limitations under the License. using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/Attachments.cs b/src/redmine-net-api/Types/Attachments.cs index 64276c23..c475d531 100644 --- a/src/redmine-net-api/Types/Attachments.cs +++ b/src/redmine-net-api/Types/Attachments.cs @@ -19,6 +19,7 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Serialization.Json; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/ChangeSet.cs b/src/redmine-net-api/Types/ChangeSet.cs index 22d7156c..adfff4fb 100644 --- a/src/redmine-net-api/Types/ChangeSet.cs +++ b/src/redmine-net-api/Types/ChangeSet.cs @@ -24,6 +24,8 @@ limitations under the License. using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Xml.Extensions; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/CustomField.cs b/src/redmine-net-api/Types/CustomField.cs index c0ab41ab..9f6b2b3f 100644 --- a/src/redmine-net-api/Types/CustomField.cs +++ b/src/redmine-net-api/Types/CustomField.cs @@ -22,6 +22,8 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/CustomFieldPossibleValue.cs b/src/redmine-net-api/Types/CustomFieldPossibleValue.cs index 571386d5..56edabd7 100644 --- a/src/redmine-net-api/Types/CustomFieldPossibleValue.cs +++ b/src/redmine-net-api/Types/CustomFieldPossibleValue.cs @@ -22,6 +22,7 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Internals; using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Serialization.Json; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/CustomFieldValue.cs b/src/redmine-net-api/Types/CustomFieldValue.cs index 17f0a442..1dacf6b8 100644 --- a/src/redmine-net-api/Types/CustomFieldValue.cs +++ b/src/redmine-net-api/Types/CustomFieldValue.cs @@ -22,6 +22,7 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Internals; using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Serialization.Json; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/Detail.cs b/src/redmine-net-api/Types/Detail.cs index 8fd71956..4f69af20 100644 --- a/src/redmine-net-api/Types/Detail.cs +++ b/src/redmine-net-api/Types/Detail.cs @@ -22,6 +22,7 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Internals; using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Serialization.Json; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/DocumentCategory.cs b/src/redmine-net-api/Types/DocumentCategory.cs index beda071d..c8935cce 100644 --- a/src/redmine-net-api/Types/DocumentCategory.cs +++ b/src/redmine-net-api/Types/DocumentCategory.cs @@ -21,6 +21,7 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json.Extensions; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/Error.cs b/src/redmine-net-api/Types/Error.cs index 0ad45ae2..f8632707 100644 --- a/src/redmine-net-api/Types/Error.cs +++ b/src/redmine-net-api/Types/Error.cs @@ -22,6 +22,7 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Internals; using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Serialization.Json; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/File.cs b/src/redmine-net-api/Types/File.cs index d218e41b..491f41d9 100644 --- a/src/redmine-net-api/Types/File.cs +++ b/src/redmine-net-api/Types/File.cs @@ -24,6 +24,9 @@ limitations under the License. using System.Xml.Serialization; using Newtonsoft.Json; using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/Group.cs b/src/redmine-net-api/Types/Group.cs index 770ce1ec..3a9fd9e1 100644 --- a/src/redmine-net-api/Types/Group.cs +++ b/src/redmine-net-api/Types/Group.cs @@ -20,9 +20,13 @@ limitations under the License. using System.Xml; using System.Xml.Serialization; using Newtonsoft.Json; +using Redmine.Net.Api.Common; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/GroupUser.cs b/src/redmine-net-api/Types/GroupUser.cs index a1eaf701..4264f82d 100644 --- a/src/redmine-net-api/Types/GroupUser.cs +++ b/src/redmine-net-api/Types/GroupUser.cs @@ -17,6 +17,7 @@ limitations under the License. using System.Diagnostics; using System.Globalization; using System.Xml.Serialization; +using Redmine.Net.Api.Common; using Redmine.Net.Api.Extensions; namespace Redmine.Net.Api.Types diff --git a/src/redmine-net-api/Types/Identifiable.cs b/src/redmine-net-api/Types/Identifiable.cs index 9973b232..42d789a7 100644 --- a/src/redmine-net-api/Types/Identifiable.cs +++ b/src/redmine-net-api/Types/Identifiable.cs @@ -23,6 +23,7 @@ limitations under the License. using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Serialization.Json; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/IdentifiableName.cs b/src/redmine-net-api/Types/IdentifiableName.cs index 27af92d7..f320426f 100644 --- a/src/redmine-net-api/Types/IdentifiableName.cs +++ b/src/redmine-net-api/Types/IdentifiableName.cs @@ -21,6 +21,8 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/Issue.cs b/src/redmine-net-api/Types/Issue.cs index b145bdc4..e6720191 100644 --- a/src/redmine-net-api/Types/Issue.cs +++ b/src/redmine-net-api/Types/Issue.cs @@ -21,9 +21,13 @@ limitations under the License. using System.Xml; using System.Xml.Serialization; using Newtonsoft.Json; +using Redmine.Net.Api.Common; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/IssueAllowedStatus.cs b/src/redmine-net-api/Types/IssueAllowedStatus.cs index d2b78d19..1fa60dba 100644 --- a/src/redmine-net-api/Types/IssueAllowedStatus.cs +++ b/src/redmine-net-api/Types/IssueAllowedStatus.cs @@ -20,6 +20,8 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/IssueCategory.cs b/src/redmine-net-api/Types/IssueCategory.cs index ebb68087..1dbc3ca5 100644 --- a/src/redmine-net-api/Types/IssueCategory.cs +++ b/src/redmine-net-api/Types/IssueCategory.cs @@ -21,6 +21,9 @@ limitations under the License. using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/IssueChild.cs b/src/redmine-net-api/Types/IssueChild.cs index 42e1ed29..a6e9adfe 100644 --- a/src/redmine-net-api/Types/IssueChild.cs +++ b/src/redmine-net-api/Types/IssueChild.cs @@ -22,6 +22,7 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json.Extensions; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/IssueCustomField.cs b/src/redmine-net-api/Types/IssueCustomField.cs index 9b65f9c0..a357cb57 100644 --- a/src/redmine-net-api/Types/IssueCustomField.cs +++ b/src/redmine-net-api/Types/IssueCustomField.cs @@ -21,8 +21,11 @@ limitations under the License. using System.Xml; using System.Xml.Serialization; using Newtonsoft.Json; +using Redmine.Net.Api.Common; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/IssuePriority.cs b/src/redmine-net-api/Types/IssuePriority.cs index b9a5a7a0..51ac6dc3 100644 --- a/src/redmine-net-api/Types/IssuePriority.cs +++ b/src/redmine-net-api/Types/IssuePriority.cs @@ -21,6 +21,7 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json.Extensions; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/IssueRelation.cs b/src/redmine-net-api/Types/IssueRelation.cs index 4f5da762..58e14a5f 100644 --- a/src/redmine-net-api/Types/IssueRelation.cs +++ b/src/redmine-net-api/Types/IssueRelation.cs @@ -23,6 +23,9 @@ limitations under the License. using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/IssueStatus.cs b/src/redmine-net-api/Types/IssueStatus.cs index 8478746e..08c7553a 100644 --- a/src/redmine-net-api/Types/IssueStatus.cs +++ b/src/redmine-net-api/Types/IssueStatus.cs @@ -21,6 +21,8 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/Journal.cs b/src/redmine-net-api/Types/Journal.cs index ee50b101..f3f989e0 100644 --- a/src/redmine-net-api/Types/Journal.cs +++ b/src/redmine-net-api/Types/Journal.cs @@ -23,6 +23,8 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/Membership.cs b/src/redmine-net-api/Types/Membership.cs index a33c97ef..9496a1c5 100644 --- a/src/redmine-net-api/Types/Membership.cs +++ b/src/redmine-net-api/Types/Membership.cs @@ -21,6 +21,8 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/MembershipRole.cs b/src/redmine-net-api/Types/MembershipRole.cs index 7a01826a..fcfab110 100644 --- a/src/redmine-net-api/Types/MembershipRole.cs +++ b/src/redmine-net-api/Types/MembershipRole.cs @@ -20,8 +20,11 @@ limitations under the License. using System.Xml; using System.Xml.Serialization; using Newtonsoft.Json; +using Redmine.Net.Api.Common; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/MyAccount.cs b/src/redmine-net-api/Types/MyAccount.cs index 944a9d32..570c5d67 100644 --- a/src/redmine-net-api/Types/MyAccount.cs +++ b/src/redmine-net-api/Types/MyAccount.cs @@ -23,6 +23,9 @@ limitations under the License. using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/MyAccountCustomField.cs b/src/redmine-net-api/Types/MyAccountCustomField.cs index 469e198e..3015296a 100644 --- a/src/redmine-net-api/Types/MyAccountCustomField.cs +++ b/src/redmine-net-api/Types/MyAccountCustomField.cs @@ -21,6 +21,7 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json.Extensions; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/News.cs b/src/redmine-net-api/Types/News.cs index 4701ceb3..6e830f72 100644 --- a/src/redmine-net-api/Types/News.cs +++ b/src/redmine-net-api/Types/News.cs @@ -23,6 +23,9 @@ limitations under the License. using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/Permission.cs b/src/redmine-net-api/Types/Permission.cs index 06811090..7612ecef 100644 --- a/src/redmine-net-api/Types/Permission.cs +++ b/src/redmine-net-api/Types/Permission.cs @@ -22,6 +22,7 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Internals; using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Serialization.Json; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs index 03393172..5859eb9a 100644 --- a/src/redmine-net-api/Types/Project.cs +++ b/src/redmine-net-api/Types/Project.cs @@ -20,9 +20,13 @@ limitations under the License. using System.Xml; using System.Xml.Serialization; using Newtonsoft.Json; +using Redmine.Net.Api.Common; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/ProjectEnabledModule.cs b/src/redmine-net-api/Types/ProjectEnabledModule.cs index 343b6006..eb91f016 100644 --- a/src/redmine-net-api/Types/ProjectEnabledModule.cs +++ b/src/redmine-net-api/Types/ProjectEnabledModule.cs @@ -17,6 +17,7 @@ limitations under the License. using System; using System.Diagnostics; using System.Xml.Serialization; +using Redmine.Net.Api.Common; using Redmine.Net.Api.Extensions; namespace Redmine.Net.Api.Types diff --git a/src/redmine-net-api/Types/ProjectMembership.cs b/src/redmine-net-api/Types/ProjectMembership.cs index cc9ae373..6e835b15 100644 --- a/src/redmine-net-api/Types/ProjectMembership.cs +++ b/src/redmine-net-api/Types/ProjectMembership.cs @@ -22,6 +22,9 @@ limitations under the License. using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/ProjectTracker.cs b/src/redmine-net-api/Types/ProjectTracker.cs index d77b5230..e5fc918d 100644 --- a/src/redmine-net-api/Types/ProjectTracker.cs +++ b/src/redmine-net-api/Types/ProjectTracker.cs @@ -17,6 +17,7 @@ limitations under the License. using System.Diagnostics; using System.Globalization; using System.Xml.Serialization; +using Redmine.Net.Api.Common; using Redmine.Net.Api.Extensions; namespace Redmine.Net.Api.Types diff --git a/src/redmine-net-api/Types/Query.cs b/src/redmine-net-api/Types/Query.cs index f0e51d8a..d77f821d 100644 --- a/src/redmine-net-api/Types/Query.cs +++ b/src/redmine-net-api/Types/Query.cs @@ -21,6 +21,8 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/Role.cs b/src/redmine-net-api/Types/Role.cs index 7a4718a5..3d5903ba 100644 --- a/src/redmine-net-api/Types/Role.cs +++ b/src/redmine-net-api/Types/Role.cs @@ -22,6 +22,8 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/Search.cs b/src/redmine-net-api/Types/Search.cs index 2397f7fa..0dec7001 100644 --- a/src/redmine-net-api/Types/Search.cs +++ b/src/redmine-net-api/Types/Search.cs @@ -23,6 +23,9 @@ limitations under the License. using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/TimeEntry.cs b/src/redmine-net-api/Types/TimeEntry.cs index e8c147c9..b4fcb3ad 100644 --- a/src/redmine-net-api/Types/TimeEntry.cs +++ b/src/redmine-net-api/Types/TimeEntry.cs @@ -24,6 +24,9 @@ limitations under the License. using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; namespace Redmine.Net.Api.Types diff --git a/src/redmine-net-api/Types/TimeEntryActivity.cs b/src/redmine-net-api/Types/TimeEntryActivity.cs index 42f31826..2e25bc03 100644 --- a/src/redmine-net-api/Types/TimeEntryActivity.cs +++ b/src/redmine-net-api/Types/TimeEntryActivity.cs @@ -21,6 +21,7 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json.Extensions; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/Tracker.cs b/src/redmine-net-api/Types/Tracker.cs index f13d8f6b..7f77249f 100644 --- a/src/redmine-net-api/Types/Tracker.cs +++ b/src/redmine-net-api/Types/Tracker.cs @@ -22,6 +22,8 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/TrackerCoreField.cs b/src/redmine-net-api/Types/TrackerCoreField.cs index a91c25e6..ec9e9360 100644 --- a/src/redmine-net-api/Types/TrackerCoreField.cs +++ b/src/redmine-net-api/Types/TrackerCoreField.cs @@ -6,6 +6,7 @@ using Newtonsoft.Json; using Redmine.Net.Api.Internals; using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Serialization.Json; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/TrackerCustomField.cs b/src/redmine-net-api/Types/TrackerCustomField.cs index f4250d4b..6dd1d213 100644 --- a/src/redmine-net-api/Types/TrackerCustomField.cs +++ b/src/redmine-net-api/Types/TrackerCustomField.cs @@ -19,6 +19,8 @@ limitations under the License. using System.Xml.Serialization; using Newtonsoft.Json; using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/Upload.cs b/src/redmine-net-api/Types/Upload.cs index 616a8868..e2815836 100644 --- a/src/redmine-net-api/Types/Upload.cs +++ b/src/redmine-net-api/Types/Upload.cs @@ -23,6 +23,8 @@ limitations under the License. using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Json.Extensions; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/User.cs b/src/redmine-net-api/Types/User.cs index fb5c71c1..ffeb7fd1 100644 --- a/src/redmine-net-api/Types/User.cs +++ b/src/redmine-net-api/Types/User.cs @@ -22,7 +22,9 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/Version.cs b/src/redmine-net-api/Types/Version.cs index 2b828c3d..1a43fd91 100644 --- a/src/redmine-net-api/Types/Version.cs +++ b/src/redmine-net-api/Types/Version.cs @@ -23,6 +23,9 @@ limitations under the License. using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; namespace Redmine.Net.Api.Types { diff --git a/src/redmine-net-api/Types/Watcher.cs b/src/redmine-net-api/Types/Watcher.cs index 2d6b7798..a90913c2 100644 --- a/src/redmine-net-api/Types/Watcher.cs +++ b/src/redmine-net-api/Types/Watcher.cs @@ -18,6 +18,7 @@ limitations under the License. using System.Diagnostics; using System.Globalization; using System.Xml.Serialization; +using Redmine.Net.Api.Common; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; diff --git a/src/redmine-net-api/Types/WikiPage.cs b/src/redmine-net-api/Types/WikiPage.cs index 44d2a8a0..75d64466 100644 --- a/src/redmine-net-api/Types/WikiPage.cs +++ b/src/redmine-net-api/Types/WikiPage.cs @@ -23,6 +23,9 @@ limitations under the License. using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Serialization.Json; +using Redmine.Net.Api.Serialization.Json.Extensions; +using Redmine.Net.Api.Serialization.Xml.Extensions; namespace Redmine.Net.Api.Types { diff --git a/tests/redmine-net-api.Tests/Bugs/RedmineApi-371.cs b/tests/redmine-net-api.Tests/Bugs/RedmineApi-371.cs index b145b739..2795f6c6 100644 --- a/tests/redmine-net-api.Tests/Bugs/RedmineApi-371.cs +++ b/tests/redmine-net-api.Tests/Bugs/RedmineApi-371.cs @@ -2,7 +2,7 @@ using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; using Redmine.Net.Api; using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Net; +using Redmine.Net.Api.Http; using Redmine.Net.Api.Types; using Xunit; diff --git a/tests/redmine-net-api.Tests/Tests/HostTests.cs b/tests/redmine-net-api.Tests/Tests/HostTests.cs index 89943a7c..d2b64bfc 100644 --- a/tests/redmine-net-api.Tests/Tests/HostTests.cs +++ b/tests/redmine-net-api.Tests/Tests/HostTests.cs @@ -1,6 +1,6 @@ using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Order; -using Redmine.Net.Api; using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Options; using Xunit; namespace Padi.DotNet.RedmineAPI.Tests.Tests diff --git a/tests/redmine-net-api.Tests/Tests/RedmineApiUrlsTests.cs b/tests/redmine-net-api.Tests/Tests/RedmineApiUrlsTests.cs index 35ffd7cc..6096164c 100644 --- a/tests/redmine-net-api.Tests/Tests/RedmineApiUrlsTests.cs +++ b/tests/redmine-net-api.Tests/Tests/RedmineApiUrlsTests.cs @@ -2,7 +2,7 @@ using Padi.DotNet.RedmineAPI.Tests.Infrastructure.Fixtures; using Redmine.Net.Api; using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Net; +using Redmine.Net.Api.Http; using Redmine.Net.Api.Net.Internal; using Redmine.Net.Api.Types; using Xunit; From 29591587df34bcdddca3df2211b55d7c1bc0645a Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 28 May 2025 16:50:47 +0300 Subject: [PATCH 526/549] Extract host validation logic to HostHelper --- src/redmine-net-api/Internals/HostHelper.cs | 172 ++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 src/redmine-net-api/Internals/HostHelper.cs diff --git a/src/redmine-net-api/Internals/HostHelper.cs b/src/redmine-net-api/Internals/HostHelper.cs new file mode 100644 index 00000000..e5a0fe0e --- /dev/null +++ b/src/redmine-net-api/Internals/HostHelper.cs @@ -0,0 +1,172 @@ +using System; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; + +namespace Redmine.Net.Api.Internals; + +internal static class HostHelper +{ + private static readonly char[] DotCharArray = ['.']; + + internal static void EnsureDomainNameIsValid(string domainName) + { + if (domainName.IsNullOrWhiteSpace()) + { + throw new RedmineException("Domain name cannot be null or empty."); + } + + if (domainName.Length > 255) + { + throw new RedmineException("Domain name cannot be longer than 255 characters."); + } + + var labels = domainName.Split(DotCharArray); + if (labels.Length == 1) + { + throw new RedmineException("Domain name is not valid."); + } + + foreach (var label in labels) + { + if (label.IsNullOrWhiteSpace() || label.Length > 63) + { + throw new RedmineException("Domain name must be between 1 and 63 characters."); + } + + if (!char.IsLetterOrDigit(label[0]) || !char.IsLetterOrDigit(label[label.Length - 1])) + { + throw new RedmineException("Domain name label starts or ends with a hyphen or invalid character."); + } + + for (var index = 0; index < label.Length; index++) + { + var ch = label[index]; + + if (!char.IsLetterOrDigit(ch) && ch != '-') + { + throw new RedmineException("Domain name contains an invalid character."); + } + + if (ch == '-' && index + 1 < label.Length && label[index + 1] == '-') + { + throw new RedmineException("Domain name contains consecutive hyphens."); + } + } + } + } + + internal static Uri CreateRedmineUri(string host, string scheme = null) + { + if (host.IsNullOrWhiteSpace()) + { + throw new RedmineException("The host is null or empty."); + } + + if (!Uri.TryCreate(host, UriKind.Absolute, out var uri)) + { + host = host.TrimEnd('/', '\\'); + EnsureDomainNameIsValid(host); + + if (!host.StartsWith(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) || + !host.StartsWith(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) + { + host = $"{scheme ?? Uri.UriSchemeHttps}://{host}"; + + if (!Uri.TryCreate(host, UriKind.Absolute, out uri)) + { + throw new RedmineException("The host is not valid."); + } + } + } + + if (!uri.IsWellFormedOriginalString()) + { + throw new RedmineException("The host is not well-formed."); + } + + scheme ??= Uri.UriSchemeHttps; + var hasScheme = false; + if (!uri.Scheme.IsNullOrWhiteSpace()) + { + if (uri.Host.IsNullOrWhiteSpace() && uri.IsAbsoluteUri && !uri.IsFile) + { + if (uri.Scheme.Equals("localhost", StringComparison.OrdinalIgnoreCase)) + { + int port = 0; + var portAsString = uri.AbsolutePath.RemoveTrailingSlash(); + if (!portAsString.IsNullOrWhiteSpace()) + { + int.TryParse(portAsString, out port); + } + + var ub = new UriBuilder(scheme, "localhost", port); + return ub.Uri; + } + } + else + { + if (!IsSchemaHttpOrHttps(uri.Scheme)) + { + throw new RedmineException("Invalid host scheme. Only HTTP and HTTPS are supported."); + } + + hasScheme = true; + } + } + else + { + if (!IsSchemaHttpOrHttps(scheme)) + { + throw new RedmineException("Invalid host scheme. Only HTTP and HTTPS are supported."); + } + } + + var uriBuilder = new UriBuilder(); + + if (uri.HostNameType == UriHostNameType.IPv6) + { + uriBuilder.Scheme = (hasScheme ? uri.Scheme : scheme ?? Uri.UriSchemeHttps); + uriBuilder.Host = uri.Host; + } + else + { + if (uri.Authority.IsNullOrWhiteSpace()) + { + if (uri.Port == -1) + { + if (int.TryParse(uri.LocalPath, out var port)) + { + uriBuilder.Port = port; + } + } + + uriBuilder.Scheme = scheme ?? Uri.UriSchemeHttps; + uriBuilder.Host = uri.Scheme; + } + else + { + uriBuilder.Scheme = uri.Scheme; + uriBuilder.Port = int.TryParse(uri.LocalPath, out var port) ? port : uri.Port; + uriBuilder.Host = uri.Host; + if (!uri.LocalPath.IsNullOrWhiteSpace() && !uri.LocalPath.Contains(".")) + { + uriBuilder.Path = uri.LocalPath; + } + } + } + + try + { + return uriBuilder.Uri; + } + catch (Exception ex) + { + throw new RedmineException($"Failed to create Redmine URI: {ex.Message}", ex); + } + } + + private static bool IsSchemaHttpOrHttps(string scheme) + { + return scheme == Uri.UriSchemeHttp || scheme == Uri.UriSchemeHttps; + } +} \ No newline at end of file From d911d9ca6084359a93a93a0e04a918fdafd7fef1 Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 28 May 2025 16:51:47 +0300 Subject: [PATCH 527/549] Enums ToLowerName --- src/redmine-net-api/Types/IssueRelation.cs | 4 ++-- src/redmine-net-api/Types/Version.cs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/redmine-net-api/Types/IssueRelation.cs b/src/redmine-net-api/Types/IssueRelation.cs index 58e14a5f..62ddfeea 100644 --- a/src/redmine-net-api/Types/IssueRelation.cs +++ b/src/redmine-net-api/Types/IssueRelation.cs @@ -116,7 +116,7 @@ public override void WriteXml(XmlWriter writer) AssertValidIssueRelationType(); writer.WriteElementString(RedmineKeys.ISSUE_TO_ID, IssueToId.ToInvariantString()); - writer.WriteElementString(RedmineKeys.RELATION_TYPE, Type.ToLowerInvariant()); + writer.WriteElementString(RedmineKeys.RELATION_TYPE, Type.ToLowerName()); if (Type == IssueRelationType.Precedes || Type == IssueRelationType.Follows) { @@ -137,7 +137,7 @@ public override void WriteJson(JsonWriter writer) using (new JsonObject(writer, RedmineKeys.RELATION)) { writer.WriteProperty(RedmineKeys.ISSUE_TO_ID, IssueToId); - writer.WriteProperty(RedmineKeys.RELATION_TYPE, Type.ToLowerInvariant()); + writer.WriteProperty(RedmineKeys.RELATION_TYPE, Type.ToLowerName()); if (Type == IssueRelationType.Precedes || Type == IssueRelationType.Follows) { diff --git a/src/redmine-net-api/Types/Version.cs b/src/redmine-net-api/Types/Version.cs index 1a43fd91..588c81f1 100644 --- a/src/redmine-net-api/Types/Version.cs +++ b/src/redmine-net-api/Types/Version.cs @@ -148,10 +148,10 @@ public override void ReadXml(XmlReader reader) public override void WriteXml(XmlWriter writer) { writer.WriteElementString(RedmineKeys.NAME, Name); - writer.WriteElementString(RedmineKeys.STATUS, Status.ToLowerInvariant()); + writer.WriteElementString(RedmineKeys.STATUS, Status.ToLowerName()); if (Sharing != VersionSharing.Unknown) { - writer.WriteElementString(RedmineKeys.SHARING, Sharing.ToLowerInvariant()); + writer.WriteElementString(RedmineKeys.SHARING, Sharing.ToLowerName()); } writer.WriteDateOrEmpty(RedmineKeys.DUE_DATE, DueDate); @@ -224,8 +224,8 @@ public override void WriteJson(JsonWriter writer) using (new JsonObject(writer, RedmineKeys.VERSION)) { writer.WriteProperty(RedmineKeys.NAME, Name); - writer.WriteProperty(RedmineKeys.STATUS, Status.ToLowerInvariant()); - writer.WriteProperty(RedmineKeys.SHARING, Sharing.ToLowerInvariant()); + writer.WriteProperty(RedmineKeys.STATUS, Status.ToLowerName()); + writer.WriteProperty(RedmineKeys.SHARING, Sharing.ToLowerName()); writer.WriteProperty(RedmineKeys.DESCRIPTION, Description); writer.WriteDateOrEmpty(RedmineKeys.DUE_DATE, DueDate); if (CustomFields != null) From 433e22a8c4515a1ff091381c13582eaf8c354c97 Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 28 May 2025 16:52:27 +0300 Subject: [PATCH 528/549] Remove logger extensions --- .../Logging/LoggerFactoryExtensions.cs | 27 ----------- .../Logging/LoggingBuilderExtensions.cs | 46 ------------------- .../Logging/RedmineLoggingOptions.cs | 5 -- 3 files changed, 78 deletions(-) delete mode 100644 src/redmine-net-api/Logging/LoggerFactoryExtensions.cs delete mode 100644 src/redmine-net-api/Logging/LoggingBuilderExtensions.cs diff --git a/src/redmine-net-api/Logging/LoggerFactoryExtensions.cs b/src/redmine-net-api/Logging/LoggerFactoryExtensions.cs deleted file mode 100644 index 62ee317b..00000000 --- a/src/redmine-net-api/Logging/LoggerFactoryExtensions.cs +++ /dev/null @@ -1,27 +0,0 @@ - -#if NET462_OR_GREATER || NETCOREAPP - -using Microsoft.Extensions.Logging; - -namespace Redmine.Net.Api.Logging; - -/// -/// -/// -public static class LoggerFactoryExtensions -{ - /// - /// Creates a Redmine logger from the Microsoft ILoggerFactory - /// - public static IRedmineLogger CreateRedmineLogger(this ILoggerFactory factory, string categoryName = "Redmine.Api") - { - if (factory == null) - { - return RedmineNullLogger.Instance; - } - - var logger = factory.CreateLogger(categoryName); - return RedmineLoggerFactory.CreateMicrosoftLogger(logger); - } -} -#endif diff --git a/src/redmine-net-api/Logging/LoggingBuilderExtensions.cs b/src/redmine-net-api/Logging/LoggingBuilderExtensions.cs deleted file mode 100644 index 18650d89..00000000 --- a/src/redmine-net-api/Logging/LoggingBuilderExtensions.cs +++ /dev/null @@ -1,46 +0,0 @@ -#if NET462_OR_GREATER || NETCOREAPP -using System; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace Redmine.Net.Api.Logging; - -/// -/// -/// -public static class LoggingBuilderExtensions -{ - /// - /// Adds a RedmineLogger provider to the DI container - /// - public static ILoggingBuilder AddRedmineLogger(this ILoggingBuilder builder, IRedmineLogger redmineLogger) - { - if (builder == null) throw new ArgumentNullException(nameof(builder)); - if (redmineLogger == null) throw new ArgumentNullException(nameof(redmineLogger)); - - builder.Services.AddSingleton(redmineLogger); - return builder; - } - - /// - /// Configures Redmine logging options - /// - public static ILoggingBuilder ConfigureRedmineLogging(this ILoggingBuilder builder, Action configure) - { - if (builder == null) throw new ArgumentNullException(nameof(builder)); - if (configure == null) throw new ArgumentNullException(nameof(configure)); - - var options = new RedmineLoggingOptions(); - configure(options); - - builder.Services.AddSingleton(options); - - return builder; - } -} - -#endif - - - - diff --git a/src/redmine-net-api/Logging/RedmineLoggingOptions.cs b/src/redmine-net-api/Logging/RedmineLoggingOptions.cs index 5ff0653a..18af7ae0 100644 --- a/src/redmine-net-api/Logging/RedmineLoggingOptions.cs +++ b/src/redmine-net-api/Logging/RedmineLoggingOptions.cs @@ -10,11 +10,6 @@ public sealed class RedmineLoggingOptions /// public LogLevel MinimumLevel { get; set; } = LogLevel.Information; - /// - /// Gets or sets whether detailed API request/response logging is enabled - /// - public bool EnableVerboseLogging { get; set; } - /// /// Gets or sets whether to include HTTP request/response details in logs /// From 4a36057637d6c3d49392057cb2a6eabd120a50ba Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 28 May 2025 16:54:07 +0300 Subject: [PATCH 529/549] Remove escape uri --- .../Extensions/RedmineManagerExtensions.cs | 65 ++++++------------- 1 file changed, 19 insertions(+), 46 deletions(-) diff --git a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs index 915e3a77..71ebcf13 100644 --- a/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs +++ b/src/redmine-net-api/Extensions/RedmineManagerExtensions.cs @@ -17,14 +17,14 @@ limitations under the License. using System; using System.Collections.Generic; using System.Collections.Specialized; -using System.Globalization; using Redmine.Net.Api.Common; #if !(NET20) using System.Threading; using System.Threading.Tasks; #endif using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Net; +using Redmine.Net.Api.Http; +using Redmine.Net.Api.Http.Extensions; using Redmine.Net.Api.Net.Internal; using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Types; @@ -47,9 +47,7 @@ public static void ArchiveProject(this RedmineManager redmineManager, string pro { var uri = redmineManager.RedmineApiUrls.ProjectArchive(projectIdentifier); - var escapedUri = Uri.EscapeDataString(uri); - - redmineManager.ApiClient.Update(escapedUri, string.Empty ,requestOptions); + redmineManager.ApiClient.Update(uri, string.Empty ,requestOptions); } /// @@ -63,9 +61,7 @@ public static void UnarchiveProject(this RedmineManager redmineManager, string p { var uri = redmineManager.RedmineApiUrls.ProjectUnarchive(projectIdentifier); - var escapedUri = Uri.EscapeDataString(uri); - - redmineManager.ApiClient.Update(escapedUri, string.Empty ,requestOptions); + redmineManager.ApiClient.Update(uri, string.Empty ,requestOptions); } /// @@ -79,9 +75,7 @@ public static void ReopenProject(this RedmineManager redmineManager, string proj { var uri = redmineManager.RedmineApiUrls.ProjectReopen(projectIdentifier); - var escapedUri = Uri.EscapeDataString(uri); - - redmineManager.ApiClient.Update(escapedUri, string.Empty ,requestOptions); + redmineManager.ApiClient.Update(uri, string.Empty ,requestOptions); } /// @@ -95,9 +89,7 @@ public static void CloseProject(this RedmineManager redmineManager, string proje { var uri = redmineManager.RedmineApiUrls.ProjectClose(projectIdentifier); - var escapedUri = Uri.EscapeDataString(uri); - - redmineManager.ApiClient.Update(escapedUri,string.Empty, requestOptions); + redmineManager.ApiClient.Update(uri,string.Empty, requestOptions); } /// @@ -114,9 +106,7 @@ public static void ProjectRepositoryAddRelatedIssue(this RedmineManager redmineM { var uri = redmineManager.RedmineApiUrls.ProjectRepositoryAddRelatedIssue(projectIdentifier, repositoryIdentifier, revision); - var escapedUri = Uri.EscapeDataString(uri); - - _ = redmineManager.ApiClient.Create(escapedUri,string.Empty, requestOptions); + _ = redmineManager.ApiClient.Create(uri,string.Empty, requestOptions); } /// @@ -134,9 +124,7 @@ public static void ProjectRepositoryRemoveRelatedIssue(this RedmineManager redmi { var uri = redmineManager.RedmineApiUrls.ProjectRepositoryRemoveRelatedIssue(projectIdentifier, repositoryIdentifier, revision, issueIdentifier); - var escapedUri = Uri.EscapeDataString(uri); - - _ = redmineManager.ApiClient.Delete(escapedUri, requestOptions); + _ = redmineManager.ApiClient.Delete(uri, requestOptions); } /// @@ -151,9 +139,7 @@ public static PagedResults GetProjectNews(this RedmineManager redmineManag { var uri = redmineManager.RedmineApiUrls.ProjectNews(projectIdentifier); - var escapedUri = Uri.EscapeDataString(uri); - - var response = redmineManager.GetPaginatedInternal(escapedUri, requestOptions); + var response = redmineManager.GetPaginatedInternal(uri, requestOptions); return response; } @@ -364,9 +350,7 @@ public static WikiPage CreateWikiPage(this RedmineManager redmineManager, string var uri = redmineManager.RedmineApiUrls.ProjectWikiPageUpdate(projectId, pageName); - var escapedUri = Uri.EscapeDataString(uri); - - var response = redmineManager.ApiClient.Update(escapedUri, payload, requestOptions); + var response = redmineManager.ApiClient.Update(uri, payload, requestOptions); return response.DeserializeTo(redmineManager.Serializer); } @@ -500,9 +484,7 @@ public static async Task ArchiveProjectAsync(this RedmineManager redmineManager, { var uri = redmineManager.RedmineApiUrls.ProjectArchive(projectIdentifier); - var escapedUri = Uri.EscapeDataString(uri); - - await redmineManager.ApiClient.DeleteAsync(escapedUri, requestOptions, cancellationToken).ConfigureAwait(false); + await redmineManager.ApiClient.DeleteAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); } /// @@ -516,9 +498,7 @@ public static async Task UnarchiveProjectAsync(this RedmineManager redmineManage { var uri = redmineManager.RedmineApiUrls.ProjectUnarchive(projectIdentifier); - var escapedUri = Uri.EscapeDataString(uri); - - await redmineManager.ApiClient.DeleteAsync(escapedUri, requestOptions, cancellationToken).ConfigureAwait(false); + await redmineManager.ApiClient.DeleteAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); } /// @@ -532,9 +512,7 @@ public static async Task CloseProjectAsync(this RedmineManager redmineManager, s { var uri = redmineManager.RedmineApiUrls.ProjectClose(projectIdentifier); - var escapedUri = Uri.EscapeDataString(uri); - - await redmineManager.ApiClient.UpdateAsync(escapedUri, string.Empty, requestOptions, cancellationToken).ConfigureAwait(false); + await redmineManager.ApiClient.UpdateAsync(uri, string.Empty, requestOptions, cancellationToken).ConfigureAwait(false); } /// @@ -548,9 +526,7 @@ public static async Task ReopenProjectAsync(this RedmineManager redmineManager, { var uri = redmineManager.RedmineApiUrls.ProjectReopen(projectIdentifier); - var escapedUri = Uri.EscapeDataString(uri); - - await redmineManager.ApiClient.UpdateAsync(escapedUri, string.Empty, requestOptions, cancellationToken).ConfigureAwait(false); + await redmineManager.ApiClient.UpdateAsync(uri, string.Empty, requestOptions, cancellationToken).ConfigureAwait(false); } /// @@ -566,9 +542,7 @@ public static async Task ProjectRepositoryAddRelatedIssueAsync(this RedmineManag { var uri = redmineManager.RedmineApiUrls.ProjectRepositoryAddRelatedIssue(projectIdentifier, repositoryIdentifier, revision); - var escapedUri = Uri.EscapeDataString(uri); - - await redmineManager.ApiClient.CreateAsync(escapedUri, string.Empty ,requestOptions, cancellationToken).ConfigureAwait(false); + await redmineManager.ApiClient.CreateAsync(uri, string.Empty ,requestOptions, cancellationToken).ConfigureAwait(false); } /// @@ -585,9 +559,7 @@ public static async Task ProjectRepositoryRemoveRelatedIssueAsync(this RedmineMa { var uri = redmineManager.RedmineApiUrls.ProjectRepositoryRemoveRelatedIssue(projectIdentifier, repositoryIdentifier, revision, issueIdentifier); - var escapedUri = Uri.EscapeDataString(uri); - - await redmineManager.ApiClient.DeleteAsync(escapedUri, requestOptions, cancellationToken).ConfigureAwait(false); + await redmineManager.ApiClient.DeleteAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); } /// @@ -686,7 +658,7 @@ public static async Task> GetProjectFilesAsync(this RedmineMa /// public static async Task> SearchAsync(this RedmineManager redmineManager, string q, - int limit = RedmineManager.DEFAULT_PAGE_SIZE_VALUE, + int limit = RedmineConstants.DEFAULT_PAGE_SIZE_VALUE, int offset = 0, SearchFilterBuilder searchFilter = null, CancellationToken cancellationToken = default) @@ -839,7 +811,8 @@ public static async Task> GetAllWikiPagesAsync(this RedmineManage var response = await redmineManager.ApiClient.GetPagedAsync(uri, requestOptions, cancellationToken).ConfigureAwait(false); - return response.DeserializeToList(redmineManager.Serializer); + var pages = response.DeserializeToPagedResults(redmineManager.Serializer); + return pages.Items as List; } /// From 4e5450f182ac0e989d25ecdfc71afb29e2a1ebaa Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 28 May 2025 16:57:12 +0300 Subject: [PATCH 530/549] Project CustomFieldsValues & IssueCustomFields --- src/redmine-net-api/Types/Project.cs | 6 ++++-- tests/redmine-net-api.Tests/Equality/ProjectTests.cs | 10 ++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs index 5859eb9a..4ac93f9b 100644 --- a/src/redmine-net-api/Types/Project.cs +++ b/src/redmine-net-api/Types/Project.cs @@ -327,7 +327,8 @@ public bool Equals(Project other) && DefaultVersion == other.DefaultVersion && Parent == other.Parent && (Trackers?.Equals(other.Trackers) ?? other.Trackers == null) - && (CustomFields?.Equals(other.CustomFields) ?? other.CustomFields == null) + && (IssueCustomFields?.Equals(other.IssueCustomFields) ?? other.IssueCustomFields == null) + && (CustomFieldValues?.Equals(other.CustomFieldValues) ?? other.CustomFieldValues == null) && (IssueCategories?.Equals(other.IssueCategories) ?? other.IssueCategories == null) && (EnabledModules?.Equals(other.EnabledModules) ?? other.EnabledModules == null) && (TimeEntryActivities?.Equals(other.TimeEntryActivities) ?? other.TimeEntryActivities == null); @@ -365,7 +366,8 @@ public override int GetHashCode() hashCode = HashCodeHelper.GetHashCode(IsPublic, hashCode); hashCode = HashCodeHelper.GetHashCode(InheritMembers, hashCode); hashCode = HashCodeHelper.GetHashCode(Trackers, hashCode); - hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); + hashCode = HashCodeHelper.GetHashCode(IssueCustomFields, hashCode); + hashCode = HashCodeHelper.GetHashCode(CustomFieldValues, hashCode); hashCode = HashCodeHelper.GetHashCode(IssueCategories, hashCode); hashCode = HashCodeHelper.GetHashCode(EnabledModules, hashCode); hashCode = HashCodeHelper.GetHashCode(TimeEntryActivities, hashCode); diff --git a/tests/redmine-net-api.Tests/Equality/ProjectTests.cs b/tests/redmine-net-api.Tests/Equality/ProjectTests.cs index a8aff18a..c09cb3c6 100644 --- a/tests/redmine-net-api.Tests/Equality/ProjectTests.cs +++ b/tests/redmine-net-api.Tests/Equality/ProjectTests.cs @@ -27,7 +27,7 @@ protected override Project CreateSampleInstance() new() { Id = 2, Name = "Feature" } ], - CustomFields = + CustomFieldValues = [ new() { Id = 1, Name = "Field1"}, new() { Id = 2, Name = "Field2"} @@ -47,7 +47,8 @@ protected override Project CreateSampleInstance() [ new() { Id = 1, Name = "Activity1" }, new() { Id = 2, Name = "Activity2" } - ] + ], + IssueCustomFields = [IssueCustomField.CreateSingle(1, "SingleCustomField", "SingleCustomFieldValue")] }; } @@ -69,7 +70,7 @@ protected override Project CreateDifferentInstance() [ new() { Id = 3, Name = "Different Bug" } ], - CustomFields = + CustomFieldValues = [ new() { Id = 3, Name = "DifferentField"} ], @@ -84,7 +85,8 @@ protected override Project CreateDifferentInstance() TimeEntryActivities = [ new() { Id = 3, Name = "DifferentActivity" } - ] + ], + IssueCustomFields = [IssueCustomField.CreateSingle(1, "DifferentSingleCustomField", "DifferentSingleCustomFieldValue")] }; } } \ No newline at end of file From 2ac90024830bbb204700583191be510772104087 Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 28 May 2025 17:06:02 +0300 Subject: [PATCH 531/549] HttpClient --- Directory.Packages.props | 12 +- .../Clients/HttpClient/HttpClientProvider.cs | 257 ++++++++++ .../HttpClient/HttpContentExtensions.cs | 20 + .../HttpClient/HttpContentPolyfills.cs | 35 ++ .../HttpResponseHeadersExtensions.cs | 22 + .../HttpClient/IRedmineHttpClientOptions.cs | 88 ++++ .../InternalRedmineApiHttpClient.Async.cs | 144 ++++++ .../InternalRedmineApiHttpClient.cs | 170 +++++++ .../HttpClient/RedmineHttpClientOptions.cs | 75 +++ .../Http/Helpers/RedmineHttpMethodHelper.cs | 63 +++ .../{ => Options}/RedmineManagerOptions.cs | 48 +- .../Options/RedmineManagerOptionsBuilder.cs | 306 ++++++++++++ src/redmine-net-api/RedmineManager.cs | 13 +- .../RedmineManagerOptionsBuilder.cs | 464 ------------------ src/redmine-net-api/redmine-net-api.csproj | 12 + 15 files changed, 1250 insertions(+), 479 deletions(-) create mode 100644 src/redmine-net-api/Http/Clients/HttpClient/HttpClientProvider.cs create mode 100644 src/redmine-net-api/Http/Clients/HttpClient/HttpContentExtensions.cs create mode 100644 src/redmine-net-api/Http/Clients/HttpClient/HttpContentPolyfills.cs create mode 100644 src/redmine-net-api/Http/Clients/HttpClient/HttpResponseHeadersExtensions.cs create mode 100644 src/redmine-net-api/Http/Clients/HttpClient/IRedmineHttpClientOptions.cs create mode 100644 src/redmine-net-api/Http/Clients/HttpClient/InternalRedmineApiHttpClient.Async.cs create mode 100644 src/redmine-net-api/Http/Clients/HttpClient/InternalRedmineApiHttpClient.cs create mode 100644 src/redmine-net-api/Http/Clients/HttpClient/RedmineHttpClientOptions.cs create mode 100644 src/redmine-net-api/Http/Helpers/RedmineHttpMethodHelper.cs rename src/redmine-net-api/{ => Options}/RedmineManagerOptions.cs (73%) create mode 100644 src/redmine-net-api/Options/RedmineManagerOptionsBuilder.cs delete mode 100644 src/redmine-net-api/RedmineManagerOptionsBuilder.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index f1465d5b..d5faa965 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,10 +1,12 @@ - |net45|net451|net452|net46|net461| + |net20|net40|net45|net451|net452|net46| |net20|net40|net45|net451|net452|net46|net461| + |net45|net451|net452|net46| + |net45|net451|net452|net46|net461| - + @@ -13,9 +15,15 @@ + + + + + + \ No newline at end of file diff --git a/src/redmine-net-api/Http/Clients/HttpClient/HttpClientProvider.cs b/src/redmine-net-api/Http/Clients/HttpClient/HttpClientProvider.cs new file mode 100644 index 00000000..3932402f --- /dev/null +++ b/src/redmine-net-api/Http/Clients/HttpClient/HttpClientProvider.cs @@ -0,0 +1,257 @@ +#if !NET20 +using System; +using System.Net.Http; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using Redmine.Net.Api.Common; +using Redmine.Net.Api.Options; + +namespace Redmine.Net.Api.Http.Clients.HttpClient; + +internal static class HttpClientProvider +{ + private static System.Net.Http.HttpClient _client; + + /// + /// Gets an HttpClient instance. If an existing client is provided, it is returned; otherwise, a new one is created. + /// + public static System.Net.Http.HttpClient GetOrCreateHttpClient(System.Net.Http.HttpClient httpClient, + RedmineManagerOptions options) + { + if (_client != null) + { + return _client; + } + + _client = httpClient ?? CreateClient(options); + + return _client; + } + + /// + /// Creates a new HttpClient instance configured with the specified options. + /// + private static System.Net.Http.HttpClient CreateClient(RedmineManagerOptions redmineManagerOptions) + { + ArgumentVerifier.ThrowIfNull(redmineManagerOptions, nameof(redmineManagerOptions)); + + var handler = + #if NET + CreateSocketHandler(redmineManagerOptions); + #elif NETFRAMEWORK + CreateHandler(redmineManagerOptions); + #endif + + var client = new System.Net.Http.HttpClient(handler, disposeHandler: true); + + if (redmineManagerOptions.BaseAddress != null) + { + client.BaseAddress = redmineManagerOptions.BaseAddress; + } + + if (redmineManagerOptions.ApiClientOptions is not RedmineHttpClientOptions options) + { + return client; + } + + if (options.Timeout.HasValue) + { + client.Timeout = options.Timeout.Value; + } + + if (options.MaxResponseContentBufferSize.HasValue) + { + client.MaxResponseContentBufferSize = options.MaxResponseContentBufferSize.Value; + } + +#if NET5_0_OR_GREATER + if (options.DefaultRequestVersion != null) + { + client.DefaultRequestVersion = options.DefaultRequestVersion; + } + + if (options.DefaultVersionPolicy != null) + { + client.DefaultVersionPolicy = options.DefaultVersionPolicy.Value; + } +#endif + + return client; + } + +#if NET + private static SocketsHttpHandler CreateSocketHandler(RedmineManagerOptions redmineManagerOptions) + { + var handler = new SocketsHttpHandler() + { + // Limit the lifetime of connections to better respect any DNS changes + PooledConnectionLifetime = TimeSpan.FromMinutes(2), + + // Check cert revocation + SslOptions = new SslClientAuthenticationOptions() + { + CertificateRevocationCheckMode = X509RevocationMode.Online, + }, + }; + + if (redmineManagerOptions.ApiClientOptions is not RedmineHttpClientOptions options) + { + return handler; + } + + if (options.CookieContainer != null) + { + handler.CookieContainer = options.CookieContainer; + } + + handler.Credentials = options.Credentials; + handler.Proxy = options.Proxy; + + if (options.AutoRedirect.HasValue) + { + handler.AllowAutoRedirect = options.AutoRedirect.Value; + } + + if (options.DecompressionFormat.HasValue) + { + handler.AutomaticDecompression = options.DecompressionFormat.Value; + } + + if (options.PreAuthenticate.HasValue) + { + handler.PreAuthenticate = options.PreAuthenticate.Value; + } + + if (options.UseCookies.HasValue) + { + handler.UseCookies = options.UseCookies.Value; + } + + if (options.UseProxy.HasValue) + { + handler.UseProxy = options.UseProxy.Value; + } + + if (options.MaxAutomaticRedirections.HasValue) + { + handler.MaxAutomaticRedirections = options.MaxAutomaticRedirections.Value; + } + + handler.DefaultProxyCredentials = options.DefaultProxyCredentials; + + if (options.MaxConnectionsPerServer.HasValue) + { + handler.MaxConnectionsPerServer = options.MaxConnectionsPerServer.Value; + } + + if (options.MaxResponseHeadersLength.HasValue) + { + handler.MaxResponseHeadersLength = options.MaxResponseHeadersLength.Value; + } + +#if NET8_0_OR_GREATER + handler.MeterFactory = options.MeterFactory; +#endif + + return handler; + } +#elif NETFRAMEWORK + private static HttpClientHandler CreateHandler(RedmineManagerOptions redmineManagerOptions) + { + var handler = new HttpClientHandler(); + return ConfigureHandler(handler, redmineManagerOptions); + } + + private static HttpClientHandler ConfigureHandler(HttpClientHandler handler, RedmineManagerOptions redmineManagerOptions) + { + if (redmineManagerOptions.ApiClientOptions is not RedmineHttpClientOptions options) + { + return handler; + } + + if (options.UseDefaultCredentials.HasValue) + { + handler.UseDefaultCredentials = options.UseDefaultCredentials.Value; + } + + if (options.CookieContainer != null) + { + handler.CookieContainer = options.CookieContainer; + } + + if (handler.SupportsAutomaticDecompression && options.DecompressionFormat.HasValue) + { + handler.AutomaticDecompression = options.DecompressionFormat.Value; + } + + if (handler.SupportsRedirectConfiguration) + { + if (options.AutoRedirect.HasValue) + { + handler.AllowAutoRedirect = options.AutoRedirect.Value; + } + + if (options.MaxAutomaticRedirections.HasValue) + { + handler.MaxAutomaticRedirections = options.MaxAutomaticRedirections.Value; + } + } + + if (options.ClientCertificateOptions != default) + { + handler.ClientCertificateOptions = options.ClientCertificateOptions; + } + + handler.Credentials = options.Credentials; + + if (options.UseProxy != null) + { + handler.UseProxy = options.UseProxy.Value; + if (handler.UseProxy && options.Proxy != null) + { + handler.Proxy = options.Proxy; + } + } + + if (options.PreAuthenticate.HasValue) + { + handler.PreAuthenticate = options.PreAuthenticate.Value; + } + + if (options.UseCookies.HasValue) + { + handler.UseCookies = options.UseCookies.Value; + } + + if (options.MaxRequestContentBufferSize.HasValue) + { + handler.MaxRequestContentBufferSize = options.MaxRequestContentBufferSize.Value; + } + +#if NET471_OR_GREATER + handler.CheckCertificateRevocationList = options.CheckCertificateRevocationList; + + if (options.DefaultProxyCredentials != null) + handler.DefaultProxyCredentials = options.DefaultProxyCredentials; + + if (options.ServerCertificateCustomValidationCallback != null) + handler.ServerCertificateCustomValidationCallback = options.ServerCertificateCustomValidationCallback; + + if (options.ServerCertificateValidationCallback != null) + handler.ServerCertificateCustomValidationCallback = options.ServerCertificateValidationCallback; + + handler.SslProtocols = options.SslProtocols; + + if (options.MaxConnectionsPerServer.HasValue) + handler.MaxConnectionsPerServer = options.MaxConnectionsPerServer.Value; + + if (options.MaxResponseHeadersLength.HasValue) + handler.MaxResponseHeadersLength = options.MaxResponseHeadersLength.Value; +#endif + + return handler; + } +#endif +} + +#endif \ No newline at end of file diff --git a/src/redmine-net-api/Http/Clients/HttpClient/HttpContentExtensions.cs b/src/redmine-net-api/Http/Clients/HttpClient/HttpContentExtensions.cs new file mode 100644 index 00000000..c1b2515c --- /dev/null +++ b/src/redmine-net-api/Http/Clients/HttpClient/HttpContentExtensions.cs @@ -0,0 +1,20 @@ +#if !NET20 + +using System.Net; + +namespace Redmine.Net.Api.Http.Clients.HttpClient; + +internal static class HttpContentExtensions +{ + public static bool IsUnprocessableEntity(this HttpStatusCode statusCode) + { + return +#if NET5_0_OR_GREATER + statusCode == HttpStatusCode.UnprocessableEntity; +#else + (int)statusCode == 422; +#endif + } +} + +#endif \ No newline at end of file diff --git a/src/redmine-net-api/Http/Clients/HttpClient/HttpContentPolyfills.cs b/src/redmine-net-api/Http/Clients/HttpClient/HttpContentPolyfills.cs new file mode 100644 index 00000000..ec5d7f4d --- /dev/null +++ b/src/redmine-net-api/Http/Clients/HttpClient/HttpContentPolyfills.cs @@ -0,0 +1,35 @@ + +#if !(NET20 || NET5_0_OR_GREATER) + +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Redmine.Net.Api.Http.Clients.HttpClient; + +internal static class HttpContentPolyfills +{ + internal static Task ReadAsStringAsync(this HttpContent httpContent, CancellationToken cancellationToken) + => httpContent.ReadAsStringAsync( +#if !NETFRAMEWORK + cancellationToken +#endif + ); + + internal static Task ReadAsStreamAsync(this HttpContent httpContent, CancellationToken cancellationToken) + => httpContent.ReadAsStreamAsync( +#if !NETFRAMEWORK + cancellationToken +#endif + ); + + internal static Task ReadAsByteArrayAsync(this HttpContent httpContent, CancellationToken cancellationToken) + => httpContent.ReadAsByteArrayAsync( +#if !NETFRAMEWORK + cancellationToken +#endif + ); +} + +#endif \ No newline at end of file diff --git a/src/redmine-net-api/Http/Clients/HttpClient/HttpResponseHeadersExtensions.cs b/src/redmine-net-api/Http/Clients/HttpClient/HttpResponseHeadersExtensions.cs new file mode 100644 index 00000000..37b44dcb --- /dev/null +++ b/src/redmine-net-api/Http/Clients/HttpClient/HttpResponseHeadersExtensions.cs @@ -0,0 +1,22 @@ +#if !NET20 +using System.Collections.Specialized; +using System.Net.Http.Headers; + +namespace Redmine.Net.Api.Http.Clients.HttpClient; + +internal static class HttpResponseHeadersExtensions +{ + public static NameValueCollection ToNameValueCollection(this HttpResponseHeaders headers) + { + if (headers == null) return null; + + var collection = new NameValueCollection(); + foreach (var header in headers) + { + var combinedValue = string.Join(", ", header.Value); + collection.Add(header.Key, combinedValue); + } + return collection; + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net-api/Http/Clients/HttpClient/IRedmineHttpClientOptions.cs b/src/redmine-net-api/Http/Clients/HttpClient/IRedmineHttpClientOptions.cs new file mode 100644 index 00000000..d8f6d9d2 --- /dev/null +++ b/src/redmine-net-api/Http/Clients/HttpClient/IRedmineHttpClientOptions.cs @@ -0,0 +1,88 @@ +#if NET40_OR_GREATER || NET +using System; +using System.Net; +using System.Net.Http; +using System.Net.Security; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +#if NET8_0_OR_GREATER +using System.Diagnostics.Metrics; +#endif + + +namespace Redmine.Net.Api.Http.Clients.HttpClient; + +/// +/// +/// +public interface IRedmineHttpClientOptions : IRedmineApiClientOptions +{ + /// + /// + /// + ClientCertificateOption ClientCertificateOptions { get; set; } + +#if NET471_OR_GREATER || NET + /// + /// + /// + ICredentials DefaultProxyCredentials { get; set; } + + /// + /// + /// + Func ServerCertificateCustomValidationCallback { get; set; } + + /// + /// + /// + SslProtocols SslProtocols { get; set; } +#endif + + /// + /// + /// + public +#if NET || NET471_OR_GREATER + Func +#else + RemoteCertificateValidationCallback +#endif + ServerCertificateValidationCallback { get; set; } + +#if NET8_0_OR_GREATER + /// + /// + /// + public IMeterFactory MeterFactory { get; set; } +#endif + + /// + /// + /// + bool SupportsAutomaticDecompression { get; set; } + + /// + /// + /// + bool SupportsProxy { get; set; } + + /// + /// + /// + bool SupportsRedirectConfiguration { get; set; } + + /// + /// + /// + Version DefaultRequestVersion { get; set; } + +#if NET + /// + /// + /// + HttpVersionPolicy? DefaultVersionPolicy { get; set; } +#endif +} + +#endif \ No newline at end of file diff --git a/src/redmine-net-api/Http/Clients/HttpClient/InternalRedmineApiHttpClient.Async.cs b/src/redmine-net-api/Http/Clients/HttpClient/InternalRedmineApiHttpClient.Async.cs new file mode 100644 index 00000000..77186037 --- /dev/null +++ b/src/redmine-net-api/Http/Clients/HttpClient/InternalRedmineApiHttpClient.Async.cs @@ -0,0 +1,144 @@ +#if !NET20 +/* + Copyright 2011 - 2025 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Http.Helpers; +using Redmine.Net.Api.Http.Messages; + +namespace Redmine.Net.Api.Http.Clients.HttpClient; + +internal sealed partial class InternalRedmineApiHttpClient +{ + protected override async Task HandleRequestAsync(string address, string verb, RequestOptions requestOptions = null, + object content = null, IProgress progress = null, CancellationToken cancellationToken = default) + { + var httpMethod = GetHttpMethod(verb); + using (var requestMessage = CreateRequestMessage(address, httpMethod, requestOptions, content as HttpContent)) + { + return await SendAsync(requestMessage, progress: progress, cancellationToken: cancellationToken).ConfigureAwait(false); + } + } + + private async Task SendAsync(HttpRequestMessage requestMessage, IProgress progress = null, CancellationToken cancellationToken = default) + { + try + { + using (var httpResponseMessage = await _httpClient + .SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken) + .ConfigureAwait(false)) + { + if (httpResponseMessage.IsSuccessStatusCode) + { + if (httpResponseMessage.StatusCode == HttpStatusCode.NoContent) + { + return CreateApiResponseMessage(httpResponseMessage.Headers, HttpStatusCode.NoContent, []); + } + + byte[] data; + + if (requestMessage.Method == HttpMethod.Get && progress != null) + { + data = await DownloadWithProgressAsync(httpResponseMessage.Content, progress, cancellationToken) + .ConfigureAwait(false); + } + else + { + data = await httpResponseMessage.Content.ReadAsByteArrayAsync(cancellationToken) + .ConfigureAwait(false); + } + + return CreateApiResponseMessage(httpResponseMessage.Headers, httpResponseMessage.StatusCode, data); + } + + var statusCode = (int)httpResponseMessage.StatusCode; + using (var stream = await httpResponseMessage.Content.ReadAsStreamAsync(cancellationToken) + .ConfigureAwait(false)) + { + RedmineExceptionHelper.MapStatusCodeToException(statusCode, stream, null, Serializer); + } + } + } + catch (OperationCanceledException ex) when (cancellationToken.IsCancellationRequested) + { + throw new RedmineApiException("Token has been cancelled", ex); + } + catch (OperationCanceledException ex) when (ex.InnerException is TimeoutException tex) + { + throw new RedmineApiException("Operation has timed out", ex); + } + catch (TaskCanceledException tcex) when (cancellationToken.IsCancellationRequested) + { + throw new RedmineApiException("Operation ahs been cancelled by user", tcex); + } + catch (TaskCanceledException tce) + { + throw new RedmineApiException(tce.Message, tce); + } + catch (HttpRequestException ex) + { + throw new RedmineApiException(ex.Message, ex); + } + catch (Exception ex) when (ex is not RedmineException) + { + throw new RedmineApiException(ex.Message, ex); + } + + return null; + } + + private static async Task DownloadWithProgressAsync(HttpContent httpContent, IProgress progress = null, CancellationToken cancellationToken = default) + { + var contentLength = httpContent.Headers.ContentLength ?? -1; + byte[] data; + + if (contentLength > 0) + { + using (var stream = await httpContent.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false)) + { + data = new byte[contentLength]; + int bytesRead; + var totalBytesRead = 0; + var buffer = new byte[8192]; + + while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) > 0) + { + cancellationToken.ThrowIfCancellationRequested(); + + Buffer.BlockCopy(buffer, 0, data, totalBytesRead, bytesRead); + totalBytesRead += bytesRead; + + var progressPercentage = (int)(totalBytesRead * 100 / contentLength); + progress?.Report(progressPercentage); + ReportProgress(progress, contentLength, totalBytesRead); + } + } + } + else + { + data = await httpContent.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false); + progress?.Report(100); + } + + return data; + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net-api/Http/Clients/HttpClient/InternalRedmineApiHttpClient.cs b/src/redmine-net-api/Http/Clients/HttpClient/InternalRedmineApiHttpClient.cs new file mode 100644 index 00000000..a68897d6 --- /dev/null +++ b/src/redmine-net-api/Http/Clients/HttpClient/InternalRedmineApiHttpClient.cs @@ -0,0 +1,170 @@ +#if !NET20 +/* + Copyright 2011 - 2024 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using Redmine.Net.Api.Authentication; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http.Constants; +using Redmine.Net.Api.Http.Messages; +using Redmine.Net.Api.Options; + +namespace Redmine.Net.Api.Http.Clients.HttpClient; + +internal sealed partial class InternalRedmineApiHttpClient : RedmineApiClient +{ + private static readonly HttpMethod PatchMethod = new HttpMethod("PATCH"); + private static readonly HttpMethod DownloadMethod = new HttpMethod("DOWNLOAD"); + private static readonly Encoding DefaultEncoding = Encoding.UTF8; + + private readonly System.Net.Http.HttpClient _httpClient; + + public InternalRedmineApiHttpClient(RedmineManagerOptions redmineManagerOptions) + : this(null, redmineManagerOptions) + { + _httpClient = HttpClientProvider.GetOrCreateHttpClient(null, redmineManagerOptions); + } + + public InternalRedmineApiHttpClient(System.Net.Http.HttpClient httpClient, + RedmineManagerOptions redmineManagerOptions) + : base(redmineManagerOptions) + { + _httpClient = httpClient; + } + + protected override object CreateContentFromPayload(string payload) + { + return new StringContent(payload, DefaultEncoding, Serializer.ContentType); + } + + protected override object CreateContentFromBytes(byte[] data) + { + var content = new ByteArrayContent(data); + content.Headers.ContentType = new MediaTypeHeaderValue(RedmineConstants.CONTENT_TYPE_APPLICATION_STREAM); + return content; + } + + protected override RedmineApiResponse HandleRequest(string address, string verb, + RequestOptions requestOptions = null, + object content = null, IProgress progress = null) + { + var httpMethod = GetHttpMethod(verb); + using (var requestMessage = CreateRequestMessage(address, httpMethod, requestOptions, content as HttpContent)) + { + // LogRequest(verb, address, requestOptions); + + var response = Send(requestMessage, progress); + + // LogResponse(response.StatusCode); + + return response; + } + } + + private RedmineApiResponse Send(HttpRequestMessage requestMessage, IProgress progress = null) + { + return TaskExtensions.Synchronize(()=>SendAsync(requestMessage, progress)); + } + + private HttpRequestMessage CreateRequestMessage(string address, HttpMethod method, + RequestOptions requestOptions = null, HttpContent content = null) + { + var httpRequest = new HttpRequestMessage(method, address); + + switch (Credentials) + { + case RedmineApiKeyAuthentication: + httpRequest.Headers.Add(RedmineConstants.API_KEY_AUTHORIZATION_HEADER_KEY, Credentials.Token); + break; + case RedmineBasicAuthentication: + httpRequest.Headers.Add(RedmineConstants.AUTHORIZATION_HEADER_KEY, Credentials.Token); + break; + } + + if (requestOptions != null) + { + if (requestOptions.QueryString != null) + { + var uriToBeAppended = httpRequest.RequestUri.ToString(); + var queryIndex = uriToBeAppended.IndexOf("?", StringComparison.Ordinal); + var hasQuery = queryIndex != -1; + + var sb = new StringBuilder(); + sb.Append('\\'); + sb.Append(uriToBeAppended); + for (var index = 0; index < requestOptions.QueryString.Count; ++index) + { + var value = requestOptions.QueryString[index]; + + if (value == null) + { + continue; + } + + var key = requestOptions.QueryString.Keys[index]; + + sb.Append(hasQuery ? '&' : '?'); + sb.Append(Uri.EscapeDataString(key)); + sb.Append('='); + sb.Append(Uri.EscapeDataString(value)); + hasQuery = true; + } + + var uriString = sb.ToString(); + + httpRequest.RequestUri = new Uri(uriString, UriKind.RelativeOrAbsolute); + } + + if (!requestOptions.ImpersonateUser.IsNullOrWhiteSpace()) + { + httpRequest.Headers.Add(RedmineConstants.IMPERSONATE_HEADER_KEY, requestOptions.ImpersonateUser); + } + } + + if (content != null) + { + httpRequest.Content = content ; + } + + return httpRequest; + } + + private static RedmineApiResponse CreateApiResponseMessage(HttpResponseHeaders headers, HttpStatusCode statusCode, byte[] content) => new RedmineApiResponse() + { + Content = content, + Headers = headers.ToNameValueCollection(), + StatusCode = statusCode, + }; + + private static HttpMethod GetHttpMethod(string verb) + { + return verb switch + { + HttpConstants.HttpVerbs.GET => HttpMethod.Get, + HttpConstants.HttpVerbs.POST => HttpMethod.Post, + HttpConstants.HttpVerbs.PUT => HttpMethod.Put, + HttpConstants.HttpVerbs.PATCH => PatchMethod, + HttpConstants.HttpVerbs.DELETE => HttpMethod.Delete, + HttpConstants.HttpVerbs.DOWNLOAD => HttpMethod.Get, + _ => throw new ArgumentException($"Unsupported HTTP verb: {verb}") + }; + } +} +#endif \ No newline at end of file diff --git a/src/redmine-net-api/Http/Clients/HttpClient/RedmineHttpClientOptions.cs b/src/redmine-net-api/Http/Clients/HttpClient/RedmineHttpClientOptions.cs new file mode 100644 index 00000000..dc8e0b6a --- /dev/null +++ b/src/redmine-net-api/Http/Clients/HttpClient/RedmineHttpClientOptions.cs @@ -0,0 +1,75 @@ +#if !NET20 +using System; +using System.Net; +using System.Net.Http; +using System.Net.Security; +using System.Security.Authentication; +using System.Security.Cryptography.X509Certificates; +#if NET8_0_OR_GREATER +using System.Diagnostics.Metrics; +#endif + +namespace Redmine.Net.Api.Http.Clients.HttpClient; + +/// +/// +/// +public sealed class RedmineHttpClientOptions: RedmineApiClientOptions +{ +#if NET8_0_OR_GREATER + /// + /// + /// + public IMeterFactory MeterFactory { get; set; } +#endif + + /// + /// + /// + public Version DefaultRequestVersion { get; set; } + +#if NET + /// + /// + /// + public HttpVersionPolicy? DefaultVersionPolicy { get; set; } +#endif + /// + /// + /// + public ICredentials DefaultProxyCredentials { get; set; } + + /// + /// + /// + public ClientCertificateOption ClientCertificateOptions { get; set; } + + + +#if NETFRAMEWORK + /// + /// + /// + public Func ServerCertificateCustomValidationCallback + { + get; + set; + } + + /// + /// + /// + public SslProtocols SslProtocols { get; set; } + #endif + /// + /// + /// + public +#if NET || NET471_OR_GREATER + Func +#else + RemoteCertificateValidationCallback +#endif + ServerCertificateValidationCallback { get; set; } +} +#endif \ No newline at end of file diff --git a/src/redmine-net-api/Http/Helpers/RedmineHttpMethodHelper.cs b/src/redmine-net-api/Http/Helpers/RedmineHttpMethodHelper.cs new file mode 100644 index 00000000..c3fc7e10 --- /dev/null +++ b/src/redmine-net-api/Http/Helpers/RedmineHttpMethodHelper.cs @@ -0,0 +1,63 @@ +using System; +using Redmine.Net.Api.Http.Constants; +#if !NET20 +using System.Net.Http; +#endif + +namespace Redmine.Net.Api.Http.Helpers; + +internal static class RedmineHttpMethodHelper +{ +#if !NET20 + private static readonly HttpMethod PatchMethod = new HttpMethod("PATCH"); + private static readonly HttpMethod DownloadMethod = new HttpMethod("DOWNLOAD"); + + /// + /// Gets an HttpMethod instance for the specified HTTP verb. + /// + /// The HTTP verb (GET, POST, etc.). + /// An HttpMethod instance corresponding to the verb. + /// Thrown when the verb is not supported. + public static HttpMethod GetHttpMethod(string verb) + { + return verb switch + { + HttpConstants.HttpVerbs.GET => HttpMethod.Get, + HttpConstants.HttpVerbs.POST => HttpMethod.Post, + HttpConstants.HttpVerbs.PUT => HttpMethod.Put, + HttpConstants.HttpVerbs.PATCH => PatchMethod, + HttpConstants.HttpVerbs.DELETE => HttpMethod.Delete, + HttpConstants.HttpVerbs.DOWNLOAD => DownloadMethod, + _ => throw new ArgumentException($"Unsupported HTTP verb: {verb}") + }; + } +#endif + /// + /// Determines whether the specified HTTP method is a GET or DOWNLOAD method. + /// + /// The HTTP method to check. + /// True if the method is GET or DOWNLOAD; otherwise, false. + public static bool IsGetOrDownload(string method) + { + return method == HttpConstants.HttpVerbs.GET || method == HttpConstants.HttpVerbs.DOWNLOAD; + } + + /// + /// Determines whether the HTTP status code represents a transient error. + /// + /// The HTTP response status code. + /// True if the status code represents a transient error; otherwise, false. + private static bool IsTransientError(int statusCode) + { + return statusCode switch + { + HttpConstants.StatusCodes.BadGateway => true, + HttpConstants.StatusCodes.GatewayTimeout => true, + HttpConstants.StatusCodes.ServiceUnavailable => true, + HttpConstants.StatusCodes.RequestTimeout => true, + HttpConstants.StatusCodes.TooManyRequests => true, + _ => false + }; + } + +} diff --git a/src/redmine-net-api/RedmineManagerOptions.cs b/src/redmine-net-api/Options/RedmineManagerOptions.cs similarity index 73% rename from src/redmine-net-api/RedmineManagerOptions.cs rename to src/redmine-net-api/Options/RedmineManagerOptions.cs index d8acf1c8..7093c073 100644 --- a/src/redmine-net-api/RedmineManagerOptions.cs +++ b/src/redmine-net-api/Options/RedmineManagerOptions.cs @@ -17,11 +17,16 @@ limitations under the License. using System; using System.Net; using Redmine.Net.Api.Authentication; +using Redmine.Net.Api.Http; +using Redmine.Net.Api.Http.Clients.WebClient; using Redmine.Net.Api.Logging; -using Redmine.Net.Api.Net.WebClient; using Redmine.Net.Api.Serialization; +#if !NET20 +using System.Net.Http; +using Redmine.Net.Api.Http.Clients.HttpClient; +#endif -namespace Redmine.Net.Api +namespace Redmine.Net.Api.Options { /// /// @@ -53,6 +58,23 @@ internal sealed class RedmineManagerOptions /// public IRedmineAuthentication Authentication { get; init; } + /// + /// Gets or sets the version of the Redmine server to which this client will connect. + /// + public Version RedmineVersion { get; init; } + + public IRedmineLogger Logger { get; init; } + + /// + /// Gets or sets additional logging configuration options + /// + public RedmineLoggingOptions LoggingOptions { get; init; } = new RedmineLoggingOptions(); + + /// + /// Gets or sets the settings for configuring the Redmine http client. + /// + public IRedmineApiClientOptions ApiClientOptions { get; set; } + /// /// Gets or sets a custom function that creates and returns a specialized instance of the WebClient class. /// @@ -61,20 +83,24 @@ internal sealed class RedmineManagerOptions /// /// Gets or sets the settings for configuring the Redmine web client. /// - public IRedmineWebClientOptions WebClientOptions { get; init; } + public RedmineWebClientOptions WebClientOptions { + get => (RedmineWebClientOptions)ApiClientOptions; + set => ApiClientOptions = value; + } + #if !NET20 /// - /// Gets or sets the version of the Redmine server to which this client will connect. + /// /// - public Version RedmineVersion { get; init; } - - internal bool VerifyServerCert { get; init; } - - public IRedmineLogger Logger { get; init; } + public HttpClient HttpClient { get; init; } /// - /// Gets or sets additional logging configuration options + /// Gets or sets the settings for configuring the Redmine http client. /// - public RedmineLoggingOptions LoggingOptions { get; init; } = new RedmineLoggingOptions(); + public RedmineHttpClientOptions HttpClientOptions { + get => (RedmineHttpClientOptions)ApiClientOptions; + set => ApiClientOptions = value; + } + #endif } } \ No newline at end of file diff --git a/src/redmine-net-api/Options/RedmineManagerOptionsBuilder.cs b/src/redmine-net-api/Options/RedmineManagerOptionsBuilder.cs new file mode 100644 index 00000000..698cfb31 --- /dev/null +++ b/src/redmine-net-api/Options/RedmineManagerOptionsBuilder.cs @@ -0,0 +1,306 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System; +using System.Net; +#if NET462_OR_GREATER || NET +using Microsoft.Extensions.Logging; +#endif +using Redmine.Net.Api.Authentication; +using Redmine.Net.Api.Http; +#if !NET20 +using Redmine.Net.Api.Http.Clients.HttpClient; +#endif +using Redmine.Net.Api.Http.Clients.WebClient; +using Redmine.Net.Api.Internals; +using Redmine.Net.Api.Logging; +using Redmine.Net.Api.Serialization; +#if NET40_OR_GREATER || NET +using System.Net.Http; +#endif +#if NET462_OR_GREATER || NET +#endif + +namespace Redmine.Net.Api.Options +{ + /// + /// + /// + public sealed class RedmineManagerOptionsBuilder + { + private IRedmineLogger _redmineLogger = RedmineNullLogger.Instance; + private Action _configureLoggingOptions; + + private enum ClientType + { + WebClient, + HttpClient, + } + private ClientType _clientType = ClientType.HttpClient; + + /// + /// + /// + public string Host { get; private set; } + + /// + /// + /// + public int PageSize { get; private set; } + + /// + /// Gets the current serialization type + /// + public SerializationType SerializationType { get; private set; } + + /// + /// + /// + public IRedmineAuthentication Authentication { get; private set; } + + /// + /// + /// + public IRedmineApiClientOptions ClientOptions { get; private set; } + + /// + /// + /// + public Func ClientFunc { get; private set; } + + /// + /// Gets or sets the version of the Redmine server to which this client will connect. + /// + public Version Version { get; set; } + + /// + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithPageSize(int pageSize) + { + PageSize = pageSize; + return this; + } + + /// + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithHost(string baseAddress) + { + Host = baseAddress; + return this; + } + + /// + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithSerializationType(SerializationType serializationType) + { + SerializationType = serializationType; + return this; + } + + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithXmlSerialization() + { + SerializationType = SerializationType.Xml; + return this; + } + + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithJsonSerialization() + { + SerializationType = SerializationType.Json; + return this; + } + + /// + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithApiKeyAuthentication(string apiKey) + { + Authentication = new RedmineApiKeyAuthentication(apiKey); + return this; + } + + /// + /// + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithBasicAuthentication(string login, string password) + { + Authentication = new RedmineBasicAuthentication(login, password); + return this; + } + + /// + /// + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithLogger(IRedmineLogger logger, Action configure = null) + { + _redmineLogger = logger ?? RedmineNullLogger.Instance; + _configureLoggingOptions = configure; + return this; + } + + /// + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithVersion(Version version) + { + Version = version; + return this; + } + + /// + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithWebClient(Func clientFunc) + { + _clientType = ClientType.WebClient; + ClientFunc = clientFunc; + return this; + } + + /// + /// Configures the client to use WebClient with default settings + /// + /// This builder instance for method chaining + public RedmineManagerOptionsBuilder UseWebClient(RedmineWebClientOptions clientOptions = null) + { + _clientType = ClientType.WebClient; + ClientOptions = clientOptions; + return this; + } + +#if NET40_OR_GREATER || NET + /// + /// + /// + public Func HttpClientFunc { get; private set; } + + /// + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithHttpClient(Func clientFunc) + { + _clientType = ClientType.HttpClient; + this.HttpClientFunc = clientFunc; + return this; + } + + /// + /// Configures the client to use HttpClient with default settings + /// + /// This builder instance for method chaining + public RedmineManagerOptionsBuilder UseHttpClient(RedmineHttpClientOptions clientOptions = null) + { + _clientType = ClientType.HttpClient; + ClientOptions = clientOptions; + return this; + } + +#endif + +#if NET462_OR_GREATER || NET + /// + /// + /// + /// + /// + /// + public RedmineManagerOptionsBuilder WithLogger(ILogger logger, Action configure = null) + { + _redmineLogger = new MicrosoftLoggerRedmineAdapter(logger); + _configureLoggingOptions = configure; + return this; + } +#endif + + /// + /// + /// + /// + internal RedmineManagerOptions Build() + { +#if NET45_OR_GREATER || NET + ClientOptions ??= _clientType switch + { + ClientType.WebClient => new RedmineWebClientOptions(), + ClientType.HttpClient => new RedmineHttpClientOptions(), + _ => throw new ArgumentOutOfRangeException() + }; +#else + ClientOptions ??= new RedmineWebClientOptions(); +#endif + + var baseAddress = HostHelper.CreateRedmineUri(Host, ClientOptions.Scheme); + + var redmineLoggingOptions = ConfigureLoggingOptions(); + + var options = new RedmineManagerOptions() + { + BaseAddress = baseAddress, + PageSize = PageSize > 0 ? PageSize : RedmineConstants.DEFAULT_PAGE_SIZE_VALUE, + Serializer = RedmineSerializerFactory.CreateSerializer(SerializationType), + RedmineVersion = Version, + Authentication = Authentication ?? new RedmineNoAuthentication(), + ApiClientOptions = ClientOptions, + Logger = _redmineLogger, + LoggingOptions = redmineLoggingOptions, + }; + + return options; + } + + private RedmineLoggingOptions ConfigureLoggingOptions() + { + if (_configureLoggingOptions == null) + { + return null; + } + + var redmineLoggingOptions = new RedmineLoggingOptions(); + _configureLoggingOptions(redmineLoggingOptions); + return redmineLoggingOptions; + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/RedmineManager.cs b/src/redmine-net-api/RedmineManager.cs index c9c92598..ca062ef8 100644 --- a/src/redmine-net-api/RedmineManager.cs +++ b/src/redmine-net-api/RedmineManager.cs @@ -25,6 +25,9 @@ limitations under the License. using Redmine.Net.Api.Http.Extensions; using Redmine.Net.Api.Internals; using Redmine.Net.Api.Logging; +#if NET40_OR_GREATER || NET +using Redmine.Net.Api.Http.Clients.HttpClient; +#endif using Redmine.Net.Api.Net.Internal; using Redmine.Net.Api.Options; using Redmine.Net.Api.Serialization; @@ -42,6 +45,7 @@ public partial class RedmineManager : IRedmineManager internal IRedmineSerializer Serializer { get; } internal RedmineApiUrls RedmineApiUrls { get; } internal IRedmineApiClient ApiClient { get; } + internal IRedmineLogger Logger { get; } /// /// @@ -85,9 +89,14 @@ private static InternalRedmineApiWebClient CreateWebClient(RedmineManagerOptions return new InternalRedmineApiWebClient(options); } #if NET40_OR_GREATER || NET + private InternalRedmineApiHttpClient CreateHttpClient(RedmineManagerOptions options) + { + return options.HttpClient != null + ? new InternalRedmineApiHttpClient(options.HttpClient, options) + : new InternalRedmineApiHttpClient(_redmineManagerOptions); + } +#endif -#if NET45_OR_GREATER - if (options.VerifyServerCert) private static void ApplyServiceManagerSettings(RedmineWebClientOptions options) { if (options == null) diff --git a/src/redmine-net-api/RedmineManagerOptionsBuilder.cs b/src/redmine-net-api/RedmineManagerOptionsBuilder.cs deleted file mode 100644 index 114424a5..00000000 --- a/src/redmine-net-api/RedmineManagerOptionsBuilder.cs +++ /dev/null @@ -1,464 +0,0 @@ -/* - Copyright 2011 - 2025 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Net; -#if NET462_OR_GREATER || NETCOREAPP -using Microsoft.Extensions.Logging; -#endif -using Redmine.Net.Api.Authentication; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Logging; -using Redmine.Net.Api.Net; -using Redmine.Net.Api.Net.WebClient; -using Redmine.Net.Api.Serialization; - -namespace Redmine.Net.Api -{ - /// - /// - /// - public sealed class RedmineManagerOptionsBuilder - { - private IRedmineLogger _redmineLogger = RedmineNullLogger.Instance; - private Action _configureLoggingOptions; - - private enum ClientType - { - WebClient, - HttpClient, - } - private ClientType _clientType = ClientType.WebClient; - - /// - /// - /// - /// - /// - public RedmineManagerOptionsBuilder WithPageSize(int pageSize) - { - this.PageSize = pageSize; - return this; - } - - /// - /// - /// - public int PageSize { get; private set; } - - /// - /// - /// - /// - /// - public RedmineManagerOptionsBuilder WithHost(string baseAddress) - { - this.Host = baseAddress; - return this; - } - - /// - /// - /// - public string Host { get; private set; } - - /// - /// - /// - /// - /// - internal RedmineManagerOptionsBuilder WithSerializationType(MimeFormat mimeFormat) - { - this.SerializationType = mimeFormat == MimeFormat.Xml ? SerializationType.Xml : SerializationType.Json; - return this; - } - - /// - /// - /// - /// - /// - public RedmineManagerOptionsBuilder WithSerializationType(SerializationType serializationType) - { - this.SerializationType = serializationType; - return this; - } - - /// - /// Gets the current serialization type - /// - public SerializationType SerializationType { get; private set; } - - /// - /// - /// - /// - /// - public RedmineManagerOptionsBuilder WithApiKeyAuthentication(string apiKey) - { - this.Authentication = new RedmineApiKeyAuthentication(apiKey); - return this; - } - - /// - /// - /// - /// - /// - /// - public RedmineManagerOptionsBuilder WithBasicAuthentication(string login, string password) - { - this.Authentication = new RedmineBasicAuthentication(login, password); - return this; - } - - /// - /// - /// - public IRedmineAuthentication Authentication { get; private set; } - - /// - /// - /// - /// - /// - public RedmineManagerOptionsBuilder WithWebClient(Func clientFunc) - { - _clientType = ClientType.WebClient; - this.ClientFunc = clientFunc; - return this; - } - - /// - /// - /// - public Func ClientFunc { get; private set; } - - /// - /// - /// - /// - /// - [Obsolete("Use WithWebClientOptions(IRedmineWebClientOptions clientOptions) instead.")] - public RedmineManagerOptionsBuilder WithWebClientOptions(IRedmineApiClientOptions clientOptions) - { - return WithWebClientOptions((IRedmineWebClientOptions)clientOptions); - } - - /// - /// - /// - /// - /// - public RedmineManagerOptionsBuilder WithWebClientOptions(IRedmineWebClientOptions clientOptions) - { - _clientType = ClientType.WebClient; - this.WebClientOptions = clientOptions; - return this; - } - - /// - /// - /// - [Obsolete("Use WebClientOptions instead.")] - public IRedmineApiClientOptions ClientOptions - { - get => WebClientOptions; - private set { } - } - - /// - /// - /// - public IRedmineWebClientOptions WebClientOptions { get; private set; } - - /// - /// - /// - /// - /// - public RedmineManagerOptionsBuilder WithVersion(Version version) - { - this.Version = version; - return this; - } - - /// - /// - /// - /// - /// - /// - public RedmineManagerOptionsBuilder WithLogger(IRedmineLogger logger, Action configure = null) - { - _redmineLogger = logger ?? RedmineNullLogger.Instance; - _configureLoggingOptions = configure; - return this; - } -#if NET462_OR_GREATER || NETCOREAPP - /// - /// - /// - /// - /// - /// - public RedmineManagerOptionsBuilder WithLogger(ILogger logger, Action configure = null) - { - _redmineLogger = new MicrosoftLoggerRedmineAdapter(logger); - _configureLoggingOptions = configure; - return this; - } -#endif - /// - /// Gets or sets the version of the Redmine server to which this client will connect. - /// - public Version Version { get; set; } - - internal RedmineManagerOptionsBuilder WithVerifyServerCert(bool verifyServerCert) - { - this.VerifyServerCert = verifyServerCert; - return this; - } - - /// - /// - /// - public bool VerifyServerCert { get; private set; } - - /// - /// - /// - /// - internal RedmineManagerOptions Build() - { - const string defaultUserAgent = "Redmine.Net.Api.Net"; - var defaultDecompressionFormat = -#if NETFRAMEWORK - DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.None; -#else - DecompressionMethods.All; -#endif - -#if NET45_OR_GREATER || NETCOREAPP - WebClientOptions ??= _clientType switch - { - ClientType.WebClient => new RedmineWebClientOptions() - { - UserAgent = defaultUserAgent, - DecompressionFormat = defaultDecompressionFormat, - }, - _ => throw new ArgumentOutOfRangeException() - }; -#else - WebClientOptions ??= new RedmineWebClientOptions() - { - UserAgent = defaultUserAgent, - DecompressionFormat = defaultDecompressionFormat, - }; -#endif - - var baseAddress = CreateRedmineUri(Host, WebClientOptions.Scheme); - RedmineLoggingOptions redmineLoggingOptions = null; - if (_configureLoggingOptions != null) - { - redmineLoggingOptions = new RedmineLoggingOptions(); - _configureLoggingOptions(redmineLoggingOptions); - } - - var options = new RedmineManagerOptions() - { - BaseAddress = baseAddress, - PageSize = PageSize > 0 ? PageSize : RedmineConstants.DEFAULT_PAGE_SIZE_VALUE, - VerifyServerCert = VerifyServerCert, - Serializer = RedmineSerializerFactory.CreateSerializer(SerializationType), - RedmineVersion = Version, - Authentication = Authentication ?? new RedmineNoAuthentication(), - WebClientOptions = WebClientOptions - Logger = _redmineLogger, - LoggingOptions = redmineLoggingOptions, - }; - - return options; - } - - private static readonly char[] DotCharArray = ['.']; - - internal static void EnsureDomainNameIsValid(string domainName) - { - if (domainName.IsNullOrWhiteSpace()) - { - throw new RedmineException("Domain name cannot be null or empty."); - } - - if (domainName.Length > 255) - { - throw new RedmineException("Domain name cannot be longer than 255 characters."); - } - - var labels = domainName.Split(DotCharArray); - if (labels.Length == 1) - { - throw new RedmineException("Domain name is not valid."); - } - foreach (var label in labels) - { - if (label.IsNullOrWhiteSpace() || label.Length > 63) - { - throw new RedmineException("Domain name must be between 1 and 63 characters."); - } - - if (!char.IsLetterOrDigit(label[0]) || !char.IsLetterOrDigit(label[label.Length - 1])) - { - throw new RedmineException("Domain name starts or ends with a hyphen."); - } - - for (var i = 0; i < label.Length; i++) - { - var c = label[i]; - - if (!char.IsLetterOrDigit(c) && c != '-') - { - throw new RedmineException("Domain name contains an invalid character."); - } - - if (c != '-') - { - continue; - } - - if (i + 1 < label.Length && (c ^ label[i+1]) == 0) - { - throw new RedmineException("Domain name contains consecutive hyphens."); - } - } - } - } - - internal static Uri CreateRedmineUri(string host, string scheme = null) - { - if (host.IsNullOrWhiteSpace() || host.Equals("string.Empty", StringComparison.OrdinalIgnoreCase)) - { - throw new RedmineException("The host is null or empty."); - } - - if (!Uri.TryCreate(host, UriKind.Absolute, out var uri)) - { - host = host.TrimEnd('/', '\\'); - EnsureDomainNameIsValid(host); - - if (!host.StartsWith(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) || !host.StartsWith(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) - { - host = $"{scheme ?? Uri.UriSchemeHttps}://{host}"; - - if (!Uri.TryCreate(host, UriKind.Absolute, out uri)) - { - throw new RedmineException("The host is not valid."); - } - } - } - - if (!uri.IsWellFormedOriginalString()) - { - throw new RedmineException("The host is not well-formed."); - } - - scheme ??= Uri.UriSchemeHttps; - var hasScheme = false; - if (!uri.Scheme.IsNullOrWhiteSpace()) - { - if (uri.Host.IsNullOrWhiteSpace() && uri.IsAbsoluteUri && !uri.IsFile) - { - if (uri.Scheme.Equals("localhost", StringComparison.OrdinalIgnoreCase)) - { - int port = 0; - var portAsString = uri.AbsolutePath.RemoveTrailingSlash(); - if (!portAsString.IsNullOrWhiteSpace()) - { - int.TryParse(portAsString, out port); - } - - var ub = new UriBuilder(scheme, "localhost", port); - return ub.Uri; - } - } - else - { - if (!IsSchemaHttpOrHttps(uri.Scheme)) - { - throw new RedmineException("Invalid host scheme. Only HTTP and HTTPS are supported."); - } - - hasScheme = true; - } - } - else - { - if (!IsSchemaHttpOrHttps(scheme)) - { - throw new RedmineException("Invalid host scheme. Only HTTP and HTTPS are supported."); - } - } - - var uriBuilder = new UriBuilder(); - - if (uri.HostNameType == UriHostNameType.IPv6) - { - uriBuilder.Scheme = (hasScheme ? uri.Scheme : scheme ?? Uri.UriSchemeHttps); - uriBuilder.Host = uri.Host; - } - else - { - if (uri.Authority.IsNullOrWhiteSpace()) - { - if (uri.Port == -1) - { - if (int.TryParse(uri.LocalPath, out var port)) - { - uriBuilder.Port = port; - } - } - - uriBuilder.Scheme = scheme ?? Uri.UriSchemeHttps; - uriBuilder.Host = uri.Scheme; - } - else - { - uriBuilder.Scheme = uri.Scheme; - uriBuilder.Port = int.TryParse(uri.LocalPath, out var port) ? port : uri.Port; - uriBuilder.Host = uri.Host; - if (!uri.LocalPath.IsNullOrWhiteSpace() && !uri.LocalPath.Contains(".")) - { - uriBuilder.Path = uri.LocalPath; - } - } - } - - try - { - return uriBuilder.Uri; - } - catch (Exception ex) - { - throw new RedmineException(ex.Message); - } - } - - private static bool IsSchemaHttpOrHttps(string scheme) - { - return scheme == Uri.UriSchemeHttp || scheme == Uri.UriSchemeHttps; - } - } -} \ No newline at end of file diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index b7aa954b..6f6c22a4 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -4,6 +4,7 @@ |net20|net40| |net20|net40|net45|net451|net452|net46|net461| + |net45|net451|net452|net46|net461| @@ -91,10 +92,17 @@ + + + + + + + @@ -104,6 +112,10 @@ + + + + From 93de57738fd9498c27d1cd9571393aa20ab6f2b1 Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 28 May 2025 16:54:49 +0300 Subject: [PATCH 532/549] Add more Wiki deserialization tests --- .../Serialization/Xml/WikiTests.cs | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/redmine-net-api.Tests/Serialization/Xml/WikiTests.cs b/tests/redmine-net-api.Tests/Serialization/Xml/WikiTests.cs index b5d3a275..a3dbdbc5 100644 --- a/tests/redmine-net-api.Tests/Serialization/Xml/WikiTests.cs +++ b/tests/redmine-net-api.Tests/Serialization/Xml/WikiTests.cs @@ -77,6 +77,55 @@ public void Should_Deserialize_Wiki_Pages() Assert.Equal(new DateTime(2008, 3, 9, 12, 7, 8, DateTimeKind.Utc).ToLocalTime(), wikiPages[0].CreatedOn); Assert.Equal(new DateTime(2008, 3, 9, 22, 41, 33, DateTimeKind.Utc).ToLocalTime(), wikiPages[0].UpdatedOn); } + + [Fact] + public void Should_Deserialize_Empty_Wiki_Pages() + { + const string input = """ + + + + """; + + var output = fixture.Serializer.DeserializeToPagedResults(input); + + Assert.NotNull(output); + Assert.Equal(0, output.TotalItems); + } + + [Fact] + public void Should_Deserialize_Wiki_With_Attachments() + { + const string input = """ + + + Te$t + QEcISExBVZ + 3 + + uAqCrmSBDUpNMOU + 2025-05-26T16:32:41Z + 2025-05-26T16:43:01Z + + + 155 + test-file_QPqCTEa + 512000 + text/plain + JIIMEcwtuZUsIHY + http://localhost:8089/attachments/download/155/test-file_QPqCTEa + + 2025-05-26T16:32:36Z + + + + """; + + var output = fixture.Serializer.Deserialize(input); + + Assert.NotNull(output); + Assert.Single(output.Attachments); + } } From 54272a35ce3945d4ed819c4fa3490b760239a1c7 Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 28 May 2025 16:46:08 +0300 Subject: [PATCH 533/549] Integration tests Integration Tests --- .../RedmineTestContainerCollection.cs | 2 + .../Fixtures/RedmineTestContainerFixture.cs | 84 +----- .../Helpers/FileGeneratorHelper.cs | 2 +- .../Helpers/IssueTestHelper.cs | 36 --- .../Helpers/RandomHelper.cs | 2 +- .../{TestHelper.cs => ConfigurationHelper.cs} | 5 +- .../Infrastructure/Constants.cs | 2 +- .../Infrastructure/RedmineOptions.cs | 2 +- .../RedmineIntegrationTestsAsync.cs | 249 ------------------ .../RedmineIntegrationTestsSync.cs | 234 ---------------- .../TestData/init-redmine.sql | 3 + .../Tests/Async/AttachmentTestsAsync.cs | 103 -------- .../Tests/Async/FileTestsAsync.cs | 86 ------ .../Tests/Async/GroupTestsAsync.cs | 144 ---------- .../Tests/Async/IssueJournalTestsAsync.cs | 49 ---- .../Tests/Async/IssueRelationTestsAsync.cs | 91 ------- .../Tests/Async/MembershipTestsAsync.cs | 180 ------------- .../Tests/Async/NewsAsyncTests.cs | 55 ---- .../Async/ProjectInformationTestsAsync.cs | 19 -- .../Tests/Async/TimeEntryTests.cs | 114 -------- .../Tests/Async/UserTestsAsync.cs | 112 -------- .../Tests/Async/VersionTestsAsync.cs | 109 -------- .../Tests/Async/WikiTestsAsync.cs | 171 ------------ .../Tests/Common/EmailNotificationType.cs | 29 ++ .../Tests/Common/IssueTestHelper.cs | 82 ++++++ .../Tests/Common/TestConstants.cs | 20 ++ .../Tests/Common/TestEntityFactory.cs | 161 +++++++++++ .../Attachment}/AttachmentTests.cs | 22 +- .../Attachment/AttachmentTestsAsync.cs | 74 ++++++ .../CustomField/CustomFieldTests.cs} | 6 +- .../CustomField/CustomFieldTestsAsync.cs} | 6 +- .../Entities/Enumeration/EnumerationTests.cs | 18 ++ .../Enumeration}/EnumerationTestsAsync.cs | 3 +- .../Tests/Entities/File/FileTests.cs | 102 +++++++ .../Tests/Entities/File/FileTestsAsync.cs | 120 +++++++++ .../Tests/Entities/Group/GroupTests.cs | 111 ++++++++ .../Tests/Entities/Group/GroupTestsAsync.cs | 129 +++++++++ .../Issue}/IssueAttachmentUploadTests.cs | 30 +-- .../Issue}/IssueAttachmentUploadTestsAsync.cs | 28 +- .../Tests/Entities/Issue/IssueTests.cs | 132 ++++++++++ .../Issue}/IssueTestsAsync.cs | 26 +- .../Issue/IssueWatcherTests.cs} | 30 +-- .../Issue}/IssueWatcherTestsAsync.cs | 23 +- .../IssueCategory/IssueCategoryTests.cs} | 3 +- .../IssueCategory}/IssueCategoryTestsAsync.cs | 3 +- .../IssueRelation/IssueRelationTests.cs | 36 +++ .../IssueRelation/IssueRelationTestsAsync.cs | 51 ++++ .../IssueStatus}/IssueStatusTests.cs | 3 +- .../IssueStatus/IssueStatusTestsAsync.cs} | 5 +- .../Journal/JournalTests.cs} | 14 +- .../Journal}/JournalTestsAsync.cs | 24 +- .../Tests/Entities/News/NewsTests.cs | 32 +++ .../Tests/Entities/News/NewsTestsAsync.cs | 52 ++++ .../Project}/ProjectTestsAsync.cs | 16 +- .../ProjectMembershipTests.cs | 84 ++++++ .../ProjectMembershipTestsAsync.cs | 123 +++++++++ .../Tests/Entities/Query/QueryTests.cs | 18 ++ .../Query}/QueryTestsAsync.cs | 6 +- .../Tests/Entities/Role/RoleTests.cs | 19 ++ .../Role}/RoleTestsAsync.cs | 3 +- .../Tests/Entities/Search/SearchTests.cs | 28 ++ .../Search}/SearchTestsAsync.cs | 3 +- .../TimeEntry/TimeEntryActivityTestsAsync.cs} | 3 +- .../Entities/TimeEntry/TimeEntryTestsAsync.cs | 91 +++++++ .../Tracker}/TrackerTestsAsync.cs | 6 +- .../{Async => Entities}/UploadTestsAsync.cs | 10 +- .../Tests/Entities/User/UserTestsAsync.cs | 242 +++++++++++++++++ .../Entities/Version/VersionTestsAsync.cs | 86 ++++++ .../Tests/Entities/Wiki/WikiTestsAsync.cs | 206 +++++++++++++++ .../Tests/Progress/ProgressTests.Async.cs | 15 +- .../Tests/Progress/ProgressTests.cs | 9 +- .../Tests/Sync/EnumerationTestsSync.cs | 12 - .../Tests/Sync/FileUploadTests.cs | 82 ------ .../Tests/Sync/GroupManagementTests.cs | 118 --------- .../Tests/Sync/IssueJournalTestsSync.cs | 37 --- .../Tests/Sync/IssueRelationTests.cs | 58 ---- .../Tests/Sync/IssueTestsAsync.cs | 124 --------- .../Tests/Sync/NewsTestsIntegration.cs | 48 ---- .../Tests/Sync/ProjectMembershipTests.cs | 103 -------- .../appsettings.local.json | 2 +- .../redmine-net-api.Integration.Tests.csproj | 2 +- 81 files changed, 2212 insertions(+), 2571 deletions(-) delete mode 100644 tests/redmine-net-api.Integration.Tests/Helpers/IssueTestHelper.cs rename tests/redmine-net-api.Integration.Tests/Infrastructure/{TestHelper.cs => ConfigurationHelper.cs} (90%) delete mode 100644 tests/redmine-net-api.Integration.Tests/RedmineIntegrationTestsAsync.cs delete mode 100644 tests/redmine-net-api.Integration.Tests/RedmineIntegrationTestsSync.cs delete mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/AttachmentTestsAsync.cs delete mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/FileTestsAsync.cs delete mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/GroupTestsAsync.cs delete mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/IssueJournalTestsAsync.cs delete mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/IssueRelationTestsAsync.cs delete mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/MembershipTestsAsync.cs delete mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/NewsAsyncTests.cs delete mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/ProjectInformationTestsAsync.cs delete mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/TimeEntryTests.cs delete mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/UserTestsAsync.cs delete mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/VersionTestsAsync.cs delete mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Async/WikiTestsAsync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Common/EmailNotificationType.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Common/IssueTestHelper.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Common/TestConstants.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Common/TestEntityFactory.cs rename tests/redmine-net-api.Integration.Tests/Tests/{Sync => Entities/Attachment}/AttachmentTests.cs (53%) create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Entities/Attachment/AttachmentTestsAsync.cs rename tests/redmine-net-api.Integration.Tests/Tests/{Sync/CustomFieldTestsSync.cs => Entities/CustomField/CustomFieldTests.cs} (58%) rename tests/redmine-net-api.Integration.Tests/Tests/{Async/CustomFieldAsyncTests.cs => Entities/CustomField/CustomFieldTestsAsync.cs} (69%) create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Entities/Enumeration/EnumerationTests.cs rename tests/redmine-net-api.Integration.Tests/Tests/{Async => Entities/Enumeration}/EnumerationTestsAsync.cs (86%) create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Entities/File/FileTests.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Entities/File/FileTestsAsync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Entities/Group/GroupTests.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Entities/Group/GroupTestsAsync.cs rename tests/redmine-net-api.Integration.Tests/Tests/{Sync => Entities/Issue}/IssueAttachmentUploadTests.cs (59%) rename tests/redmine-net-api.Integration.Tests/Tests/{Async => Entities/Issue}/IssueAttachmentUploadTestsAsync.cs (55%) create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueTests.cs rename tests/redmine-net-api.Integration.Tests/Tests/{Async => Entities/Issue}/IssueTestsAsync.cs (85%) rename tests/redmine-net-api.Integration.Tests/Tests/{Sync/IssueWatcherTestsAsync.cs => Entities/Issue/IssueWatcherTests.cs} (52%) rename tests/redmine-net-api.Integration.Tests/Tests/{Async => Entities/Issue}/IssueWatcherTestsAsync.cs (73%) rename tests/redmine-net-api.Integration.Tests/Tests/{Sync/IssueCategoryTestsSync.cs => Entities/IssueCategory/IssueCategoryTests.cs} (93%) rename tests/redmine-net-api.Integration.Tests/Tests/{Async => Entities/IssueCategory}/IssueCategoryTestsAsync.cs (95%) create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueRelation/IssueRelationTests.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueRelation/IssueRelationTestsAsync.cs rename tests/redmine-net-api.Integration.Tests/Tests/{Sync => Entities/IssueStatus}/IssueStatusTests.cs (75%) rename tests/redmine-net-api.Integration.Tests/Tests/{Async/IssueStatusAsyncTests.cs => Entities/IssueStatus/IssueStatusTestsAsync.cs} (67%) rename tests/redmine-net-api.Integration.Tests/Tests/{Sync/JournalManagementTests.cs => Entities/Journal/JournalTests.cs} (70%) rename tests/redmine-net-api.Integration.Tests/Tests/{Async => Entities/Journal}/JournalTestsAsync.cs (64%) create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Entities/News/NewsTests.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Entities/News/NewsTestsAsync.cs rename tests/redmine-net-api.Integration.Tests/Tests/{Async => Entities/Project}/ProjectTestsAsync.cs (76%) create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Entities/ProjectMembership/ProjectMembershipTests.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Entities/ProjectMembership/ProjectMembershipTestsAsync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Entities/Query/QueryTests.cs rename tests/redmine-net-api.Integration.Tests/Tests/{Async => Entities/Query}/QueryTestsAsync.cs (58%) create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Entities/Role/RoleTests.cs rename tests/redmine-net-api.Integration.Tests/Tests/{Async => Entities/Role}/RoleTestsAsync.cs (76%) create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Entities/Search/SearchTests.cs rename tests/redmine-net-api.Integration.Tests/Tests/{Async => Entities/Search}/SearchTestsAsync.cs (83%) rename tests/redmine-net-api.Integration.Tests/Tests/{Async/TimeEntryActivityTests.cs => Entities/TimeEntry/TimeEntryActivityTestsAsync.cs} (78%) create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Entities/TimeEntry/TimeEntryTestsAsync.cs rename tests/redmine-net-api.Integration.Tests/Tests/{Async => Entities/Tracker}/TrackerTestsAsync.cs (61%) rename tests/redmine-net-api.Integration.Tests/Tests/{Async => Entities}/UploadTestsAsync.cs (86%) create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Entities/User/UserTestsAsync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Entities/Version/VersionTestsAsync.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Entities/Wiki/WikiTestsAsync.cs delete mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Sync/EnumerationTestsSync.cs delete mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Sync/FileUploadTests.cs delete mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Sync/GroupManagementTests.cs delete mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueJournalTestsSync.cs delete mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueRelationTests.cs delete mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueTestsAsync.cs delete mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Sync/NewsTestsIntegration.cs delete mode 100644 tests/redmine-net-api.Integration.Tests/Tests/Sync/ProjectMembershipTests.cs diff --git a/tests/redmine-net-api.Integration.Tests/Fixtures/RedmineTestContainerCollection.cs b/tests/redmine-net-api.Integration.Tests/Fixtures/RedmineTestContainerCollection.cs index 61b0cafb..0ca114d1 100644 --- a/tests/redmine-net-api.Integration.Tests/Fixtures/RedmineTestContainerCollection.cs +++ b/tests/redmine-net-api.Integration.Tests/Fixtures/RedmineTestContainerCollection.cs @@ -1,3 +1,5 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; + namespace Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; [CollectionDefinition(Constants.RedmineTestContainerCollection)] diff --git a/tests/redmine-net-api.Integration.Tests/Fixtures/RedmineTestContainerFixture.cs b/tests/redmine-net-api.Integration.Tests/Fixtures/RedmineTestContainerFixture.cs index 33173b11..a840849b 100644 --- a/tests/redmine-net-api.Integration.Tests/Fixtures/RedmineTestContainerFixture.cs +++ b/tests/redmine-net-api.Integration.Tests/Fixtures/RedmineTestContainerFixture.cs @@ -3,9 +3,9 @@ using DotNet.Testcontainers.Containers; using DotNet.Testcontainers.Networks; using Npgsql; -using Padi.DotNet.RedmineAPI.Tests; -using Padi.DotNet.RedmineAPI.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; using Redmine.Net.Api; +using Redmine.Net.Api.Options; using Testcontainers.PostgreSql; using Xunit.Abstractions; @@ -35,7 +35,7 @@ public class RedmineTestContainerFixture : IAsyncLifetime public RedmineTestContainerFixture() { - _redmineOptions = TestHelper.GetConfiguration(); + _redmineOptions = ConfigurationHelper.GetConfiguration(); if (_redmineOptions.Mode != TestContainerMode.UseExisting) { @@ -43,48 +43,6 @@ public RedmineTestContainerFixture() } } - // private static string GetExistingRedmineUrl() - // { - // return GetConfigValue("TestContainer:ExistingRedmineUrl") ?? "http://localhost:3000/"; - // } - // - // private static string GetRedmineUsername() - // { - // return GetConfigValue("Redmine:Username") ?? DefaultRedmineUser; - // } - // - // private static string GetRedminePassword() - // { - // return GetConfigValue("Redmine:Password") ?? DefaultRedminePassword; - // } - - /// - /// Gets configuration value from environment variables or appsettings.json - /// - // private static string GetConfigValue(string key) - // { - // var envKey = key.Replace(":", "__"); - // var envValue = Environment.GetEnvironmentVariable(envKey); - // if (!string.IsNullOrEmpty(envValue)) - // { - // return envValue; - // } - // - // try - // { - // var config = new ConfigurationBuilder() - // .AddJsonFile("appsettings.json", optional: true) - // .AddJsonFile("appsettings.local.json", optional: true) - // .Build(); - // - // return config[key]; - // } - // catch - // { - // return null; - // } - // } - /// /// Detects if running in a CI/CD environment /// @@ -94,30 +52,6 @@ private static bool IsRunningInCiEnvironment() !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("GITHUB_ACTIONS")); } - - /// - /// Gets container mode from configuration - /// - // private static TestContainerMode GetContainerMode() - // { - // var mode = GetConfigValue("TestContainer:Mode"); - // - // if (string.IsNullOrEmpty(mode)) - // { - // if (IsRunningInCiEnvironment()) - // { - // return TestContainerMode.CreateNewWithRandomPorts; - // } - // - // return TestContainerMode.CreateNewWithRandomPorts; - // } - // - // return mode.ToLowerInvariant() switch - // { - // "existing" => TestContainerMode.UseExisting, - // _ => TestContainerMode.CreateNewWithRandomPorts - // }; - // } private void BuildContainers() { @@ -125,8 +59,7 @@ private void BuildContainers() .WithDriver(NetworkDriver.Bridge) .Build(); - var postgresBuilder - = new PostgreSqlBuilder() + var postgresBuilder = new PostgreSqlBuilder() .WithImage(PostgresImage) .WithNetwork(Network) .WithNetworkAliases(RedmineNetworkAlias) @@ -150,8 +83,7 @@ var postgresBuilder PostgresContainer = postgresBuilder.Build(); - var redmineBuilder - = new ContainerBuilder() + var redmineBuilder = new ContainerBuilder() .WithImage(RedmineImage) .WithNetwork(Network) .WithPortBinding(RedminePort, assignRandomHostPort: true) @@ -213,7 +145,11 @@ public async Task InitializeAsync() RedmineHost = $"http://{RedmineContainer.Hostname}:{RedmineContainer.GetMappedPublicPort(RedminePort)}"; } - rmgBuilder.WithHost(RedmineHost); + rmgBuilder + .WithHost(RedmineHost) + .UseHttpClient() + //.UseWebClient() + .WithXmlSerialization(); RedmineManager = new RedmineManager(rmgBuilder); } diff --git a/tests/redmine-net-api.Integration.Tests/Helpers/FileGeneratorHelper.cs b/tests/redmine-net-api.Integration.Tests/Helpers/FileGeneratorHelper.cs index 62975276..7ec2cc47 100644 --- a/tests/redmine-net-api.Integration.Tests/Helpers/FileGeneratorHelper.cs +++ b/tests/redmine-net-api.Integration.Tests/Helpers/FileGeneratorHelper.cs @@ -1,6 +1,6 @@ using System.Text; using Redmine.Net.Api; -using Redmine.Net.Api.Net; +using Redmine.Net.Api.Http; using Redmine.Net.Api.Types; using File = System.IO.File; diff --git a/tests/redmine-net-api.Integration.Tests/Helpers/IssueTestHelper.cs b/tests/redmine-net-api.Integration.Tests/Helpers/IssueTestHelper.cs deleted file mode 100644 index 5fbae108..00000000 --- a/tests/redmine-net-api.Integration.Tests/Helpers/IssueTestHelper.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; - -internal static class IssueTestHelper -{ - internal static readonly IdentifiableName ProjectIdName = IdentifiableName.Create(1); - - internal static Issue CreateIssue(List customFields = null, List watchers = null, - List uploads = null) - => new() - { - Project = ProjectIdName, - Subject = RandomHelper.GenerateText(9), - Description = RandomHelper.GenerateText(18), - Tracker = 1.ToIdentifier(), - Status = 1.ToIssueStatusIdentifier(), - Priority = 2.ToIdentifier(), - CustomFields = customFields, - Watchers = watchers, - Uploads = uploads - }; - - internal static void AssertBasic(Issue expected, Issue actual) - { - Assert.NotNull(actual); - Assert.True(actual.Id > 0); - Assert.Equal(expected.Subject, actual.Subject); - Assert.Equal(expected.Description, actual.Description); - Assert.Equal(expected.Project.Id, actual.Project.Id); - Assert.Equal(expected.Tracker.Id, actual.Tracker.Id); - Assert.Equal(expected.Status.Id, actual.Status.Id); - Assert.Equal(expected.Priority.Id, actual.Priority.Id); - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Helpers/RandomHelper.cs b/tests/redmine-net-api.Integration.Tests/Helpers/RandomHelper.cs index e7814fc8..822a7984 100644 --- a/tests/redmine-net-api.Integration.Tests/Helpers/RandomHelper.cs +++ b/tests/redmine-net-api.Integration.Tests/Helpers/RandomHelper.cs @@ -1,6 +1,6 @@ using System.Text; -namespace Padi.DotNet.RedmineAPI.Integration.Tests; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; internal static class RandomHelper { diff --git a/tests/redmine-net-api.Integration.Tests/Infrastructure/TestHelper.cs b/tests/redmine-net-api.Integration.Tests/Infrastructure/ConfigurationHelper.cs similarity index 90% rename from tests/redmine-net-api.Integration.Tests/Infrastructure/TestHelper.cs rename to tests/redmine-net-api.Integration.Tests/Infrastructure/ConfigurationHelper.cs index 418058e6..19a92aa9 100644 --- a/tests/redmine-net-api.Integration.Tests/Infrastructure/TestHelper.cs +++ b/tests/redmine-net-api.Integration.Tests/Infrastructure/ConfigurationHelper.cs @@ -1,9 +1,8 @@ using Microsoft.Extensions.Configuration; -using Padi.DotNet.RedmineAPI.Tests.Infrastructure; -namespace Padi.DotNet.RedmineAPI.Tests +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure { - internal static class TestHelper + internal static class ConfigurationHelper { private static IConfigurationRoot GetIConfigurationRoot(string outputPath) { diff --git a/tests/redmine-net-api.Integration.Tests/Infrastructure/Constants.cs b/tests/redmine-net-api.Integration.Tests/Infrastructure/Constants.cs index 93861d62..85806f08 100644 --- a/tests/redmine-net-api.Integration.Tests/Infrastructure/Constants.cs +++ b/tests/redmine-net-api.Integration.Tests/Infrastructure/Constants.cs @@ -1,4 +1,4 @@ -namespace Padi.DotNet.RedmineAPI.Integration.Tests; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; public static class Constants { diff --git a/tests/redmine-net-api.Integration.Tests/Infrastructure/RedmineOptions.cs b/tests/redmine-net-api.Integration.Tests/Infrastructure/RedmineOptions.cs index 6139c23b..2c5f5d09 100644 --- a/tests/redmine-net-api.Integration.Tests/Infrastructure/RedmineOptions.cs +++ b/tests/redmine-net-api.Integration.Tests/Infrastructure/RedmineOptions.cs @@ -1,6 +1,6 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -namespace Padi.DotNet.RedmineAPI.Tests.Infrastructure +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure { public sealed class TestContainerOptions { diff --git a/tests/redmine-net-api.Integration.Tests/RedmineIntegrationTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/RedmineIntegrationTestsAsync.cs deleted file mode 100644 index 356fa51e..00000000 --- a/tests/redmine-net-api.Integration.Tests/RedmineIntegrationTestsAsync.cs +++ /dev/null @@ -1,249 +0,0 @@ -using System.Collections.Specialized; -using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Redmine.Net.Api; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Net; -using Redmine.Net.Api.Types; -using Version = Redmine.Net.Api.Types.Version; - -namespace Padi.DotNet.RedmineAPI.Integration.Tests; - -[Collection(Constants.RedmineTestContainerCollection)] -public class RedmineIntegrationTestsAsync(RedmineTestContainerFixture fixture) -{ - private readonly RedmineManager _redmineManager = fixture.RedmineManager; - - [Fact] - public async Task Should_ReturnProjectsAsync() - { - var list = await _redmineManager.GetAsync(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public async Task Should_ReturnRolesAsync() - { - var list = await _redmineManager.GetAsync(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public async Task Should_ReturnAttachmentsAsync() - { - var list = await _redmineManager.GetAsync(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public async Task Should_ReturnCustomFieldsAsync() - { - var list = await _redmineManager.GetAsync(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public async Task Should_ReturnGroupsAsync() - { - var list = await _redmineManager.GetAsync(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public async Task Should_ReturnFilesAsync() - { - var list = await _redmineManager.GetAsync(new RequestOptions() - { - QueryString = new NameValueCollection() - { - { RedmineKeys.PROJECT_ID, 1.ToString() } - } - }); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public async Task Should_ReturnIssuesAsync() - { - var list = await _redmineManager.GetAsync(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public async Task Should_ReturnIssueWithVersionsAsync() - { - var issue = await _redmineManager.GetAsync(5.ToInvariantString(), - new RequestOptions { - QueryString = new NameValueCollection() - { - { RedmineKeys.INCLUDE, RedmineKeys.WATCHERS } - } - } - ); - Assert.NotNull(issue); - } - - [Fact] - public async Task Should_ReturnIssueCategoriesAsync() - { - var list = await _redmineManager.GetAsync(new RequestOptions() - { - QueryString = new NameValueCollection() - { - { RedmineKeys.PROJECT_ID, 1.ToString() } - } - }); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public async Task Should_ReturnIssueCustomFieldsAsync() - { - var list = await _redmineManager.GetAsync(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public async Task Should_ReturnIssuePrioritiesAsync() - { - var list = await _redmineManager.GetAsync(new RequestOptions() - { - QueryString = new NameValueCollection() - { - { RedmineKeys.ISSUE_ID, 1.ToString() } - } - }); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public async Task Should_ReturnIssueRelationsAsync() - { - var list = await _redmineManager.GetAsync(new RequestOptions() - { - QueryString = new NameValueCollection() - { - { RedmineKeys.ISSUE_ID, 1.ToString() } - } - }); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public async Task Should_ReturnIssueStatusesAsync() - { - var list = await _redmineManager.GetAsync(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public async Task Should_ReturnJournalsAsync() - { - var list = await _redmineManager.GetAsync(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public async Task Should_ReturnNewsAsync() - { - var list = await _redmineManager.GetAsync(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public async Task Should_ReturnProjectMembershipsAsync() - { - var list = await _redmineManager.GetAsync(new RequestOptions() - { - QueryString = new NameValueCollection() - { - { RedmineKeys.PROJECT_ID, 1.ToString() } - } - }); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public async Task Should_ReturnQueriesAsync() - { - var list = await _redmineManager.GetAsync(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public async Task Should_ReturnSearchesAsync() - { - var list = await _redmineManager.GetAsync(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public async Task Should_ReturnTimeEntriesAsync() - { - var list = await _redmineManager.GetAsync(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public async Task Should_ReturnTimeEntryActivitiesAsync() - { - var list = await _redmineManager.GetAsync(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public async Task Should_ReturnTrackersAsync() - { - var list = await _redmineManager.GetAsync(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public async Task Should_ReturnUsersAsync() - { - var list = await _redmineManager.GetAsync(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public async Task Should_ReturnVersionsAsync() - { - var list = await _redmineManager.GetAsync(new RequestOptions() - { - QueryString = new NameValueCollection() - { - { RedmineKeys.PROJECT_ID, 1.ToString() } - } - }); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public async Task Should_ReturnWatchersAsync() - { - var list = await _redmineManager.GetAsync(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/RedmineIntegrationTestsSync.cs b/tests/redmine-net-api.Integration.Tests/RedmineIntegrationTestsSync.cs deleted file mode 100644 index 024b719b..00000000 --- a/tests/redmine-net-api.Integration.Tests/RedmineIntegrationTestsSync.cs +++ /dev/null @@ -1,234 +0,0 @@ -using System.Collections.Specialized; -using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Redmine.Net.Api; -using Redmine.Net.Api.Net; -using Redmine.Net.Api.Types; -using Version = Redmine.Net.Api.Types.Version; - -namespace Padi.DotNet.RedmineAPI.Integration.Tests; - -[Collection(Constants.RedmineTestContainerCollection)] -public class RedmineIntegrationTestsSync(RedmineTestContainerFixture fixture) -{ - private readonly RedmineManager _redmineManager = fixture.RedmineManager; - - [Fact] - public void Should_ReturnProjects() - { - var list = _redmineManager.Get(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public void Should_ReturnRoles() - { - var list = _redmineManager.Get(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public void Should_ReturnAttachments() - { - var list = _redmineManager.Get(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public void Should_ReturnCustomFields() - { - var list = _redmineManager.Get(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public void Should_ReturnGroups() - { - var list = _redmineManager.Get(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public void Should_ReturnFiles() - { - var list = _redmineManager.Get(new RequestOptions() - { - QueryString = new NameValueCollection() - { - { RedmineKeys.PROJECT_ID, 1.ToString() } - } - }); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public void Should_ReturnIssues() - { - var list = _redmineManager.Get(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public void Should_ReturnIssueCategories() - { - var list = _redmineManager.Get(new RequestOptions() - { - QueryString = new NameValueCollection() - { - { RedmineKeys.PROJECT_ID, 1.ToString() } - } - }); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public void Should_ReturnIssueCustomFields() - { - var list = _redmineManager.Get(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public void Should_ReturnIssuePriorities() - { - var list = _redmineManager.Get(new RequestOptions() - { - QueryString = new NameValueCollection() - { - { RedmineKeys.ISSUE_ID, 1.ToString() } - } - }); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public void Should_ReturnIssueRelations() - { - var list = _redmineManager.Get(new RequestOptions() - { - QueryString = new NameValueCollection() - { - { RedmineKeys.ISSUE_ID, 1.ToString() } - } - }); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public void Should_ReturnIssueStatuses() - { - var list = _redmineManager.Get(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public void Should_ReturnJournals() - { - var list = _redmineManager.Get(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public void Should_ReturnNews() - { - var list = _redmineManager.Get(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public void Should_ReturnProjectMemberships() - { - var list = _redmineManager.Get(new RequestOptions() - { - QueryString = new NameValueCollection() - { - { RedmineKeys.PROJECT_ID, 1.ToString() } - } - }); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public void Should_ReturnQueries() - { - var list = _redmineManager.Get(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public void Should_ReturnSearches() - { - var list = _redmineManager.Get(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public void Should_ReturnTimeEntries() - { - var list = _redmineManager.Get(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public void Should_ReturnTimeEntryActivities() - { - var list = _redmineManager.Get(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public void Should_ReturnTrackers() - { - var list = _redmineManager.Get(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public void Should_ReturnUsers() - { - var list = _redmineManager.Get(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public void Should_ReturnVersions() - { - var list = _redmineManager.Get(new RequestOptions() - { - QueryString = new NameValueCollection() - { - { RedmineKeys.PROJECT_ID, 1.ToString() } - } - }); - Assert.NotNull(list); - Assert.NotEmpty(list); - } - - [Fact] - public void Should_ReturnWatchers() - { - var list = _redmineManager.Get(); - Assert.NotNull(list); - Assert.NotEmpty(list); - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/TestData/init-redmine.sql b/tests/redmine-net-api.Integration.Tests/TestData/init-redmine.sql index d511ec2b..85fabbf1 100644 --- a/tests/redmine-net-api.Integration.Tests/TestData/init-redmine.sql +++ b/tests/redmine-net-api.Integration.Tests/TestData/init-redmine.sql @@ -53,6 +53,7 @@ values (1, 'Bug', 1, false, 0, 1, null), insert into projects (id, name, description, homepage, is_public, parent_id, created_on, updated_on, identifier, status, lft, rgt, inherit_members, default_version_id, default_assigned_to_id, default_issue_query_id) values (1, 'Project-Test', null, '', true, null, '2024-09-02 10:14:33.789394', '2024-09-02 10:14:33.789394', 'project-test', 1, 1, 2, false, null, null, null); +insert into public.wikis (id, project_id, start_page, status) values (1, 1, 'Wiki', 1); insert into versions (id, project_id, name, description, effective_date, created_on, updated_on, wiki_page_title, status, sharing) values (1, 1, 'version1', '', null, '2025-04-28 17:56:49.245993', '2025-04-28 17:56:49.245993', '', 'open', 'none'), @@ -65,3 +66,5 @@ values (5, 1, 1, '#380', '', null, 1, 1, null, 2, 2, 90, 1, '2025-04-28 17:58:4 insert into watchers (id, watchable_type, watchable_id, user_id) values (8, 'Issue', 5, 90), (9, 'Issue', 5, 91); + + diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/AttachmentTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/AttachmentTestsAsync.cs deleted file mode 100644 index 4750b31f..00000000 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/AttachmentTestsAsync.cs +++ /dev/null @@ -1,103 +0,0 @@ -using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; -using Redmine.Net.Api.Net; -using Redmine.Net.Api.Types; - -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; - -[Collection(Constants.RedmineTestContainerCollection)] -public class AttachmentTestsAsync(RedmineTestContainerFixture fixture) -{ - [Fact] - public async Task CreateIssueWithAttachment_Should_Succeed() - { - // Arrange - var upload = FileTestHelper.UploadRandom500KbFile(fixture.RedmineManager); - Assert.NotNull(upload); - - // Act - var issue = IssueTestHelper.CreateIssue(uploads: [upload]); - var createdIssue = await fixture.RedmineManager.CreateAsync(issue); - - // Assert - Assert.NotNull(createdIssue); - Assert.True(createdIssue.Id > 0); - } - - [Fact] - public async Task GetIssueWithAttachments_Should_Succeed() - { - // Arrange - var upload = FileTestHelper.UploadRandom500KbFile(fixture.RedmineManager); - var issue = IssueTestHelper.CreateIssue(uploads: [upload]); - var createdIssue = await fixture.RedmineManager.CreateAsync(issue); - - // Act - var retrievedIssue = await fixture.RedmineManager.GetAsync( - createdIssue.Id.ToString(), - RequestOptions.Include("attachments")); - - // Assert - Assert.NotNull(retrievedIssue); - Assert.NotNull(retrievedIssue.Attachments); - Assert.NotEmpty(retrievedIssue.Attachments); - } - - [Fact] - public async Task GetAttachmentById_Should_Succeed() - { - // Arrange - var upload = FileTestHelper.UploadRandom500KbFile(fixture.RedmineManager); - var issue = IssueTestHelper.CreateIssue(uploads: [upload]); - var createdIssue = await fixture.RedmineManager.CreateAsync(issue); - - var retrievedIssue = await fixture.RedmineManager.GetAsync( - createdIssue.Id.ToString(), - RequestOptions.Include("attachments")); - - var attachment = retrievedIssue.Attachments.FirstOrDefault(); - Assert.NotNull(attachment); - - // Act - var downloadedAttachment = await fixture.RedmineManager.GetAsync(attachment.Id.ToString()); - - // Assert - Assert.NotNull(downloadedAttachment); - Assert.Equal(attachment.Id, downloadedAttachment.Id); - Assert.Equal(attachment.FileName, downloadedAttachment.FileName); - } - - [Fact] - public async Task UploadLargeFile_Should_Succeed() - { - // Arrange & Act - var upload = await FileTestHelper.UploadRandom1MbFileAsync(fixture.RedmineManager); - - // Assert - Assert.NotNull(upload); - Assert.NotEmpty(upload.Token); - } - - [Fact] - public async Task UploadMultipleFiles_Should_Succeed() - { - // Arrange & Act - var upload1 = await FileTestHelper.UploadRandom500KbFileAsync(fixture.RedmineManager); - Assert.NotNull(upload1); - Assert.NotEmpty(upload1.Token); - - var upload2 = await FileTestHelper.UploadRandom500KbFileAsync(fixture.RedmineManager); - Assert.NotNull(upload2); - Assert.NotEmpty(upload2.Token); - - // Assert - var issue = IssueTestHelper.CreateIssue(uploads: [upload1, upload2]); - var createdIssue = await fixture.RedmineManager.CreateAsync(issue); - - var retrievedIssue = await fixture.RedmineManager.GetAsync( - createdIssue.Id.ToString(), - RequestOptions.Include("attachments")); - - Assert.Equal(2, retrievedIssue.Attachments.Count); - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/FileTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/FileTestsAsync.cs deleted file mode 100644 index f678d5d9..00000000 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/FileTestsAsync.cs +++ /dev/null @@ -1,86 +0,0 @@ -using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Extensions; -using File = Redmine.Net.Api.Types.File; - -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; - -[Collection(Constants.RedmineTestContainerCollection)] -public class FileTestsAsync(RedmineTestContainerFixture fixture) -{ - private const string PROJECT_ID = "1"; - - [Fact] - public async Task CreateFile_Should_Succeed() - { - var (_, token) = await UploadFileAsync(); - - var filePayload = new File - { - Token = token, - }; - - var createdFile = await fixture.RedmineManager.CreateAsync(filePayload, PROJECT_ID); - Assert.Null(createdFile); - - var files = await fixture.RedmineManager.GetProjectFilesAsync(PROJECT_ID); - - //Assert - Assert.NotNull(files); - Assert.NotEmpty(files.Items); - } - - [Fact] - public async Task CreateFile_Without_Token_Should_Fail() - { - await Assert.ThrowsAsync(() => fixture.RedmineManager.CreateAsync( - new File { Filename = "VBpMc.txt" }, PROJECT_ID)); - } - - [Fact] - public async Task CreateFile_With_OptionalParameters_Should_Succeed() - { - var (fileName, token) = await UploadFileAsync(); - - var filePayload = new File - { - Token = token, - Filename = fileName, - Description = RandomHelper.GenerateText(9), - ContentType = "text/plain", - }; - - var createdFile = await fixture.RedmineManager.CreateAsync(filePayload, PROJECT_ID); - Assert.Null(createdFile); - } - - [Fact] - public async Task CreateFile_With_Version_Should_Succeed() - { - var (fileName, token) = await UploadFileAsync(); - - var filePayload = new File - { - Token = token, - Filename = fileName, - Description = RandomHelper.GenerateText(9), - ContentType = "text/plain", - Version = 1.ToIdentifier(), - }; - - var createdFile = await fixture.RedmineManager.CreateAsync(filePayload, PROJECT_ID); - Assert.Null(createdFile); - } - - private async Task<(string,string)> UploadFileAsync() - { - var bytes = "Hello World!"u8.ToArray(); - var fileName = $"{RandomHelper.GenerateText(5)}.txt"; - var upload = await fixture.RedmineManager.UploadFileAsync(bytes, fileName); - - Assert.NotNull(upload); - Assert.NotNull(upload.Token); - - return (fileName, upload.Token); - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/GroupTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/GroupTestsAsync.cs deleted file mode 100644 index 313a7b67..00000000 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/GroupTestsAsync.cs +++ /dev/null @@ -1,144 +0,0 @@ -using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Net; -using Redmine.Net.Api.Types; - -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; - -[Collection(Constants.RedmineTestContainerCollection)] -public class GroupTestsAsync(RedmineTestContainerFixture fixture) -{ - private async Task CreateTestGroupAsync() - { - var group = new Group - { - Name = $"Test Group {Guid.NewGuid()}" - }; - - return await fixture.RedmineManager.CreateAsync(group); - } - - [Fact] - public async Task GetAllGroups_Should_Succeed() - { - // Act - var groups = await fixture.RedmineManager.GetAsync(); - - // Assert - Assert.NotNull(groups); - } - - [Fact] - public async Task CreateGroup_Should_Succeed() - { - // Arrange - var group = new Group - { - Name = $"Test Group {Guid.NewGuid()}" - }; - - // Act - var createdGroup = await fixture.RedmineManager.CreateAsync(group); - - // Assert - Assert.NotNull(createdGroup); - Assert.True(createdGroup.Id > 0); - Assert.Equal(group.Name, createdGroup.Name); - } - - [Fact] - public async Task GetGroup_Should_Succeed() - { - // Arrange - var createdGroup = await CreateTestGroupAsync(); - Assert.NotNull(createdGroup); - - // Act - var retrievedGroup = await fixture.RedmineManager.GetAsync(createdGroup.Id.ToInvariantString()); - - // Assert - Assert.NotNull(retrievedGroup); - Assert.Equal(createdGroup.Id, retrievedGroup.Id); - Assert.Equal(createdGroup.Name, retrievedGroup.Name); - } - - [Fact] - public async Task UpdateGroup_Should_Succeed() - { - // Arrange - var createdGroup = await CreateTestGroupAsync(); - Assert.NotNull(createdGroup); - - var updatedName = $"Updated Test Group {Guid.NewGuid()}"; - createdGroup.Name = updatedName; - - // Act - await fixture.RedmineManager.UpdateAsync(createdGroup.Id.ToInvariantString(), createdGroup); - var retrievedGroup = await fixture.RedmineManager.GetAsync(createdGroup.Id.ToInvariantString()); - - // Assert - Assert.NotNull(retrievedGroup); - Assert.Equal(createdGroup.Id, retrievedGroup.Id); - Assert.Equal(updatedName, retrievedGroup.Name); - } - - [Fact] - public async Task DeleteGroup_Should_Succeed() - { - // Arrange - var createdGroup = await CreateTestGroupAsync(); - Assert.NotNull(createdGroup); - - var groupId = createdGroup.Id.ToInvariantString(); - - // Act - await fixture.RedmineManager.DeleteAsync(groupId); - - // Assert - await Assert.ThrowsAsync(async () => - await fixture.RedmineManager.GetAsync(groupId)); - } - - [Fact] - public async Task AddUserToGroup_Should_Succeed() - { - // Arrange - var group = await CreateTestGroupAsync(); - Assert.NotNull(group); - - // Assuming there's at least one user in the system (typically Admin with ID 1) - var userId = 1; - - // Act - await fixture.RedmineManager.AddUserToGroupAsync(group.Id, userId); - var updatedGroup = await fixture.RedmineManager.GetAsync(group.Id.ToString(), RequestOptions.Include("users")); - - // Assert - Assert.NotNull(updatedGroup); - Assert.NotNull(updatedGroup.Users); - Assert.Contains(updatedGroup.Users, u => u.Id == userId); - } - - [Fact] - public async Task RemoveUserFromGroup_Should_Succeed() - { - // Arrange - var group = await CreateTestGroupAsync(); - Assert.NotNull(group); - - // Assuming there's at least one user in the system (typically Admin with ID 1) - var userId = 1; - - // First add the user to the group - await fixture.RedmineManager.AddUserToGroupAsync(group.Id, userId); - - // Act - await fixture.RedmineManager.RemoveUserFromGroupAsync(group.Id, userId); - var updatedGroup = await fixture.RedmineManager.GetAsync(group.Id.ToString(), RequestOptions.Include("users")); - - // Assert - Assert.NotNull(updatedGroup); - // Assert.DoesNotContain(updatedGroup.Users ?? new List(), u => u.Id == userId); - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueJournalTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueJournalTestsAsync.cs deleted file mode 100644 index 73dffec4..00000000 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueJournalTestsAsync.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Redmine.Net.Api.Net; -using Redmine.Net.Api.Types; - -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; - -[Collection(Constants.RedmineTestContainerCollection)] -public class IssueJournalTestsAsync(RedmineTestContainerFixture fixture) -{ - [Fact] - public async Task GetIssueWithJournals_Should_Succeed() - { - // Arrange - // Create an issue - var issue = new Issue - { - Project = new IdentifiableName { Id = 1 }, - Tracker = new IdentifiableName { Id = 1 }, - Status = new IssueStatus { Id = 1 }, - Priority = new IdentifiableName { Id = 4 }, - Subject = $"Test issue for journals {Guid.NewGuid()}", - Description = "Test issue description" - }; - - var createdIssue = await fixture.RedmineManager.CreateAsync(issue); - Assert.NotNull(createdIssue); - - // Update the issue to create a journal entry - var updateIssue = new Issue - { - Notes = "This is a test note that should appear in journals", - Subject = $"Updated subject {Guid.NewGuid()}" - }; - - await fixture.RedmineManager.UpdateAsync(createdIssue.Id.ToString(), updateIssue); - - // Act - // Get the issue with journals - var retrievedIssue = - await fixture.RedmineManager.GetAsync(createdIssue.Id.ToString(), - RequestOptions.Include("journals")); - - // Assert - Assert.NotNull(retrievedIssue); - Assert.NotNull(retrievedIssue.Journals); - Assert.NotEmpty(retrievedIssue.Journals); - Assert.Contains(retrievedIssue.Journals, j => j.Notes?.Contains("test note") == true); - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueRelationTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueRelationTestsAsync.cs deleted file mode 100644 index 1cf208cc..00000000 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueRelationTestsAsync.cs +++ /dev/null @@ -1,91 +0,0 @@ -using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Redmine.Net.Api.Net; -using Redmine.Net.Api.Types; - -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; - -[Collection(Constants.RedmineTestContainerCollection)] -public class IssueRelationTestsAsync(RedmineTestContainerFixture fixture) -{ - private async Task<(Issue firstIssue, Issue secondIssue)> CreateTestIssuesAsync() - { - var issue1 = new Issue - { - Project = new IdentifiableName { Id = 1 }, - Tracker = new IdentifiableName { Id = 1 }, - Status = new IssueStatus { Id = 1 }, - Priority = new IdentifiableName { Id = 4 }, - Subject = $"Test issue 1 subject {Guid.NewGuid()}", - Description = "Test issue 1 description" - }; - - var issue2 = new Issue - { - Project = new IdentifiableName { Id = 1 }, - Tracker = new IdentifiableName { Id = 1 }, - Status = new IssueStatus { Id = 1 }, - Priority = new IdentifiableName { Id = 4 }, - Subject = $"Test issue 2 subject {Guid.NewGuid()}", - Description = "Test issue 2 description" - }; - - var createdIssue1 = await fixture.RedmineManager.CreateAsync(issue1); - var createdIssue2 = await fixture.RedmineManager.CreateAsync(issue2); - - return (createdIssue1, createdIssue2); - } - - private async Task CreateTestIssueRelationAsync() - { - var (issue1, issue2) = await CreateTestIssuesAsync(); - - var relation = new IssueRelation - { - IssueId = issue1.Id, - IssueToId = issue2.Id, - Type = IssueRelationType.Relates - }; - - return await fixture.RedmineManager.CreateAsync( relation, issue1.Id.ToString()); - } - - [Fact] - public async Task CreateIssueRelation_Should_Succeed() - { - // Arrange - var (issue1, issue2) = await CreateTestIssuesAsync(); - - var relation = new IssueRelation - { - IssueId = issue1.Id, - IssueToId = issue2.Id, - Type = IssueRelationType.Relates - }; - - // Act - var createdRelation = await fixture.RedmineManager.CreateAsync(relation, issue1.Id.ToString()); - - // Assert - Assert.NotNull(createdRelation); - Assert.True(createdRelation.Id > 0); - Assert.Equal(relation.IssueId, createdRelation.IssueId); - Assert.Equal(relation.IssueToId, createdRelation.IssueToId); - Assert.Equal(relation.Type, createdRelation.Type); - } - - [Fact] - public async Task DeleteIssueRelation_Should_Succeed() - { - // Arrange - var relation = await CreateTestIssueRelationAsync(); - Assert.NotNull(relation); - - // Act & Assert - await fixture.RedmineManager.DeleteAsync(relation.Id.ToString()); - - // Verify the relation no longer exists by checking the issue doesn't have it - var issue = await fixture.RedmineManager.GetAsync(relation.IssueId.ToString(), RequestOptions.Include("relations")); - - Assert.Null(issue.Relations?.FirstOrDefault(r => r.Id == relation.Id)); - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/MembershipTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/MembershipTestsAsync.cs deleted file mode 100644 index b8f36de3..00000000 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/MembershipTestsAsync.cs +++ /dev/null @@ -1,180 +0,0 @@ -using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; - -[Collection(Constants.RedmineTestContainerCollection)] -public class MembershipTestsAsync(RedmineTestContainerFixture fixture) -{ - private const string PROJECT_ID = "1"; - - private async Task CreateTestMembershipAsync() - { - var roles = await fixture.RedmineManager.GetAsync(); - Assert.NotEmpty(roles); - - var user = new User - { - Login = RandomHelper.GenerateText(10), - FirstName = RandomHelper.GenerateText(8), - LastName = RandomHelper.GenerateText(9), - Email = $"{RandomHelper.GenerateText(5)}@example.com", - Password = "password123", - MustChangePassword = false, - Status = UserStatus.StatusActive - }; - - var createdUser = await fixture.RedmineManager.CreateAsync(user); - Assert.NotNull(createdUser); - - var membership = new ProjectMembership - { - User = new IdentifiableName { Id = createdUser.Id }, - Roles = [new MembershipRole { Id = roles[0].Id }] - }; - - return await fixture.RedmineManager.CreateAsync(membership, PROJECT_ID); - } - - [Fact] - public async Task GetProjectMemberships_Should_Succeed() - { - // Act - var memberships = await fixture.RedmineManager.GetProjectMembershipsAsync(PROJECT_ID); - - // Assert - Assert.NotNull(memberships); - } - - [Fact] - public async Task CreateMembership_Should_Succeed() - { - // Arrange - var roles = await fixture.RedmineManager.GetAsync(); - Assert.NotEmpty(roles); - - var user = new User - { - Login = RandomHelper.GenerateText(10), - FirstName = RandomHelper.GenerateText(8), - LastName = RandomHelper.GenerateText(9), - Email = $"{RandomHelper.GenerateText(5)}@example.com", - Password = "password123", - MustChangePassword = false, - Status = UserStatus.StatusActive - }; - - var createdUser = await fixture.RedmineManager.CreateAsync(user); - Assert.NotNull(createdUser); - - var membership = new ProjectMembership - { - User = new IdentifiableName { Id = createdUser.Id }, - Roles = [new MembershipRole { Id = roles[0].Id }] - }; - - // Act - var createdMembership = await fixture.RedmineManager.CreateAsync(membership, PROJECT_ID); - - // Assert - Assert.NotNull(createdMembership); - Assert.True(createdMembership.Id > 0); - Assert.Equal(membership.User.Id, createdMembership.User.Id); - Assert.NotEmpty(createdMembership.Roles); - } - - [Fact] - public async Task UpdateMembership_Should_Succeed() - { - // Arrange - var membership = await CreateTestMembershipAsync(); - Assert.NotNull(membership); - - var roles = await fixture.RedmineManager.GetAsync(); - Assert.NotEmpty(roles); - - // Change roles - var newRoleId = roles.FirstOrDefault(r => membership.Roles.All(mr => mr.Id != r.Id))?.Id ?? roles.First().Id; - membership.Roles = [new MembershipRole { Id = newRoleId }]; - - // Act - await fixture.RedmineManager.UpdateAsync(membership.Id.ToString(), membership); - - // Get the updated membership from project memberships - var updatedMemberships = await fixture.RedmineManager.GetProjectMembershipsAsync(PROJECT_ID); - var updatedMembership = updatedMemberships.Items.FirstOrDefault(m => m.Id == membership.Id); - - // Assert - Assert.NotNull(updatedMembership); - Assert.Contains(updatedMembership.Roles, r => r.Id == newRoleId); - } - - [Fact] - public async Task DeleteMembership_Should_Succeed() - { - // Arrange - var membership = await CreateTestMembershipAsync(); - Assert.NotNull(membership); - - var membershipId = membership.Id.ToString(); - - // Act - await fixture.RedmineManager.DeleteAsync(membershipId); - - // Get project memberships - var updatedMemberships = await fixture.RedmineManager.GetProjectMembershipsAsync(PROJECT_ID); - - // Assert - Assert.DoesNotContain(updatedMemberships.Items, m => m.Id == membership.Id); - } - - [Fact] - public async Task GetProjectMemberships_ShouldReturnMemberships() - { - // Test implementation - } - - [Fact] - public async Task GetProjectMembership_WithValidId_ShouldReturnMembership() - { - // Test implementation - } - - [Fact] - public async Task CreateProjectMembership_WithValidData_ShouldSucceed() - { - // Test implementation - } - - [Fact] - public async Task CreateProjectMembership_WithInvalidData_ShouldFail() - { - // Test implementation - } - - [Fact] - public async Task UpdateProjectMembership_WithValidData_ShouldSucceed() - { - // Test implementation - } - - [Fact] - public async Task UpdateProjectMembership_WithInvalidData_ShouldFail() - { - // Test implementation - } - - [Fact] - public async Task DeleteProjectMembership_WithValidId_ShouldSucceed() - { - // Test implementation - } - - [Fact] - public async Task DeleteProjectMembership_WithInvalidId_ShouldFail() - { - // Test implementation - } - -} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/NewsAsyncTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/NewsAsyncTests.cs deleted file mode 100644 index ca392425..00000000 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/NewsAsyncTests.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; - -[Collection(Constants.RedmineTestContainerCollection)] -public class NewsTestsAsync(RedmineTestContainerFixture fixture) -{ - private const string PROJECT_ID = "1"; - - [Fact] - public async Task GetAllNews_Should_Succeed() - { - // Arrange - _ = await fixture.RedmineManager.AddProjectNewsAsync(PROJECT_ID, new News() - { - Title = RandomHelper.GenerateText(5), - Summary = RandomHelper.GenerateText(10), - Description = RandomHelper.GenerateText(20), - }); - - _ = await fixture.RedmineManager.AddProjectNewsAsync("2", new News() - { - Title = RandomHelper.GenerateText(5), - Summary = RandomHelper.GenerateText(10), - Description = RandomHelper.GenerateText(20), - }); - - - // Act - var news = await fixture.RedmineManager.GetAsync(); - - // Assert - Assert.NotNull(news); - } - - [Fact] - public async Task GetProjectNews_Should_Succeed() - { - // Arrange - var newsCreated = await fixture.RedmineManager.AddProjectNewsAsync(PROJECT_ID, new News() - { - Title = RandomHelper.GenerateText(5), - Summary = RandomHelper.GenerateText(10), - Description = RandomHelper.GenerateText(20), - }); - - // Act - var news = await fixture.RedmineManager.GetProjectNewsAsync(PROJECT_ID); - - // Assert - Assert.NotNull(news); - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/ProjectInformationTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/ProjectInformationTestsAsync.cs deleted file mode 100644 index 7997d845..00000000 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/ProjectInformationTestsAsync.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Redmine.Net.Api.Extensions; - -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; - -[Collection(Constants.RedmineTestContainerCollection)] -public class ProjectInformationTestsAsync(RedmineTestContainerFixture fixture) -{ - [Fact] - public async Task GetCurrentUserInfo_Should_Succeed() - { - // Act - var currentUser = await fixture.RedmineManager.GetCurrentUserAsync(); - - // Assert - Assert.NotNull(currentUser); - Assert.True(currentUser.Id > 0); - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/TimeEntryTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/TimeEntryTests.cs deleted file mode 100644 index 4039c6c6..00000000 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/TimeEntryTests.cs +++ /dev/null @@ -1,114 +0,0 @@ -using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; - -[Collection(Constants.RedmineTestContainerCollection)] -public class TimeEntryTestsAsync(RedmineTestContainerFixture fixture) -{ - private async Task CreateTestTimeEntryAsync() - { - var project = await fixture.RedmineManager.GetAsync(1.ToInvariantString()); - var issueData = IssueTestHelper.CreateIssue(); - var issue = await fixture.RedmineManager.CreateAsync(issueData); - - var timeEntry = new TimeEntry - { - Project = project, - Issue = issue.ToIdentifiableName(), - SpentOn = DateTime.Now.Date, - Hours = 1.5m, - Activity = 8.ToIdentifier(), - Comments = $"Test time entry comments {Guid.NewGuid()}", - }; - return await fixture.RedmineManager.CreateAsync(timeEntry); - } - - [Fact] - public async Task CreateTimeEntry_Should_Succeed() - { - //Arrange - var issueData = IssueTestHelper.CreateIssue(); - var issue = await fixture.RedmineManager.CreateAsync(issueData); - var timeEntryData = new TimeEntry - { - Project = 1.ToIdentifier(), - Issue = issue.ToIdentifiableName(), - SpentOn = DateTime.Now.Date, - Hours = 1.5m, - Activity = 8.ToIdentifier(), - Comments = $"Initial create test comments {Guid.NewGuid()}", - }; - - //Act - var createdTimeEntry = await fixture.RedmineManager.CreateAsync(timeEntryData); - - //Assert - Assert.NotNull(createdTimeEntry); - Assert.True(createdTimeEntry.Id > 0); - Assert.Equal(timeEntryData.Hours, createdTimeEntry.Hours); - Assert.Equal(timeEntryData.Comments, createdTimeEntry.Comments); - Assert.Equal(timeEntryData.Project.Id, createdTimeEntry.Project.Id); - Assert.Equal(timeEntryData.Issue.Id, createdTimeEntry.Issue.Id); - Assert.Equal(timeEntryData.Activity.Id, createdTimeEntry.Activity.Id); - } - - [Fact] - public async Task GetTimeEntry_Should_Succeed() - { - //Arrange - var createdTimeEntry = await CreateTestTimeEntryAsync(); - Assert.NotNull(createdTimeEntry); - - //Act - var retrievedTimeEntry = await fixture.RedmineManager.GetAsync(createdTimeEntry.Id.ToInvariantString()); - - //Assert - Assert.NotNull(retrievedTimeEntry); - Assert.Equal(createdTimeEntry.Id, retrievedTimeEntry.Id); - Assert.Equal(createdTimeEntry.Hours, retrievedTimeEntry.Hours); - Assert.Equal(createdTimeEntry.Comments, retrievedTimeEntry.Comments); - } - - [Fact] - public async Task UpdateTimeEntry_Should_Succeed() - { - //Arrange - var createdTimeEntry = await CreateTestTimeEntryAsync(); - Assert.NotNull(createdTimeEntry); - - var updatedComments = $"Updated test time entry comments {Guid.NewGuid()}"; - var updatedHours = 2.5m; - createdTimeEntry.Comments = updatedComments; - createdTimeEntry.Hours = updatedHours; - - //Act - await fixture.RedmineManager.UpdateAsync(createdTimeEntry.Id.ToInvariantString(), createdTimeEntry); - var retrievedTimeEntry = await fixture.RedmineManager.GetAsync(createdTimeEntry.Id.ToInvariantString()); - - //Assert - Assert.NotNull(retrievedTimeEntry); - Assert.Equal(createdTimeEntry.Id, retrievedTimeEntry.Id); - Assert.Equal(updatedComments, retrievedTimeEntry.Comments); - Assert.Equal(updatedHours, retrievedTimeEntry.Hours); - } - - [Fact] - public async Task DeleteTimeEntry_Should_Succeed() - { - //Arrange - var createdTimeEntry = await CreateTestTimeEntryAsync(); - Assert.NotNull(createdTimeEntry); - - var timeEntryId = createdTimeEntry.Id.ToInvariantString(); - - //Act - await fixture.RedmineManager.DeleteAsync(timeEntryId); - - //Assert - await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetAsync(timeEntryId)); - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/UserTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/UserTestsAsync.cs deleted file mode 100644 index d974a0c8..00000000 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/UserTestsAsync.cs +++ /dev/null @@ -1,112 +0,0 @@ -using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; - -[Collection(Constants.RedmineTestContainerCollection)] -public class UserTestsAsync(RedmineTestContainerFixture fixture) -{ - private async Task CreateTestUserAsync() - { - var user = new User - { - Login = RandomHelper.GenerateText(12), - FirstName = RandomHelper.GenerateText(8), - LastName = RandomHelper.GenerateText(10), - Email = $"{RandomHelper.GenerateText(5)}.{RandomHelper.GenerateText(4)}@gmail.com", - Password = "password123", - AuthenticationModeId = null, - MustChangePassword = false, - Status = UserStatus.StatusActive - }; - return await fixture.RedmineManager.CreateAsync(user); - } - - [Fact] - public async Task CreateUser_Should_Succeed() - { - //Arrange - var userData = new User - { - Login = RandomHelper.GenerateText(5), - FirstName = RandomHelper.GenerateText(5), - LastName = RandomHelper.GenerateText(5), - Password = "password123", - MailNotification = "only_my_events", - AuthenticationModeId = null, - MustChangePassword = false, - Status = UserStatus.StatusActive, - }; - - userData.Email = $"{userData.FirstName}.{userData.LastName}@gmail.com"; - - //Act - var createdUser = await fixture.RedmineManager.CreateAsync(userData); - - //Assert - Assert.NotNull(createdUser); - Assert.True(createdUser.Id > 0); - Assert.Equal(userData.Login, createdUser.Login); - Assert.Equal(userData.FirstName, createdUser.FirstName); - Assert.Equal(userData.LastName, createdUser.LastName); - Assert.Equal(userData.Email, createdUser.Email); - } - - [Fact] - public async Task GetUser_Should_Succeed() - { - - //Arrange - var createdUser = await CreateTestUserAsync(); - Assert.NotNull(createdUser); - - //Act - var retrievedUser = - await fixture.RedmineManager.GetAsync(createdUser.Id.ToInvariantString()); - - //Assert - Assert.NotNull(retrievedUser); - Assert.Equal(createdUser.Id, retrievedUser.Id); - Assert.Equal(createdUser.Login, retrievedUser.Login); - Assert.Equal(createdUser.FirstName, retrievedUser.FirstName); - } - - [Fact] - public async Task UpdateUser_Should_Succeed() - { - - //Arrange - var createdUser = await CreateTestUserAsync(); - Assert.NotNull(createdUser); - - var updatedFirstName = RandomHelper.GenerateText(10); - createdUser.FirstName = updatedFirstName; - - //Act - await fixture.RedmineManager.UpdateAsync(createdUser.Id.ToInvariantString(), createdUser); - var retrievedUser = - await fixture.RedmineManager.GetAsync(createdUser.Id.ToInvariantString()); - - //Assert - Assert.NotNull(retrievedUser); - Assert.Equal(createdUser.Id, retrievedUser.Id); - Assert.Equal(updatedFirstName, retrievedUser.FirstName); - } - - [Fact] - public async Task DeleteUser_Should_Succeed() - { - //Arrange - var createdUser = await CreateTestUserAsync(); - Assert.NotNull(createdUser); - var userId = createdUser.Id.ToInvariantString(); - - //Act - await fixture.RedmineManager.DeleteAsync(userId); - - //Assert - await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetAsync(userId)); - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/VersionTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/VersionTestsAsync.cs deleted file mode 100644 index 4fa4b834..00000000 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/VersionTestsAsync.cs +++ /dev/null @@ -1,109 +0,0 @@ -using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; -using Version = Redmine.Net.Api.Types.Version; - -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; - -[Collection(Constants.RedmineTestContainerCollection)] -public class VersionTestsAsync(RedmineTestContainerFixture fixture) -{ - private const string PROJECT_ID = "1"; - - private async Task CreateTestVersionAsync() - { - var version = new Version - { - Name = RandomHelper.GenerateText(10), - Description = RandomHelper.GenerateText(15), - Status = VersionStatus.Open, - Sharing = VersionSharing.None, - DueDate = DateTime.Now.Date.AddDays(30) - }; - return await fixture.RedmineManager.CreateAsync(version, PROJECT_ID); - } - - [Fact] - public async Task CreateVersion_Should_Succeed() - { - //Arrange - var versionSuffix = RandomHelper.GenerateText(6); - var versionData = new Version - { - Name = $"Test Version Create {versionSuffix}", - Description = $"Initial create test description {Guid.NewGuid()}", - Status = VersionStatus.Open, - Sharing = VersionSharing.System, - DueDate = DateTime.Now.Date.AddDays(10) - }; - - //Act - var createdVersion = await fixture.RedmineManager.CreateAsync(versionData, PROJECT_ID); - - //Assert - Assert.NotNull(createdVersion); - Assert.True(createdVersion.Id > 0); - Assert.Equal(versionData.Name, createdVersion.Name); - Assert.Equal(versionData.Description, createdVersion.Description); - Assert.Equal(versionData.Status, createdVersion.Status); - Assert.Equal(PROJECT_ID, createdVersion.Project.Id.ToInvariantString()); - } - - [Fact] - public async Task GetVersion_Should_Succeed() - { - - //Arrange - var createdVersion = await CreateTestVersionAsync(); - Assert.NotNull(createdVersion); - - //Act - var retrievedVersion = await fixture.RedmineManager.GetAsync(createdVersion.Id.ToInvariantString()); - - //Assert - Assert.NotNull(retrievedVersion); - Assert.Equal(createdVersion.Id, retrievedVersion.Id); - Assert.Equal(createdVersion.Name, retrievedVersion.Name); - Assert.Equal(createdVersion.Description, retrievedVersion.Description); - } - - [Fact] - public async Task UpdateVersion_Should_Succeed() - { - //Arrange - var createdVersion = await CreateTestVersionAsync(); - Assert.NotNull(createdVersion); - - var updatedDescription = RandomHelper.GenerateText(20); - var updatedStatus = VersionStatus.Locked; - createdVersion.Description = updatedDescription; - createdVersion.Status = updatedStatus; - - //Act - await fixture.RedmineManager.UpdateAsync(createdVersion.Id.ToInvariantString(), createdVersion); - var retrievedVersion = await fixture.RedmineManager.GetAsync(createdVersion.Id.ToInvariantString()); - - //Assert - Assert.NotNull(retrievedVersion); - Assert.Equal(createdVersion.Id, retrievedVersion.Id); - Assert.Equal(updatedDescription, retrievedVersion.Description); - Assert.Equal(updatedStatus, retrievedVersion.Status); - } - - [Fact] - public async Task DeleteVersion_Should_Succeed() - { - //Arrange - var createdVersion = await CreateTestVersionAsync(); - Assert.NotNull(createdVersion); - var versionId = createdVersion.Id.ToInvariantString(); - - //Act - await fixture.RedmineManager.DeleteAsync(versionId); - - //Assert - await Assert.ThrowsAsync(async () => - await fixture.RedmineManager.GetAsync(versionId)); - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/WikiTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Async/WikiTestsAsync.cs deleted file mode 100644 index 1325a656..00000000 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/WikiTestsAsync.cs +++ /dev/null @@ -1,171 +0,0 @@ -using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; - -[Collection(Constants.RedmineTestContainerCollection)] -public class WikiTestsAsync(RedmineTestContainerFixture fixture) -{ - private const string PROJECT_ID = "1"; - private const string WIKI_PAGE_TITLE = "TestWikiPage"; - - private async Task CreateOrUpdateTestWikiPageAsync() - { - var wikiPage = new WikiPage - { - Title = WIKI_PAGE_TITLE, - Text = $"Test wiki page content {Guid.NewGuid()}", - Comments = "Initial wiki page creation" - }; - - return await fixture.RedmineManager.CreateWikiPageAsync(PROJECT_ID, "wikiPageName", wikiPage); - } - - [Fact] - public async Task CreateOrUpdateWikiPage_Should_Succeed() - { - // Arrange - var wikiPage = new WikiPage - { - Title = $"TestWikiPage_{Guid.NewGuid()}".Replace("-", "").Substring(0, 20), - Text = "Test wiki page content", - Comments = "Initial wiki page creation" - }; - - // Act - var createdPage = await fixture.RedmineManager.CreateWikiPageAsync(PROJECT_ID, "wikiPageName", wikiPage); - - // Assert - Assert.Null(createdPage); - } - - [Fact] - public async Task GetWikiPage_Should_Succeed() - { - // Arrange - var createdPage = await CreateOrUpdateTestWikiPageAsync(); - Assert.Null(createdPage); - - // Act - var retrievedPage = await fixture.RedmineManager.GetWikiPageAsync(PROJECT_ID, WIKI_PAGE_TITLE); - - // Assert - Assert.NotNull(retrievedPage); - Assert.Equal(createdPage.Title, retrievedPage.Title); - Assert.Equal(createdPage.Text, retrievedPage.Text); - } - - [Fact] - public async Task GetAllWikiPages_Should_Succeed() - { - // Arrange - await CreateOrUpdateTestWikiPageAsync(); - - // Act - var wikiPages = await fixture.RedmineManager.GetAllWikiPagesAsync(PROJECT_ID); - - // Assert - Assert.NotNull(wikiPages); - Assert.NotEmpty(wikiPages); - } - - [Fact] - public async Task DeleteWikiPage_Should_Succeed() - { - // Arrange - var wikiPageName = RandomHelper.GenerateText(7); - - var wikiPage = new WikiPage - { - Title = RandomHelper.GenerateText(5), - Text = "Test wiki page content for deletion", - Comments = "Initial wiki page creation for deletion test" - }; - - var createdPage = await fixture.RedmineManager.CreateWikiPageAsync(PROJECT_ID, wikiPageName, wikiPage); - Assert.NotNull(createdPage); - - // Act - await fixture.RedmineManager.DeleteWikiPageAsync(PROJECT_ID, wikiPageName); - - // Assert - await Assert.ThrowsAsync(async () => - await fixture.RedmineManager.GetWikiPageAsync(PROJECT_ID, wikiPageName)); - } - - private async Task<(string pageTitle, string ProjectId, string PageTitle)> CreateTestWikiPageAsync( - string pageTitleSuffix = null, - string initialText = "Default initial text for wiki page.", - string initialComments = "Initial comments for wiki page.") - { - var pageTitle = RandomHelper.GenerateText(5); - var wikiPageData = new WikiPage - { - Title = RandomHelper.GenerateText(5), - Text = initialText, - Comments = initialComments, - Version = 0 - }; - - var createdPage = await fixture.RedmineManager.CreateWikiPageAsync(PROJECT_ID, pageTitle, wikiPageData); - - Assert.Null(createdPage); - // Assert.Equal(pageTitle, createdPage.Title); - // Assert.True(createdPage.Id > 0, "Created WikiPage should have a valid ID."); - // Assert.Equal(initialText, createdPage.Text); - - return (pageTitle, PROJECT_ID, pageTitle); - } - - [Fact] - public async Task CreateWikiPage_Should_Succeed() - { - //Arrange - var pageTitle = RandomHelper.GenerateText("NewWikiPage"); - var text = "This is the content of a new wiki page."; - var comments = "Creation comment for new wiki page."; - var wikiPageData = new WikiPage { Text = text, Comments = comments }; - - //Act - var createdPage = await fixture.RedmineManager.CreateWikiPageAsync(PROJECT_ID, pageTitle, wikiPageData); - - //Assert - Assert.NotNull(createdPage); - Assert.Equal(pageTitle, createdPage.Title); - Assert.Equal(text, createdPage.Text); - Assert.True(createdPage.Version >= 0); - - } - - [Fact] - public async Task UpdateWikiPage_Should_Succeed() - { - //Arrange - var pageTitle = RandomHelper.GenerateText(8); - var text = "This is the content of a new wiki page."; - var comments = "Creation comment for new wiki page."; - var wikiPageData = new WikiPage { Text = text, Comments = comments }; - var createdPage = await fixture.RedmineManager.CreateWikiPageAsync(PROJECT_ID, pageTitle, wikiPageData); - - var updatedText = $"Updated wiki text content {Guid.NewGuid():N}"; - var updatedComments = "These are updated comments for the wiki page update."; - - var wikiPageToUpdate = new WikiPage - { - Text = updatedText, - Comments = updatedComments, - Version = 1 - }; - - //Act - await fixture.RedmineManager.UpdateWikiPageAsync(PROJECT_ID, pageTitle, wikiPageToUpdate); - var retrievedPage = await fixture.RedmineManager.GetWikiPageAsync(1.ToInvariantString(), createdPage.Title, version: 1); - - //Assert - Assert.NotNull(retrievedPage); - Assert.Equal(updatedText, retrievedPage.Text); - Assert.Equal(updatedComments, retrievedPage.Comments); - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Common/EmailNotificationType.cs b/tests/redmine-net-api.Integration.Tests/Tests/Common/EmailNotificationType.cs new file mode 100644 index 00000000..2c046dad --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Common/EmailNotificationType.cs @@ -0,0 +1,29 @@ +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; + +public sealed record EmailNotificationType +{ + public static readonly EmailNotificationType OnlyMyEvents = new EmailNotificationType(1, "only_my_events"); + public static readonly EmailNotificationType OnlyAssigned = new EmailNotificationType(2, "only_assigned"); + public static readonly EmailNotificationType OnlyOwner = new EmailNotificationType(3, "only_owner"); + public static readonly EmailNotificationType None = new EmailNotificationType(0, ""); + + public int Id { get; } + public string Name { get; } + + private EmailNotificationType(int id, string name) + { + Id = id; + Name = name; + } + + public static EmailNotificationType FromId(int id) + { + return id switch + { + 1 => OnlyMyEvents, + 2 => OnlyAssigned, + 3 => OnlyOwner, + _ => None + }; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Common/IssueTestHelper.cs b/tests/redmine-net-api.Integration.Tests/Tests/Common/IssueTestHelper.cs new file mode 100644 index 00000000..1be46ec3 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Common/IssueTestHelper.cs @@ -0,0 +1,82 @@ +using Redmine.Net.Api; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; + +internal static class IssueTestHelper +{ + internal static void AssertBasic(Issue expected, Issue actual) + { + Assert.NotNull(actual); + Assert.True(actual.Id > 0); + Assert.Equal(expected.Subject, actual.Subject); + Assert.Equal(expected.Description, actual.Description); + Assert.Equal(expected.Project.Id, actual.Project.Id); + Assert.Equal(expected.Tracker.Id, actual.Tracker.Id); + Assert.Equal(expected.Status.Id, actual.Status.Id); + Assert.Equal(expected.Priority.Id, actual.Priority.Id); + } + + internal static (Issue, Issue payload) CreateRandomIssue(RedmineManager redmineManager, int projectId = TestConstants.Projects.DefaultProjectId, + int trackerId = 1, + int priorityId = 2, + int statusId = 1, + string subject = null, + List customFields = null, + List watchers = null, + List uploads = null) + { + var issuePayload = TestEntityFactory.CreateRandomIssuePayload(projectId, trackerId, priorityId, statusId, + subject, customFields, watchers, uploads); + var issue = redmineManager.Create(issuePayload); + Assert.NotNull(issue); + return (issue, issuePayload); + } + + internal static async Task<(Issue, Issue payload)> CreateRandomIssueAsync(RedmineManager redmineManager, int projectId = TestConstants.Projects.DefaultProjectId, + int trackerId = 1, + int priorityId = 2, + int statusId = 1, + string subject = null, + List customFields = null, + List watchers = null, + List uploads = null) + { + var issuePayload = TestEntityFactory.CreateRandomIssuePayload(projectId, trackerId, priorityId, statusId, + subject, customFields, watchers, uploads); + var issue = await redmineManager.CreateAsync(issuePayload); + Assert.NotNull(issue); + return (issue, issuePayload); + } + + public static (Issue first, Issue second) CreateRandomTwoIssues(RedmineManager redmineManager) + { + return (Build(), Build()); + + Issue Build() => redmineManager.Create(TestEntityFactory.CreateRandomIssuePayload()); + } + + public static (IssueRelation issueRelation, Issue firstIssue, Issue secondIssue) CreateRandomIssueRelation(RedmineManager redmineManager, IssueRelationType issueRelationType = IssueRelationType.Relates) + { + var (i1, i2) = CreateRandomTwoIssues(redmineManager); + var rel = TestEntityFactory.CreateRandomIssueRelationPayload(i1.Id, i2.Id, issueRelationType); + var relation = redmineManager.Create(rel, i1.Id.ToString()); + return (relation, i1, i2); + } + + public static async Task<(Issue first, Issue second)> CreateRandomTwoIssuesAsync(RedmineManager redmineManager) + { + return (await BuildAsync(), await BuildAsync()); + + async Task BuildAsync() => await redmineManager.CreateAsync(TestEntityFactory.CreateRandomIssuePayload()); + } + + public static async Task<(IssueRelation issueRelation, Issue firstIssue, Issue secondIssue)> CreateRandomIssueRelationAsync(RedmineManager redmineManager, IssueRelationType issueRelationType = IssueRelationType.Relates) + { + var (i1, i2) = await CreateRandomTwoIssuesAsync(redmineManager); + var rel = TestEntityFactory.CreateRandomIssueRelationPayload(i1.Id, i2.Id, issueRelationType); + var relation = redmineManager.Create(rel, i1.Id.ToString()); + return (relation, i1, i2); + } + +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Common/TestConstants.cs b/tests/redmine-net-api.Integration.Tests/Tests/Common/TestConstants.cs new file mode 100644 index 00000000..bf16727b --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Common/TestConstants.cs @@ -0,0 +1,20 @@ +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; + +public static class TestConstants +{ + public static class Projects + { + public const int DefaultProjectId = 1; + public const string DefaultProjectIdentifier = "1"; + public static readonly IdentifiableName DefaultProject = DefaultProject.ToIdentifiableName(); + } + + public static class Users + { + public const string DefaultPassword = "password123"; + } + +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Common/TestEntityFactory.cs b/tests/redmine-net-api.Integration.Tests/Tests/Common/TestEntityFactory.cs new file mode 100644 index 00000000..bb721f70 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Common/TestEntityFactory.cs @@ -0,0 +1,161 @@ +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; + +public static class TestEntityFactory +{ + public static Issue CreateRandomIssuePayload( + int projectId = TestConstants.Projects.DefaultProjectId, + int trackerId = 1, + int priorityId = 2, + int statusId = 1, + string subject = null, + List customFields = null, + List watchers = null, + List uploads = null) + => new() + { + Project = projectId.ToIdentifier(), + Subject = subject ?? RandomHelper.GenerateText(9), + Description = RandomHelper.GenerateText(18), + Tracker = trackerId.ToIdentifier(), + Status = statusId.ToIssueStatusIdentifier(), + Priority = priorityId.ToIdentifier(), + CustomFields = customFields, + Watchers = watchers, + Uploads = uploads + }; + + public static User CreateRandomUserPayload(UserStatus status = UserStatus.StatusActive, int? authenticationModeId = null, + EmailNotificationType emailNotificationType = null) + { + var user = new Redmine.Net.Api.Types.User + { + Login = RandomHelper.GenerateText(12), + FirstName = RandomHelper.GenerateText(8), + LastName = RandomHelper.GenerateText(10), + Email = RandomHelper.GenerateEmail(), + Password = TestConstants.Users.DefaultPassword, + AuthenticationModeId = authenticationModeId, + MailNotification = emailNotificationType?.Name, + MustChangePassword = false, + Status = status, + }; + + return user; + } + + public static Group CreateRandomGroupPayload(string name = null, List userIds = null) + { + var group = new Redmine.Net.Api.Types.Group(name ?? RandomHelper.GenerateText(9)); + if (userIds == null || userIds.Count == 0) + { + return group; + } + foreach (var userId in userIds) + { + group.Users = [IdentifiableName.Create(userId)]; + } + return group; + } + + public static Group CreateRandomGroupPayload(string name = null, List userGroups = null) + { + var group = new Redmine.Net.Api.Types.Group(name ?? RandomHelper.GenerateText(9)); + if (userGroups == null || userGroups.Count == 0) + { + return group; + } + + group.Users = userGroups; + return group; + } + + public static (string pageName, WikiPage wikiPage) CreateRandomWikiPagePayload(string pageName = null, int version = 0, List uploads = null) + { + pageName = (pageName ?? RandomHelper.GenerateText(8)); + if (char.IsLower(pageName[0])) + { + pageName = char.ToUpper(pageName[0]) + pageName[1..]; + } + var wikiPage = new WikiPage + { + Text = RandomHelper.GenerateText(10), + Comments = RandomHelper.GenerateText(15), + Version = version, + Uploads = uploads, + }; + + return (pageName, wikiPage); + } + + public static Redmine.Net.Api.Types.Version CreateRandomVersionPayload(string name = null, + VersionStatus status = VersionStatus.Open, + VersionSharing sharing = VersionSharing.None, + int dueDateDays = 30, + string wikiPageName = null, + float? estimatedHours = null, + float? spentHours = null) + { + var version = new Redmine.Net.Api.Types.Version + { + Name = name ?? RandomHelper.GenerateText(10), + Description = RandomHelper.GenerateText(15), + Status = status, + Sharing = sharing, + DueDate = DateTime.Now.Date.AddDays(dueDateDays), + EstimatedHours = estimatedHours, + SpentHours = spentHours, + WikiPageTitle = wikiPageName, + }; + + return version; + } + + public static Redmine.Net.Api.Types.News CreateRandomNewsPayload(string title = null, List uploads = null) + { + return new Redmine.Net.Api.Types.News() + { + Title = title ?? RandomHelper.GenerateText(5), + Summary = RandomHelper.GenerateText(10), + Description = RandomHelper.GenerateText(20), + Uploads = uploads + }; + } + + public static IssueCustomField CreateRandomIssueCustomFieldWithMultipleValuesPayload() + { + return IssueCustomField.CreateMultiple(1, RandomHelper.GenerateText(8), + [RandomHelper.GenerateText(4), RandomHelper.GenerateText(4)]); + } + + public static IssueCustomField CreateRandomIssueCustomFieldWithSingleValuePayload() + { + return IssueCustomField.CreateSingle(1, RandomHelper.GenerateText(8), RandomHelper.GenerateText(4)); + } + + public static IssueRelation CreateRandomIssueRelationPayload(int issueId, int issueToId, IssueRelationType issueRelationType = IssueRelationType.Relates) + { + return new IssueRelation { IssueId = issueId, IssueToId = issueToId, Type = issueRelationType };; + } + + public static Redmine.Net.Api.Types.TimeEntry CreateRandomTimeEntryPayload(int projectId, int issueId, DateTime? spentOn = null, decimal hours = 1.5m, int? activityId = null) + { + var timeEntry = new Redmine.Net.Api.Types.TimeEntry + { + Project = projectId.ToIdentifier(), + Issue = issueId.ToIdentifier(), + SpentOn = spentOn ?? DateTime.Now.Date, + Hours = hours, + Comments = RandomHelper.GenerateText(10), + }; + + if (activityId != null) + { + timeEntry.Activity = activityId.Value.ToIdentifier(); + } + + return timeEntry; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Sync/AttachmentTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Attachment/AttachmentTests.cs similarity index 53% rename from tests/redmine-net-api.Integration.Tests/Tests/Sync/AttachmentTests.cs rename to tests/redmine-net-api.Integration.Tests/Tests/Entities/Attachment/AttachmentTests.cs index 56110e14..d0b7e133 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Sync/AttachmentTests.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Attachment/AttachmentTests.cs @@ -1,34 +1,34 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; -using Redmine.Net.Api.Net; -using Redmine.Net.Api.Types; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api; +using Redmine.Net.Api.Http; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Sync; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Attachment; [Collection(Constants.RedmineTestContainerCollection)] public class AttachmentTests(RedmineTestContainerFixture fixture) { [Fact] - public void UploadAndGetAttachment_Should_Succeed() + public void Attachment_UploadToIssue_Should_Succeed() { // Arrange var upload = FileTestHelper.UploadRandom500KbFile(fixture.RedmineManager); Assert.NotNull(upload); Assert.NotEmpty(upload.Token); - var issue = IssueTestHelper.CreateIssue(uploads: [upload]); - var createdIssue = fixture.RedmineManager.Create(issue); - Assert.NotNull(createdIssue); + var (issue, _) = IssueTestHelper.CreateRandomIssue(fixture.RedmineManager,uploads: [upload]); + Assert.NotNull(issue); // Act - var retrievedIssue = fixture.RedmineManager.Get( - createdIssue.Id.ToString(), - RequestOptions.Include("attachments")); + var retrievedIssue = fixture.RedmineManager.Get( + issue.Id.ToString(), + RequestOptions.Include(RedmineKeys.ATTACHMENTS)); var attachment = retrievedIssue.Attachments.FirstOrDefault(); Assert.NotNull(attachment); - var downloadedAttachment = fixture.RedmineManager.Get(attachment.Id.ToString()); + var downloadedAttachment = fixture.RedmineManager.Get(attachment.Id.ToString()); // Assert Assert.NotNull(downloadedAttachment); diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Attachment/AttachmentTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Attachment/AttachmentTestsAsync.cs new file mode 100644 index 00000000..e5bc284b --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Attachment/AttachmentTestsAsync.cs @@ -0,0 +1,74 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api; +using Redmine.Net.Api.Http; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Attachment; + +[Collection(Constants.RedmineTestContainerCollection)] +public class AttachmentTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task Attachment_GetIssueWithAttachments_Should_Succeed() + { + // Arrange + var upload = await FileTestHelper.UploadRandom500KbFileAsync(fixture.RedmineManager); + var (issue, _) = await IssueTestHelper.CreateRandomIssueAsync(fixture.RedmineManager, uploads: [upload]); + + // Act + var retrievedIssue = await fixture.RedmineManager.GetAsync( + issue.Id.ToString(), + RequestOptions.Include(RedmineKeys.ATTACHMENTS)); + + // Assert + Assert.NotNull(retrievedIssue); + Assert.NotNull(retrievedIssue.Attachments); + Assert.NotEmpty(retrievedIssue.Attachments); + } + + [Fact] + public async Task Attachment_GetByIssueId_Should_Succeed() + { + // Arrange + var upload = await FileTestHelper.UploadRandom500KbFileAsync(fixture.RedmineManager); + var (issue, _) = await IssueTestHelper.CreateRandomIssueAsync(fixture.RedmineManager, uploads: [upload]); + + var retrievedIssue = await fixture.RedmineManager.GetAsync( + issue.Id.ToString(), + RequestOptions.Include(RedmineKeys.ATTACHMENTS)); + + var attachment = retrievedIssue.Attachments.FirstOrDefault(); + Assert.NotNull(attachment); + + // Act + var downloadedAttachment = await fixture.RedmineManager.GetAsync(attachment.Id.ToString()); + + // Assert + Assert.NotNull(downloadedAttachment); + Assert.Equal(attachment.Id, downloadedAttachment.Id); + Assert.Equal(attachment.FileName, downloadedAttachment.FileName); + } + + [Fact] + public async Task Attachment_Upload_MultipleFiles_Should_Succeed() + { + // Arrange & Act + var upload1 = await FileTestHelper.UploadRandom500KbFileAsync(fixture.RedmineManager); + Assert.NotNull(upload1); + Assert.NotEmpty(upload1.Token); + + var upload2 = await FileTestHelper.UploadRandom500KbFileAsync(fixture.RedmineManager); + Assert.NotNull(upload2); + Assert.NotEmpty(upload2.Token); + + // Assert + var (issue, _) = await IssueTestHelper.CreateRandomIssueAsync(fixture.RedmineManager, uploads: [upload1, upload2]); + + var retrievedIssue = await fixture.RedmineManager.GetAsync( + issue.Id.ToString(), + RequestOptions.Include(RedmineKeys.ATTACHMENTS)); + + Assert.Equal(2, retrievedIssue.Attachments.Count); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Sync/CustomFieldTestsSync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/CustomField/CustomFieldTests.cs similarity index 58% rename from tests/redmine-net-api.Integration.Tests/Tests/Sync/CustomFieldTestsSync.cs rename to tests/redmine-net-api.Integration.Tests/Tests/Entities/CustomField/CustomFieldTests.cs index a4f1ebaf..9f64cdd4 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Sync/CustomFieldTestsSync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/CustomField/CustomFieldTests.cs @@ -1,7 +1,7 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Redmine.Net.Api.Types; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Sync; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.CustomField; [Collection(Constants.RedmineTestContainerCollection)] public class CustomFieldTests(RedmineTestContainerFixture fixture) @@ -10,7 +10,7 @@ public class CustomFieldTests(RedmineTestContainerFixture fixture) public void GetAllCustomFields_Should_Return_Null() { // Act - var customFields = fixture.RedmineManager.Get(); + var customFields = fixture.RedmineManager.Get(); // Assert Assert.Null(customFields); diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/CustomFieldAsyncTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/CustomField/CustomFieldTestsAsync.cs similarity index 69% rename from tests/redmine-net-api.Integration.Tests/Tests/Async/CustomFieldAsyncTests.cs rename to tests/redmine-net-api.Integration.Tests/Tests/Entities/CustomField/CustomFieldTestsAsync.cs index 47cf9a7e..684882b7 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/CustomFieldAsyncTests.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/CustomField/CustomFieldTestsAsync.cs @@ -1,7 +1,7 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Redmine.Net.Api.Types; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.CustomField; [Collection(Constants.RedmineTestContainerCollection)] public class CustomFieldTestsAsync(RedmineTestContainerFixture fixture) @@ -10,7 +10,7 @@ public class CustomFieldTestsAsync(RedmineTestContainerFixture fixture) public async Task GetAllCustomFields_Should_Return_Null() { // Act - var customFields = await fixture.RedmineManager.GetAsync(); + var customFields = await fixture.RedmineManager.GetAsync(); // Assert Assert.Null(customFields); diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Enumeration/EnumerationTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Enumeration/EnumerationTests.cs new file mode 100644 index 00000000..5436dbce --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Enumeration/EnumerationTests.cs @@ -0,0 +1,18 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Enumeration; + +[Collection(Constants.RedmineTestContainerCollection)] +public class EnumerationTests(RedmineTestContainerFixture fixture) +{ + [Fact] + public void GetDocumentCategories_Should_Succeed() => Assert.NotNull(fixture.RedmineManager.Get()); + + [Fact] + public void GetIssuePriorities_Should_Succeed() => Assert.NotNull(fixture.RedmineManager.Get()); + + [Fact] + public void GetTimeEntryActivities_Should_Succeed() => Assert.NotNull(fixture.RedmineManager.Get()); +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/EnumerationTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Enumeration/EnumerationTestsAsync.cs similarity index 86% rename from tests/redmine-net-api.Integration.Tests/Tests/Async/EnumerationTestsAsync.cs rename to tests/redmine-net-api.Integration.Tests/Tests/Entities/Enumeration/EnumerationTestsAsync.cs index 00448051..4731d5a0 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/EnumerationTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Enumeration/EnumerationTestsAsync.cs @@ -1,7 +1,8 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; using Redmine.Net.Api.Types; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Enumeration; [Collection(Constants.RedmineTestContainerCollection)] public class EnumerationTestsAsync(RedmineTestContainerFixture fixture) diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/File/FileTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/File/FileTests.cs new file mode 100644 index 00000000..010677f9 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/File/FileTests.cs @@ -0,0 +1,102 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api.Extensions; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.File; + +[Collection(Constants.RedmineTestContainerCollection)] +public class FileTests(RedmineTestContainerFixture fixture) +{ + [Fact] + public void CreateFile_Should_Succeed() + { + var (_, token) = UploadFile(); + + var filePayload = new Redmine.Net.Api.Types.File { Token = token }; + + var createdFile = fixture.RedmineManager.Create(filePayload, TestConstants.Projects.DefaultProjectIdentifier); + Assert.Null(createdFile); // the API returns null on success when no extra fields were provided + + var files = fixture.RedmineManager.GetProjectFiles(TestConstants.Projects.DefaultProjectIdentifier); + + // Assert + Assert.NotNull(files); + Assert.NotEmpty(files.Items); + } + + [Fact] + public void CreateFile_Without_Token_Should_Fail() + { + Assert.ThrowsAny(() => + fixture.RedmineManager.Create(new Redmine.Net.Api.Types.File { Filename = "project_file.zip" }, TestConstants.Projects.DefaultProjectIdentifier)); + } + + [Fact] + public void CreateFile_With_OptionalParameters_Should_Succeed() + { + var (fileName, token) = UploadFile(); + + var filePayload = new Redmine.Net.Api.Types.File + { + Token = token, + Filename = fileName, + Description = RandomHelper.GenerateText(9), + ContentType = "text/plain", + }; + + _ = fixture.RedmineManager.Create(filePayload, TestConstants.Projects.DefaultProjectIdentifier); + + var files = fixture.RedmineManager.GetProjectFiles(TestConstants.Projects.DefaultProjectIdentifier); + + var file = files.Items.FirstOrDefault(x => x.Filename == fileName); + + Assert.NotNull(file); + Assert.True(file.Id > 0); + Assert.NotEmpty(file.Digest); + Assert.Equal(filePayload.Description, file.Description); + Assert.Equal(filePayload.ContentType, file.ContentType); + Assert.EndsWith($"/attachments/download/{file.Id}/{fileName}", file.ContentUrl); + Assert.Equal(filePayload.Version, file.Version); + } + + [Fact] + public void CreateFile_With_Version_Should_Succeed() + { + var (fileName, token) = UploadFile(); + + var filePayload = new Redmine.Net.Api.Types.File + { + Token = token, + Filename = fileName, + Description = RandomHelper.GenerateText(9), + ContentType = "text/plain", + Version = 1.ToIdentifier(), + }; + + _ = fixture.RedmineManager.Create(filePayload, TestConstants.Projects.DefaultProjectIdentifier); + + var files = fixture.RedmineManager.GetProjectFiles(TestConstants.Projects.DefaultProjectIdentifier); + var file = files.Items.FirstOrDefault(x => x.Filename == fileName); + + Assert.NotNull(file); + Assert.True(file.Id > 0); + Assert.NotEmpty(file.Digest); + Assert.Equal(filePayload.Description, file.Description); + Assert.Equal(filePayload.ContentType, file.ContentType); + Assert.EndsWith($"/attachments/download/{file.Id}/{fileName}", file.ContentUrl); + Assert.Equal(filePayload.Version.Id, file.Version.Id); + } + + private (string fileName, string token) UploadFile() + { + var bytes = "Hello World!"u8.ToArray(); + var fileName = $"{RandomHelper.GenerateText(5)}.txt"; + var upload = fixture.RedmineManager.UploadFile(bytes, fileName); + + Assert.NotNull(upload); + Assert.NotNull(upload.Token); + + return (fileName, upload.Token); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/File/FileTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/File/FileTestsAsync.cs new file mode 100644 index 00000000..5363e416 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/File/FileTestsAsync.cs @@ -0,0 +1,120 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http; +using Redmine.Net.Api.Http.Extensions; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.File; + +[Collection(Constants.RedmineTestContainerCollection)] +public class FileTestsAsync(RedmineTestContainerFixture fixture) +{ + private const string PROJECT_ID = TestConstants.Projects.DefaultProjectIdentifier; + + [Fact] + public async Task CreateFile_Should_Succeed() + { + var (_, token) = await UploadFileAsync(); + + var filePayload = new Redmine.Net.Api.Types.File + { + Token = token, + }; + + var createdFile = await fixture.RedmineManager.CreateAsync(filePayload, PROJECT_ID); + Assert.Null(createdFile); + + var files = await fixture.RedmineManager.GetProjectFilesAsync(PROJECT_ID, + new RequestOptions(){ QueryString = RedmineKeys.LIMIT.WithInt(1)}); + + //Assert + Assert.NotNull(files); + Assert.NotEmpty(files.Items); + } + + [Fact] + public async Task CreateFile_Without_Token_Should_Fail() + { + await Assert.ThrowsAsync(() => fixture.RedmineManager.CreateAsync( + new Redmine.Net.Api.Types.File { Filename = "VBpMc.txt" }, PROJECT_ID)); + } + + [Fact] + public async Task CreateFile_With_OptionalParameters_Should_Succeed() + { + var (fileName, token) = await UploadFileAsync(); + + var filePayload = new Redmine.Net.Api.Types.File + { + Token = token, + Filename = fileName, + Description = RandomHelper.GenerateText(9), + ContentType = "text/plain", + }; + + _ = await fixture.RedmineManager.CreateAsync(filePayload, PROJECT_ID); + var files = await fixture.RedmineManager.GetProjectFilesAsync(PROJECT_ID); + var file = files.Items.FirstOrDefault(x => x.Filename == fileName); + + Assert.NotNull(file); + Assert.True(file.Id > 0); + Assert.NotEmpty(file.Digest); + Assert.Equal(filePayload.Description, file.Description); + Assert.Equal(filePayload.ContentType, file.ContentType); + Assert.EndsWith($"/attachments/download/{file.Id}/{fileName}", file.ContentUrl); + Assert.Equal(filePayload.Version, file.Version); + } + + [Fact] + public async Task CreateFile_With_Version_Should_Succeed() + { + var (fileName, token) = await UploadFileAsync(); + + var filePayload = new Redmine.Net.Api.Types.File + { + Token = token, + Filename = fileName, + Description = RandomHelper.GenerateText(9), + ContentType = "text/plain", + Version = 1.ToIdentifier(), + }; + + _ = await fixture.RedmineManager.CreateAsync(filePayload, PROJECT_ID); + var files = await fixture.RedmineManager.GetProjectFilesAsync(PROJECT_ID); + var file = files.Items.FirstOrDefault(x => x.Filename == fileName); + + Assert.NotNull(file); + Assert.True(file.Id > 0); + Assert.NotEmpty(file.Digest); + Assert.Equal(filePayload.Description, file.Description); + Assert.Equal(filePayload.ContentType, file.ContentType); + Assert.EndsWith($"/attachments/download/{file.Id}/{fileName}", file.ContentUrl); + Assert.Equal(filePayload.Version.Id, file.Version.Id); + } + + [Fact] + public async Task File_UploadLargeFile_Should_Succeed() + { + // Arrange & Act + var upload = await FileTestHelper.UploadRandom1MbFileAsync(fixture.RedmineManager); + + // Assert + Assert.NotNull(upload); + Assert.NotEmpty(upload.Token); + } + + private async Task<(string,string)> UploadFileAsync() + { + var bytes = "Hello World!"u8.ToArray(); + var fileName = $"{RandomHelper.GenerateText(5)}.txt"; + var upload = await fixture.RedmineManager.UploadFileAsync(bytes, fileName); + + Assert.NotNull(upload); + Assert.NotNull(upload.Token); + + return (fileName, upload.Token); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Group/GroupTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Group/GroupTests.cs new file mode 100644 index 00000000..d2a4c4d1 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Group/GroupTests.cs @@ -0,0 +1,111 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Group; + +[Collection(Constants.RedmineTestContainerCollection)] +public class GroupTests(RedmineTestContainerFixture fixture) +{ + [Fact] + public void GetAllGroups_Should_Succeed() + { + var groups = fixture.RedmineManager.Get(); + + Assert.NotNull(groups); + } + + [Fact] + public void CreateGroup_Should_Succeed() + { + var groupPayload = TestEntityFactory.CreateRandomGroupPayload(userIds: null); + var group = fixture.RedmineManager.Create(groupPayload); + Assert.NotNull(group); + + Assert.NotNull(group); + Assert.True(group.Id > 0); + Assert.Equal(group.Name, group.Name); + } + + [Fact] + public void GetGroup_Should_Succeed() + { + var groupPayload = TestEntityFactory.CreateRandomGroupPayload(userIds: null); + var group = fixture.RedmineManager.Create(groupPayload); + Assert.NotNull(group); + + var retrievedGroup = fixture.RedmineManager.Get(group.Id.ToInvariantString()); + + Assert.NotNull(retrievedGroup); + Assert.Equal(group.Id, retrievedGroup.Id); + Assert.Equal(group.Name, retrievedGroup.Name); + } + + [Fact] + public void UpdateGroup_Should_Succeed() + { + var groupPayload = TestEntityFactory.CreateRandomGroupPayload(userIds: null); + var group = fixture.RedmineManager.Create(groupPayload); + Assert.NotNull(group); + + group.Name = RandomHelper.GenerateText(7); + + fixture.RedmineManager.Update(group.Id.ToInvariantString(), group); + var retrievedGroup = fixture.RedmineManager.Get(group.Id.ToInvariantString()); + + Assert.NotNull(retrievedGroup); + Assert.Equal(group.Id, retrievedGroup.Id); + Assert.Equal(group.Name, retrievedGroup.Name); + } + + [Fact] + public void DeleteGroup_Should_Succeed() + { + var groupPayload = TestEntityFactory.CreateRandomGroupPayload(userIds: null); + var group = fixture.RedmineManager.Create(groupPayload); + Assert.NotNull(group); + + var groupId = group.Id.ToInvariantString(); + + fixture.RedmineManager.Delete(groupId); + + Assert.Throws(() => + fixture.RedmineManager.Get(groupId)); + } + + [Fact] + public void AddUserToGroup_Should_Succeed() + { + var groupPayload = TestEntityFactory.CreateRandomGroupPayload(userIds: null); + var group = fixture.RedmineManager.Create(groupPayload); + Assert.NotNull(group); + + var userId = 1; + + fixture.RedmineManager.AddUserToGroup(group.Id, userId); + var updatedGroup = fixture.RedmineManager.Get(group.Id.ToString(), RequestOptions.Include(RedmineKeys.USERS)); + + Assert.NotNull(updatedGroup); + Assert.NotNull(updatedGroup.Users); + Assert.Contains(updatedGroup.Users, u => u.Id == userId); + } + + [Fact] + public void RemoveUserFromGroup_Should_Succeed() + { + var groupPayload = TestEntityFactory.CreateRandomGroupPayload(userIds: null); + var group = fixture.RedmineManager.Create(groupPayload); + Assert.NotNull(group); + + fixture.RedmineManager.AddUserToGroup(group.Id, userId: 1); + + fixture.RedmineManager.RemoveUserFromGroup(group.Id, userId: 1); + var updatedGroup = fixture.RedmineManager.Get(group.Id.ToString(), RequestOptions.Include(RedmineKeys.USERS)); + + Assert.NotNull(updatedGroup); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Group/GroupTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Group/GroupTestsAsync.cs new file mode 100644 index 00000000..d67a2e0c --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Group/GroupTestsAsync.cs @@ -0,0 +1,129 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Group; + +[Collection(Constants.RedmineTestContainerCollection)] +public class GroupTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task GetAllGroups_Should_Succeed() + { + // Act + var groups = await fixture.RedmineManager.GetAsync(); + + // Assert + Assert.NotNull(groups); + } + + [Fact] + public async Task CreateGroup_Should_Succeed() + { + // Arrange + var groupPayload = TestEntityFactory.CreateRandomGroupPayload(userIds: null); + + // Act + var group = await fixture.RedmineManager.CreateAsync(groupPayload); + + // Assert + Assert.NotNull(group); + Assert.True(group.Id > 0); + Assert.Equal(groupPayload.Name, group.Name); + } + + [Fact] + public async Task GetGroup_Should_Succeed() + { + // Arrange + var groupPayload = TestEntityFactory.CreateRandomGroupPayload(userIds: null); + var group = await fixture.RedmineManager.CreateAsync(groupPayload); + Assert.NotNull(group); + + // Act + var retrievedGroup = await fixture.RedmineManager.GetAsync(group.Id.ToInvariantString()); + + // Assert + Assert.NotNull(retrievedGroup); + Assert.Equal(group.Id, retrievedGroup.Id); + Assert.Equal(group.Name, retrievedGroup.Name); + } + + [Fact] + public async Task UpdateGroup_Should_Succeed() + { + // Arrange + var groupPayload = TestEntityFactory.CreateRandomGroupPayload(userIds: null); + var group = await fixture.RedmineManager.CreateAsync(groupPayload); + Assert.NotNull(group); + + group.Name = RandomHelper.GenerateText(7); + + // Act + await fixture.RedmineManager.UpdateAsync(group.Id.ToInvariantString(), group); + var retrievedGroup = await fixture.RedmineManager.GetAsync(group.Id.ToInvariantString()); + + // Assert + Assert.NotNull(retrievedGroup); + Assert.Equal(group.Id, retrievedGroup.Id); + Assert.Equal(group.Name, retrievedGroup.Name); + } + + [Fact] + public async Task DeleteGroup_Should_Succeed() + { + // Arrange + var groupPayload = TestEntityFactory.CreateRandomGroupPayload(userIds: null); + var group = await fixture.RedmineManager.CreateAsync(groupPayload); + Assert.NotNull(group); + + var groupId = group.Id.ToInvariantString(); + + // Act + await fixture.RedmineManager.DeleteAsync(groupId); + + // Assert + await Assert.ThrowsAsync(async () => + await fixture.RedmineManager.GetAsync(groupId)); + } + + [Fact] + public async Task AddUserToGroup_Should_Succeed() + { + // Arrange + var groupPayload = TestEntityFactory.CreateRandomGroupPayload(userIds: null); + var group = await fixture.RedmineManager.CreateAsync(groupPayload); + Assert.NotNull(group); + + // Act + await fixture.RedmineManager.AddUserToGroupAsync(group.Id, userId: 1); + var updatedGroup = await fixture.RedmineManager.GetAsync(group.Id.ToString(), RequestOptions.Include(RedmineKeys.USERS)); + + // Assert + Assert.NotNull(updatedGroup); + Assert.NotNull(updatedGroup.Users); + Assert.Contains(updatedGroup.Users, ug => ug.Id == 1); + } + + [Fact] + public async Task RemoveUserFromGroup_Should_Succeed() + { + // Arrange + var groupPayload = TestEntityFactory.CreateRandomGroupPayload(userIds: null); + var group = await fixture.RedmineManager.CreateAsync(groupPayload); + Assert.NotNull(group); + + await fixture.RedmineManager.AddUserToGroupAsync(group.Id, userId: 1); + + // Act + await fixture.RedmineManager.RemoveUserFromGroupAsync(group.Id, userId: 1); + var updatedGroup = await fixture.RedmineManager.GetAsync(group.Id.ToString(), RequestOptions.Include(RedmineKeys.USERS)); + + // Assert + Assert.NotNull(updatedGroup); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueAttachmentUploadTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueAttachmentUploadTests.cs similarity index 59% rename from tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueAttachmentUploadTests.cs rename to tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueAttachmentUploadTests.cs index 6e5129af..527a5135 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueAttachmentUploadTests.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueAttachmentUploadTests.cs @@ -1,9 +1,10 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; -using Redmine.Net.Api.Net; -using Redmine.Net.Api.Types; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api; +using Redmine.Net.Api.Http; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Sync; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Issue; [Collection(Constants.RedmineTestContainerCollection)] public class IssueAttachmentTests(RedmineTestContainerFixture fixture) @@ -11,30 +12,27 @@ public class IssueAttachmentTests(RedmineTestContainerFixture fixture) [Fact] public void UploadAttachmentAndAttachToIssue_Should_Succeed() { - // Arrange – create issue - var issue = IssueTestHelper.CreateIssue(); - var createdIssue = fixture.RedmineManager.Create(issue); - Assert.NotNull(createdIssue); - - // Upload a file + // Arrange + var (issue, _) = IssueTestHelper.CreateRandomIssue(fixture.RedmineManager); + var content = "Test attachment content"u8.ToArray(); var fileName = "test_attachment.txt"; var upload = fixture.RedmineManager.UploadFile(content, fileName); Assert.NotNull(upload); Assert.NotEmpty(upload.Token); - // Update issue with upload token - var updateIssue = new Issue + // Act + var updateIssue = new Redmine.Net.Api.Types.Issue { Subject = $"Test issue for attachment {RandomHelper.GenerateText(5)}", Uploads = [upload] }; - fixture.RedmineManager.Update(createdIssue.Id.ToString(), updateIssue); + fixture.RedmineManager.Update(issue.Id.ToString(), updateIssue); - // Act - var retrievedIssue = fixture.RedmineManager.Get( - createdIssue.Id.ToString(), - RequestOptions.Include("attachments")); + + var retrievedIssue = fixture.RedmineManager.Get( + issue.Id.ToString(), + RequestOptions.Include(RedmineKeys.ATTACHMENTS)); // Assert Assert.NotNull(retrievedIssue); diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueAttachmentUploadTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueAttachmentUploadTestsAsync.cs similarity index 55% rename from tests/redmine-net-api.Integration.Tests/Tests/Async/IssueAttachmentUploadTestsAsync.cs rename to tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueAttachmentUploadTestsAsync.cs index f87b6780..be0dc85e 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueAttachmentUploadTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueAttachmentUploadTestsAsync.cs @@ -1,8 +1,11 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Redmine.Net.Api.Net; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api; +using Redmine.Net.Api.Http; using Redmine.Net.Api.Types; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Issue; [Collection(Constants.RedmineTestContainerCollection)] public class IssueAttachmentTestsAsync(RedmineTestContainerFixture fixture) @@ -11,39 +14,26 @@ public class IssueAttachmentTestsAsync(RedmineTestContainerFixture fixture) public async Task UploadAttachmentAndAttachToIssue_Should_Succeed() { // Arrange - var issue = new Issue - { - Project = new IdentifiableName { Id = 1 }, - Tracker = new IdentifiableName { Id = 1 }, - Status = new IssueStatus() { Id = 1 }, - Priority = new IdentifiableName { Id = 4 }, - Subject = $"Test issue for attachment {Guid.NewGuid()}", - Description = "Test issue description" - }; + var (issue, _) = await IssueTestHelper.CreateRandomIssueAsync(fixture.RedmineManager); - var createdIssue = await fixture.RedmineManager.CreateAsync(issue); - Assert.NotNull(createdIssue); - - // Upload a file var fileContent = "Test attachment content"u8.ToArray(); var filename = "test_attachment.txt"; - var upload = await fixture.RedmineManager.UploadFileAsync(fileContent, filename); Assert.NotNull(upload); Assert.NotEmpty(upload.Token); // Prepare issue with attachment - var updateIssue = new Issue + var updateIssue = new Redmine.Net.Api.Types.Issue { Subject = $"Test issue for attachment {RandomHelper.GenerateText(5)}", Uploads = [upload] }; // Act - await fixture.RedmineManager.UpdateAsync(createdIssue.Id.ToString(), updateIssue); + await fixture.RedmineManager.UpdateAsync(issue.Id.ToString(), updateIssue); var retrievedIssue = - await fixture.RedmineManager.GetAsync(createdIssue.Id.ToString(), RequestOptions.Include("attachments")); + await fixture.RedmineManager.GetAsync(issue.Id.ToString(), RequestOptions.Include(RedmineKeys.ATTACHMENTS)); // Assert Assert.NotNull(retrievedIssue); diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueTests.cs new file mode 100644 index 00000000..2c1fb974 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueTests.cs @@ -0,0 +1,132 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Issue; + +[Collection(Constants.RedmineTestContainerCollection)] +public class IssueTests(RedmineTestContainerFixture fixture) +{ + [Fact] + public void CreateIssue_Should_Succeed() + { + //Arrange + var (issue, _) = IssueTestHelper.CreateRandomIssue(fixture.RedmineManager); + + // Assert + Assert.NotNull(issue); + Assert.True(issue.Id > 0); + } + + [Fact] + public void CreateIssue_With_IssueCustomField_Should_Succeed() + { + //Arrange + var (issue, _) = IssueTestHelper.CreateRandomIssue(fixture.RedmineManager, customFields: + [ + TestEntityFactory.CreateRandomIssueCustomFieldWithSingleValuePayload() + ]); + + // Assert + Assert.NotNull(issue); + Assert.True(issue.Id > 0); + } + + [Fact] + public void GetIssue_Should_Succeed() + { + //Arrange + var (issue, issuePayload) = IssueTestHelper.CreateRandomIssue(fixture.RedmineManager); + + Assert.NotNull(issue); + Assert.True(issue.Id > 0); + + var issueId = issue.Id.ToInvariantString(); + + //Act + var retrievedIssue = fixture.RedmineManager.Get(issueId); + + //Assert + IssueTestHelper.AssertBasic(issuePayload, retrievedIssue); + } + + [Fact] + public void UpdateIssue_Should_Succeed() + { + //Arrange + var (issue, _) = IssueTestHelper.CreateRandomIssue(fixture.RedmineManager); + + issue.Subject = RandomHelper.GenerateText(9); + issue.Description = RandomHelper.GenerateText(18); + issue.Status = 2.ToIssueStatusIdentifier(); + issue.Notes = RandomHelper.GenerateText("Note"); + + var issueId = issue.Id.ToInvariantString(); + + //Act + fixture.RedmineManager.Update(issueId, issue); + var updatedIssue = fixture.RedmineManager.Get(issueId); + + //Assert + IssueTestHelper.AssertBasic(issue, updatedIssue); + Assert.Equal(issue.Subject, updatedIssue.Subject); + Assert.Equal(issue.Description, updatedIssue.Description); + Assert.Equal(issue.Status.Id, updatedIssue.Status.Id); + } + + [Fact] + public void DeleteIssue_Should_Succeed() + { + //Arrange + var (issue, _) = IssueTestHelper.CreateRandomIssue(fixture.RedmineManager); + + var issueId = issue.Id.ToInvariantString(); + + //Act + fixture.RedmineManager.Delete(issueId); + + //Assert + Assert.Throws(() => fixture.RedmineManager.Get(issueId)); + } + + [Fact] + public void GetIssue_With_Watchers_And_Relations_Should_Succeed() + { + var userPayload = TestEntityFactory.CreateRandomUserPayload(); + var createdUser = fixture.RedmineManager.Create(userPayload); + Assert.NotNull(createdUser); + + var userId = createdUser.Id; + + var (firstIssue, _) = IssueTestHelper.CreateRandomIssue(fixture.RedmineManager, customFields: + [ + IssueCustomField.CreateMultiple(1, RandomHelper.GenerateText(8), + [RandomHelper.GenerateText(4), RandomHelper.GenerateText(4)]) + ], watchers: + [new Watcher() { Id = 1 }, new Watcher() { Id = userId }]); + + var (secondIssue, _) = IssueTestHelper.CreateRandomIssue(fixture.RedmineManager, + customFields: [TestEntityFactory.CreateRandomIssueCustomFieldWithMultipleValuesPayload()], + watchers: [new Watcher() { Id = 1 }, new Watcher() { Id = userId }]); + + var issueRelation = new IssueRelation() + { + Type = IssueRelationType.Relates, + IssueToId = firstIssue.Id, + }; + _ = fixture.RedmineManager.Create(issueRelation, secondIssue.Id.ToInvariantString()); + + //Act + var retrievedIssue = fixture.RedmineManager.Get(secondIssue.Id.ToInvariantString(), + RequestOptions.Include($"{Include.Issue.Watchers},{Include.Issue.Relations}")); + + //Assert + IssueTestHelper.AssertBasic(secondIssue, retrievedIssue); + Assert.NotNull(retrievedIssue.Watchers); + Assert.NotNull(retrievedIssue.Relations); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueTestsAsync.cs similarity index 85% rename from tests/redmine-net-api.Integration.Tests/Tests/Async/IssueTestsAsync.cs rename to tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueTestsAsync.cs index 5b078c05..1e39a2fc 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueTestsAsync.cs @@ -1,20 +1,22 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Net; +using Redmine.Net.Api.Http; using Redmine.Net.Api.Types; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Issue; [Collection(Constants.RedmineTestContainerCollection)] public class IssueTestsAsync(RedmineTestContainerFixture fixture) { - private static readonly IdentifiableName ProjectIdName = IdentifiableName.Create(1); + private static readonly IdentifiableName ProjectIdName = IdentifiableName.Create(1); - private async Task CreateTestIssueAsync(List customFields = null, + private async Task CreateTestIssueAsync(List customFields = null, List watchers = null) { - var issue = new Issue + var issue = new Redmine.Net.Api.Types.Issue { Project = ProjectIdName, Subject = RandomHelper.GenerateText(9), @@ -32,7 +34,7 @@ private async Task CreateTestIssueAsync(List customFiel public async Task CreateIssue_Should_Succeed() { //Arrange - var issueData = new Issue + var issueData = new Redmine.Net.Api.Types.Issue { Project = ProjectIdName, Subject = RandomHelper.GenerateText(9), @@ -51,7 +53,7 @@ public async Task CreateIssue_Should_Succeed() //Act var cr = await fixture.RedmineManager.CreateAsync(issueData); - var createdIssue = await fixture.RedmineManager.GetAsync(cr.Id.ToString()); + var createdIssue = await fixture.RedmineManager.GetAsync(cr.Id.ToString()); //Assert Assert.NotNull(createdIssue); @@ -77,7 +79,7 @@ public async Task GetIssue_Should_Succeed() var issueId = createdIssue.Id.ToInvariantString(); //Act - var retrievedIssue = await fixture.RedmineManager.GetAsync(issueId); + var retrievedIssue = await fixture.RedmineManager.GetAsync(issueId); //Assert Assert.NotNull(retrievedIssue); @@ -107,7 +109,7 @@ public async Task UpdateIssue_Should_Succeed() //Act await fixture.RedmineManager.UpdateAsync(issueId, createdIssue); - var retrievedIssue = await fixture.RedmineManager.GetAsync(issueId); + var retrievedIssue = await fixture.RedmineManager.GetAsync(issueId); //Assert Assert.NotNull(retrievedIssue); @@ -127,10 +129,10 @@ public async Task DeleteIssue_Should_Succeed() var issueId = createdIssue.Id.ToInvariantString(); //Act - await fixture.RedmineManager.DeleteAsync(issueId); + await fixture.RedmineManager.DeleteAsync(issueId); //Assert - await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetAsync(issueId)); + await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetAsync(issueId)); } [Fact] @@ -146,7 +148,7 @@ public async Task GetIssue_With_Watchers_And_Relations_Should_Succeed() Assert.NotNull(createdIssue); //Act - var retrievedIssue = await fixture.RedmineManager.GetAsync(createdIssue.Id.ToInvariantString(), + var retrievedIssue = await fixture.RedmineManager.GetAsync(createdIssue.Id.ToInvariantString(), RequestOptions.Include($"{Include.Issue.Watchers},{Include.Issue.Relations}")); //Assert diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueWatcherTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueWatcherTests.cs similarity index 52% rename from tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueWatcherTestsAsync.cs rename to tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueWatcherTests.cs index c493aa97..c46dc941 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueWatcherTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueWatcherTests.cs @@ -1,30 +1,26 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; -using Redmine.Net.Api.Net; -using Redmine.Net.Api.Types; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Sync; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Issue; [Collection(Constants.RedmineTestContainerCollection)] public class IssueWatcherTests(RedmineTestContainerFixture fixture) { - private Issue CreateTestIssue() - { - var issue = IssueTestHelper.CreateIssue(); - return fixture.RedmineManager.Create(issue); - } - [Fact] public void AddWatcher_Should_Succeed() { - var issue = CreateTestIssue(); - var userId = 1; // existing user + var (issue, _) = IssueTestHelper.CreateRandomIssue(fixture.RedmineManager); + const int userId = 1; fixture.RedmineManager.AddWatcherToIssue(issue.Id, userId); - var updated = fixture.RedmineManager.Get( + var updated = fixture.RedmineManager.Get( issue.Id.ToString(), - RequestOptions.Include("watchers")); + RequestOptions.Include(RedmineKeys.WATCHERS)); Assert.Contains(updated.Watchers, w => w.Id == userId); } @@ -32,15 +28,15 @@ public void AddWatcher_Should_Succeed() [Fact] public void RemoveWatcher_Should_Succeed() { - var issue = CreateTestIssue(); - var userId = 1; + var (issue, _) = IssueTestHelper.CreateRandomIssue(fixture.RedmineManager); + const int userId = 1; fixture.RedmineManager.AddWatcherToIssue(issue.Id, userId); fixture.RedmineManager.RemoveWatcherFromIssue(issue.Id, userId); - var updated = fixture.RedmineManager.Get( + var updated = fixture.RedmineManager.Get( issue.Id.ToString(), - RequestOptions.Include("watchers")); + RequestOptions.Include(RedmineKeys.WATCHERS)); Assert.DoesNotContain(updated.Watchers ?? [], w => w.Id == userId); } diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueWatcherTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueWatcherTestsAsync.cs similarity index 73% rename from tests/redmine-net-api.Integration.Tests/Tests/Async/IssueWatcherTestsAsync.cs rename to tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueWatcherTestsAsync.cs index 6da2a071..fb96377f 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueWatcherTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueWatcherTestsAsync.cs @@ -1,16 +1,18 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api; using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Net; +using Redmine.Net.Api.Http; using Redmine.Net.Api.Types; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Issue; [Collection(Constants.RedmineTestContainerCollection)] public class IssueWatcherTestsAsync(RedmineTestContainerFixture fixture) { - private async Task CreateTestIssueAsync() + private async Task CreateTestIssueAsync() { - var issue = new Issue + var issue = new Redmine.Net.Api.Types.Issue { Project = new IdentifiableName { Id = 1 }, Tracker = new IdentifiableName { Id = 1 }, @@ -30,14 +32,12 @@ public async Task AddWatcher_Should_Succeed() var issue = await CreateTestIssueAsync(); Assert.NotNull(issue); - // Assuming there's at least one user in the system (typically Admin with ID 1) - var userId = 1; + const int userId = 1; // Act await fixture.RedmineManager.AddWatcherToIssueAsync(issue.Id, userId); - // Get updated issue with watchers - var updatedIssue = await fixture.RedmineManager.GetAsync(issue.Id.ToString(), RequestOptions.Include("watchers")); + var updatedIssue = await fixture.RedmineManager.GetAsync(issue.Id.ToString(), RequestOptions.Include(RedmineKeys.WATCHERS)); // Assert Assert.NotNull(updatedIssue); @@ -52,17 +52,14 @@ public async Task RemoveWatcher_Should_Succeed() var issue = await CreateTestIssueAsync(); Assert.NotNull(issue); - // Assuming there's at least one user in the system (typically Admin with ID 1) - var userId = 1; + const int userId = 1; - // Add watcher first await fixture.RedmineManager.AddWatcherToIssueAsync(issue.Id, userId); // Act await fixture.RedmineManager.RemoveWatcherFromIssueAsync(issue.Id, userId); - // Get updated issue with watchers - var updatedIssue = await fixture.RedmineManager.GetAsync(issue.Id.ToString(), RequestOptions.Include("watchers")); + var updatedIssue = await fixture.RedmineManager.GetAsync(issue.Id.ToString(), RequestOptions.Include(RedmineKeys.WATCHERS)); // Assert Assert.NotNull(updatedIssue); diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueCategoryTestsSync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueCategory/IssueCategoryTests.cs similarity index 93% rename from tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueCategoryTestsSync.cs rename to tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueCategory/IssueCategoryTests.cs index e2c98413..166cfc6f 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueCategoryTestsSync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueCategory/IssueCategoryTests.cs @@ -1,9 +1,10 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Types; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Sync; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Issue; [Collection(Constants.RedmineTestContainerCollection)] public class IssueCategoryTests(RedmineTestContainerFixture fixture) diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueCategoryTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueCategory/IssueCategoryTestsAsync.cs similarity index 95% rename from tests/redmine-net-api.Integration.Tests/Tests/Async/IssueCategoryTestsAsync.cs rename to tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueCategory/IssueCategoryTestsAsync.cs index 62b772cd..d96361b7 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueCategoryTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueCategory/IssueCategoryTestsAsync.cs @@ -1,9 +1,10 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Types; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Issue; [Collection(Constants.RedmineTestContainerCollection)] public class IssueCategoryTestsAsync(RedmineTestContainerFixture fixture) diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueRelation/IssueRelationTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueRelation/IssueRelationTests.cs new file mode 100644 index 00000000..d2f72d88 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueRelation/IssueRelationTests.cs @@ -0,0 +1,36 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api; +using Redmine.Net.Api.Http; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Issue; + +[Collection(Constants.RedmineTestContainerCollection)] +public class IssueRelationTests(RedmineTestContainerFixture fixture) +{ + [Fact] + public void CreateIssueRelation_Should_Succeed() + { + var (relation, i1, i2) = IssueTestHelper.CreateRandomIssueRelation(fixture.RedmineManager); + + Assert.NotNull(relation); + Assert.True(relation.Id > 0); + Assert.Equal(i1.Id, relation.IssueId); + Assert.Equal(i2.Id, relation.IssueToId); + } + + [Fact] + public void DeleteIssueRelation_Should_Succeed() + { + var (rel, _, _) = IssueTestHelper.CreateRandomIssueRelation(fixture.RedmineManager); + fixture.RedmineManager.Delete(rel.Id.ToString()); + + var issue = fixture.RedmineManager.Get( + rel.IssueId.ToString(), + RequestOptions.Include(RedmineKeys.RELATIONS)); + + Assert.Null(issue.Relations?.FirstOrDefault(r => r.Id == rel.Id)); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueRelation/IssueRelationTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueRelation/IssueRelationTestsAsync.cs new file mode 100644 index 00000000..5adbd6d5 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueRelation/IssueRelationTestsAsync.cs @@ -0,0 +1,51 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api; +using Redmine.Net.Api.Http; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Issue; + +[Collection(Constants.RedmineTestContainerCollection)] +public class IssueRelationTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task CreateIssueRelation_Should_Succeed() + { + // Arrange + var (issue1, issue2) = await IssueTestHelper.CreateRandomTwoIssuesAsync(fixture.RedmineManager); + + var relation = new IssueRelation + { + IssueId = issue1.Id, + IssueToId = issue2.Id, + Type = IssueRelationType.Relates + }; + + // Act + var createdRelation = await fixture.RedmineManager.CreateAsync(relation, issue1.Id.ToString()); + + // Assert + Assert.NotNull(createdRelation); + Assert.True(createdRelation.Id > 0); + Assert.Equal(relation.IssueId, createdRelation.IssueId); + Assert.Equal(relation.IssueToId, createdRelation.IssueToId); + Assert.Equal(relation.Type, createdRelation.Type); + } + + [Fact] + public async Task DeleteIssueRelation_Should_Succeed() + { + // Arrange + var (relation, _, _) = await IssueTestHelper.CreateRandomIssueRelationAsync(fixture.RedmineManager); + Assert.NotNull(relation); + + // Act & Assert + await fixture.RedmineManager.DeleteAsync(relation.Id.ToString()); + + var issue = await fixture.RedmineManager.GetAsync(relation.IssueId.ToString(), RequestOptions.Include(RedmineKeys.RELATIONS)); + + Assert.Null(issue.Relations?.FirstOrDefault(r => r.Id == relation.Id)); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueStatusTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueStatus/IssueStatusTests.cs similarity index 75% rename from tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueStatusTests.cs rename to tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueStatus/IssueStatusTests.cs index 0f7ec5ea..94e099c1 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueStatusTests.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueStatus/IssueStatusTests.cs @@ -1,7 +1,8 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; using Redmine.Net.Api.Types; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Sync; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Issue; [Collection(Constants.RedmineTestContainerCollection)] public class IssueStatusTests(RedmineTestContainerFixture fixture) diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueStatusAsyncTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueStatus/IssueStatusTestsAsync.cs similarity index 67% rename from tests/redmine-net-api.Integration.Tests/Tests/Async/IssueStatusAsyncTests.cs rename to tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueStatus/IssueStatusTestsAsync.cs index 75d1bdbf..5422befa 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/IssueStatusAsyncTests.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueStatus/IssueStatusTestsAsync.cs @@ -1,10 +1,11 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; using Redmine.Net.Api.Types; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Issue; [Collection(Constants.RedmineTestContainerCollection)] -public class IssueStatusTestsAsync(RedmineTestContainerFixture fixture) +public class IssueStatusAsyncTests(RedmineTestContainerFixture fixture) { [Fact] public async Task GetAllIssueStatuses_Should_Succeed() diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Sync/JournalManagementTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Journal/JournalTests.cs similarity index 70% rename from tests/redmine-net-api.Integration.Tests/Tests/Sync/JournalManagementTests.cs rename to tests/redmine-net-api.Integration.Tests/Tests/Entities/Journal/JournalTests.cs index aa88df48..ed59ccae 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Sync/JournalManagementTests.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Journal/JournalTests.cs @@ -1,18 +1,18 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; using Redmine.Net.Api; using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Net; -using Redmine.Net.Api.Types; +using Redmine.Net.Api.Http; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Sync; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Issue; [Collection(Constants.RedmineTestContainerCollection)] public class JournalTests(RedmineTestContainerFixture fixture) { - private Issue CreateTestIssue() + private Redmine.Net.Api.Types.Issue CreateRandomIssue() { - var issue = IssueTestHelper.CreateIssue(); + var issue = TestEntityFactory.CreateRandomIssuePayload(); return fixture.RedmineManager.Create(issue); } @@ -20,14 +20,14 @@ private Issue CreateTestIssue() public void Get_Issue_With_Journals_Should_Succeed() { // Arrange - var testIssue = CreateTestIssue(); + var testIssue = CreateRandomIssue(); Assert.NotNull(testIssue); testIssue.Notes = "This is a test note to create a journal entry."; fixture.RedmineManager.Update(testIssue.Id.ToInvariantString(), testIssue); // Act - var issueWithJournals = fixture.RedmineManager.Get( + var issueWithJournals = fixture.RedmineManager.Get( testIssue.Id.ToInvariantString(), RequestOptions.Include(RedmineKeys.JOURNALS)); diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/JournalTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Journal/JournalTestsAsync.cs similarity index 64% rename from tests/redmine-net-api.Integration.Tests/Tests/Async/JournalTestsAsync.cs rename to tests/redmine-net-api.Integration.Tests/Tests/Entities/Journal/JournalTestsAsync.cs index 754118b5..608bf1a7 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/JournalTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Journal/JournalTestsAsync.cs @@ -1,33 +1,27 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; using Redmine.Net.Api; using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Net; +using Redmine.Net.Api.Http; using Redmine.Net.Api.Types; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Issue; [Collection(Constants.RedmineTestContainerCollection)] public class JournalTestsAsync(RedmineTestContainerFixture fixture) { - private async Task CreateTestIssueAsync() + private async Task CreateRandomIssueAsync() { - var issue = new Issue - { - Project = IdentifiableName.Create(1), - Subject = RandomHelper.GenerateText(13), - Description = RandomHelper.GenerateText(19), - Tracker = 1.ToIdentifier(), - Status = 1.ToIssueStatusIdentifier(), - Priority = 2.ToIdentifier(), - }; - return await fixture.RedmineManager.CreateAsync(issue); + var issuePayload = TestEntityFactory.CreateRandomIssuePayload(); + return await fixture.RedmineManager.CreateAsync(issuePayload); } [Fact] public async Task Get_Issue_With_Journals_Should_Succeed() { //Arrange - var testIssue = await CreateTestIssueAsync(); + var testIssue = await CreateRandomIssueAsync(); Assert.NotNull(testIssue); var issueIdToTest = testIssue.Id.ToInvariantString(); @@ -36,7 +30,7 @@ public async Task Get_Issue_With_Journals_Should_Succeed() await fixture.RedmineManager.UpdateAsync(issueIdToTest, testIssue); //Act - var issueWithJournals = await fixture.RedmineManager.GetAsync( + var issueWithJournals = await fixture.RedmineManager.GetAsync( issueIdToTest, RequestOptions.Include(RedmineKeys.JOURNALS)); diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/News/NewsTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/News/NewsTests.cs new file mode 100644 index 00000000..190d25b7 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/News/NewsTests.cs @@ -0,0 +1,32 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api.Extensions; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.News; + +[Collection(Constants.RedmineTestContainerCollection)] +public class NewsTests(RedmineTestContainerFixture fixture) +{ + [Fact] + public void GetAllNews_Should_Succeed() + { + _ = fixture.RedmineManager.AddProjectNews(TestConstants.Projects.DefaultProjectIdentifier, TestEntityFactory.CreateRandomNewsPayload()); + + _ = fixture.RedmineManager.AddProjectNews("2", TestEntityFactory.CreateRandomNewsPayload()); + + var news = fixture.RedmineManager.Get(); + + Assert.NotNull(news); + } + + [Fact] + public void GetProjectNews_Should_Succeed() + { + _ = fixture.RedmineManager.AddProjectNews(TestConstants.Projects.DefaultProjectIdentifier, TestEntityFactory.CreateRandomNewsPayload()); + + var news = fixture.RedmineManager.GetProjectNews(TestConstants.Projects.DefaultProjectIdentifier); + + Assert.NotNull(news); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/News/NewsTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/News/NewsTestsAsync.cs new file mode 100644 index 00000000..88945ab2 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/News/NewsTestsAsync.cs @@ -0,0 +1,52 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api.Extensions; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.News; + +[Collection(Constants.RedmineTestContainerCollection)] +public class NewsTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task GetAllNews_Should_Succeed() + { + // Arrange + _ = await fixture.RedmineManager.AddProjectNewsAsync(TestConstants.Projects.DefaultProjectIdentifier, TestEntityFactory.CreateRandomNewsPayload()); + + _ = await fixture.RedmineManager.AddProjectNewsAsync("2", TestEntityFactory.CreateRandomNewsPayload()); + + // Act + var news = await fixture.RedmineManager.GetAsync(); + + // Assert + Assert.NotNull(news); + } + + [Fact] + public async Task GetProjectNews_Should_Succeed() + { + // Arrange + var newsCreated = await fixture.RedmineManager.AddProjectNewsAsync(TestConstants.Projects.DefaultProjectIdentifier, TestEntityFactory.CreateRandomNewsPayload()); + + // Act + var news = await fixture.RedmineManager.GetProjectNewsAsync(TestConstants.Projects.DefaultProjectIdentifier); + + // Assert + Assert.NotNull(news); + } + + [Fact] + public async Task News_AddWithUploads_Should_Succeed() + { + // Arrange + var newsPayload = TestEntityFactory.CreateRandomNewsPayload(); + var newsCreated = await fixture.RedmineManager.AddProjectNewsAsync(TestConstants.Projects.DefaultProjectIdentifier, newsPayload); + + // Act + var news = await fixture.RedmineManager.GetProjectNewsAsync(TestConstants.Projects.DefaultProjectIdentifier); + + // Assert + Assert.NotNull(news); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/ProjectTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Project/ProjectTestsAsync.cs similarity index 76% rename from tests/redmine-net-api.Integration.Tests/Tests/Async/ProjectTestsAsync.cs rename to tests/redmine-net-api.Integration.Tests/Tests/Entities/Project/ProjectTestsAsync.cs index 2909bbf0..aaec371b 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/ProjectTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Project/ProjectTestsAsync.cs @@ -1,18 +1,20 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Types; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Project; [Collection(Constants.RedmineTestContainerCollection)] public class ProjectTestsAsync(RedmineTestContainerFixture fixture) { - private async Task CreateEntityAsync(string subjectSuffix = null) + private async Task CreateEntityAsync(string subjectSuffix = null) { - var entity = new Project + var entity = new Redmine.Net.Api.Types.Project { - Identifier = RandomHelper.GenerateText(5), + Identifier = RandomHelper.GenerateText(5).ToLowerInvariant(), Name = "test-random", }; @@ -24,7 +26,7 @@ public async Task CreateProject_Should_Succeed() { //Arrange var projectName = RandomHelper.GenerateText(7); - var data = new Project + var data = new Redmine.Net.Api.Types.Project { Name = projectName, Identifier = projectName.ToLowerInvariant(), @@ -67,7 +69,7 @@ public async Task DeleteIssue_Should_Succeed() var id = createdEntity.Id.ToInvariantString(); //Act - await fixture.RedmineManager.DeleteAsync(id); + await fixture.RedmineManager.DeleteAsync(id); await Task.Delay(200); @@ -77,7 +79,7 @@ public async Task DeleteIssue_Should_Succeed() async Task TestCode() { - await fixture.RedmineManager.GetAsync(id); + await fixture.RedmineManager.GetAsync(id); } } } \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/ProjectMembership/ProjectMembershipTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/ProjectMembership/ProjectMembershipTests.cs new file mode 100644 index 00000000..c9cca3ba --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/ProjectMembership/ProjectMembershipTests.cs @@ -0,0 +1,84 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.ProjectMembership; + +[Collection(Constants.RedmineTestContainerCollection)] +public class ProjectMembershipTests(RedmineTestContainerFixture fixture) +{ + private Redmine.Net.Api.Types.ProjectMembership CreateRandomProjectMembership() + { + var roles = fixture.RedmineManager.Get(); + Assert.NotEmpty(roles); + + var user = TestEntityFactory.CreateRandomUserPayload(); + var createdUser = fixture.RedmineManager.Create(user); + Assert.NotNull(createdUser); + + var membership = new Redmine.Net.Api.Types.ProjectMembership + { + User = new IdentifiableName { Id = createdUser.Id }, + Roles = [new MembershipRole { Id = roles[0].Id }] + }; + + return fixture.RedmineManager.Create(membership, TestConstants.Projects.DefaultProjectIdentifier); + } + + [Fact] + public void GetProjectMemberships_WithValidProjectId_ShouldReturnMemberships() + { + var memberships = fixture.RedmineManager.GetProjectMemberships(TestConstants.Projects.DefaultProjectIdentifier); + Assert.NotNull(memberships); + } + + [Fact] + public void CreateProjectMembership_WithValidData_ShouldSucceed() + { + var membership = CreateRandomProjectMembership(); + + Assert.NotNull(membership); + Assert.True(membership.Id > 0); + Assert.NotNull(membership.User); + Assert.NotEmpty(membership.Roles); + } + + [Fact] + public void UpdateProjectMembership_WithValidData_ShouldSucceed() + { + var membership = CreateRandomProjectMembership(); + Assert.NotNull(membership); + + var roles = fixture.RedmineManager.Get(); + Assert.NotEmpty(roles); + + var newRoleId = roles.First(r => membership.Roles.All(mr => mr.Id != r.Id)).Id; + membership.Roles = [new MembershipRole { Id = newRoleId }]; + + // Act + fixture.RedmineManager.Update(membership.Id.ToString(), membership); + + var updatedMembership = fixture.RedmineManager.Get(membership.Id.ToString()); + + // Assert + Assert.NotNull(updatedMembership); + Assert.Contains(updatedMembership.Roles, r => r.Id == newRoleId); + } + + [Fact] + public void DeleteProjectMembership_WithValidId_ShouldSucceed() + { + // Arrange + var membership = CreateRandomProjectMembership(); + Assert.NotNull(membership); + + // Act + fixture.RedmineManager.Delete(membership.Id.ToString()); + + // Assert + Assert.Throws(() => fixture.RedmineManager.Get(membership.Id.ToString())); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/ProjectMembership/ProjectMembershipTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/ProjectMembership/ProjectMembershipTestsAsync.cs new file mode 100644 index 00000000..ee17f90f --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/ProjectMembership/ProjectMembershipTestsAsync.cs @@ -0,0 +1,123 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.ProjectMembership; + +[Collection(Constants.RedmineTestContainerCollection)] +public class ProjectMembershipTestsAsync(RedmineTestContainerFixture fixture) +{ + private async Task CreateRandomMembershipAsync() + { + var roles = await fixture.RedmineManager.GetAsync(); + Assert.NotEmpty(roles); + + var userPayload = TestEntityFactory.CreateRandomUserPayload(); + var user = await fixture.RedmineManager.CreateAsync(userPayload); + Assert.NotNull(user); + + var membership = new Redmine.Net.Api.Types.ProjectMembership + { + User = new IdentifiableName { Id = user.Id }, + Roles = [new MembershipRole { Id = roles[0].Id }] + }; + + return await fixture.RedmineManager.CreateAsync(membership, TestConstants.Projects.DefaultProjectIdentifier); + } + + [Fact] + public async Task GetProjectMemberships_WithValidProjectId_ShouldReturnMemberships() + { + // Act + var memberships = await fixture.RedmineManager.GetProjectMembershipsAsync(TestConstants.Projects.DefaultProjectIdentifier); + + // Assert + Assert.NotNull(memberships); + } + + [Fact] + public async Task CreateProjectMembership_WithValidData_ShouldSucceed() + { + // Arrange & Act + var projectMembership = await CreateRandomMembershipAsync(); + + // Assert + Assert.NotNull(projectMembership); + Assert.True(projectMembership.Id > 0); + Assert.NotNull(projectMembership.User); + Assert.NotEmpty(projectMembership.Roles); + } + + [Fact] + public async Task UpdateProjectMembership_WithValidData_ShouldSucceed() + { + // Arrange + var membership = await CreateRandomMembershipAsync(); + Assert.NotNull(membership); + + var roles = await fixture.RedmineManager.GetAsync(); + Assert.NotEmpty(roles); + + var newRoleId = roles.FirstOrDefault(r => membership.Roles.All(mr => mr.Id != r.Id))?.Id ?? roles.First().Id; + membership.Roles = [new MembershipRole { Id = newRoleId }]; + + // Act + await fixture.RedmineManager.UpdateAsync(membership.Id.ToString(), membership); + + var updatedMembership = await fixture.RedmineManager.GetAsync(membership.Id.ToString()); + + // Assert + Assert.NotNull(updatedMembership); + Assert.Contains(updatedMembership.Roles, r => r.Id == newRoleId); + } + + [Fact] + public async Task DeleteProjectMembership_WithValidId_ShouldSucceed() + { + // Arrange + var membership = await CreateRandomMembershipAsync(); + Assert.NotNull(membership); + + var membershipId = membership.Id.ToString(); + + // Act + await fixture.RedmineManager.DeleteAsync(membershipId); + + // Assert + await Assert.ThrowsAsync(() => fixture.RedmineManager.GetAsync(membershipId)); + } + + [Fact] + public async Task GetProjectMemberships_ShouldReturnMemberships() + { + // Test implementation + } + + [Fact] + public async Task GetProjectMembership_WithValidId_ShouldReturnMembership() + { + // Test implementation + } + + [Fact] + public async Task CreateProjectMembership_WithInvalidData_ShouldFail() + { + // Test implementation + } + + [Fact] + public async Task UpdateProjectMembership_WithInvalidData_ShouldFail() + { + // Test implementation + } + + [Fact] + public async Task DeleteProjectMembership_WithInvalidId_ShouldFail() + { + // Test implementation + } + +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Query/QueryTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Query/QueryTests.cs new file mode 100644 index 00000000..b4af0e84 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Query/QueryTests.cs @@ -0,0 +1,18 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Query; + +[Collection(Constants.RedmineTestContainerCollection)] +public class QueryTests(RedmineTestContainerFixture fixture) +{ + [Fact] + public void GetAllQueries_Should_Succeed() + { + // Act + var queries = fixture.RedmineManager.Get(); + + // Assert + Assert.NotNull(queries); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/QueryTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Query/QueryTestsAsync.cs similarity index 58% rename from tests/redmine-net-api.Integration.Tests/Tests/Async/QueryTestsAsync.cs rename to tests/redmine-net-api.Integration.Tests/Tests/Entities/Query/QueryTestsAsync.cs index 0ee98155..c8bb6d16 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/QueryTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Query/QueryTestsAsync.cs @@ -1,7 +1,7 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Redmine.Net.Api.Types; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Query; [Collection(Constants.RedmineTestContainerCollection)] public class QueryTestsAsync(RedmineTestContainerFixture fixture) @@ -10,7 +10,7 @@ public class QueryTestsAsync(RedmineTestContainerFixture fixture) public async Task GetAllQueries_Should_Succeed() { // Act - var queries = await fixture.RedmineManager.GetAsync(); + var queries = await fixture.RedmineManager.GetAsync(); // Assert Assert.NotNull(queries); diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Role/RoleTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Role/RoleTests.cs new file mode 100644 index 00000000..165c1c4f --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Role/RoleTests.cs @@ -0,0 +1,19 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Role; + +[Collection(Constants.RedmineTestContainerCollection)] +public class RoleTests(RedmineTestContainerFixture fixture) +{ + [Fact] + public void Get_All_Roles_Should_Succeed() + { + //Act + var roles = fixture.RedmineManager.Get(); + + //Assert + Assert.NotNull(roles); + Assert.NotEmpty(roles); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/RoleTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Role/RoleTestsAsync.cs similarity index 76% rename from tests/redmine-net-api.Integration.Tests/Tests/Async/RoleTestsAsync.cs rename to tests/redmine-net-api.Integration.Tests/Tests/Entities/Role/RoleTestsAsync.cs index cc163d64..fb1dbd52 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/RoleTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Role/RoleTestsAsync.cs @@ -1,6 +1,7 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Role; [Collection(Constants.RedmineTestContainerCollection)] public class RoleTestsAsync(RedmineTestContainerFixture fixture) diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Search/SearchTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Search/SearchTests.cs new file mode 100644 index 00000000..0f4fc789 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Search/SearchTests.cs @@ -0,0 +1,28 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api; +using Redmine.Net.Api.Extensions; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Search; + +[Collection(Constants.RedmineTestContainerCollection)] +public class SearchTests(RedmineTestContainerFixture fixture) +{ + [Fact] + public void Search_Should_Succeed() + { + // Arrange + var searchBuilder = new SearchFilterBuilder + { + IncludeIssues = true, + IncludeWikiPages = true + }; + + // Act + var results = fixture.RedmineManager.Search("query_string",100, searchFilter:searchBuilder); + + // Assert + Assert.NotNull(results); + Assert.Null(results.Items); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/SearchTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Search/SearchTestsAsync.cs similarity index 83% rename from tests/redmine-net-api.Integration.Tests/Tests/Async/SearchTestsAsync.cs rename to tests/redmine-net-api.Integration.Tests/Tests/Entities/Search/SearchTestsAsync.cs index a05add93..ba9f151e 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/SearchTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Search/SearchTestsAsync.cs @@ -1,8 +1,9 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; using Redmine.Net.Api; using Redmine.Net.Api.Extensions; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Search; [Collection(Constants.RedmineTestContainerCollection)] public class SearchTestsAsync(RedmineTestContainerFixture fixture) diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/TimeEntryActivityTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/TimeEntry/TimeEntryActivityTestsAsync.cs similarity index 78% rename from tests/redmine-net-api.Integration.Tests/Tests/Async/TimeEntryActivityTests.cs rename to tests/redmine-net-api.Integration.Tests/Tests/Entities/TimeEntry/TimeEntryActivityTestsAsync.cs index 79260dcb..b2e9546b 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/TimeEntryActivityTests.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/TimeEntry/TimeEntryActivityTestsAsync.cs @@ -1,7 +1,8 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; using Redmine.Net.Api.Types; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.TimeEntry; [Collection(Constants.RedmineTestContainerCollection)] public class TimeEntryActivityTestsAsync(RedmineTestContainerFixture fixture) diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/TimeEntry/TimeEntryTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/TimeEntry/TimeEntryTestsAsync.cs new file mode 100644 index 00000000..6a273b4d --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/TimeEntry/TimeEntryTestsAsync.cs @@ -0,0 +1,91 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.TimeEntry; + +[Collection(Constants.RedmineTestContainerCollection)] +public class TimeEntryTestsAsync(RedmineTestContainerFixture fixture) +{ + private async Task<(Redmine.Net.Api.Types.TimeEntry, Redmine.Net.Api.Types.TimeEntry payload)> CreateRandomTestTimeEntryAsync() + { + var (issue, _) = await IssueTestHelper.CreateRandomIssueAsync(fixture.RedmineManager); + + var timeEntry = TestEntityFactory.CreateRandomTimeEntryPayload(TestConstants.Projects.DefaultProjectId, issue.Id); + return (await fixture.RedmineManager.CreateAsync(timeEntry), timeEntry); + } + + [Fact] + public async Task CreateTimeEntry_Should_Succeed() + { + //Arrange & Act + var (timeEntry, timeEntryPayload) = await CreateRandomTestTimeEntryAsync(); + + //Assert + Assert.NotNull(timeEntry); + Assert.True(timeEntry.Id > 0); + Assert.Equal(timeEntryPayload.Hours, timeEntry.Hours); + Assert.Equal(timeEntryPayload.Comments, timeEntry.Comments); + Assert.Equal(timeEntryPayload.Project.Id, timeEntry.Project.Id); + Assert.Equal(timeEntryPayload.Issue.Id, timeEntry.Issue.Id); + Assert.Equal(timeEntryPayload.Activity.Id, timeEntry.Activity.Id); + } + + [Fact] + public async Task GetTimeEntry_Should_Succeed() + { + //Arrange + var (createdTimeEntry,_) = await CreateRandomTestTimeEntryAsync(); + Assert.NotNull(createdTimeEntry); + + //Act + var retrievedTimeEntry = await fixture.RedmineManager.GetAsync(createdTimeEntry.Id.ToInvariantString()); + + //Assert + Assert.NotNull(retrievedTimeEntry); + Assert.Equal(createdTimeEntry.Id, retrievedTimeEntry.Id); + Assert.Equal(createdTimeEntry.Hours, retrievedTimeEntry.Hours); + Assert.Equal(createdTimeEntry.Comments, retrievedTimeEntry.Comments); + } + + [Fact] + public async Task UpdateTimeEntry_Should_Succeed() + { + //Arrange + var (createdTimeEntry,_) = await CreateRandomTestTimeEntryAsync(); + Assert.NotNull(createdTimeEntry); + + var updatedComments = $"Updated test time entry comments {Guid.NewGuid()}"; + var updatedHours = 2.5m; + createdTimeEntry.Comments = updatedComments; + createdTimeEntry.Hours = updatedHours; + + //Act + await fixture.RedmineManager.UpdateAsync(createdTimeEntry.Id.ToInvariantString(), createdTimeEntry); + var retrievedTimeEntry = await fixture.RedmineManager.GetAsync(createdTimeEntry.Id.ToInvariantString()); + + //Assert + Assert.NotNull(retrievedTimeEntry); + Assert.Equal(createdTimeEntry.Id, retrievedTimeEntry.Id); + Assert.Equal(updatedComments, retrievedTimeEntry.Comments); + Assert.Equal(updatedHours, retrievedTimeEntry.Hours); + } + + [Fact] + public async Task DeleteTimeEntry_Should_Succeed() + { + //Arrange + var (createdTimeEntry,_) = await CreateRandomTestTimeEntryAsync(); + Assert.NotNull(createdTimeEntry); + + var timeEntryId = createdTimeEntry.Id.ToInvariantString(); + + //Act + await fixture.RedmineManager.DeleteAsync(timeEntryId); + + //Assert + await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetAsync(timeEntryId)); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/TrackerTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Tracker/TrackerTestsAsync.cs similarity index 61% rename from tests/redmine-net-api.Integration.Tests/Tests/Async/TrackerTestsAsync.cs rename to tests/redmine-net-api.Integration.Tests/Tests/Entities/Tracker/TrackerTestsAsync.cs index 0b549009..c09016c9 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/TrackerTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Tracker/TrackerTestsAsync.cs @@ -1,7 +1,7 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Redmine.Net.Api.Types; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Tracker; [Collection(Constants.RedmineTestContainerCollection)] public class TrackerTestsAsync(RedmineTestContainerFixture fixture) @@ -10,7 +10,7 @@ public class TrackerTestsAsync(RedmineTestContainerFixture fixture) public async Task Get_All_Trackers_Should_Succeed() { //Act - var trackers = await fixture.RedmineManager.GetAsync(); + var trackers = await fixture.RedmineManager.GetAsync(); //Assert Assert.NotNull(trackers); diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Async/UploadTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/UploadTestsAsync.cs similarity index 86% rename from tests/redmine-net-api.Integration.Tests/Tests/Async/UploadTestsAsync.cs rename to tests/redmine-net-api.Integration.Tests/Tests/Entities/UploadTestsAsync.cs index 727464d1..4cd293cf 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Async/UploadTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/UploadTestsAsync.cs @@ -1,11 +1,13 @@ using System.Text; using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; using Redmine.Net.Api; using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Net; +using Redmine.Net.Api.Http; using Redmine.Net.Api.Types; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Async; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities; [Collection(Constants.RedmineTestContainerCollection)] public class UploadTestsAsync(RedmineTestContainerFixture fixture) @@ -19,7 +21,7 @@ public async Task Upload_Attachment_To_Issue_Should_Succeed() Assert.NotNull(uploadFile); Assert.NotNull(uploadFile.Token); - var issue = await fixture.RedmineManager.CreateAsync(new Issue() + var issue = await fixture.RedmineManager.CreateAsync(new Redmine.Net.Api.Types.Issue() { Project = 1.ToIdentifier(), Subject = "Creating an issue with a uploaded file", @@ -38,7 +40,7 @@ public async Task Upload_Attachment_To_Issue_Should_Succeed() Assert.NotNull(issue); - var files = await fixture.RedmineManager.GetAsync(issue.Id.ToInvariantString(), RequestOptions.Include(RedmineKeys.ATTACHMENTS)); + var files = await fixture.RedmineManager.GetAsync(issue.Id.ToInvariantString(), RequestOptions.Include(RedmineKeys.ATTACHMENTS)); Assert.NotNull(files); Assert.Single(files.Attachments); diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/User/UserTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/User/UserTestsAsync.cs new file mode 100644 index 00000000..833f7566 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/User/UserTestsAsync.cs @@ -0,0 +1,242 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http; +using Redmine.Net.Api.Http.Extensions; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.User; + +[Collection(Constants.RedmineTestContainerCollection)] +public class UserTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task CreateUser_WithValidData_ShouldSucceed() + { + //Arrange + var userPayload = TestEntityFactory.CreateRandomUserPayload(emailNotificationType: EmailNotificationType.OnlyMyEvents); + + //Act + var createdUser = await fixture.RedmineManager.CreateAsync(userPayload); + + //Assert + Assert.NotNull(createdUser); + Assert.True(createdUser.Id > 0); + Assert.Equal(userPayload.Login, createdUser.Login); + Assert.Equal(userPayload.FirstName, createdUser.FirstName); + Assert.Equal(userPayload.LastName, createdUser.LastName); + Assert.Equal(userPayload.Email, createdUser.Email); + } + + [Fact] + public async Task GetUser_WithValidId_ShouldReturnUser() + { + //Arrange + var userPayload = TestEntityFactory.CreateRandomUserPayload(); + var user = await fixture.RedmineManager.CreateAsync(userPayload); + Assert.NotNull(user); + + //Act + var retrievedUser = await fixture.RedmineManager.GetAsync(user.Id.ToInvariantString()); + + //Assert + Assert.NotNull(retrievedUser); + Assert.Equal(user.Id, retrievedUser.Id); + Assert.Equal(user.Login, retrievedUser.Login); + Assert.Equal(user.FirstName, retrievedUser.FirstName); + } + + [Fact] + public async Task UpdateUser_WithValidData_ShouldSucceed() + { + //Arrange + var userPayload = TestEntityFactory.CreateRandomUserPayload(); + var user = await fixture.RedmineManager.CreateAsync(userPayload); + Assert.NotNull(user); + + user.FirstName = RandomHelper.GenerateText(10); + + //Act + await fixture.RedmineManager.UpdateAsync(user.Id.ToInvariantString(), user); + var retrievedUser = await fixture.RedmineManager.GetAsync(user.Id.ToInvariantString()); + + //Assert + Assert.NotNull(retrievedUser); + Assert.Equal(user.Id, retrievedUser.Id); + Assert.Equal(user.FirstName, retrievedUser.FirstName); + } + + [Fact] + public async Task DeleteUser_WithValidId_ShouldSucceed() + { + //Arrange + var userPayload = TestEntityFactory.CreateRandomUserPayload(); + var user = await fixture.RedmineManager.CreateAsync(userPayload); + Assert.NotNull(user); + + var userId = user.Id.ToInvariantString(); + + //Act + await fixture.RedmineManager.DeleteAsync(userId); + + //Assert + await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetAsync(userId)); + } + + [Fact] + public async Task GetCurrentUser_ShouldReturnUserDetails() + { + var currentUser = await fixture.RedmineManager.GetCurrentUserAsync(); + Assert.NotNull(currentUser); + } + + [Fact] + public async Task GetUsers_WithActiveStatus_ShouldReturnUsers() + { + var userPayload = TestEntityFactory.CreateRandomUserPayload(); + var user = await fixture.RedmineManager.CreateAsync(userPayload); + Assert.NotNull(user); + + var users = await fixture.RedmineManager.GetAsync(new RequestOptions() + { + QueryString = RedmineKeys.STATUS.WithItem(((int)UserStatus.StatusActive).ToString()) + }); + + Assert.NotNull(users); + Assert.True(users.Count > 0, "User count == 0"); + } + + [Fact] + public async Task GetUsers_WithLockedStatus_ShouldReturnUsers() + { + var userPayload = TestEntityFactory.CreateRandomUserPayload(status: UserStatus.StatusLocked); + var user = await fixture.RedmineManager.CreateAsync(userPayload); + Assert.NotNull(user); + + var users = await fixture.RedmineManager.GetAsync(new RequestOptions() + { + QueryString = RedmineKeys.STATUS.WithItem(((int)UserStatus.StatusLocked).ToString()) + }); + + Assert.NotNull(users); + Assert.True(users.Count >= 1, "User(Locked) count == 0"); + } + + [Fact] + public async Task GetUsers_WithRegisteredStatus_ShouldReturnUsers() + { + var userPayload = TestEntityFactory.CreateRandomUserPayload(status: UserStatus.StatusRegistered); + var user = await fixture.RedmineManager.CreateAsync(userPayload); + Assert.NotNull(user); + + var users = await fixture.RedmineManager.GetAsync(new RequestOptions() + { + QueryString = RedmineKeys.STATUS.WithInt((int)UserStatus.StatusRegistered) + }); + + Assert.NotNull(users); + Assert.True(users.Count >= 1, "User(Registered) count == 0"); + } + + [Fact] + public async Task GetUser_WithGroupsAndMemberships_ShouldIncludeRelatedData() + { + var roles = await fixture.RedmineManager.GetAsync(); + Assert.NotEmpty(roles); + + var userPayload = TestEntityFactory.CreateRandomUserPayload(); + var user = await fixture.RedmineManager.CreateAsync(userPayload); + Assert.NotNull(user); + + var membership = new Redmine.Net.Api.Types.ProjectMembership + { + User = new IdentifiableName { Id = user.Id }, + Roles = [new MembershipRole { Id = roles[0].Id }] + }; + + var groupPayload = new Redmine.Net.Api.Types.Group() + { + Name = RandomHelper.GenerateText(3), + Users = [IdentifiableName.Create(user.Id)] + }; + + var group = await fixture.RedmineManager.CreateAsync(groupPayload); + Assert.NotNull(group); + + // Act + var projectMembership = await fixture.RedmineManager.CreateAsync(membership, TestConstants.Projects.DefaultProjectIdentifier); + Assert.NotNull(projectMembership); + + user = await fixture.RedmineManager.GetAsync(user.Id.ToInvariantString(), + RequestOptions.Include($"{RedmineKeys.GROUPS},{RedmineKeys.MEMBERSHIPS}")); + + Assert.NotNull(user); + Assert.NotNull(user.Groups); + Assert.NotNull(user.Memberships); + + Assert.True(user.Groups.Count > 0, "Group count == 0"); + Assert.True(user.Memberships.Count > 0, "Membership count == 0"); + } + + [Fact] + public async Task GetUsers_ByGroupId_ShouldReturnFilteredUsers() + { + var userPayload = TestEntityFactory.CreateRandomUserPayload(); + var user = await fixture.RedmineManager.CreateAsync(userPayload); + Assert.NotNull(user); + + var groupPayload = TestEntityFactory.CreateRandomGroupPayload(userIds: [user.Id]); + var group = await fixture.RedmineManager.CreateAsync(groupPayload); + Assert.NotNull(group); + + var users = await fixture.RedmineManager.GetAsync(new RequestOptions() + { + QueryString = RedmineKeys.GROUP_ID.WithInt(group.Id) + }); + + Assert.NotNull(users); + Assert.True(users.Count > 0, "User count == 0"); + } + + [Fact] + public async Task AddUserToGroup_WithValidIds_ShouldSucceed() + { + var userPayload = TestEntityFactory.CreateRandomUserPayload(); + var user = await fixture.RedmineManager.CreateAsync(userPayload); + Assert.NotNull(user); + + var groupPayload = TestEntityFactory.CreateRandomGroupPayload(name: null, userIds: null); + var group = await fixture.RedmineManager.CreateAsync(groupPayload); + Assert.NotNull(group); + + await fixture.RedmineManager.AddUserToGroupAsync(group.Id, user.Id); + + user = fixture.RedmineManager.Get(user.Id.ToInvariantString(), RequestOptions.Include(RedmineKeys.GROUPS)); + + Assert.NotNull(user); + Assert.NotNull(user.Groups); + Assert.NotNull(user.Groups.FirstOrDefault(g => g.Id == group.Id)); + } + + [Fact] + public async Task RemoveUserFromGroup_WithValidIds_ShouldSucceed() + { + var userPayload = TestEntityFactory.CreateRandomUserPayload(); + var user = await fixture.RedmineManager.CreateAsync(userPayload); + Assert.NotNull(user); + + var groupPayload = TestEntityFactory.CreateRandomGroupPayload(userIds: [user.Id]); + var group = await fixture.RedmineManager.CreateAsync(groupPayload); + Assert.NotNull(group); + + await fixture.RedmineManager.RemoveUserFromGroupAsync(group.Id, user.Id); + + user = await fixture.RedmineManager.GetAsync(user.Id.ToInvariantString(), RequestOptions.Include(RedmineKeys.GROUPS)); + + Assert.NotNull(user); + Assert.True(user.Groups == null || user.Groups.FirstOrDefault(g => g.Id == group.Id) == null); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Version/VersionTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Version/VersionTestsAsync.cs new file mode 100644 index 00000000..0a325719 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Version/VersionTestsAsync.cs @@ -0,0 +1,86 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Version; + +[Collection(Constants.RedmineTestContainerCollection)] +public class VersionTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task CreateVersion_Should_Succeed() + { + // Arrange + var versionPayload = TestEntityFactory.CreateRandomVersionPayload(); + + // Act + var version = await fixture.RedmineManager.CreateAsync(versionPayload, TestConstants.Projects.DefaultProjectIdentifier); + + // Assert + Assert.NotNull(version); + Assert.True(version.Id > 0); + Assert.Equal(versionPayload.Name, version.Name); + Assert.Equal(versionPayload.Description, version.Description); + Assert.Equal(versionPayload.Status, version.Status); + Assert.Equal(TestConstants.Projects.DefaultProjectId, version.Project.Id); + } + + [Fact] + public async Task GetVersion_Should_Succeed() + { + // Arrange + var versionPayload = TestEntityFactory.CreateRandomVersionPayload(); + var version = await fixture.RedmineManager.CreateAsync(versionPayload, TestConstants.Projects.DefaultProjectIdentifier); + Assert.NotNull(version); + + // Act + var retrievedVersion = await fixture.RedmineManager.GetAsync(version.Id.ToInvariantString()); + + // Assert + Assert.NotNull(retrievedVersion); + Assert.Equal(version.Id, retrievedVersion.Id); + Assert.Equal(version.Name, retrievedVersion.Name); + Assert.Equal(version.Description, retrievedVersion.Description); + } + + [Fact] + public async Task UpdateVersion_Should_Succeed() + { + // Arrange + var versionPayload = TestEntityFactory.CreateRandomVersionPayload(); + var version = await fixture.RedmineManager.CreateAsync(versionPayload, TestConstants.Projects.DefaultProjectIdentifier); + Assert.NotNull(version); + + version.Description = RandomHelper.GenerateText(20); + version.Status = VersionStatus.Locked; + + // Act + await fixture.RedmineManager.UpdateAsync(version.Id.ToString(), version); + var retrievedVersion = await fixture.RedmineManager.GetAsync(version.Id.ToInvariantString()); + + // Assert + Assert.NotNull(retrievedVersion); + Assert.Equal(version.Id, retrievedVersion.Id); + Assert.Equal(version.Description, retrievedVersion.Description); + Assert.Equal(version.Status, retrievedVersion.Status); + } + + [Fact] + public async Task DeleteVersion_Should_Succeed() + { + // Arrange + var versionPayload = TestEntityFactory.CreateRandomVersionPayload(); + var version = await fixture.RedmineManager.CreateAsync(versionPayload, TestConstants.Projects.DefaultProjectIdentifier); + Assert.NotNull(version); + + // Act + await fixture.RedmineManager.DeleteAsync(version); + + // Assert + await Assert.ThrowsAsync(async () => + await fixture.RedmineManager.GetAsync(version)); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Wiki/WikiTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Wiki/WikiTestsAsync.cs new file mode 100644 index 00000000..d54a28a1 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Wiki/WikiTestsAsync.cs @@ -0,0 +1,206 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Wiki; + +[Collection(Constants.RedmineTestContainerCollection)] +public class WikiTestsAsync(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task CreateWikiPage_WithValidData_ShouldSucceed() + { + // Arrange + var (pageName, wikiPagePayload) = TestEntityFactory.CreateRandomWikiPagePayload(); + + // Act + var wikiPage = await fixture.RedmineManager.CreateWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, pageName, wikiPagePayload); + + // Assert + Assert.NotNull(wikiPage); + } + + [Fact] + public async Task GetWikiPage_WithValidTitle_ShouldReturnPage() + { + // Arrange + var (pageName, wikiPagePayload) = TestEntityFactory.CreateRandomWikiPagePayload(); + + // Act + var wikiPage = await fixture.RedmineManager.CreateWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, pageName, wikiPagePayload); + Assert.NotNull(wikiPage); + + var retrievedPage = await fixture.RedmineManager.GetWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, wikiPage.Title); + + // Assert + Assert.NotNull(retrievedPage); + Assert.Equal(pageName, retrievedPage.Title); + Assert.Equal(wikiPage.Text, retrievedPage.Text); + } + + [Fact] + public async Task GetAllWikiPages_ForValidProject_ShouldReturnPages() + { + // Arrange + var (firstPageName, firstWikiPagePayload) = TestEntityFactory.CreateRandomWikiPagePayload(); + var (secondPageName, secondWikiPagePayload) = TestEntityFactory.CreateRandomWikiPagePayload(); + + // Act + var wikiPage = await fixture.RedmineManager.CreateWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, firstPageName, firstWikiPagePayload); + Assert.NotNull(wikiPage); + + var wikiPage2 = await fixture.RedmineManager.CreateWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, secondPageName, secondWikiPagePayload); + Assert.NotNull(wikiPage2); + + // Act + var wikiPages = await fixture.RedmineManager.GetAllWikiPagesAsync(TestConstants.Projects.DefaultProjectIdentifier); + + // Assert + Assert.NotNull(wikiPages); + Assert.NotEmpty(wikiPages); + } + + [Fact] + public async Task DeleteWikiPage_WithValidTitle_ShouldSucceed() + { + // Arrange + var (pageName, wikiPagePayload) = TestEntityFactory.CreateRandomWikiPagePayload(); + + // Act + var wikiPage = await fixture.RedmineManager.CreateWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, pageName, wikiPagePayload); + Assert.NotNull(wikiPage); + + // Act + await fixture.RedmineManager.DeleteWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, wikiPage.Title); + + // Assert + await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, wikiPage.Title)); + } + + [Fact] + public async Task CreateWikiPage_Should_Succeed() + { + // Arrange + var (pageName, wikiPagePayload) = TestEntityFactory.CreateRandomWikiPagePayload(); + + // Act + var wikiPage = await fixture.RedmineManager.CreateWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, pageName, wikiPagePayload); + + // Assert + Assert.NotNull(wikiPage); + Assert.NotNull(wikiPage.Author); + Assert.NotNull(wikiPage.CreatedOn); + Assert.Equal(DateTime.Now, wikiPage.CreatedOn.Value, TimeSpan.FromSeconds(5)); + Assert.Equal(pageName, wikiPage.Title); + Assert.Equal(wikiPagePayload.Text, wikiPage.Text); + Assert.Equal(1, wikiPage.Version); + } + + [Fact] + public async Task UpdateWikiPage_WithValidData_ShouldSucceed() + { + // Arrange + var (pageName, wikiPagePayload) = TestEntityFactory.CreateRandomWikiPagePayload(); + + var wikiPage = await fixture.RedmineManager.CreateWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, pageName, wikiPagePayload); + Assert.NotNull(wikiPage); + + wikiPage.Text = "Updated wiki text content"; + wikiPage.Comments = "These are updated comments for the wiki page update."; + + // Act + await fixture.RedmineManager.UpdateWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, pageName, wikiPage); + + var retrievedPage = await fixture.RedmineManager.GetWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, wikiPage.Title); + + // Assert + Assert.NotNull(retrievedPage); + Assert.Equal(wikiPage.Text, retrievedPage.Text); + Assert.Equal(wikiPage.Comments, retrievedPage.Comments); + } + + [Fact] + public async Task GetWikiPage_WithNameAndAttachments_ShouldReturnCompleteData() + { + // Arrange + var fileUpload = await FileTestHelper.UploadRandom500KbFileAsync(fixture.RedmineManager); + Assert.NotNull(fileUpload); + Assert.NotEmpty(fileUpload.Token); + + fileUpload.ContentType = "text/plain"; + fileUpload.Description = RandomHelper.GenerateText(15); + fileUpload.FileName = "hello-world.txt"; + + var (pageName, wikiPagePayload) = TestEntityFactory.CreateRandomWikiPagePayload(pageName: RandomHelper.GenerateText(prefix: "Te$t"), uploads: [fileUpload]); + + var wikiPage = await fixture.RedmineManager.CreateWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, pageName, wikiPagePayload); + Assert.NotNull(wikiPage); + + // Act + var page = await fixture.RedmineManager.GetWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, pageName, RequestOptions.Include(RedmineKeys.ATTACHMENTS)); + + // Assert + Assert.NotNull(page); + Assert.Equal(pageName, page.Title); + Assert.NotNull(page.Comments); + Assert.NotNull(page.Author); + Assert.NotNull(page.CreatedOn); + Assert.Equal(DateTime.Now, page.CreatedOn.Value, TimeSpan.FromSeconds(5)); + + Assert.NotNull(page.Attachments); + Assert.NotEmpty(page.Attachments); + + var attachment = page.Attachments.FirstOrDefault(x => x.FileName == fileUpload.FileName); + Assert.NotNull(attachment); + Assert.Equal("text/plain", attachment.ContentType); + Assert.NotNull(attachment.Description); + Assert.Equal(attachment.FileName, attachment.FileName); + Assert.EndsWith($"/attachments/download/{attachment.Id}/{attachment.FileName}", attachment.ContentUrl); + Assert.True(attachment.FileSize > 0); + } + + [Fact] + public async Task GetWikiPage_WithOldVersion_ShouldReturnHistoricalData() + { + //Arrange + var (pageName, wikiPagePayload) = TestEntityFactory.CreateRandomWikiPagePayload(); + + var wikiPage = await fixture.RedmineManager.CreateWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, pageName, wikiPagePayload); + Assert.NotNull(wikiPage); + + wikiPage.Text = RandomHelper.GenerateText(8); + wikiPage.Comments = RandomHelper.GenerateText(9); + + // Act + await fixture.RedmineManager.UpdateWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, pageName, wikiPage); + + var oldPage = await fixture.RedmineManager.GetWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, wikiPage.Title, version: 1); + + // Assert + Assert.NotNull(oldPage); + Assert.Equal(wikiPagePayload.Text, oldPage.Text); + Assert.Equal(wikiPagePayload.Comments, oldPage.Comments); + Assert.Equal(1, oldPage.Version); + } + + [Fact] + public async Task GetWikiPage_WithSpecialChars_ShouldReturnPage() + { + //Arrange + var (pageName, wikiPagePayload) = TestEntityFactory.CreateRandomWikiPagePayload(pageName: "some-page-with-umlauts-and-other-special-chars-äöüÄÖÜß"); + + var wikiPage = await fixture.RedmineManager.CreateWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, pageName, wikiPagePayload); + Assert.Null(wikiPage); //it seems that Redmine returns 204 (No content) when the page name contains special characters + + // Act + var page = await fixture.RedmineManager.GetWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, pageName); + + // Assert + Assert.NotNull(page); + Assert.Equal(pageName, page.Title); + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Progress/ProgressTests.Async.cs b/tests/redmine-net-api.Integration.Tests/Tests/Progress/ProgressTests.Async.cs index be6fc659..2849cd37 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Progress/ProgressTests.Async.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Progress/ProgressTests.Async.cs @@ -1,17 +1,18 @@ -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests; +using Redmine.Net.Api.Exceptions; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Progress; public partial class ProgressTests { [Fact] - public async Task DownloadFileAsync_ReportsProgress() + public async Task DownloadFileAsync_WithValidUrl_ShouldReportProgress() { // Arrange var progressTracker = new ProgressTracker(); // Act var result = await fixture.RedmineManager.DownloadFileAsync( - TEST_DOWNLOAD_URL, - null, + "",null, progressTracker, CancellationToken.None); @@ -23,7 +24,7 @@ public async Task DownloadFileAsync_ReportsProgress() } [Fact] - public async Task DownloadFileAsync_WithCancellation_StopsDownload() + public async Task DownloadFileAsync_WithCancellation_ShouldStopDownload() { // Arrange var progressTracker = new ProgressTracker(); @@ -40,10 +41,10 @@ public async Task DownloadFileAsync_WithCancellation_StopsDownload() }; // Act & Assert - await Assert.ThrowsAnyAsync(async () => + await Assert.ThrowsAnyAsync(async () => { await fixture.RedmineManager.DownloadFileAsync( - TEST_DOWNLOAD_URL, + "", null, progressTracker, cts.Token); diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Progress/ProgressTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Progress/ProgressTests.cs index 1f0d2bcc..b9ed86a9 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Progress/ProgressTests.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Progress/ProgressTests.cs @@ -1,20 +1,19 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Progress; [Collection(Constants.RedmineTestContainerCollection)] public partial class ProgressTests(RedmineTestContainerFixture fixture) { - private const string TEST_DOWNLOAD_URL = "attachments/download/86/Manual_de_control_fiscal_versiune%20finala_RO_24_07_2023.pdf"; - [Fact] - public void DownloadFile_Sync_ReportsProgress() + public void DownloadFile_WithValidUrl_ShouldReportProgress() { // Arrange var progressTracker = new ProgressTracker(); // Act - var result = fixture.RedmineManager.DownloadFile(TEST_DOWNLOAD_URL, progressTracker); + var result = fixture.RedmineManager.DownloadFile("", progressTracker); // Assert Assert.NotNull(result); diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Sync/EnumerationTestsSync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Sync/EnumerationTestsSync.cs deleted file mode 100644 index a3776452..00000000 --- a/tests/redmine-net-api.Integration.Tests/Tests/Sync/EnumerationTestsSync.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Redmine.Net.Api.Types; - -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Sync; - -[Collection(Constants.RedmineTestContainerCollection)] -public class EnumerationTests(RedmineTestContainerFixture fixture) -{ - [Fact] public void GetDocumentCategories_Should_Succeed() => Assert.NotNull(fixture.RedmineManager.Get()); - [Fact] public void GetIssuePriorities_Should_Succeed() => Assert.NotNull(fixture.RedmineManager.Get()); - [Fact] public void GetTimeEntryActivities_Should_Succeed() => Assert.NotNull(fixture.RedmineManager.Get()); -} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Sync/FileUploadTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Sync/FileUploadTests.cs deleted file mode 100644 index d5745fc5..00000000 --- a/tests/redmine-net-api.Integration.Tests/Tests/Sync/FileUploadTests.cs +++ /dev/null @@ -1,82 +0,0 @@ -using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Redmine.Net.Api.Extensions; -using File = Redmine.Net.Api.Types.File; - -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Sync; - -[Collection(Constants.RedmineTestContainerCollection)] -public class FileTests(RedmineTestContainerFixture fixture) -{ - private const string PROJECT_ID = "1"; - - [Fact] - public void CreateFile_Should_Succeed() - { - var (_, token) = UploadFile(); - - var filePayload = new File { Token = token }; - - var createdFile = fixture.RedmineManager.Create(filePayload, PROJECT_ID); - Assert.Null(createdFile); // the API returns null on success when no extra fields were provided - - var files = fixture.RedmineManager.GetProjectFiles(PROJECT_ID); - - // Assert - Assert.NotNull(files); - Assert.NotEmpty(files.Items); - } - - [Fact] - public void CreateFile_Without_Token_Should_Fail() - { - Assert.ThrowsAny(() => - fixture.RedmineManager.Create(new File { Filename = "project_file.zip" }, PROJECT_ID)); - } - - [Fact] - public void CreateFile_With_OptionalParameters_Should_Succeed() - { - var (fileName, token) = UploadFile(); - - var filePayload = new File - { - Token = token, - Filename = fileName, - Description = RandomHelper.GenerateText(9), - ContentType = "text/plain", - }; - - var createdFile = fixture.RedmineManager.Create(filePayload, PROJECT_ID); - Assert.NotNull(createdFile); - } - - [Fact] - public void CreateFile_With_Version_Should_Succeed() - { - var (fileName, token) = UploadFile(); - - var filePayload = new File - { - Token = token, - Filename = fileName, - Description = RandomHelper.GenerateText(9), - ContentType = "text/plain", - Version = 1.ToIdentifier(), - }; - - var createdFile = fixture.RedmineManager.Create(filePayload, PROJECT_ID); - Assert.NotNull(createdFile); - } - - private (string fileName, string token) UploadFile() - { - var bytes = "Hello World!"u8.ToArray(); - var fileName = $"{RandomHelper.GenerateText(5)}.txt"; - var upload = fixture.RedmineManager.UploadFile(bytes, fileName); - - Assert.NotNull(upload); - Assert.NotNull(upload.Token); - - return (fileName, upload.Token); - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Sync/GroupManagementTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Sync/GroupManagementTests.cs deleted file mode 100644 index dae479ed..00000000 --- a/tests/redmine-net-api.Integration.Tests/Tests/Sync/GroupManagementTests.cs +++ /dev/null @@ -1,118 +0,0 @@ -using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Net; -using Redmine.Net.Api.Types; - -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Sync; - -[Collection(Constants.RedmineTestContainerCollection)] -public class GroupTests(RedmineTestContainerFixture fixture) -{ - private Group CreateTestGroup() - { - var group = new Group - { - Name = $"Test Group {Guid.NewGuid()}" - }; - - return fixture.RedmineManager.Create(group); - } - - [Fact] - public void GetAllGroups_Should_Succeed() - { - var groups = fixture.RedmineManager.Get(); - - Assert.NotNull(groups); - } - - [Fact] - public void CreateGroup_Should_Succeed() - { - var group = new Group { Name = $"Test Group {Guid.NewGuid()}" }; - - var createdGroup = fixture.RedmineManager.Create(group); - - Assert.NotNull(createdGroup); - Assert.True(createdGroup.Id > 0); - Assert.Equal(group.Name, createdGroup.Name); - } - - [Fact] - public void GetGroup_Should_Succeed() - { - var createdGroup = CreateTestGroup(); - Assert.NotNull(createdGroup); - - var retrievedGroup = fixture.RedmineManager.Get(createdGroup.Id.ToInvariantString()); - - Assert.NotNull(retrievedGroup); - Assert.Equal(createdGroup.Id, retrievedGroup.Id); - Assert.Equal(createdGroup.Name, retrievedGroup.Name); - } - - [Fact] - public void UpdateGroup_Should_Succeed() - { - var createdGroup = CreateTestGroup(); - Assert.NotNull(createdGroup); - - var updatedName = $"Updated Test Group {Guid.NewGuid()}"; - createdGroup.Name = updatedName; - - fixture.RedmineManager.Update(createdGroup.Id.ToInvariantString(), createdGroup); - var retrievedGroup = fixture.RedmineManager.Get(createdGroup.Id.ToInvariantString()); - - Assert.NotNull(retrievedGroup); - Assert.Equal(createdGroup.Id, retrievedGroup.Id); - Assert.Equal(updatedName, retrievedGroup.Name); - } - - [Fact] - public void DeleteGroup_Should_Succeed() - { - var createdGroup = CreateTestGroup(); - Assert.NotNull(createdGroup); - - var groupId = createdGroup.Id.ToInvariantString(); - - fixture.RedmineManager.Delete(groupId); - - Assert.Throws(() => - fixture.RedmineManager.Get(groupId)); - } - - [Fact] - public void AddUserToGroup_Should_Succeed() - { - var group = CreateTestGroup(); - Assert.NotNull(group); - - var userId = 1; // assuming Admin - - fixture.RedmineManager.AddUserToGroup(group.Id, userId); - var updatedGroup = fixture.RedmineManager.Get(group.Id.ToString(), RequestOptions.Include("users")); - - Assert.NotNull(updatedGroup); - Assert.NotNull(updatedGroup.Users); - Assert.Contains(updatedGroup.Users, u => u.Id == userId); - } - - [Fact] - public void RemoveUserFromGroup_Should_Succeed() - { - var group = CreateTestGroup(); - Assert.NotNull(group); - - var userId = 1; // assuming Admin - - fixture.RedmineManager.AddUserToGroup(group.Id, userId); - - fixture.RedmineManager.RemoveUserFromGroup(group.Id, userId); - var updatedGroup = fixture.RedmineManager.Get(group.Id.ToString(), RequestOptions.Include("users")); - - Assert.NotNull(updatedGroup); - // Assert.DoesNotContain(updatedGroup.Users ?? new List(), u => u.Id == userId); - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueJournalTestsSync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueJournalTestsSync.cs deleted file mode 100644 index d0339d8e..00000000 --- a/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueJournalTestsSync.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; -using Redmine.Net.Api.Net; -using Redmine.Net.Api.Types; - -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Sync; - -[Collection(Constants.RedmineTestContainerCollection)] -public class IssueJournalTests(RedmineTestContainerFixture fixture) -{ - [Fact] - public void GetIssueWithJournals_Should_Succeed() - { - // Arrange - var issue = IssueTestHelper.CreateIssue(); - var createdIssue = fixture.RedmineManager.Create(issue); - Assert.NotNull(createdIssue); - - // Add note to create the journal - var update = new Issue - { - Notes = "This is a test note that should appear in journals", - Subject = $"Updated subject {Guid.NewGuid()}" - }; - fixture.RedmineManager.Update(createdIssue.Id.ToString(), update); - - // Act - var retrievedIssue = fixture.RedmineManager.Get( - createdIssue.Id.ToString(), - RequestOptions.Include("journals")); - - // Assert - Assert.NotNull(retrievedIssue); - Assert.NotEmpty(retrievedIssue.Journals); - Assert.Contains(retrievedIssue.Journals, j => j.Notes?.Contains("test note") == true); - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueRelationTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueRelationTests.cs deleted file mode 100644 index ab863135..00000000 --- a/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueRelationTests.cs +++ /dev/null @@ -1,58 +0,0 @@ -using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Redmine.Net.Api.Net; -using Redmine.Net.Api.Types; - -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Sync; - -[Collection(Constants.RedmineTestContainerCollection)] -public class IssueRelationTests(RedmineTestContainerFixture fixture) -{ - private (Issue first, Issue second) CreateTwoIssues() - { - Issue Build(string subject) => fixture.RedmineManager.Create(new Issue - { - Project = new IdentifiableName { Id = 1 }, - Tracker = new IdentifiableName { Id = 1 }, - Status = new IssueStatus { Id = 1 }, - Priority = new IdentifiableName { Id = 4 }, - Subject = subject, - Description = "desc" - }); - - return (Build($"Issue1 {Guid.NewGuid()}"), Build($"Issue2 {Guid.NewGuid()}")); - } - - private IssueRelation CreateRelation() - { - var (i1, i2) = CreateTwoIssues(); - var rel = new IssueRelation { IssueId = i1.Id, IssueToId = i2.Id, Type = IssueRelationType.Relates }; - return fixture.RedmineManager.Create(rel, i1.Id.ToString()); - } - - [Fact] - public void CreateIssueRelation_Should_Succeed() - { - var (i1, i2) = CreateTwoIssues(); - var rel = fixture.RedmineManager.Create( - new IssueRelation { IssueId = i1.Id, IssueToId = i2.Id, Type = IssueRelationType.Relates }, - i1.Id.ToString()); - - Assert.NotNull(rel); - Assert.True(rel.Id > 0); - Assert.Equal(i1.Id, rel.IssueId); - Assert.Equal(i2.Id, rel.IssueToId); - } - - [Fact] - public void DeleteIssueRelation_Should_Succeed() - { - var rel = CreateRelation(); - fixture.RedmineManager.Delete(rel.Id.ToString()); - - var issue = fixture.RedmineManager.Get( - rel.IssueId.ToString(), - RequestOptions.Include("relations")); - - Assert.Null(issue.Relations?.FirstOrDefault(r => r.Id == rel.Id)); - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueTestsAsync.cs deleted file mode 100644 index 451b749d..00000000 --- a/tests/redmine-net-api.Integration.Tests/Tests/Sync/IssueTestsAsync.cs +++ /dev/null @@ -1,124 +0,0 @@ -using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; -using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Net; -using Redmine.Net.Api.Types; - -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Sync; - -[Collection(Constants.RedmineTestContainerCollection)] -public class IssueTests(RedmineTestContainerFixture fixture) -{ - [Fact] - public void CreateIssue_Should_Succeed() - { - //Arrange - var issue = IssueTestHelper.CreateIssue(); - var createdIssue = fixture.RedmineManager.Create(issue); - - // Assert - Assert.NotNull(createdIssue); - Assert.True(createdIssue.Id > 0); - } - - [Fact] - public void CreateIssue_With_IssueCustomField_Should_Succeed() - { - //Arrange - var issue = IssueTestHelper.CreateIssue(customFields: - [ - IssueCustomField.CreateSingle(1, RandomHelper.GenerateText(8), RandomHelper.GenerateText(4)) - ]); - var createdIssue = fixture.RedmineManager.Create(issue); - // Assert - Assert.NotNull(createdIssue); - Assert.True(createdIssue.Id > 0); - } - - [Fact] - public void GetIssue_Should_Succeed() - { - //Arrange - var issue = IssueTestHelper.CreateIssue(); - var createdIssue = fixture.RedmineManager.Create(issue); - - Assert.NotNull(createdIssue); - Assert.True(createdIssue.Id > 0); - - var issueId = issue.Id.ToInvariantString(); - - //Act - var retrievedIssue = fixture.RedmineManager.Get(issueId); - - //Assert - IssueTestHelper.AssertBasic(issue, retrievedIssue); - } - - [Fact] - public void UpdateIssue_Should_Succeed() - { - //Arrange - var issue = IssueTestHelper.CreateIssue(); - Assert.NotNull(issue); - - var updatedSubject = RandomHelper.GenerateText(9); - var updatedDescription = RandomHelper.GenerateText(18); - var updatedStatusId = 2; - - issue.Subject = updatedSubject; - issue.Description = updatedDescription; - issue.Status = updatedStatusId.ToIssueStatusIdentifier(); - issue.Notes = RandomHelper.GenerateText("Note"); - - var issueId = issue.Id.ToInvariantString(); - - //Act - fixture.RedmineManager.Update(issueId, issue); - var retrievedIssue = fixture.RedmineManager.Get(issueId); - - //Assert - IssueTestHelper.AssertBasic(issue, retrievedIssue); - Assert.Equal(updatedSubject, retrievedIssue.Subject); - Assert.Equal(updatedDescription, retrievedIssue.Description); - Assert.Equal(updatedStatusId, retrievedIssue.Status.Id); - } - - [Fact] - public void DeleteIssue_Should_Succeed() - { - //Arrange - var issue = IssueTestHelper.CreateIssue(); - Assert.NotNull(issue); - - var issueId = issue.Id.ToInvariantString(); - - //Act - fixture.RedmineManager.Delete(issueId); - - //Assert - Assert.Throws(() => fixture.RedmineManager.Get(issueId)); - } - - [Fact] - public void GetIssue_With_Watchers_And_Relations_Should_Succeed() - { - var issue = IssueTestHelper.CreateIssue( - [ - IssueCustomField.CreateMultiple(1, RandomHelper.GenerateText(8), - [RandomHelper.GenerateText(4), RandomHelper.GenerateText(4)]) - ], - [new Watcher() { Id = 1 }, new Watcher() { Id = 2 }]); - - Assert.NotNull(issue); - - //Act - var retrievedIssue = fixture.RedmineManager.Get(issue.Id.ToInvariantString(), - RequestOptions.Include($"{Include.Issue.Watchers},{Include.Issue.Relations}")); - - //Assert - IssueTestHelper.AssertBasic(issue, retrievedIssue); - Assert.NotNull(retrievedIssue.Relations); - Assert.NotNull(retrievedIssue.Watchers); - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Sync/NewsTestsIntegration.cs b/tests/redmine-net-api.Integration.Tests/Tests/Sync/NewsTestsIntegration.cs deleted file mode 100644 index 2823ce83..00000000 --- a/tests/redmine-net-api.Integration.Tests/Tests/Sync/NewsTestsIntegration.cs +++ /dev/null @@ -1,48 +0,0 @@ -using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Sync; - -[Collection(Constants.RedmineTestContainerCollection)] -public class NewsTests(RedmineTestContainerFixture fixture) -{ - private const string PROJECT_ID = "1"; - - [Fact] - public void GetAllNews_Should_Succeed() - { - _ = fixture.RedmineManager.AddProjectNews(PROJECT_ID, new News - { - Title = RandomHelper.GenerateText(5), - Summary = RandomHelper.GenerateText(10), - Description = RandomHelper.GenerateText(20), - }); - - _ = fixture.RedmineManager.AddProjectNews("2", new News - { - Title = RandomHelper.GenerateText(5), - Summary = RandomHelper.GenerateText(10), - Description = RandomHelper.GenerateText(20), - }); - - var news = fixture.RedmineManager.Get(); - - Assert.NotNull(news); - } - - [Fact] - public void GetProjectNews_Should_Succeed() - { - _ = fixture.RedmineManager.AddProjectNews(PROJECT_ID, new News - { - Title = RandomHelper.GenerateText(5), - Summary = RandomHelper.GenerateText(10), - Description = RandomHelper.GenerateText(20), - }); - - var news = fixture.RedmineManager.GetProjectNews(PROJECT_ID); - - Assert.NotNull(news); - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Sync/ProjectMembershipTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Sync/ProjectMembershipTests.cs deleted file mode 100644 index 419582ba..00000000 --- a/tests/redmine-net-api.Integration.Tests/Tests/Sync/ProjectMembershipTests.cs +++ /dev/null @@ -1,103 +0,0 @@ -using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; - -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Sync; - -[Collection(Constants.RedmineTestContainerCollection)] -public class MembershipTests(RedmineTestContainerFixture fixture) -{ - private const string PROJECT_ID = "1"; - - private ProjectMembership CreateTestMembership() - { - var roles = fixture.RedmineManager.Get(); - Assert.NotEmpty(roles); - - var user = new User - { - Login = RandomHelper.GenerateText(10), - FirstName = RandomHelper.GenerateText(8), - LastName = RandomHelper.GenerateText(9), - Email = $"{RandomHelper.GenerateText(5)}@example.com", - Password = "password123", - MustChangePassword = false, - Status = UserStatus.StatusActive - }; - var createdUser = fixture.RedmineManager.Create(user); - Assert.NotNull(createdUser); - - var membership = new ProjectMembership - { - User = new IdentifiableName { Id = createdUser.Id }, - Roles = [new MembershipRole { Id = roles[0].Id }] - }; - - return fixture.RedmineManager.Create(membership, PROJECT_ID); - } - - [Fact] - public void GetProjectMemberships_Should_Succeed() - { - var memberships = fixture.RedmineManager.GetProjectMemberships(PROJECT_ID); - Assert.NotNull(memberships); - } - - [Fact] - public void CreateMembership_Should_Succeed() - { - var roles = fixture.RedmineManager.Get(); - Assert.NotEmpty(roles); - - var user = new User - { - Login = RandomHelper.GenerateText(10), - FirstName = RandomHelper.GenerateText(8), - LastName = RandomHelper.GenerateText(9), - Email = $"{RandomHelper.GenerateText(5)}@example.com", - Password = "password123", - MustChangePassword = false, - Status = UserStatus.StatusActive - }; - var createdUser = fixture.RedmineManager.Create(user); - - var membership = new ProjectMembership - { - User = new IdentifiableName { Id = createdUser.Id }, - Roles = [new MembershipRole { Id = roles[0].Id }] - }; - var createdMembership = fixture.RedmineManager.Create(membership, PROJECT_ID); - - Assert.NotNull(createdMembership); - Assert.True(createdMembership.Id > 0); - Assert.Equal(membership.User.Id, createdMembership.User.Id); - Assert.NotEmpty(createdMembership.Roles); - } - - [Fact] - public void UpdateMembership_Should_Succeed() - { - var membership = CreateTestMembership(); - - var roles = fixture.RedmineManager.Get(); - var newRoleId = roles.First(r => membership.Roles.All(mr => mr.Id != r.Id)).Id; - membership.Roles = [new MembershipRole { Id = newRoleId }]; - - fixture.RedmineManager.Update(membership.Id.ToString(), membership); - - var updatedMemberships = fixture.RedmineManager.GetProjectMemberships(PROJECT_ID); - var updated = updatedMemberships.Items.First(m => m.Id == membership.Id); - - Assert.Contains(updated.Roles, r => r.Id == newRoleId); - } - - [Fact] - public void DeleteMembership_Should_Succeed() - { - var membership = CreateTestMembership(); - fixture.RedmineManager.Delete(membership.Id.ToString()); - - var afterDelete = fixture.RedmineManager.GetProjectMemberships(PROJECT_ID); - Assert.DoesNotContain(afterDelete.Items, m => m.Id == membership.Id); - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/appsettings.local.json b/tests/redmine-net-api.Integration.Tests/appsettings.local.json index 20d6ceb0..9c508d1e 100644 --- a/tests/redmine-net-api.Integration.Tests/appsettings.local.json +++ b/tests/redmine-net-api.Integration.Tests/appsettings.local.json @@ -4,7 +4,7 @@ "Url": "http://localhost:8089/", "AuthenticationMode": "ApiKey", "Authentication": { - "ApiKey": "026389abb8e5d5b31fe7864c4ed174e6f3c9783c" + "ApiKey": "61d6fa45ca2c570372b08b8c54b921e5fc39335a" } } } diff --git a/tests/redmine-net-api.Integration.Tests/redmine-net-api.Integration.Tests.csproj b/tests/redmine-net-api.Integration.Tests/redmine-net-api.Integration.Tests.csproj index 65bc9aba..9f80bcd1 100644 --- a/tests/redmine-net-api.Integration.Tests/redmine-net-api.Integration.Tests.csproj +++ b/tests/redmine-net-api.Integration.Tests/redmine-net-api.Integration.Tests.csproj @@ -1,4 +1,4 @@ - + |net40|net45|net451|net452|net46|net461| From 4a249808b439c5c56ba434f5a6894a762d429236 Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 28 May 2025 17:07:00 +0300 Subject: [PATCH 534/549] ... --- src/redmine-net-api/Logging/RedmineLoggingOptions.cs | 2 +- src/redmine-net-api/Types/User.cs | 2 +- src/redmine-net-api/Types/UserStatus.cs | 4 ---- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/redmine-net-api/Logging/RedmineLoggingOptions.cs b/src/redmine-net-api/Logging/RedmineLoggingOptions.cs index 18af7ae0..d7b510d4 100644 --- a/src/redmine-net-api/Logging/RedmineLoggingOptions.cs +++ b/src/redmine-net-api/Logging/RedmineLoggingOptions.cs @@ -6,7 +6,7 @@ namespace Redmine.Net.Api.Logging; public sealed class RedmineLoggingOptions { /// - /// Gets or sets the minimum log level + /// Gets or sets the minimum log level. The default value is LogLevel.Information /// public LogLevel MinimumLevel { get; set; } = LogLevel.Information; diff --git a/src/redmine-net-api/Types/User.cs b/src/redmine-net-api/Types/User.cs index ffeb7fd1..0afc34ad 100644 --- a/src/redmine-net-api/Types/User.cs +++ b/src/redmine-net-api/Types/User.cs @@ -157,7 +157,7 @@ public sealed class User : Identifiable /// Gets or sets the user's mail_notification. /// /// - /// only_my_events, only_assigned, ... + /// only_my_events, only_assigned, only_owner /// public string MailNotification { get; set; } diff --git a/src/redmine-net-api/Types/UserStatus.cs b/src/redmine-net-api/Types/UserStatus.cs index 86542357..c14b6a38 100644 --- a/src/redmine-net-api/Types/UserStatus.cs +++ b/src/redmine-net-api/Types/UserStatus.cs @@ -21,10 +21,6 @@ namespace Redmine.Net.Api.Types /// public enum UserStatus { - /// - /// - /// - StatusAnonymous = 0, /// /// /// From 8851299804fb4ae171659779c6b06df549ef5843 Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 28 May 2025 17:46:48 +0300 Subject: [PATCH 535/549] Change collection type from IList to List --- src/redmine-net-api/Common/PagedResults.cs | 4 ++-- src/redmine-net-api/Extensions/ListExtensions.cs | 2 +- src/redmine-net-api/Types/CustomField.cs | 6 +++--- src/redmine-net-api/Types/Group.cs | 6 +++--- src/redmine-net-api/Types/Issue.cs | 16 ++++++++-------- src/redmine-net-api/Types/IssueCustomField.cs | 2 +- src/redmine-net-api/Types/Journal.cs | 2 +- src/redmine-net-api/Types/Membership.cs | 2 +- src/redmine-net-api/Types/Project.cs | 8 ++++---- src/redmine-net-api/Types/ProjectMembership.cs | 2 +- src/redmine-net-api/Types/Role.cs | 2 +- src/redmine-net-api/Types/TimeEntry.cs | 2 +- src/redmine-net-api/Types/Version.cs | 2 +- src/redmine-net-api/Types/WikiPage.cs | 4 ++-- .../Helpers/RandomHelper.cs | 2 +- 15 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/redmine-net-api/Common/PagedResults.cs b/src/redmine-net-api/Common/PagedResults.cs index 2aeb5f03..af1e82fd 100644 --- a/src/redmine-net-api/Common/PagedResults.cs +++ b/src/redmine-net-api/Common/PagedResults.cs @@ -30,7 +30,7 @@ public sealed class PagedResults where TOut: class /// /// /// - public PagedResults(IEnumerable items, int total, int offset, int pageSize) + public PagedResults(List items, int total, int offset, int pageSize) { Items = items; TotalItems = total; @@ -75,6 +75,6 @@ public PagedResults(IEnumerable items, int total, int offset, int pageSize /// /// /// - public IEnumerable Items { get; } + public List Items { get; } } } \ No newline at end of file diff --git a/src/redmine-net-api/Extensions/ListExtensions.cs b/src/redmine-net-api/Extensions/ListExtensions.cs index a8dce8c1..48ef0705 100755 --- a/src/redmine-net-api/Extensions/ListExtensions.cs +++ b/src/redmine-net-api/Extensions/ListExtensions.cs @@ -54,7 +54,7 @@ public static IList Clone(this IList listToClone, bool resetId) where T /// The list to be cloned. /// Specifies whether to reset the ID for each cloned item. /// A new list containing cloned copies of the elements from the original list. Returns null if the original list is null. - public static IList Clone(this List listToClone, bool resetId) where T : ICloneable + public static List Clone(this List listToClone, bool resetId) where T : ICloneable { if (listToClone == null) { diff --git a/src/redmine-net-api/Types/CustomField.cs b/src/redmine-net-api/Types/CustomField.cs index 9f6b2b3f..0c0ca47f 100644 --- a/src/redmine-net-api/Types/CustomField.cs +++ b/src/redmine-net-api/Types/CustomField.cs @@ -98,17 +98,17 @@ public sealed class CustomField : IdentifiableName, IEquatable /// /// /// - public IList PossibleValues { get; internal set; } + public List PossibleValues { get; internal set; } /// /// /// - public IList Trackers { get; internal set; } + public List Trackers { get; internal set; } /// /// /// - public IList Roles { get; internal set; } + public List Roles { get; internal set; } #endregion #region Implementation of IXmlSerializable diff --git a/src/redmine-net-api/Types/Group.cs b/src/redmine-net-api/Types/Group.cs index 3a9fd9e1..8d3d01d1 100644 --- a/src/redmine-net-api/Types/Group.cs +++ b/src/redmine-net-api/Types/Group.cs @@ -55,19 +55,19 @@ public Group(string name) /// /// Represents the group's users. /// - public IList Users { get; set; } + public List Users { get; set; } /// /// Gets or sets the custom fields. /// /// The custom fields. - public IList CustomFields { get; internal set; } + public List CustomFields { get; internal set; } /// /// Gets or sets the custom fields. /// /// The custom fields. - public IList Memberships { get; internal set; } + public List Memberships { get; internal set; } #endregion #region Implementation of IXmlSerializable diff --git a/src/redmine-net-api/Types/Issue.cs b/src/redmine-net-api/Types/Issue.cs index e6720191..1298f214 100644 --- a/src/redmine-net-api/Types/Issue.cs +++ b/src/redmine-net-api/Types/Issue.cs @@ -137,7 +137,7 @@ public sealed class Issue : /// Gets or sets the custom fields. /// /// The custom fields. - public IList CustomFields { get; set; } + public List CustomFields { get; set; } /// /// Gets or sets the created on. @@ -212,7 +212,7 @@ public sealed class Issue : /// /// The journals. /// - public IList Journals { get; set; } + public List Journals { get; set; } /// /// Gets or sets the change sets. @@ -220,7 +220,7 @@ public sealed class Issue : /// /// The change sets. /// - public IList ChangeSets { get; set; } + public List ChangeSets { get; set; } /// /// Gets or sets the attachments. @@ -228,7 +228,7 @@ public sealed class Issue : /// /// The attachments. /// - public IList Attachments { get; set; } + public List Attachments { get; set; } /// /// Gets or sets the issue relations. @@ -236,7 +236,7 @@ public sealed class Issue : /// /// The issue relations. /// - public IList Relations { get; set; } + public List Relations { get; set; } /// /// Gets or sets the issue children. @@ -245,7 +245,7 @@ public sealed class Issue : /// The issue children. /// NOTE: Only Id, tracker and subject are filled. /// - public IList Children { get; set; } + public List Children { get; set; } /// /// Gets or sets the attachments. @@ -253,12 +253,12 @@ public sealed class Issue : /// /// The attachment. /// - public IList Uploads { get; set; } + public List Uploads { get; set; } /// /// /// - public IList Watchers { get; set; } + public List Watchers { get; set; } /// /// diff --git a/src/redmine-net-api/Types/IssueCustomField.cs b/src/redmine-net-api/Types/IssueCustomField.cs index a357cb57..0333e832 100644 --- a/src/redmine-net-api/Types/IssueCustomField.cs +++ b/src/redmine-net-api/Types/IssueCustomField.cs @@ -44,7 +44,7 @@ public sealed class IssueCustomField : /// Gets or sets the value. /// /// The value. - public IList Values { get; set; } + public List Values { get; set; } /// /// diff --git a/src/redmine-net-api/Types/Journal.cs b/src/redmine-net-api/Types/Journal.cs index f3f989e0..916967a1 100644 --- a/src/redmine-net-api/Types/Journal.cs +++ b/src/redmine-net-api/Types/Journal.cs @@ -83,7 +83,7 @@ public sealed class Journal : /// /// The details. /// - public IList Details { get; internal set; } + public List Details { get; internal set; } /// /// diff --git a/src/redmine-net-api/Types/Membership.cs b/src/redmine-net-api/Types/Membership.cs index 9496a1c5..d03fa95f 100644 --- a/src/redmine-net-api/Types/Membership.cs +++ b/src/redmine-net-api/Types/Membership.cs @@ -53,7 +53,7 @@ public sealed class Membership : Identifiable /// Gets or sets the type. /// /// The type. - public IList Roles { get; internal set; } + public List Roles { get; internal set; } #endregion #region Implementation of IXmlSerialization diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs index 4ac93f9b..5ff1f11f 100644 --- a/src/redmine-net-api/Types/Project.cs +++ b/src/redmine-net-api/Types/Project.cs @@ -108,7 +108,7 @@ public sealed class Project : IdentifiableName, IEquatable /// The trackers. /// /// Available in Redmine starting with 2.6.0 version. - public IList Trackers { get; set; } + public List Trackers { get; set; } /// /// Gets or sets the enabled modules. @@ -117,7 +117,7 @@ public sealed class Project : IdentifiableName, IEquatable /// The enabled modules. /// /// Available in Redmine starting with 2.6.0 version. - public IList EnabledModules { get; set; } + public List EnabledModules { get; set; } /// /// @@ -136,13 +136,13 @@ public sealed class Project : IdentifiableName, IEquatable /// The issue categories. /// /// Available in Redmine starting with the 2.6.0 version. - public IList IssueCategories { get; internal set; } + public List IssueCategories { get; internal set; } /// /// Gets the time entry activities. /// /// Available in Redmine starting with the 3.4.0 version. - public IList TimeEntryActivities { get; internal set; } + public List TimeEntryActivities { get; internal set; } /// /// diff --git a/src/redmine-net-api/Types/ProjectMembership.cs b/src/redmine-net-api/Types/ProjectMembership.cs index 6e835b15..8528a7de 100644 --- a/src/redmine-net-api/Types/ProjectMembership.cs +++ b/src/redmine-net-api/Types/ProjectMembership.cs @@ -68,7 +68,7 @@ public sealed class ProjectMembership : Identifiable /// Gets or sets the type. /// /// The type. - public IList Roles { get; set; } + public List Roles { get; set; } #endregion #region Implementation of IXmlSerialization diff --git a/src/redmine-net-api/Types/Role.cs b/src/redmine-net-api/Types/Role.cs index 3d5903ba..62a3ec29 100644 --- a/src/redmine-net-api/Types/Role.cs +++ b/src/redmine-net-api/Types/Role.cs @@ -41,7 +41,7 @@ public sealed class Role : IdentifiableName, IEquatable /// /// The issue relations. /// - public IList Permissions { get; internal set; } + public List Permissions { get; internal set; } /// /// diff --git a/src/redmine-net-api/Types/TimeEntry.cs b/src/redmine-net-api/Types/TimeEntry.cs index b4fcb3ad..f3580a49 100644 --- a/src/redmine-net-api/Types/TimeEntry.cs +++ b/src/redmine-net-api/Types/TimeEntry.cs @@ -106,7 +106,7 @@ public string Comments /// Gets or sets the custom fields. /// /// The custom fields. - public IList CustomFields { get; set; } + public List CustomFields { get; set; } #endregion #region Implementation of IXmlSerialization diff --git a/src/redmine-net-api/Types/Version.cs b/src/redmine-net-api/Types/Version.cs index 588c81f1..4d6f9ac0 100644 --- a/src/redmine-net-api/Types/Version.cs +++ b/src/redmine-net-api/Types/Version.cs @@ -98,7 +98,7 @@ public sealed class Version : IdentifiableName, IEquatable /// Gets the custom fields. /// /// The custom fields. - public IList CustomFields { get; internal set; } + public List CustomFields { get; internal set; } #endregion #region Implementation of IXmlSerializable diff --git a/src/redmine-net-api/Types/WikiPage.cs b/src/redmine-net-api/Types/WikiPage.cs index 75d64466..2b9f35e9 100644 --- a/src/redmine-net-api/Types/WikiPage.cs +++ b/src/redmine-net-api/Types/WikiPage.cs @@ -85,7 +85,7 @@ public sealed class WikiPage : Identifiable /// /// The attachments. /// - public IList Attachments { get; set; } + public List Attachments { get; set; } /// /// Sets the uploads. @@ -94,7 +94,7 @@ public sealed class WikiPage : Identifiable /// The uploads. /// /// Availability starting with redmine version 3.3 - public IList Uploads { get; set; } + public List Uploads { get; set; } #endregion #region Implementation of IXmlSerializable diff --git a/tests/redmine-net-api.Integration.Tests/Helpers/RandomHelper.cs b/tests/redmine-net-api.Integration.Tests/Helpers/RandomHelper.cs index 822a7984..ff4923b5 100644 --- a/tests/redmine-net-api.Integration.Tests/Helpers/RandomHelper.cs +++ b/tests/redmine-net-api.Integration.Tests/Helpers/RandomHelper.cs @@ -202,7 +202,7 @@ public static string GenerateFullName(int firstNameLength = 6, int lastNameLengt } // Fisher-Yates shuffle algorithm - public static void Shuffle(this IList list) + public static void Shuffle(this List list) { var n = list.Count; var random = ThreadRandom.Value; From 67451e48d03b2d1a116ffdecd6e135732a74b1cf Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 28 May 2025 17:46:58 +0300 Subject: [PATCH 536/549] Fix tests --- .../redmine-net-api.Tests/Tests/HostTests.cs | 1 - .../Tests/RedmineApiUrlsTests.cs | 390 +++++++----------- 2 files changed, 147 insertions(+), 244 deletions(-) diff --git a/tests/redmine-net-api.Tests/Tests/HostTests.cs b/tests/redmine-net-api.Tests/Tests/HostTests.cs index d2b64bfc..dd4459bb 100644 --- a/tests/redmine-net-api.Tests/Tests/HostTests.cs +++ b/tests/redmine-net-api.Tests/Tests/HostTests.cs @@ -13,7 +13,6 @@ public sealed class HostTests [InlineData(null)] [InlineData("")] [InlineData(" ")] - [InlineData("string.Empty")] [InlineData("localhost")] [InlineData("http://")] [InlineData("")] diff --git a/tests/redmine-net-api.Tests/Tests/RedmineApiUrlsTests.cs b/tests/redmine-net-api.Tests/Tests/RedmineApiUrlsTests.cs index 6096164c..8f030881 100644 --- a/tests/redmine-net-api.Tests/Tests/RedmineApiUrlsTests.cs +++ b/tests/redmine-net-api.Tests/Tests/RedmineApiUrlsTests.cs @@ -14,62 +14,33 @@ namespace Padi.DotNet.RedmineAPI.Tests.Tests; public class RedmineApiUrlsTests(RedmineApiUrlsFixture fixture) : IClassFixture { + private string GetUriWithFormat(string path) + { + return string.Format(path, fixture.Format); + } + [Fact] public void MyAccount_ReturnsCorrectUrl() { var result = fixture.Sut.MyAccount(); - Assert.Equal("my/account.json", result); - } - - [Theory] - [MemberData(nameof(ProjectOperationsData))] - public void ProjectOperations_ReturnsCorrectUrl(string projectId, Func operation, string expected) - { - var result = operation(projectId); - Assert.Equal(expected, result); + Assert.Equal(GetUriWithFormat("my/account.{0}"), result); } [Theory] - [MemberData(nameof(WikiOperationsData))] - public void WikiOperations_ReturnsCorrectUrl(string projectId, string pageName, Func operation, string expected) - { - var result = operation(projectId, pageName); - Assert.Equal(expected, result); - } - - [Theory] - [InlineData("123", "456", "issues/123/watchers/456.json")] + [InlineData("123", "456", "issues/123/watchers/456.{0}")] public void IssueWatcherRemove_WithValidIds_ReturnsCorrectUrl(string issueId, string userId, string expected) { var result = fixture.Sut.IssueWatcherRemove(issueId, userId); - Assert.Equal(expected, result); + Assert.Equal(GetUriWithFormat(expected), result); } [Theory] - [InlineData(null, "456")] - [InlineData("123", null)] - [InlineData("", "456")] - [InlineData("123", "")] - public void IssueWatcherRemove_WithInvalidIds_ThrowsRedmineException(string issueId, string userId) - { - Assert.Throws(() => fixture.Sut.IssueWatcherRemove(issueId, userId)); - } - - [Theory] - [MemberData(nameof(AttachmentOperationsData))] - public void AttachmentOperations_WithValidInput_ReturnsCorrectUrl(string input, Func operation, string expected) - { - var result = operation(input); - Assert.Equal(expected, result); - } - - [Theory] - [InlineData("test.txt", "uploads.json?filename=test.txt")] - [InlineData("file with spaces.pdf", "uploads.json?filename=file%20with%20spaces.pdf")] + [InlineData("test.txt", "uploads.{0}?filename=test.txt")] + [InlineData("file with spaces.pdf", "uploads.{0}?filename=file%20with%20spaces.pdf")] public void UploadFragment_WithFileName_ReturnsCorrectlyEncodedUrl(string fileName, string expected) { var result = fixture.Sut.UploadFragment(fileName); - Assert.Equal(expected, result); + Assert.Equal(GetUriWithFormat(expected), result); } [Theory] @@ -77,9 +48,9 @@ public void UploadFragment_WithFileName_ReturnsCorrectlyEncodedUrl(string fileNa [InlineData("project1", "issue_categories")] public void ProjectParentFragment_ForDifferentTypes_ReturnsCorrectUrl(string projectId, string fragment) { - var expected = $"projects/{projectId}/{fragment}.json"; + var expected = $"projects/{projectId}/{fragment}.{{0}}"; var result = fixture.Sut.ProjectParentFragment(projectId, fragment); - Assert.Equal(expected, result); + Assert.Equal(GetUriWithFormat(expected), result); } [Theory] @@ -87,9 +58,9 @@ public void ProjectParentFragment_ForDifferentTypes_ReturnsCorrectUrl(string pro [InlineData("issue1", "watchers")] public void IssueParentFragment_ForDifferentTypes_ReturnsCorrectUrl(string issueId, string fragment) { - var expected = $"issues/{issueId}/{fragment}.json"; + var expected = $"issues/{issueId}/{fragment}.{{0}}"; var result = fixture.Sut.IssueParentFragment(issueId, fragment); - Assert.Equal(expected, result); + Assert.Equal(GetUriWithFormat(expected), result); } [Theory] @@ -97,7 +68,7 @@ public void IssueParentFragment_ForDifferentTypes_ReturnsCorrectUrl(string issue public void GetFragment_ForAllTypes_ReturnsCorrectUrl(Type type, string id, string expected) { var result = fixture.Sut.GetFragment(type, id); - Assert.Equal(expected, result); + Assert.Equal(GetUriWithFormat(expected), result); } [Theory] @@ -105,7 +76,7 @@ public void GetFragment_ForAllTypes_ReturnsCorrectUrl(Type type, string id, stri public void CreateEntity_ForAllTypes_ReturnsCorrectUrl(Type type, string ownerId, string expected) { var result = fixture.Sut.CreateEntityFragment(type, ownerId); - Assert.Equal(expected, result); + Assert.Equal(GetUriWithFormat(expected), result); } [Theory] @@ -113,7 +84,7 @@ public void CreateEntity_ForAllTypes_ReturnsCorrectUrl(Type type, string ownerId public void GetList_ForAllTypes_ReturnsCorrectUrl(Type type, string ownerId, string expected) { var result = fixture.Sut.GetListFragment(type, ownerId); - Assert.Equal(expected, result); + Assert.Equal(GetUriWithFormat(expected), result); } [Theory] @@ -137,7 +108,7 @@ public void GetListFragment_WithIssueIdInRequestOptions_ReturnsCorrectUrl(Type t }; var result = fixture.Sut.GetListFragment(type, requestOptions); - Assert.Equal(expected, result); + Assert.Equal(GetUriWithFormat(expected), result); } [Theory] @@ -153,7 +124,7 @@ public void GetListFragment_WithProjectIdInRequestOptions_ReturnsCorrectUrl(Type }; var result = fixture.Sut.GetListFragment(type, requestOptions); - Assert.Equal(expected, result); + Assert.Equal(GetUriWithFormat(expected), result); } [Theory] @@ -170,7 +141,7 @@ public void GetListFragment_WithBothIds_PrioritizesProjectId(Type type, string p }; var result = fixture.Sut.GetListFragment(type, requestOptions); - Assert.Equal(expected, result); + Assert.Equal(GetUriWithFormat(expected), result); } [Theory] @@ -178,7 +149,7 @@ public void GetListFragment_WithBothIds_PrioritizesProjectId(Type type, string p public void GetListFragment_WithNoIds_ReturnsDefaultUrl(Type type, string expected) { var result = fixture.Sut.GetListFragment(type, new RequestOptions()); - Assert.Equal(expected, result); + Assert.Equal(GetUriWithFormat(expected), result); } [Theory] @@ -186,7 +157,7 @@ public void GetListFragment_WithNoIds_ReturnsDefaultUrl(Type type, string expect public void GetListFragment_ForAllTypes_ReturnsCorrectUrl(Type type, string parentId, string expected) { var result = fixture.Sut.GetListFragment(type, parentId); - Assert.Equal(expected, result); + Assert.Equal(GetUriWithFormat(expected), result); } [Theory] @@ -194,7 +165,7 @@ public void GetListFragment_ForAllTypes_ReturnsCorrectUrl(Type type, string pare public void GetListFragment_WithEmptyOptions_ReturnsCorrectUrl(Type type, RequestOptions requestOptions, string expected) { var result = fixture.Sut.GetListFragment(type, requestOptions); - Assert.Equal(expected, result); + Assert.Equal(GetUriWithFormat(expected), result); } [Theory] @@ -202,7 +173,7 @@ public void GetListFragment_WithEmptyOptions_ReturnsCorrectUrl(Type type, Reques public void GetListFragment_WithNullOptions_ReturnsCorrectUrl(Type type, string parentId, string expected) { var result = fixture.Sut.GetListFragment(type, parentId); - Assert.Equal(expected, result); + Assert.Equal(GetUriWithFormat(expected), result); } [Theory] @@ -210,7 +181,7 @@ public void GetListFragment_WithNullOptions_ReturnsCorrectUrl(Type type, string public void GetListFragment_WithNullRequestOptions_ReturnsDefaultUrl(Type type, string expected) { var result = fixture.Sut.GetListFragment(type, (RequestOptions)null); - Assert.Equal(expected, result); + Assert.Equal(GetUriWithFormat(expected), result); } [Theory] @@ -223,7 +194,7 @@ public void GetListFragment_WithEmptyQueryString_ReturnsDefaultUrl(Type type, st }; var result = fixture.Sut.GetListFragment(type, requestOptions); - Assert.Equal(expected, result); + Assert.Equal(GetUriWithFormat(expected), result); } [Fact] @@ -240,7 +211,7 @@ public void GetListFragment_WithCustomQueryParameters_DoesNotAffectUrl() }; var result = fixture.Sut.GetListFragment(requestOptions); - Assert.Equal("issues.json", result); + Assert.Equal(GetUriWithFormat("issues.{0}"), result); } [Theory] @@ -260,13 +231,13 @@ public static TheoryData GetListWithBothIdsTestDat typeof(Version), "project1", "issue1", - "projects/project1/versions.json" + "projects/project1/versions.{0}" }, { typeof(IssueCategory), "project2", "issue2", - "projects/project2/issue_categories.json" + "projects/project2/issue_categories.{0}" } }; } @@ -275,28 +246,28 @@ public class RedmineTypeTestData : TheoryData { public RedmineTypeTestData() { - Add(null, "issues.json"); - Add(null,"projects.json"); - Add(null,"users.json"); - Add(null,"time_entries.json"); - Add(null,"custom_fields.json"); - Add(null,"groups.json"); - Add(null,"news.json"); - Add(null,"queries.json"); - Add(null,"roles.json"); - Add(null,"issue_statuses.json"); - Add(null,"trackers.json"); - Add(null,"enumerations/issue_priorities.json"); - Add(null,"enumerations/time_entry_activities.json"); - Add("1","projects/1/versions.json"); - Add("1","projects/1/issue_categories.json"); - Add("1","projects/1/memberships.json"); - Add("1","issues/1/relations.json"); - Add(null,"attachments.json"); - Add(null,"custom_fields.json"); - Add(null,"journals.json"); - Add(null,"search.json"); - Add(null,"watchers.json"); + Add(null, "issues.{0}"); + Add(null,"projects.{0}"); + Add(null,"users.{0}"); + Add(null,"time_entries.{0}"); + Add(null,"custom_fields.{0}"); + Add(null,"groups.{0}"); + Add(null,"news.{0}"); + Add(null,"queries.{0}"); + Add(null,"roles.{0}"); + Add(null,"issue_statuses.{0}"); + Add(null,"trackers.{0}"); + Add(null,"enumerations/issue_priorities.{0}"); + Add(null,"enumerations/time_entry_activities.{0}"); + Add("1","projects/1/versions.{0}"); + Add("1","projects/1/issue_categories.{0}"); + Add("1","projects/1/memberships.{0}"); + Add("1","issues/1/relations.{0}"); + Add(null,"attachments.{0}"); + Add(null,"custom_fields.{0}"); + Add(null,"journals.{0}"); + Add(null,"search.{0}"); + Add(null,"watchers.{0}"); } private void Add(string parentId, string expected) where T : class, new() @@ -309,28 +280,28 @@ public static TheoryData GetFragmentTestData() { return new TheoryData { - { typeof(Attachment), "1", "attachments/1.json" }, - { typeof(CustomField), "2", "custom_fields/2.json" }, - { typeof(Group), "3", "groups/3.json" }, - { typeof(Issue), "4", "issues/4.json" }, - { typeof(IssueCategory), "5", "issue_categories/5.json" }, - { typeof(IssueCustomField), "6", "custom_fields/6.json" }, - { typeof(IssuePriority), "7", "enumerations/issue_priorities/7.json" }, - { typeof(IssueRelation), "8", "relations/8.json" }, - { typeof(IssueStatus), "9", "issue_statuses/9.json" }, - { typeof(Journal), "10", "journals/10.json" }, - { typeof(News), "11", "news/11.json" }, - { typeof(Project), "12", "projects/12.json" }, - { typeof(ProjectMembership), "13", "memberships/13.json" }, - { typeof(Query), "14", "queries/14.json" }, - { typeof(Role), "15", "roles/15.json" }, - { typeof(Search), "16", "search/16.json" }, - { typeof(TimeEntry), "17", "time_entries/17.json" }, - { typeof(TimeEntryActivity), "18", "enumerations/time_entry_activities/18.json" }, - { typeof(Tracker), "19", "trackers/19.json" }, - { typeof(User), "20", "users/20.json" }, - { typeof(Version), "21", "versions/21.json" }, - { typeof(Watcher), "22", "watchers/22.json" } + { typeof(Attachment), "1", "attachments/1.{0}" }, + { typeof(CustomField), "2", "custom_fields/2.{0}" }, + { typeof(Group), "3", "groups/3.{0}" }, + { typeof(Issue), "4", "issues/4.{0}" }, + { typeof(IssueCategory), "5", "issue_categories/5.{0}" }, + { typeof(IssueCustomField), "6", "custom_fields/6.{0}" }, + { typeof(IssuePriority), "7", "enumerations/issue_priorities/7.{0}" }, + { typeof(IssueRelation), "8", "relations/8.{0}" }, + { typeof(IssueStatus), "9", "issue_statuses/9.{0}" }, + { typeof(Journal), "10", "journals/10.{0}" }, + { typeof(News), "11", "news/11.{0}" }, + { typeof(Project), "12", "projects/12.{0}" }, + { typeof(ProjectMembership), "13", "memberships/13.{0}" }, + { typeof(Query), "14", "queries/14.{0}" }, + { typeof(Role), "15", "roles/15.{0}" }, + { typeof(Search), "16", "search/16.{0}" }, + { typeof(TimeEntry), "17", "time_entries/17.{0}" }, + { typeof(TimeEntryActivity), "18", "enumerations/time_entry_activities/18.{0}" }, + { typeof(Tracker), "19", "trackers/19.{0}" }, + { typeof(User), "20", "users/20.{0}" }, + { typeof(Version), "21", "versions/21.{0}" }, + { typeof(Watcher), "22", "watchers/22.{0}" } }; } @@ -338,29 +309,29 @@ public static TheoryData CreateEntityTestData() { return new TheoryData { - { typeof(Version), "project1", "projects/project1/versions.json" }, - { typeof(IssueCategory), "project1", "projects/project1/issue_categories.json" }, - { typeof(ProjectMembership), "project1", "projects/project1/memberships.json" }, + { typeof(Version), "project1", "projects/project1/versions.{0}" }, + { typeof(IssueCategory), "project1", "projects/project1/issue_categories.{0}" }, + { typeof(ProjectMembership), "project1", "projects/project1/memberships.{0}" }, - { typeof(IssueRelation), "issue1", "issues/issue1/relations.json" }, + { typeof(IssueRelation), "issue1", "issues/issue1/relations.{0}" }, - { typeof(File), "project1", "projects/project1/files.json" }, - { typeof(Upload), null, "uploads.json" }, - { typeof(Attachment), "issue1", "/attachments/issues/issue1.json" }, + { typeof(File), "project1", "projects/project1/files.{0}" }, + { typeof(Upload), null, "uploads.{0}" }, + { typeof(Attachment), "issue1", "/attachments/issues/issue1.{0}" }, - { typeof(Issue), null, "issues.json" }, - { typeof(Project), null, "projects.json" }, - { typeof(User), null, "users.json" }, - { typeof(TimeEntry), null, "time_entries.json" }, - { typeof(News), null, "news.json" }, - { typeof(Query), null, "queries.json" }, - { typeof(Role), null, "roles.json" }, - { typeof(Group), null, "groups.json" }, - { typeof(CustomField), null, "custom_fields.json" }, - { typeof(IssueStatus), null, "issue_statuses.json" }, - { typeof(Tracker), null, "trackers.json" }, - { typeof(IssuePriority), null, "enumerations/issue_priorities.json" }, - { typeof(TimeEntryActivity), null, "enumerations/time_entry_activities.json" } + { typeof(Issue), null, "issues.{0}" }, + { typeof(Project), null, "projects.{0}" }, + { typeof(User), null, "users.{0}" }, + { typeof(TimeEntry), null, "time_entries.{0}" }, + { typeof(News), null, "news.{0}" }, + { typeof(Query), null, "queries.{0}" }, + { typeof(Role), null, "roles.{0}" }, + { typeof(Group), null, "groups.{0}" }, + { typeof(CustomField), null, "custom_fields.{0}" }, + { typeof(IssueStatus), null, "issue_statuses.{0}" }, + { typeof(Tracker), null, "trackers.{0}" }, + { typeof(IssuePriority), null, "enumerations/issue_priorities.{0}" }, + { typeof(TimeEntryActivity), null, "enumerations/time_entry_activities.{0}" } }; } @@ -382,28 +353,28 @@ public static TheoryData GetListEntityRequestOptio }; return new TheoryData { - { typeof(Version), rqWithProjectId, "projects/project1/versions.json" }, - { typeof(IssueCategory), rqWithProjectId, "projects/project1/issue_categories.json" }, - { typeof(ProjectMembership), rqWithProjectId, "projects/project1/memberships.json" }, + { typeof(Version), rqWithProjectId, "projects/project1/versions.{0}" }, + { typeof(IssueCategory), rqWithProjectId, "projects/project1/issue_categories.{0}" }, + { typeof(ProjectMembership), rqWithProjectId, "projects/project1/memberships.{0}" }, - { typeof(IssueRelation), rqWithPIssueId, "issues/issue1/relations.json" }, + { typeof(IssueRelation), rqWithPIssueId, "issues/issue1/relations.{0}" }, - { typeof(File), rqWithProjectId, "projects/project1/files.json" }, - { typeof(Attachment), rqWithPIssueId, "attachments.json" }, + { typeof(File), rqWithProjectId, "projects/project1/files.{0}" }, + { typeof(Attachment), rqWithPIssueId, "attachments.{0}" }, - { typeof(Issue), null, "issues.json" }, - { typeof(Project), null, "projects.json" }, - { typeof(User), null, "users.json" }, - { typeof(TimeEntry), null, "time_entries.json" }, - { typeof(News), null, "news.json" }, - { typeof(Query), null, "queries.json" }, - { typeof(Role), null, "roles.json" }, - { typeof(Group), null, "groups.json" }, - { typeof(CustomField), null, "custom_fields.json" }, - { typeof(IssueStatus), null, "issue_statuses.json" }, - { typeof(Tracker), null, "trackers.json" }, - { typeof(IssuePriority), null, "enumerations/issue_priorities.json" }, - { typeof(TimeEntryActivity), null, "enumerations/time_entry_activities.json" } + { typeof(Issue), null, "issues.{0}" }, + { typeof(Project), null, "projects.{0}" }, + { typeof(User), null, "users.{0}" }, + { typeof(TimeEntry), null, "time_entries.{0}" }, + { typeof(News), null, "news.{0}" }, + { typeof(Query), null, "queries.{0}" }, + { typeof(Role), null, "roles.{0}" }, + { typeof(Group), null, "groups.{0}" }, + { typeof(CustomField), null, "custom_fields.{0}" }, + { typeof(IssueStatus), null, "issue_statuses.{0}" }, + { typeof(Tracker), null, "trackers.{0}" }, + { typeof(IssuePriority), null, "enumerations/issue_priorities.{0}" }, + { typeof(TimeEntryActivity), null, "enumerations/time_entry_activities.{0}" } }; } @@ -411,27 +382,27 @@ public static TheoryData GetListTestData() { return new TheoryData { - { typeof(Version), "project1", "projects/project1/versions.json" }, - { typeof(IssueCategory), "project1", "projects/project1/issue_categories.json" }, - { typeof(ProjectMembership), "project1", "projects/project1/memberships.json" }, + { typeof(Version), "project1", "projects/project1/versions.{0}" }, + { typeof(IssueCategory), "project1", "projects/project1/issue_categories.{0}" }, + { typeof(ProjectMembership), "project1", "projects/project1/memberships.{0}" }, - { typeof(IssueRelation), "issue1", "issues/issue1/relations.json" }, + { typeof(IssueRelation), "issue1", "issues/issue1/relations.{0}" }, - { typeof(File), "project1", "projects/project1/files.json" }, + { typeof(File), "project1", "projects/project1/files.{0}" }, - { typeof(Issue), null, "issues.json" }, - { typeof(Project), null, "projects.json" }, - { typeof(User), null, "users.json" }, - { typeof(TimeEntry), null, "time_entries.json" }, - { typeof(News), null, "news.json" }, - { typeof(Query), null, "queries.json" }, - { typeof(Role), null, "roles.json" }, - { typeof(Group), null, "groups.json" }, - { typeof(CustomField), null, "custom_fields.json" }, - { typeof(IssueStatus), null, "issue_statuses.json" }, - { typeof(Tracker), null, "trackers.json" }, - { typeof(IssuePriority), null, "enumerations/issue_priorities.json" }, - { typeof(TimeEntryActivity), null, "enumerations/time_entry_activities.json" } + { typeof(Issue), null, "issues.{0}" }, + { typeof(Project), null, "projects.{0}" }, + { typeof(User), null, "users.{0}" }, + { typeof(TimeEntry), null, "time_entries.{0}" }, + { typeof(News), null, "news.{0}" }, + { typeof(Query), null, "queries.{0}" }, + { typeof(Role), null, "roles.{0}" }, + { typeof(Group), null, "groups.{0}" }, + { typeof(CustomField), null, "custom_fields.{0}" }, + { typeof(IssueStatus), null, "issue_statuses.{0}" }, + { typeof(Tracker), null, "trackers.{0}" }, + { typeof(IssuePriority), null, "enumerations/issue_priorities.{0}" }, + { typeof(TimeEntryActivity), null, "enumerations/time_entry_activities.{0}" } }; } @@ -439,7 +410,7 @@ public static TheoryData GetListWithIssueIdTestData() { return new TheoryData { - { typeof(IssueRelation), "issue1", "issues/issue1/relations.json" }, + { typeof(IssueRelation), "issue1", "issues/issue1/relations.{0}" }, }; } @@ -447,10 +418,10 @@ public static TheoryData GetListWithProjectIdTestData() { return new TheoryData { - { typeof(Version), "1", "projects/1/versions.json" }, - { typeof(IssueCategory), "1", "projects/1/issue_categories.json" }, - { typeof(ProjectMembership), "1", "projects/1/memberships.json" }, - { typeof(File), "1", "projects/1/files.json" }, + { typeof(Version), "1", "projects/1/versions.{0}" }, + { typeof(IssueCategory), "1", "projects/1/issue_categories.{0}" }, + { typeof(ProjectMembership), "1", "projects/1/memberships.{0}" }, + { typeof(File), "1", "projects/1/files.{0}" }, }; } @@ -458,9 +429,9 @@ public static TheoryData GetListWithNullRequestOptionsTestData() { return new TheoryData { - { typeof(Issue), "issues.json" }, - { typeof(Project), "projects.json" }, - { typeof(User), "users.json" } + { typeof(Issue), "issues.{0}" }, + { typeof(Project), "projects.{0}" }, + { typeof(User), "users.{0}" } }; } @@ -468,9 +439,9 @@ public static TheoryData GetListWithEmptyQueryStringTestData() { return new TheoryData { - { typeof(Issue), "issues.json" }, - { typeof(Project), "projects.json" }, - { typeof(User), "users.json" } + { typeof(Issue), "issues.{0}" }, + { typeof(Project), "projects.{0}" }, + { typeof(User), "users.{0}" } }; } @@ -489,11 +460,11 @@ public static TheoryData GetListWithNoIdsTestData() { return new TheoryData { - { typeof(Issue), "issues.json" }, - { typeof(Project), "projects.json" }, - { typeof(User), "users.json" }, - { typeof(TimeEntry), "time_entries.json" }, - { typeof(CustomField), "custom_fields.json" } + { typeof(Issue), "issues.{0}" }, + { typeof(Project), "projects.{0}" }, + { typeof(User), "users.{0}" }, + { typeof(TimeEntry), "time_entries.{0}" }, + { typeof(CustomField), "custom_fields.{0}" } }; } @@ -505,71 +476,4 @@ public static TheoryData InvalidTypeTestData() typeof(int) ]; } - - public static TheoryData, string> AttachmentOperationsData() - { - var fixture = new RedmineApiUrlsFixture(); - return new TheoryData, string> - { - { - "123", - id => fixture.Sut.AttachmentUpdate(id), - "attachments/issues/123.json" - }, - { - "456", - id => fixture.Sut.IssueWatcherAdd(id), - "issues/456/watchers.json" - } - }; - } - - public static TheoryData, string> ProjectOperationsData() - { - var fixture = new RedmineApiUrlsFixture(); - return new TheoryData, string> - { - { - "test-project", - id => fixture.Sut.ProjectClose(id), - "projects/test-project/close.json" - }, - { - "test-project", - id => fixture.Sut.ProjectReopen(id), - "projects/test-project/reopen.json" - }, - { - "test-project", - id => fixture.Sut.ProjectArchive(id), - "projects/test-project/archive.json" - }, - { - "test-project", - id => fixture.Sut.ProjectUnarchive(id), - "projects/test-project/unarchive.json" - } - }; - } - - public static TheoryData, string> WikiOperationsData() - { - var fixture = new RedmineApiUrlsFixture(); - return new TheoryData, string> - { - { - "project1", - "page1", - (id, page) => fixture.Sut.ProjectWikiPage(id, page), - "projects/project1/wiki/page1.json" - }, - { - "project1", - "page1", - (id, page) => fixture.Sut.ProjectWikiPageCreate(id, page), - "projects/project1/wiki/page1.json" - } - }; - } - } \ No newline at end of file From e23721f6b02048e7950cf1f7c284a4dd497dab11 Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 28 May 2025 18:26:07 +0300 Subject: [PATCH 537/549] Remove unused namespaces --- src/redmine-net-api/Extensions/EnumExtensions.cs | 1 - .../Http/Clients/WebClient/InternalRedmineApiWebClient.cs | 1 - .../Http/Clients/WebClient/RedmineWebClientOptions.cs | 1 - src/redmine-net-api/Http/Clients/WebClient/WebClientProvider.cs | 2 -- .../Http/Extensions/NameValueCollectionExtensions.cs | 1 - src/redmine-net-api/Http/IRedmineApiClient.cs | 2 -- src/redmine-net-api/Http/IRedmineApiClientOptions.cs | 1 - src/redmine-net-api/Http/RedmineApiClient.cs | 2 -- src/redmine-net-api/Http/RedmineApiClientOptions.cs | 1 - src/redmine-net-api/SearchFilterBuilder.cs | 1 - src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs | 1 - src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs | 2 -- src/redmine-net-api/Types/Attachment.cs | 1 - src/redmine-net-api/Types/Attachments.cs | 2 -- src/redmine-net-api/Types/ChangeSet.cs | 2 -- src/redmine-net-api/Types/CustomFieldPossibleValue.cs | 1 - src/redmine-net-api/Types/CustomFieldValue.cs | 1 - src/redmine-net-api/Types/Detail.cs | 1 - src/redmine-net-api/Types/Error.cs | 1 - src/redmine-net-api/Types/File.cs | 1 - src/redmine-net-api/Types/Group.cs | 1 - src/redmine-net-api/Types/GroupUser.cs | 1 - src/redmine-net-api/Types/Identifiable.cs | 1 - src/redmine-net-api/Types/IdentifiableName.cs | 1 - src/redmine-net-api/Types/Issue.cs | 1 - src/redmine-net-api/Types/IssueCategory.cs | 1 - src/redmine-net-api/Types/IssueRelation.cs | 1 - src/redmine-net-api/Types/MembershipRole.cs | 1 - src/redmine-net-api/Types/MyAccount.cs | 1 - src/redmine-net-api/Types/News.cs | 1 - src/redmine-net-api/Types/Permission.cs | 1 - src/redmine-net-api/Types/Project.cs | 1 - src/redmine-net-api/Types/ProjectMembership.cs | 1 - src/redmine-net-api/Types/ProjectTracker.cs | 1 - src/redmine-net-api/Types/Search.cs | 1 - src/redmine-net-api/Types/TimeEntry.cs | 1 - src/redmine-net-api/Types/TrackerCoreField.cs | 1 - src/redmine-net-api/Types/Upload.cs | 2 -- src/redmine-net-api/Types/Version.cs | 1 - src/redmine-net-api/Types/Watcher.cs | 1 - src/redmine-net-api/Types/WikiPage.cs | 1 - 41 files changed, 48 deletions(-) diff --git a/src/redmine-net-api/Extensions/EnumExtensions.cs b/src/redmine-net-api/Extensions/EnumExtensions.cs index 60b6499e..848fdc2e 100644 --- a/src/redmine-net-api/Extensions/EnumExtensions.cs +++ b/src/redmine-net-api/Extensions/EnumExtensions.cs @@ -1,4 +1,3 @@ -using System; using Redmine.Net.Api.Authentication; using Redmine.Net.Api.Types; diff --git a/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.cs b/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.cs index f08e3474..775b7bed 100644 --- a/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.cs +++ b/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.cs @@ -17,7 +17,6 @@ limitations under the License. using System; using System.Collections.Specialized; using System.Globalization; -using System.IO; using System.Net; using System.Text; using Redmine.Net.Api.Authentication; diff --git a/src/redmine-net-api/Http/Clients/WebClient/RedmineWebClientOptions.cs b/src/redmine-net-api/Http/Clients/WebClient/RedmineWebClientOptions.cs index 013d5a82..65291f8e 100644 --- a/src/redmine-net-api/Http/Clients/WebClient/RedmineWebClientOptions.cs +++ b/src/redmine-net-api/Http/Clients/WebClient/RedmineWebClientOptions.cs @@ -18,7 +18,6 @@ limitations under the License. #if (NET45_OR_GREATER || NET) using System.Net.Security; #endif -using System.Security.Cryptography.X509Certificates; namespace Redmine.Net.Api.Http.Clients.WebClient; /// diff --git a/src/redmine-net-api/Http/Clients/WebClient/WebClientProvider.cs b/src/redmine-net-api/Http/Clients/WebClient/WebClientProvider.cs index ff8c664b..10af757d 100644 --- a/src/redmine-net-api/Http/Clients/WebClient/WebClientProvider.cs +++ b/src/redmine-net-api/Http/Clients/WebClient/WebClientProvider.cs @@ -1,5 +1,3 @@ -using System; -using System.Net; using System.Text; using Redmine.Net.Api.Options; diff --git a/src/redmine-net-api/Http/Extensions/NameValueCollectionExtensions.cs b/src/redmine-net-api/Http/Extensions/NameValueCollectionExtensions.cs index 3cf95a1d..5199775f 100644 --- a/src/redmine-net-api/Http/Extensions/NameValueCollectionExtensions.cs +++ b/src/redmine-net-api/Http/Extensions/NameValueCollectionExtensions.cs @@ -14,7 +14,6 @@ You may obtain a copy of the License at limitations under the License. */ -using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; diff --git a/src/redmine-net-api/Http/IRedmineApiClient.cs b/src/redmine-net-api/Http/IRedmineApiClient.cs index 203d9599..ed3b23c9 100644 --- a/src/redmine-net-api/Http/IRedmineApiClient.cs +++ b/src/redmine-net-api/Http/IRedmineApiClient.cs @@ -20,8 +20,6 @@ limitations under the License. using System.Threading; using System.Threading.Tasks; #endif -using Redmine.Net.Api.Net; -using Redmine.Net.Api.Net.Internal; namespace Redmine.Net.Api.Http; /// diff --git a/src/redmine-net-api/Http/IRedmineApiClientOptions.cs b/src/redmine-net-api/Http/IRedmineApiClientOptions.cs index 44089261..df00aaef 100644 --- a/src/redmine-net-api/Http/IRedmineApiClientOptions.cs +++ b/src/redmine-net-api/Http/IRedmineApiClientOptions.cs @@ -18,7 +18,6 @@ limitations under the License. using System.Collections.Generic; using System.Net; using System.Net.Cache; -using System.Net.Security; using System.Security.Cryptography.X509Certificates; namespace Redmine.Net.Api.Http diff --git a/src/redmine-net-api/Http/RedmineApiClient.cs b/src/redmine-net-api/Http/RedmineApiClient.cs index c105e59e..0f2889e4 100644 --- a/src/redmine-net-api/Http/RedmineApiClient.cs +++ b/src/redmine-net-api/Http/RedmineApiClient.cs @@ -2,8 +2,6 @@ using Redmine.Net.Api.Authentication; using Redmine.Net.Api.Http.Constants; using Redmine.Net.Api.Http.Messages; -using Redmine.Net.Api.Net; -using Redmine.Net.Api.Net.Internal; using Redmine.Net.Api.Options; using Redmine.Net.Api.Serialization; diff --git a/src/redmine-net-api/Http/RedmineApiClientOptions.cs b/src/redmine-net-api/Http/RedmineApiClientOptions.cs index 1cf7f6cc..7aa7f4e0 100644 --- a/src/redmine-net-api/Http/RedmineApiClientOptions.cs +++ b/src/redmine-net-api/Http/RedmineApiClientOptions.cs @@ -5,7 +5,6 @@ #if NET || NET471_OR_GREATER using System.Net.Http; #endif -using System.Net.Security; using System.Security.Cryptography.X509Certificates; namespace Redmine.Net.Api.Http; diff --git a/src/redmine-net-api/SearchFilterBuilder.cs b/src/redmine-net-api/SearchFilterBuilder.cs index 9b2b8c9c..8f376aa6 100644 --- a/src/redmine-net-api/SearchFilterBuilder.cs +++ b/src/redmine-net-api/SearchFilterBuilder.cs @@ -16,7 +16,6 @@ limitations under the License. using System; using System.Collections.Specialized; -using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Http.Extensions; namespace Redmine.Net.Api diff --git a/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs b/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs index cd078537..bbc2c488 100644 --- a/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs +++ b/src/redmine-net-api/Serialization/Json/JsonRedmineSerializer.cs @@ -21,7 +21,6 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Common; using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Serialization.Json.Extensions; namespace Redmine.Net.Api.Serialization.Json diff --git a/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs b/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs index e86111df..266c14b0 100644 --- a/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs +++ b/src/redmine-net-api/Serialization/Xml/XmlRedmineSerializer.cs @@ -20,8 +20,6 @@ limitations under the License. using System.Xml.Serialization; using Redmine.Net.Api.Common; using Redmine.Net.Api.Exceptions; -using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Internals; using Redmine.Net.Api.Serialization.Xml.Extensions; namespace Redmine.Net.Api.Serialization.Xml diff --git a/src/redmine-net-api/Types/Attachment.cs b/src/redmine-net-api/Types/Attachment.cs index 56afc0c5..a57f4dfa 100644 --- a/src/redmine-net-api/Types/Attachment.cs +++ b/src/redmine-net-api/Types/Attachment.cs @@ -21,7 +21,6 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Serialization.Json; using Redmine.Net.Api.Serialization.Json.Extensions; using Redmine.Net.Api.Serialization.Xml.Extensions; diff --git a/src/redmine-net-api/Types/Attachments.cs b/src/redmine-net-api/Types/Attachments.cs index c475d531..15b5e68f 100644 --- a/src/redmine-net-api/Types/Attachments.cs +++ b/src/redmine-net-api/Types/Attachments.cs @@ -15,10 +15,8 @@ limitations under the License. */ using System.Collections.Generic; -using System.Globalization; using Newtonsoft.Json; using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Serialization.Json; namespace Redmine.Net.Api.Types diff --git a/src/redmine-net-api/Types/ChangeSet.cs b/src/redmine-net-api/Types/ChangeSet.cs index adfff4fb..1ea9e906 100644 --- a/src/redmine-net-api/Types/ChangeSet.cs +++ b/src/redmine-net-api/Types/ChangeSet.cs @@ -21,9 +21,7 @@ limitations under the License. using System.Xml.Schema; using System.Xml.Serialization; using Newtonsoft.Json; -using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Serialization.Json; using Redmine.Net.Api.Serialization.Xml.Extensions; diff --git a/src/redmine-net-api/Types/CustomFieldPossibleValue.cs b/src/redmine-net-api/Types/CustomFieldPossibleValue.cs index 56edabd7..2e3f6318 100644 --- a/src/redmine-net-api/Types/CustomFieldPossibleValue.cs +++ b/src/redmine-net-api/Types/CustomFieldPossibleValue.cs @@ -21,7 +21,6 @@ limitations under the License. using System.Xml.Serialization; using Newtonsoft.Json; using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Serialization.Json; namespace Redmine.Net.Api.Types diff --git a/src/redmine-net-api/Types/CustomFieldValue.cs b/src/redmine-net-api/Types/CustomFieldValue.cs index 1dacf6b8..c7f4995e 100644 --- a/src/redmine-net-api/Types/CustomFieldValue.cs +++ b/src/redmine-net-api/Types/CustomFieldValue.cs @@ -21,7 +21,6 @@ limitations under the License. using System.Xml.Serialization; using Newtonsoft.Json; using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Serialization.Json; namespace Redmine.Net.Api.Types diff --git a/src/redmine-net-api/Types/Detail.cs b/src/redmine-net-api/Types/Detail.cs index 4f69af20..3a3f89ba 100644 --- a/src/redmine-net-api/Types/Detail.cs +++ b/src/redmine-net-api/Types/Detail.cs @@ -21,7 +21,6 @@ limitations under the License. using System.Xml.Serialization; using Newtonsoft.Json; using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Serialization.Json; namespace Redmine.Net.Api.Types diff --git a/src/redmine-net-api/Types/Error.cs b/src/redmine-net-api/Types/Error.cs index f8632707..45b82e26 100644 --- a/src/redmine-net-api/Types/Error.cs +++ b/src/redmine-net-api/Types/Error.cs @@ -21,7 +21,6 @@ limitations under the License. using System.Xml.Serialization; using Newtonsoft.Json; using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Serialization.Json; namespace Redmine.Net.Api.Types diff --git a/src/redmine-net-api/Types/File.cs b/src/redmine-net-api/Types/File.cs index 491f41d9..5b9efe39 100644 --- a/src/redmine-net-api/Types/File.cs +++ b/src/redmine-net-api/Types/File.cs @@ -23,7 +23,6 @@ limitations under the License. using System.Xml; using System.Xml.Serialization; using Newtonsoft.Json; -using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Serialization.Json; using Redmine.Net.Api.Serialization.Json.Extensions; using Redmine.Net.Api.Serialization.Xml.Extensions; diff --git a/src/redmine-net-api/Types/Group.cs b/src/redmine-net-api/Types/Group.cs index 8d3d01d1..8209e3b3 100644 --- a/src/redmine-net-api/Types/Group.cs +++ b/src/redmine-net-api/Types/Group.cs @@ -23,7 +23,6 @@ limitations under the License. using Redmine.Net.Api.Common; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Serialization.Json; using Redmine.Net.Api.Serialization.Json.Extensions; using Redmine.Net.Api.Serialization.Xml.Extensions; diff --git a/src/redmine-net-api/Types/GroupUser.cs b/src/redmine-net-api/Types/GroupUser.cs index 4264f82d..0151058b 100644 --- a/src/redmine-net-api/Types/GroupUser.cs +++ b/src/redmine-net-api/Types/GroupUser.cs @@ -15,7 +15,6 @@ limitations under the License. */ using System.Diagnostics; -using System.Globalization; using System.Xml.Serialization; using Redmine.Net.Api.Common; using Redmine.Net.Api.Extensions; diff --git a/src/redmine-net-api/Types/Identifiable.cs b/src/redmine-net-api/Types/Identifiable.cs index 42d789a7..7b48caf8 100644 --- a/src/redmine-net-api/Types/Identifiable.cs +++ b/src/redmine-net-api/Types/Identifiable.cs @@ -22,7 +22,6 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Serialization.Json; namespace Redmine.Net.Api.Types diff --git a/src/redmine-net-api/Types/IdentifiableName.cs b/src/redmine-net-api/Types/IdentifiableName.cs index f320426f..ad0ac001 100644 --- a/src/redmine-net-api/Types/IdentifiableName.cs +++ b/src/redmine-net-api/Types/IdentifiableName.cs @@ -16,7 +16,6 @@ limitations under the License. using System; using System.Diagnostics; -using System.Globalization; using System.Xml; using Newtonsoft.Json; using Redmine.Net.Api.Extensions; diff --git a/src/redmine-net-api/Types/Issue.cs b/src/redmine-net-api/Types/Issue.cs index 1298f214..4cfbc308 100644 --- a/src/redmine-net-api/Types/Issue.cs +++ b/src/redmine-net-api/Types/Issue.cs @@ -24,7 +24,6 @@ limitations under the License. using Redmine.Net.Api.Common; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Serialization.Json; using Redmine.Net.Api.Serialization.Json.Extensions; using Redmine.Net.Api.Serialization.Xml.Extensions; diff --git a/src/redmine-net-api/Types/IssueCategory.cs b/src/redmine-net-api/Types/IssueCategory.cs index 1dbc3ca5..b66c575f 100644 --- a/src/redmine-net-api/Types/IssueCategory.cs +++ b/src/redmine-net-api/Types/IssueCategory.cs @@ -20,7 +20,6 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Serialization.Json; using Redmine.Net.Api.Serialization.Json.Extensions; using Redmine.Net.Api.Serialization.Xml.Extensions; diff --git a/src/redmine-net-api/Types/IssueRelation.cs b/src/redmine-net-api/Types/IssueRelation.cs index 62ddfeea..c790cabc 100644 --- a/src/redmine-net-api/Types/IssueRelation.cs +++ b/src/redmine-net-api/Types/IssueRelation.cs @@ -22,7 +22,6 @@ limitations under the License. using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Serialization.Json; using Redmine.Net.Api.Serialization.Json.Extensions; using Redmine.Net.Api.Serialization.Xml.Extensions; diff --git a/src/redmine-net-api/Types/MembershipRole.cs b/src/redmine-net-api/Types/MembershipRole.cs index fcfab110..36a3aefd 100644 --- a/src/redmine-net-api/Types/MembershipRole.cs +++ b/src/redmine-net-api/Types/MembershipRole.cs @@ -16,7 +16,6 @@ limitations under the License. using System; using System.Diagnostics; -using System.Globalization; using System.Xml; using System.Xml.Serialization; using Newtonsoft.Json; diff --git a/src/redmine-net-api/Types/MyAccount.cs b/src/redmine-net-api/Types/MyAccount.cs index 570c5d67..d325c04f 100644 --- a/src/redmine-net-api/Types/MyAccount.cs +++ b/src/redmine-net-api/Types/MyAccount.cs @@ -22,7 +22,6 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Serialization.Json; using Redmine.Net.Api.Serialization.Json.Extensions; using Redmine.Net.Api.Serialization.Xml.Extensions; diff --git a/src/redmine-net-api/Types/News.cs b/src/redmine-net-api/Types/News.cs index 6e830f72..22415dc0 100644 --- a/src/redmine-net-api/Types/News.cs +++ b/src/redmine-net-api/Types/News.cs @@ -22,7 +22,6 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Serialization.Json; using Redmine.Net.Api.Serialization.Json.Extensions; using Redmine.Net.Api.Serialization.Xml.Extensions; diff --git a/src/redmine-net-api/Types/Permission.cs b/src/redmine-net-api/Types/Permission.cs index 7612ecef..69e14eea 100644 --- a/src/redmine-net-api/Types/Permission.cs +++ b/src/redmine-net-api/Types/Permission.cs @@ -21,7 +21,6 @@ limitations under the License. using System.Xml.Serialization; using Newtonsoft.Json; using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Serialization.Json; namespace Redmine.Net.Api.Types diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs index 5ff1f11f..39d9a325 100644 --- a/src/redmine-net-api/Types/Project.cs +++ b/src/redmine-net-api/Types/Project.cs @@ -23,7 +23,6 @@ limitations under the License. using Redmine.Net.Api.Common; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Serialization.Json; using Redmine.Net.Api.Serialization.Json.Extensions; using Redmine.Net.Api.Serialization.Xml.Extensions; diff --git a/src/redmine-net-api/Types/ProjectMembership.cs b/src/redmine-net-api/Types/ProjectMembership.cs index 8528a7de..42ef7e5a 100644 --- a/src/redmine-net-api/Types/ProjectMembership.cs +++ b/src/redmine-net-api/Types/ProjectMembership.cs @@ -21,7 +21,6 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Serialization.Json; using Redmine.Net.Api.Serialization.Json.Extensions; using Redmine.Net.Api.Serialization.Xml.Extensions; diff --git a/src/redmine-net-api/Types/ProjectTracker.cs b/src/redmine-net-api/Types/ProjectTracker.cs index e5fc918d..a3c93011 100644 --- a/src/redmine-net-api/Types/ProjectTracker.cs +++ b/src/redmine-net-api/Types/ProjectTracker.cs @@ -15,7 +15,6 @@ limitations under the License. */ using System.Diagnostics; -using System.Globalization; using System.Xml.Serialization; using Redmine.Net.Api.Common; using Redmine.Net.Api.Extensions; diff --git a/src/redmine-net-api/Types/Search.cs b/src/redmine-net-api/Types/Search.cs index 0dec7001..37792323 100644 --- a/src/redmine-net-api/Types/Search.cs +++ b/src/redmine-net-api/Types/Search.cs @@ -22,7 +22,6 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Serialization.Json; using Redmine.Net.Api.Serialization.Json.Extensions; using Redmine.Net.Api.Serialization.Xml.Extensions; diff --git a/src/redmine-net-api/Types/TimeEntry.cs b/src/redmine-net-api/Types/TimeEntry.cs index f3580a49..f02cebbf 100644 --- a/src/redmine-net-api/Types/TimeEntry.cs +++ b/src/redmine-net-api/Types/TimeEntry.cs @@ -23,7 +23,6 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Serialization.Json; using Redmine.Net.Api.Serialization.Json.Extensions; using Redmine.Net.Api.Serialization.Xml.Extensions; diff --git a/src/redmine-net-api/Types/TrackerCoreField.cs b/src/redmine-net-api/Types/TrackerCoreField.cs index ec9e9360..aad3d8c1 100644 --- a/src/redmine-net-api/Types/TrackerCoreField.cs +++ b/src/redmine-net-api/Types/TrackerCoreField.cs @@ -5,7 +5,6 @@ using System.Xml.Serialization; using Newtonsoft.Json; using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Serialization.Json; namespace Redmine.Net.Api.Types diff --git a/src/redmine-net-api/Types/Upload.cs b/src/redmine-net-api/Types/Upload.cs index e2815836..d5efeb7e 100644 --- a/src/redmine-net-api/Types/Upload.cs +++ b/src/redmine-net-api/Types/Upload.cs @@ -20,9 +20,7 @@ limitations under the License. using System.Xml.Schema; using System.Xml.Serialization; using Newtonsoft.Json; -using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Serialization.Json; using Redmine.Net.Api.Serialization.Json.Extensions; diff --git a/src/redmine-net-api/Types/Version.cs b/src/redmine-net-api/Types/Version.cs index 4d6f9ac0..4c8c9152 100644 --- a/src/redmine-net-api/Types/Version.cs +++ b/src/redmine-net-api/Types/Version.cs @@ -22,7 +22,6 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Serialization.Json; using Redmine.Net.Api.Serialization.Json.Extensions; using Redmine.Net.Api.Serialization.Xml.Extensions; diff --git a/src/redmine-net-api/Types/Watcher.cs b/src/redmine-net-api/Types/Watcher.cs index a90913c2..20d3409e 100644 --- a/src/redmine-net-api/Types/Watcher.cs +++ b/src/redmine-net-api/Types/Watcher.cs @@ -16,7 +16,6 @@ limitations under the License. using System; using System.Diagnostics; -using System.Globalization; using System.Xml.Serialization; using Redmine.Net.Api.Common; using Redmine.Net.Api.Extensions; diff --git a/src/redmine-net-api/Types/WikiPage.cs b/src/redmine-net-api/Types/WikiPage.cs index 2b9f35e9..566cf58f 100644 --- a/src/redmine-net-api/Types/WikiPage.cs +++ b/src/redmine-net-api/Types/WikiPage.cs @@ -22,7 +22,6 @@ limitations under the License. using Newtonsoft.Json; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Internals; -using Redmine.Net.Api.Serialization; using Redmine.Net.Api.Serialization.Json; using Redmine.Net.Api.Serialization.Json.Extensions; using Redmine.Net.Api.Serialization.Xml.Extensions; From 2aa07abcf34d740bad120cdaca11cc5eb0ed4da5 Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 28 May 2025 18:37:02 +0300 Subject: [PATCH 538/549] Remove redundant uncheck context --- src/redmine-net-api/IRedmineManager.cs | 2 +- src/redmine-net-api/Types/Attachment.cs | 23 ++++----- src/redmine-net-api/Types/ChangeSet.cs | 15 +++--- src/redmine-net-api/Types/CustomField.cs | 37 +++++++------- src/redmine-net-api/Types/CustomFieldValue.cs | 9 ++-- src/redmine-net-api/Types/Detail.cs | 15 +++--- src/redmine-net-api/Types/DocumentCategory.cs | 11 ++-- src/redmine-net-api/Types/Error.cs | 9 ++-- src/redmine-net-api/Types/Group.cs | 13 ++--- src/redmine-net-api/Types/Identifiable.cs | 9 ++-- src/redmine-net-api/Types/IdentifiableName.cs | 9 ++-- .../Types/IssueAllowedStatus.cs | 9 ++-- src/redmine-net-api/Types/IssueCategory.cs | 13 ++--- src/redmine-net-api/Types/IssueChild.cs | 11 ++-- src/redmine-net-api/Types/IssueCustomField.cs | 11 ++-- src/redmine-net-api/Types/IssuePriority.cs | 11 ++-- src/redmine-net-api/Types/IssueRelation.cs | 15 +++--- src/redmine-net-api/Types/Journal.cs | 21 ++++---- src/redmine-net-api/Types/Membership.cs | 15 +++--- src/redmine-net-api/Types/MembershipRole.cs | 9 ++-- src/redmine-net-api/Types/MyAccount.cs | 27 +++++----- .../Types/MyAccountCustomField.cs | 9 ++-- src/redmine-net-api/Types/News.cs | 27 +++++----- src/redmine-net-api/Types/NewsComment.cs | 13 ++--- src/redmine-net-api/Types/Permission.cs | 9 ++-- src/redmine-net-api/Types/Project.cs | 43 ++++++++-------- .../Types/ProjectMembership.cs | 15 +++--- src/redmine-net-api/Types/Query.cs | 11 ++-- src/redmine-net-api/Types/Role.cs | 17 +++---- src/redmine-net-api/Types/Search.cs | 19 +++---- src/redmine-net-api/Types/TimeEntry.cs | 27 +++++----- .../Types/TimeEntryActivity.cs | 11 ++-- src/redmine-net-api/Types/Tracker.cs | 13 ++--- src/redmine-net-api/Types/TrackerCoreField.cs | 9 ++-- src/redmine-net-api/Types/Upload.cs | 15 +++--- src/redmine-net-api/Types/User.cs | 51 +++++++++---------- src/redmine-net-api/Types/Version.cs | 29 +++++------ src/redmine-net-api/Types/Watcher.cs | 9 ++-- src/redmine-net-api/Types/WikiPage.cs | 23 ++++----- 39 files changed, 265 insertions(+), 379 deletions(-) diff --git a/src/redmine-net-api/IRedmineManager.cs b/src/redmine-net-api/IRedmineManager.cs index e7f487dc..e4830a43 100644 --- a/src/redmine-net-api/IRedmineManager.cs +++ b/src/redmine-net-api/IRedmineManager.cs @@ -26,7 +26,7 @@ namespace Redmine.Net.Api; /// /// /// -public partial interface IRedmineManager +public interface IRedmineManager { /// /// diff --git a/src/redmine-net-api/Types/Attachment.cs b/src/redmine-net-api/Types/Attachment.cs index a57f4dfa..dc36465c 100644 --- a/src/redmine-net-api/Types/Attachment.cs +++ b/src/redmine-net-api/Types/Attachment.cs @@ -219,19 +219,16 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(FileName, hashCode); - hashCode = HashCodeHelper.GetHashCode(FileSize, hashCode); - hashCode = HashCodeHelper.GetHashCode(ContentType, hashCode); - hashCode = HashCodeHelper.GetHashCode(Description, hashCode); - hashCode = HashCodeHelper.GetHashCode(ContentUrl, hashCode); - hashCode = HashCodeHelper.GetHashCode(ThumbnailUrl, hashCode); - hashCode = HashCodeHelper.GetHashCode(Author, hashCode); - hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); - return hashCode; - } + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(FileName, hashCode); + hashCode = HashCodeHelper.GetHashCode(FileSize, hashCode); + hashCode = HashCodeHelper.GetHashCode(ContentType, hashCode); + hashCode = HashCodeHelper.GetHashCode(Description, hashCode); + hashCode = HashCodeHelper.GetHashCode(ContentUrl, hashCode); + hashCode = HashCodeHelper.GetHashCode(ThumbnailUrl, hashCode); + hashCode = HashCodeHelper.GetHashCode(Author, hashCode); + hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/ChangeSet.cs b/src/redmine-net-api/Types/ChangeSet.cs index 1ea9e906..45e4ff1e 100644 --- a/src/redmine-net-api/Types/ChangeSet.cs +++ b/src/redmine-net-api/Types/ChangeSet.cs @@ -194,15 +194,12 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = 17; - hashCode = HashCodeHelper.GetHashCode(Revision, hashCode); - hashCode = HashCodeHelper.GetHashCode(User, hashCode); - hashCode = HashCodeHelper.GetHashCode(Comments, hashCode); - hashCode = HashCodeHelper.GetHashCode(CommittedOn, hashCode); - return hashCode; - } + var hashCode = 17; + hashCode = HashCodeHelper.GetHashCode(Revision, hashCode); + hashCode = HashCodeHelper.GetHashCode(User, hashCode); + hashCode = HashCodeHelper.GetHashCode(Comments, hashCode); + hashCode = HashCodeHelper.GetHashCode(CommittedOn, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/CustomField.cs b/src/redmine-net-api/Types/CustomField.cs index 0c0ca47f..6aeea0e8 100644 --- a/src/redmine-net-api/Types/CustomField.cs +++ b/src/redmine-net-api/Types/CustomField.cs @@ -246,26 +246,23 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(CustomizedType, hashCode); - hashCode = HashCodeHelper.GetHashCode(Description, hashCode); - hashCode = HashCodeHelper.GetHashCode(FieldFormat, hashCode); - hashCode = HashCodeHelper.GetHashCode(Regexp, hashCode); - hashCode = HashCodeHelper.GetHashCode(MinLength, hashCode); - hashCode = HashCodeHelper.GetHashCode(MaxLength, hashCode); - hashCode = HashCodeHelper.GetHashCode(IsRequired, hashCode); - hashCode = HashCodeHelper.GetHashCode(IsFilter, hashCode); - hashCode = HashCodeHelper.GetHashCode(Searchable, hashCode); - hashCode = HashCodeHelper.GetHashCode(Multiple, hashCode); - hashCode = HashCodeHelper.GetHashCode(DefaultValue, hashCode); - hashCode = HashCodeHelper.GetHashCode(Visible, hashCode); - hashCode = HashCodeHelper.GetHashCode(PossibleValues, hashCode); - hashCode = HashCodeHelper.GetHashCode(Trackers, hashCode); - hashCode = HashCodeHelper.GetHashCode(Roles, hashCode); - return hashCode; - } + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(CustomizedType, hashCode); + hashCode = HashCodeHelper.GetHashCode(Description, hashCode); + hashCode = HashCodeHelper.GetHashCode(FieldFormat, hashCode); + hashCode = HashCodeHelper.GetHashCode(Regexp, hashCode); + hashCode = HashCodeHelper.GetHashCode(MinLength, hashCode); + hashCode = HashCodeHelper.GetHashCode(MaxLength, hashCode); + hashCode = HashCodeHelper.GetHashCode(IsRequired, hashCode); + hashCode = HashCodeHelper.GetHashCode(IsFilter, hashCode); + hashCode = HashCodeHelper.GetHashCode(Searchable, hashCode); + hashCode = HashCodeHelper.GetHashCode(Multiple, hashCode); + hashCode = HashCodeHelper.GetHashCode(DefaultValue, hashCode); + hashCode = HashCodeHelper.GetHashCode(Visible, hashCode); + hashCode = HashCodeHelper.GetHashCode(PossibleValues, hashCode); + hashCode = HashCodeHelper.GetHashCode(Trackers, hashCode); + hashCode = HashCodeHelper.GetHashCode(Roles, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/CustomFieldValue.cs b/src/redmine-net-api/Types/CustomFieldValue.cs index c7f4995e..eb2b20af 100644 --- a/src/redmine-net-api/Types/CustomFieldValue.cs +++ b/src/redmine-net-api/Types/CustomFieldValue.cs @@ -168,12 +168,9 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = 17; - hashCode = HashCodeHelper.GetHashCode(Info, hashCode); - return hashCode; - } + var hashCode = 17; + hashCode = HashCodeHelper.GetHashCode(Info, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/Detail.cs b/src/redmine-net-api/Types/Detail.cs index 3a3f89ba..45a1217c 100644 --- a/src/redmine-net-api/Types/Detail.cs +++ b/src/redmine-net-api/Types/Detail.cs @@ -212,16 +212,13 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = 17; - hashCode = HashCodeHelper.GetHashCode(Property, hashCode); - hashCode = HashCodeHelper.GetHashCode(Name, hashCode); - hashCode = HashCodeHelper.GetHashCode(OldValue, hashCode); - hashCode = HashCodeHelper.GetHashCode(NewValue, hashCode); + var hashCode = 17; + hashCode = HashCodeHelper.GetHashCode(Property, hashCode); + hashCode = HashCodeHelper.GetHashCode(Name, hashCode); + hashCode = HashCodeHelper.GetHashCode(OldValue, hashCode); + hashCode = HashCodeHelper.GetHashCode(NewValue, hashCode); - return hashCode; - } + return hashCode; } /// diff --git a/src/redmine-net-api/Types/DocumentCategory.cs b/src/redmine-net-api/Types/DocumentCategory.cs index c8935cce..d43637ac 100644 --- a/src/redmine-net-api/Types/DocumentCategory.cs +++ b/src/redmine-net-api/Types/DocumentCategory.cs @@ -156,13 +156,10 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(IsDefault, hashCode); - hashCode = HashCodeHelper.GetHashCode(IsActive, hashCode); - return hashCode; - } + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(IsDefault, hashCode); + hashCode = HashCodeHelper.GetHashCode(IsActive, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/Error.cs b/src/redmine-net-api/Types/Error.cs index 45b82e26..2c6db1c8 100644 --- a/src/redmine-net-api/Types/Error.cs +++ b/src/redmine-net-api/Types/Error.cs @@ -141,12 +141,9 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = 17; - hashCode = HashCodeHelper.GetHashCode(Info, hashCode); - return hashCode; - } + var hashCode = 17; + hashCode = HashCodeHelper.GetHashCode(Info, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/Group.cs b/src/redmine-net-api/Types/Group.cs index 8209e3b3..09152356 100644 --- a/src/redmine-net-api/Types/Group.cs +++ b/src/redmine-net-api/Types/Group.cs @@ -192,14 +192,11 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(Users, hashCode); - hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); - hashCode = HashCodeHelper.GetHashCode(Memberships, hashCode); - return hashCode; - } + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Users, hashCode); + hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); + hashCode = HashCodeHelper.GetHashCode(Memberships, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/Identifiable.cs b/src/redmine-net-api/Types/Identifiable.cs index 7b48caf8..2aa8a686 100644 --- a/src/redmine-net-api/Types/Identifiable.cs +++ b/src/redmine-net-api/Types/Identifiable.cs @@ -121,12 +121,9 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = 17; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); - return hashCode; - } + var hashCode = 17; + hashCode = HashCodeHelper.GetHashCode(Id, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/IdentifiableName.cs b/src/redmine-net-api/Types/IdentifiableName.cs index ad0ac001..04d87997 100644 --- a/src/redmine-net-api/Types/IdentifiableName.cs +++ b/src/redmine-net-api/Types/IdentifiableName.cs @@ -202,12 +202,9 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(Name, hashCode); - return hashCode; - } + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Name, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/IssueAllowedStatus.cs b/src/redmine-net-api/Types/IssueAllowedStatus.cs index 1fa60dba..14ecdb72 100644 --- a/src/redmine-net-api/Types/IssueAllowedStatus.cs +++ b/src/redmine-net-api/Types/IssueAllowedStatus.cs @@ -101,12 +101,9 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(IsClosed, hashCode); - return hashCode; - } + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(IsClosed, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/IssueCategory.cs b/src/redmine-net-api/Types/IssueCategory.cs index b66c575f..f31a64ed 100644 --- a/src/redmine-net-api/Types/IssueCategory.cs +++ b/src/redmine-net-api/Types/IssueCategory.cs @@ -174,14 +174,11 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(Project, hashCode); - hashCode = HashCodeHelper.GetHashCode(AssignTo, hashCode); - hashCode = HashCodeHelper.GetHashCode(Name, hashCode); - return hashCode; - } + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Project, hashCode); + hashCode = HashCodeHelper.GetHashCode(AssignTo, hashCode); + hashCode = HashCodeHelper.GetHashCode(Name, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/IssueChild.cs b/src/redmine-net-api/Types/IssueChild.cs index a6e9adfe..fa903468 100644 --- a/src/redmine-net-api/Types/IssueChild.cs +++ b/src/redmine-net-api/Types/IssueChild.cs @@ -139,13 +139,10 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(Tracker, hashCode); - hashCode = HashCodeHelper.GetHashCode(Subject, hashCode); - return hashCode; - } + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Tracker, hashCode); + hashCode = HashCodeHelper.GetHashCode(Subject, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/IssueCustomField.cs b/src/redmine-net-api/Types/IssueCustomField.cs index 0333e832..e9923f79 100644 --- a/src/redmine-net-api/Types/IssueCustomField.cs +++ b/src/redmine-net-api/Types/IssueCustomField.cs @@ -241,13 +241,10 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(Values, hashCode); - hashCode = HashCodeHelper.GetHashCode(Multiple, hashCode); - return hashCode; - } + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Values, hashCode); + hashCode = HashCodeHelper.GetHashCode(Multiple, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/IssuePriority.cs b/src/redmine-net-api/Types/IssuePriority.cs index 51ac6dc3..914ffacd 100644 --- a/src/redmine-net-api/Types/IssuePriority.cs +++ b/src/redmine-net-api/Types/IssuePriority.cs @@ -140,13 +140,10 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(IsDefault, hashCode); - hashCode = HashCodeHelper.GetHashCode(IsActive, hashCode); - return hashCode; - } + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(IsDefault, hashCode); + hashCode = HashCodeHelper.GetHashCode(IsActive, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/IssueRelation.cs b/src/redmine-net-api/Types/IssueRelation.cs index c790cabc..9abbe720 100644 --- a/src/redmine-net-api/Types/IssueRelation.cs +++ b/src/redmine-net-api/Types/IssueRelation.cs @@ -248,15 +248,12 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(IssueId, hashCode); - hashCode = HashCodeHelper.GetHashCode(IssueToId, hashCode); - hashCode = HashCodeHelper.GetHashCode(Type, hashCode); - hashCode = HashCodeHelper.GetHashCode(Delay, hashCode); - return hashCode; - } + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(IssueId, hashCode); + hashCode = HashCodeHelper.GetHashCode(IssueToId, hashCode); + hashCode = HashCodeHelper.GetHashCode(Type, hashCode); + hashCode = HashCodeHelper.GetHashCode(Delay, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/Journal.cs b/src/redmine-net-api/Types/Journal.cs index 916967a1..d461aac3 100644 --- a/src/redmine-net-api/Types/Journal.cs +++ b/src/redmine-net-api/Types/Journal.cs @@ -212,18 +212,15 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(User, hashCode); - hashCode = HashCodeHelper.GetHashCode(Notes, hashCode); - hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(Details, hashCode); - hashCode = HashCodeHelper.GetHashCode(PrivateNotes, hashCode); - hashCode = HashCodeHelper.GetHashCode(UpdatedOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(UpdatedBy, hashCode); - return hashCode; - } + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(User, hashCode); + hashCode = HashCodeHelper.GetHashCode(Notes, hashCode); + hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(Details, hashCode); + hashCode = HashCodeHelper.GetHashCode(PrivateNotes, hashCode); + hashCode = HashCodeHelper.GetHashCode(UpdatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(UpdatedBy, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/Membership.cs b/src/redmine-net-api/Types/Membership.cs index d03fa95f..d6831787 100644 --- a/src/redmine-net-api/Types/Membership.cs +++ b/src/redmine-net-api/Types/Membership.cs @@ -150,15 +150,12 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(Group, hashCode); - hashCode = HashCodeHelper.GetHashCode(Project, hashCode); - hashCode = HashCodeHelper.GetHashCode(User, hashCode); - hashCode = HashCodeHelper.GetHashCode(Roles, hashCode); - return hashCode; - } + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Group, hashCode); + hashCode = HashCodeHelper.GetHashCode(Project, hashCode); + hashCode = HashCodeHelper.GetHashCode(User, hashCode); + hashCode = HashCodeHelper.GetHashCode(Roles, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/MembershipRole.cs b/src/redmine-net-api/Types/MembershipRole.cs index 36a3aefd..7e150a9f 100644 --- a/src/redmine-net-api/Types/MembershipRole.cs +++ b/src/redmine-net-api/Types/MembershipRole.cs @@ -139,12 +139,9 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(Inherited, hashCode); - return hashCode; - } + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Inherited, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/MyAccount.cs b/src/redmine-net-api/Types/MyAccount.cs index d325c04f..754e03f0 100644 --- a/src/redmine-net-api/Types/MyAccount.cs +++ b/src/redmine-net-api/Types/MyAccount.cs @@ -214,21 +214,18 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = 17; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); - hashCode = HashCodeHelper.GetHashCode(Login, hashCode); - hashCode = HashCodeHelper.GetHashCode(FirstName, hashCode); - hashCode = HashCodeHelper.GetHashCode(LastName, hashCode); - hashCode = HashCodeHelper.GetHashCode(ApiKey, hashCode); - hashCode = HashCodeHelper.GetHashCode(Email, hashCode); - hashCode = HashCodeHelper.GetHashCode(IsAdmin, hashCode); - hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(LastLoginOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); - return hashCode; - } + var hashCode = 17; + hashCode = HashCodeHelper.GetHashCode(Id, hashCode); + hashCode = HashCodeHelper.GetHashCode(Login, hashCode); + hashCode = HashCodeHelper.GetHashCode(FirstName, hashCode); + hashCode = HashCodeHelper.GetHashCode(LastName, hashCode); + hashCode = HashCodeHelper.GetHashCode(ApiKey, hashCode); + hashCode = HashCodeHelper.GetHashCode(Email, hashCode); + hashCode = HashCodeHelper.GetHashCode(IsAdmin, hashCode); + hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(LastLoginOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/MyAccountCustomField.cs b/src/redmine-net-api/Types/MyAccountCustomField.cs index 3015296a..4abf9723 100644 --- a/src/redmine-net-api/Types/MyAccountCustomField.cs +++ b/src/redmine-net-api/Types/MyAccountCustomField.cs @@ -135,12 +135,9 @@ public bool Equals(MyAccountCustomField other) /// public override int GetHashCode() { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(Value, hashCode); - return hashCode; - } + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Value, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/News.cs b/src/redmine-net-api/Types/News.cs index 22415dc0..b31e519c 100644 --- a/src/redmine-net-api/Types/News.cs +++ b/src/redmine-net-api/Types/News.cs @@ -237,21 +237,18 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = 17; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); - hashCode = HashCodeHelper.GetHashCode(Project, hashCode); - hashCode = HashCodeHelper.GetHashCode(Author, hashCode); - hashCode = HashCodeHelper.GetHashCode(Title, hashCode); - hashCode = HashCodeHelper.GetHashCode(Summary, hashCode); - hashCode = HashCodeHelper.GetHashCode(Description, hashCode); - hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(Comments, hashCode); - hashCode = HashCodeHelper.GetHashCode(Attachments, hashCode); - hashCode = HashCodeHelper.GetHashCode(Uploads, hashCode); - return hashCode; - } + var hashCode = 17; + hashCode = HashCodeHelper.GetHashCode(Id, hashCode); + hashCode = HashCodeHelper.GetHashCode(Project, hashCode); + hashCode = HashCodeHelper.GetHashCode(Author, hashCode); + hashCode = HashCodeHelper.GetHashCode(Title, hashCode); + hashCode = HashCodeHelper.GetHashCode(Summary, hashCode); + hashCode = HashCodeHelper.GetHashCode(Description, hashCode); + hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(Comments, hashCode); + hashCode = HashCodeHelper.GetHashCode(Attachments, hashCode); + hashCode = HashCodeHelper.GetHashCode(Uploads, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/NewsComment.cs b/src/redmine-net-api/Types/NewsComment.cs index b80953e4..aaee7ae9 100644 --- a/src/redmine-net-api/Types/NewsComment.cs +++ b/src/redmine-net-api/Types/NewsComment.cs @@ -125,15 +125,12 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = 17; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); - hashCode = HashCodeHelper.GetHashCode(Author, hashCode); - hashCode = HashCodeHelper.GetHashCode(Content, hashCode); + var hashCode = 17; + hashCode = HashCodeHelper.GetHashCode(Id, hashCode); + hashCode = HashCodeHelper.GetHashCode(Author, hashCode); + hashCode = HashCodeHelper.GetHashCode(Content, hashCode); - return hashCode; - } + return hashCode; } /// diff --git a/src/redmine-net-api/Types/Permission.cs b/src/redmine-net-api/Types/Permission.cs index 69e14eea..bf411f5e 100644 --- a/src/redmine-net-api/Types/Permission.cs +++ b/src/redmine-net-api/Types/Permission.cs @@ -120,12 +120,9 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = 17; - hashCode = HashCodeHelper.GetHashCode(Info, hashCode); - return hashCode; - } + var hashCode = 17; + hashCode = HashCodeHelper.GetHashCode(Info, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/Project.cs b/src/redmine-net-api/Types/Project.cs index 39d9a325..c993461a 100644 --- a/src/redmine-net-api/Types/Project.cs +++ b/src/redmine-net-api/Types/Project.cs @@ -352,29 +352,26 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(Identifier, hashCode); - hashCode = HashCodeHelper.GetHashCode(Description, hashCode); - hashCode = HashCodeHelper.GetHashCode(Parent, hashCode); - hashCode = HashCodeHelper.GetHashCode(HomePage, hashCode); - hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(UpdatedOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(Status, hashCode); - hashCode = HashCodeHelper.GetHashCode(IsPublic, hashCode); - hashCode = HashCodeHelper.GetHashCode(InheritMembers, hashCode); - hashCode = HashCodeHelper.GetHashCode(Trackers, hashCode); - hashCode = HashCodeHelper.GetHashCode(IssueCustomFields, hashCode); - hashCode = HashCodeHelper.GetHashCode(CustomFieldValues, hashCode); - hashCode = HashCodeHelper.GetHashCode(IssueCategories, hashCode); - hashCode = HashCodeHelper.GetHashCode(EnabledModules, hashCode); - hashCode = HashCodeHelper.GetHashCode(TimeEntryActivities, hashCode); - hashCode = HashCodeHelper.GetHashCode(DefaultAssignee, hashCode); - hashCode = HashCodeHelper.GetHashCode(DefaultVersion, hashCode); - - return hashCode; - } + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Identifier, hashCode); + hashCode = HashCodeHelper.GetHashCode(Description, hashCode); + hashCode = HashCodeHelper.GetHashCode(Parent, hashCode); + hashCode = HashCodeHelper.GetHashCode(HomePage, hashCode); + hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(UpdatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(Status, hashCode); + hashCode = HashCodeHelper.GetHashCode(IsPublic, hashCode); + hashCode = HashCodeHelper.GetHashCode(InheritMembers, hashCode); + hashCode = HashCodeHelper.GetHashCode(Trackers, hashCode); + hashCode = HashCodeHelper.GetHashCode(IssueCustomFields, hashCode); + hashCode = HashCodeHelper.GetHashCode(CustomFieldValues, hashCode); + hashCode = HashCodeHelper.GetHashCode(IssueCategories, hashCode); + hashCode = HashCodeHelper.GetHashCode(EnabledModules, hashCode); + hashCode = HashCodeHelper.GetHashCode(TimeEntryActivities, hashCode); + hashCode = HashCodeHelper.GetHashCode(DefaultAssignee, hashCode); + hashCode = HashCodeHelper.GetHashCode(DefaultVersion, hashCode); + + return hashCode; } /// diff --git a/src/redmine-net-api/Types/ProjectMembership.cs b/src/redmine-net-api/Types/ProjectMembership.cs index 42ef7e5a..64b6fb0d 100644 --- a/src/redmine-net-api/Types/ProjectMembership.cs +++ b/src/redmine-net-api/Types/ProjectMembership.cs @@ -197,15 +197,12 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(Project, hashCode); - hashCode = HashCodeHelper.GetHashCode(User, hashCode); - hashCode = HashCodeHelper.GetHashCode(Group, hashCode); - hashCode = HashCodeHelper.GetHashCode(Roles, hashCode); - return hashCode; - } + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Project, hashCode); + hashCode = HashCodeHelper.GetHashCode(User, hashCode); + hashCode = HashCodeHelper.GetHashCode(Group, hashCode); + hashCode = HashCodeHelper.GetHashCode(Roles, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/Query.cs b/src/redmine-net-api/Types/Query.cs index d77f821d..97ade4da 100644 --- a/src/redmine-net-api/Types/Query.cs +++ b/src/redmine-net-api/Types/Query.cs @@ -141,13 +141,10 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(IsPublic, hashCode); - hashCode = HashCodeHelper.GetHashCode(ProjectId, hashCode); - return hashCode; - } + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(IsPublic, hashCode); + hashCode = HashCodeHelper.GetHashCode(ProjectId, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/Role.cs b/src/redmine-net-api/Types/Role.cs index 62a3ec29..33052455 100644 --- a/src/redmine-net-api/Types/Role.cs +++ b/src/redmine-net-api/Types/Role.cs @@ -168,16 +168,13 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(IsAssignable, hashCode); - hashCode = HashCodeHelper.GetHashCode(IssuesVisibility, hashCode); - hashCode = HashCodeHelper.GetHashCode(TimeEntriesVisibility, hashCode); - hashCode = HashCodeHelper.GetHashCode(UsersVisibility, hashCode); - hashCode = HashCodeHelper.GetHashCode(Permissions, hashCode); - return hashCode; - } + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(IsAssignable, hashCode); + hashCode = HashCodeHelper.GetHashCode(IssuesVisibility, hashCode); + hashCode = HashCodeHelper.GetHashCode(TimeEntriesVisibility, hashCode); + hashCode = HashCodeHelper.GetHashCode(UsersVisibility, hashCode); + hashCode = HashCodeHelper.GetHashCode(Permissions, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/Search.cs b/src/redmine-net-api/Types/Search.cs index 37792323..e24cd5cf 100644 --- a/src/redmine-net-api/Types/Search.cs +++ b/src/redmine-net-api/Types/Search.cs @@ -148,17 +148,14 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = 397; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); - hashCode = HashCodeHelper.GetHashCode(Title, hashCode); - hashCode = HashCodeHelper.GetHashCode(Type, hashCode); - hashCode = HashCodeHelper.GetHashCode(Url, hashCode); - hashCode = HashCodeHelper.GetHashCode(Description, hashCode); - hashCode = HashCodeHelper.GetHashCode(DateTime, hashCode); - return hashCode; - } + var hashCode = 397; + hashCode = HashCodeHelper.GetHashCode(Id, hashCode); + hashCode = HashCodeHelper.GetHashCode(Title, hashCode); + hashCode = HashCodeHelper.GetHashCode(Type, hashCode); + hashCode = HashCodeHelper.GetHashCode(Url, hashCode); + hashCode = HashCodeHelper.GetHashCode(Description, hashCode); + hashCode = HashCodeHelper.GetHashCode(DateTime, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/TimeEntry.cs b/src/redmine-net-api/Types/TimeEntry.cs index f02cebbf..ac98d3d7 100644 --- a/src/redmine-net-api/Types/TimeEntry.cs +++ b/src/redmine-net-api/Types/TimeEntry.cs @@ -261,21 +261,18 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(Issue, hashCode); - hashCode = HashCodeHelper.GetHashCode(Project, hashCode); - hashCode = HashCodeHelper.GetHashCode(SpentOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(Hours, hashCode); - hashCode = HashCodeHelper.GetHashCode(Activity, hashCode); - hashCode = HashCodeHelper.GetHashCode(User, hashCode); - hashCode = HashCodeHelper.GetHashCode(Comments, hashCode); - hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(UpdatedOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); - return hashCode; - } + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Issue, hashCode); + hashCode = HashCodeHelper.GetHashCode(Project, hashCode); + hashCode = HashCodeHelper.GetHashCode(SpentOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(Hours, hashCode); + hashCode = HashCodeHelper.GetHashCode(Activity, hashCode); + hashCode = HashCodeHelper.GetHashCode(User, hashCode); + hashCode = HashCodeHelper.GetHashCode(Comments, hashCode); + hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(UpdatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/TimeEntryActivity.cs b/src/redmine-net-api/Types/TimeEntryActivity.cs index 2e25bc03..0e9a2d46 100644 --- a/src/redmine-net-api/Types/TimeEntryActivity.cs +++ b/src/redmine-net-api/Types/TimeEntryActivity.cs @@ -156,13 +156,10 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(IsDefault, hashCode); - hashCode = HashCodeHelper.GetHashCode(IsActive, hashCode); - return hashCode; - } + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(IsDefault, hashCode); + hashCode = HashCodeHelper.GetHashCode(IsActive, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/Tracker.cs b/src/redmine-net-api/Types/Tracker.cs index 7f77249f..ff9576a6 100644 --- a/src/redmine-net-api/Types/Tracker.cs +++ b/src/redmine-net-api/Types/Tracker.cs @@ -148,14 +148,11 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - int hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(DefaultStatus, hashCode); - hashCode = HashCodeHelper.GetHashCode(Description, hashCode); - hashCode = HashCodeHelper.GetHashCode(EnabledStandardFields, hashCode); - return hashCode; - } + int hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(DefaultStatus, hashCode); + hashCode = HashCodeHelper.GetHashCode(Description, hashCode); + hashCode = HashCodeHelper.GetHashCode(EnabledStandardFields, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/TrackerCoreField.cs b/src/redmine-net-api/Types/TrackerCoreField.cs index aad3d8c1..f46b1cb5 100644 --- a/src/redmine-net-api/Types/TrackerCoreField.cs +++ b/src/redmine-net-api/Types/TrackerCoreField.cs @@ -127,12 +127,9 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = 17; - hashCode = HashCodeHelper.GetHashCode(Name, hashCode); - return hashCode; - } + var hashCode = 17; + hashCode = HashCodeHelper.GetHashCode(Name, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/Upload.cs b/src/redmine-net-api/Types/Upload.cs index d5efeb7e..657572dc 100644 --- a/src/redmine-net-api/Types/Upload.cs +++ b/src/redmine-net-api/Types/Upload.cs @@ -195,15 +195,12 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = 17; - hashCode = HashCodeHelper.GetHashCode(Token, hashCode); - hashCode = HashCodeHelper.GetHashCode(FileName, hashCode); - hashCode = HashCodeHelper.GetHashCode(Description, hashCode); - hashCode = HashCodeHelper.GetHashCode(ContentType, hashCode); - return hashCode; - } + var hashCode = 17; + hashCode = HashCodeHelper.GetHashCode(Token, hashCode); + hashCode = HashCodeHelper.GetHashCode(FileName, hashCode); + hashCode = HashCodeHelper.GetHashCode(Description, hashCode); + hashCode = HashCodeHelper.GetHashCode(ContentType, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/User.cs b/src/redmine-net-api/Types/User.cs index 0afc34ad..06b0b46c 100644 --- a/src/redmine-net-api/Types/User.cs +++ b/src/redmine-net-api/Types/User.cs @@ -386,33 +386,30 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = 17; - hashCode = HashCodeHelper.GetHashCode(Id, hashCode); - hashCode = HashCodeHelper.GetHashCode(AvatarUrl, hashCode); - hashCode = HashCodeHelper.GetHashCode(Login, hashCode); - hashCode = HashCodeHelper.GetHashCode(FirstName, hashCode); - hashCode = HashCodeHelper.GetHashCode(LastName, hashCode); - hashCode = HashCodeHelper.GetHashCode(Email, hashCode); - hashCode = HashCodeHelper.GetHashCode(MailNotification, hashCode); - hashCode = HashCodeHelper.GetHashCode(ApiKey, hashCode); - hashCode = HashCodeHelper.GetHashCode(TwoFactorAuthenticationScheme, hashCode); - hashCode = HashCodeHelper.GetHashCode(AuthenticationModeId, hashCode); - hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(LastLoginOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(Status, hashCode); - hashCode = HashCodeHelper.GetHashCode(MustChangePassword, hashCode); - hashCode = HashCodeHelper.GetHashCode(IsAdmin, hashCode); - hashCode = HashCodeHelper.GetHashCode(PasswordChangedOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(UpdatedOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); - hashCode = HashCodeHelper.GetHashCode(Memberships, hashCode); - hashCode = HashCodeHelper.GetHashCode(Groups, hashCode); - hashCode = HashCodeHelper.GetHashCode(GeneratePassword, hashCode); - hashCode = HashCodeHelper.GetHashCode(SendInformation, hashCode); - return hashCode; - } + var hashCode = 17; + hashCode = HashCodeHelper.GetHashCode(Id, hashCode); + hashCode = HashCodeHelper.GetHashCode(AvatarUrl, hashCode); + hashCode = HashCodeHelper.GetHashCode(Login, hashCode); + hashCode = HashCodeHelper.GetHashCode(FirstName, hashCode); + hashCode = HashCodeHelper.GetHashCode(LastName, hashCode); + hashCode = HashCodeHelper.GetHashCode(Email, hashCode); + hashCode = HashCodeHelper.GetHashCode(MailNotification, hashCode); + hashCode = HashCodeHelper.GetHashCode(ApiKey, hashCode); + hashCode = HashCodeHelper.GetHashCode(TwoFactorAuthenticationScheme, hashCode); + hashCode = HashCodeHelper.GetHashCode(AuthenticationModeId, hashCode); + hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(LastLoginOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(Status, hashCode); + hashCode = HashCodeHelper.GetHashCode(MustChangePassword, hashCode); + hashCode = HashCodeHelper.GetHashCode(IsAdmin, hashCode); + hashCode = HashCodeHelper.GetHashCode(PasswordChangedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(UpdatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); + hashCode = HashCodeHelper.GetHashCode(Memberships, hashCode); + hashCode = HashCodeHelper.GetHashCode(Groups, hashCode); + hashCode = HashCodeHelper.GetHashCode(GeneratePassword, hashCode); + hashCode = HashCodeHelper.GetHashCode(SendInformation, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/Version.cs b/src/redmine-net-api/Types/Version.cs index 4c8c9152..abe37fe7 100644 --- a/src/redmine-net-api/Types/Version.cs +++ b/src/redmine-net-api/Types/Version.cs @@ -279,22 +279,19 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(Project, hashCode); - hashCode = HashCodeHelper.GetHashCode(Description, hashCode); - hashCode = HashCodeHelper.GetHashCode(Status, hashCode); - hashCode = HashCodeHelper.GetHashCode(DueDate, hashCode); - hashCode = HashCodeHelper.GetHashCode(Sharing, hashCode); - hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(UpdatedOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); - hashCode = HashCodeHelper.GetHashCode(WikiPageTitle, hashCode); - hashCode = HashCodeHelper.GetHashCode(EstimatedHours, hashCode); - hashCode = HashCodeHelper.GetHashCode(SpentHours, hashCode); - return hashCode; - } + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Project, hashCode); + hashCode = HashCodeHelper.GetHashCode(Description, hashCode); + hashCode = HashCodeHelper.GetHashCode(Status, hashCode); + hashCode = HashCodeHelper.GetHashCode(DueDate, hashCode); + hashCode = HashCodeHelper.GetHashCode(Sharing, hashCode); + hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(UpdatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(CustomFields, hashCode); + hashCode = HashCodeHelper.GetHashCode(WikiPageTitle, hashCode); + hashCode = HashCodeHelper.GetHashCode(EstimatedHours, hashCode); + hashCode = HashCodeHelper.GetHashCode(SpentHours, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/Watcher.cs b/src/redmine-net-api/Types/Watcher.cs index 20d3409e..57607455 100644 --- a/src/redmine-net-api/Types/Watcher.cs +++ b/src/redmine-net-api/Types/Watcher.cs @@ -95,12 +95,9 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(Name, hashCode); - return hashCode; - } + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Name, hashCode); + return hashCode; } /// diff --git a/src/redmine-net-api/Types/WikiPage.cs b/src/redmine-net-api/Types/WikiPage.cs index 566cf58f..b81461bd 100644 --- a/src/redmine-net-api/Types/WikiPage.cs +++ b/src/redmine-net-api/Types/WikiPage.cs @@ -247,19 +247,16 @@ public override bool Equals(object obj) /// public override int GetHashCode() { - unchecked - { - var hashCode = base.GetHashCode(); - hashCode = HashCodeHelper.GetHashCode(Title, hashCode); - hashCode = HashCodeHelper.GetHashCode(Text, hashCode); - hashCode = HashCodeHelper.GetHashCode(Comments, hashCode); - hashCode = HashCodeHelper.GetHashCode(Version, hashCode); - hashCode = HashCodeHelper.GetHashCode(Author, hashCode); - hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(UpdatedOn, hashCode); - hashCode = HashCodeHelper.GetHashCode(Attachments, hashCode); - return hashCode; - } + var hashCode = base.GetHashCode(); + hashCode = HashCodeHelper.GetHashCode(Title, hashCode); + hashCode = HashCodeHelper.GetHashCode(Text, hashCode); + hashCode = HashCodeHelper.GetHashCode(Comments, hashCode); + hashCode = HashCodeHelper.GetHashCode(Version, hashCode); + hashCode = HashCodeHelper.GetHashCode(Author, hashCode); + hashCode = HashCodeHelper.GetHashCode(CreatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(UpdatedOn, hashCode); + hashCode = HashCodeHelper.GetHashCode(Attachments, hashCode); + return hashCode; } /// From 54c3be1cfc20f17f18a02d739ff67515e35966ea Mon Sep 17 00:00:00 2001 From: Padi Date: Thu, 29 May 2025 11:01:23 +0300 Subject: [PATCH 539/549] Update README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 4cf48cb0..9a2add82 100755 --- a/README.md +++ b/README.md @@ -93,6 +93,11 @@ Detailed API reference, guides, and tutorials are available in the [GitHub Wiki] See the [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines. +## 💬 Join on Slack + +Want to talk about Redmine integration, features, or contribute ideas? +Join Slack channel here: [dotnet-redmine](https://join.slack.com/t/dotnet-redmine/shared_invite/zt-36cvwm98j-10Sw3w4LITk1N6eqKKHWRw) + ## 🤝 Contributors From deb774d2a4b3cf0acb20954c9ccdcfdecdbffb86 Mon Sep 17 00:00:00 2001 From: Padi Date: Mon, 2 Jun 2025 13:17:42 +0300 Subject: [PATCH 540/549] Error handling improvements --- .../Exceptions/ConflictException.cs | 88 -------- .../Exceptions/ForbiddenException.cs | 89 -------- .../InternalServerErrorException.cs | 89 -------- .../NameResolutionFailureException.cs | 88 -------- .../Exceptions/NotAcceptableException.cs | 88 -------- .../Exceptions/NotFoundException.cs | 89 -------- .../Exceptions/RedmineApiException.cs | 207 ++++++++++++++---- .../Exceptions/RedmineException.cs | 72 +++--- .../Exceptions/RedmineForbiddenException.cs | 82 +++++++ .../RedmineNotAcceptableException.cs | 56 +++++ .../Exceptions/RedmineNotFoundException.cs | 80 +++++++ .../RedmineOperationCanceledException.cs | 85 +++++++ .../RedmineSerializationException.cs | 56 +++-- .../Exceptions/RedmineTimeoutException.cs | 83 ++++--- .../RedmineUnauthorizedException.cs | 79 +++++++ .../RedmineUnprocessableEntityException.cs | 63 ++++++ .../Exceptions/UnauthorizedException.cs | 92 -------- .../InternalRedmineApiHttpClient.Async.cs | 37 ++-- .../InternalRedmineApiWebClient.Async.cs | 31 ++- .../WebClient/InternalRedmineApiWebClient.cs | 186 +++++----------- .../Clients/WebClient/WebClientExtensions.cs | 8 + .../Http/Constants/HttpConstants.cs | 2 + .../Http/Helpers/RedmineExceptionHelper.cs | 99 +++------ 23 files changed, 852 insertions(+), 997 deletions(-) delete mode 100644 src/redmine-net-api/Exceptions/ConflictException.cs delete mode 100644 src/redmine-net-api/Exceptions/ForbiddenException.cs delete mode 100644 src/redmine-net-api/Exceptions/InternalServerErrorException.cs delete mode 100644 src/redmine-net-api/Exceptions/NameResolutionFailureException.cs delete mode 100644 src/redmine-net-api/Exceptions/NotAcceptableException.cs delete mode 100644 src/redmine-net-api/Exceptions/NotFoundException.cs create mode 100644 src/redmine-net-api/Exceptions/RedmineForbiddenException.cs create mode 100644 src/redmine-net-api/Exceptions/RedmineNotAcceptableException.cs create mode 100644 src/redmine-net-api/Exceptions/RedmineNotFoundException.cs create mode 100644 src/redmine-net-api/Exceptions/RedmineOperationCanceledException.cs create mode 100644 src/redmine-net-api/Exceptions/RedmineUnauthorizedException.cs create mode 100644 src/redmine-net-api/Exceptions/RedmineUnprocessableEntityException.cs delete mode 100644 src/redmine-net-api/Exceptions/UnauthorizedException.cs diff --git a/src/redmine-net-api/Exceptions/ConflictException.cs b/src/redmine-net-api/Exceptions/ConflictException.cs deleted file mode 100644 index 183baf56..00000000 --- a/src/redmine-net-api/Exceptions/ConflictException.cs +++ /dev/null @@ -1,88 +0,0 @@ -/* - Copyright 2011 - 2025 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Globalization; -using System.Runtime.Serialization; - -namespace Redmine.Net.Api.Exceptions -{ - /// - /// - /// - [Serializable] - public sealed class ConflictException : RedmineException - { - /// - /// Initializes a new instance of the class. - /// - public ConflictException() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - public ConflictException(string message) - : base(message) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - public ConflictException(string format, params object[] args) - : base(string.Format(CultureInfo.InvariantCulture,format, args)) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - public ConflictException(string message, Exception innerException) - : base(message, innerException) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - /// - public ConflictException(string format, Exception innerException, params object[] args) - : base(string.Format(CultureInfo.InvariantCulture,format, args), innerException) - { - } - -#if !(NET8_0_OR_GREATER) - /// - /// - /// - /// - /// - private ConflictException(SerializationInfo serializationInfo, StreamingContext streamingContext):base(serializationInfo, streamingContext) - { - - } -#endif - } -} \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/ForbiddenException.cs b/src/redmine-net-api/Exceptions/ForbiddenException.cs deleted file mode 100644 index e5e4d8ca..00000000 --- a/src/redmine-net-api/Exceptions/ForbiddenException.cs +++ /dev/null @@ -1,89 +0,0 @@ -/* - Copyright 2011 - 2025 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Globalization; -using System.Runtime.Serialization; - -namespace Redmine.Net.Api.Exceptions -{ - /// - /// - /// - [Serializable] - public sealed class ForbiddenException : RedmineException - { - /// - /// Initializes a new instance of the class. - /// - public ForbiddenException() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - public ForbiddenException(string message) - : base(message) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - public ForbiddenException(string format, params object[] args) - : base(string.Format(CultureInfo.InvariantCulture,format, args)) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - public ForbiddenException(string message, Exception innerException) - : base(message, innerException) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - /// - public ForbiddenException(string format, Exception innerException, params object[] args) - : base(string.Format(CultureInfo.InvariantCulture,format, args), innerException) - { - } - -#if !(NET8_0_OR_GREATER) - /// - /// - /// - /// - /// - /// - private ForbiddenException(SerializationInfo serializationInfo, StreamingContext streamingContext):base(serializationInfo, streamingContext) - { - - } -#endif - } -} \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/InternalServerErrorException.cs b/src/redmine-net-api/Exceptions/InternalServerErrorException.cs deleted file mode 100644 index 5d12673c..00000000 --- a/src/redmine-net-api/Exceptions/InternalServerErrorException.cs +++ /dev/null @@ -1,89 +0,0 @@ -/* - Copyright 2011 - 2025 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Globalization; -using System.Runtime.Serialization; - -namespace Redmine.Net.Api.Exceptions -{ - /// - /// - /// - [Serializable] - public sealed class InternalServerErrorException : RedmineException - { - /// - /// Initializes a new instance of the class. - /// - public InternalServerErrorException() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - public InternalServerErrorException(string message) - : base(message) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - public InternalServerErrorException(string format, params object[] args) - : base(string.Format(CultureInfo.InvariantCulture,format, args)) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - public InternalServerErrorException(string message, Exception innerException) - : base(message, innerException) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - /// - public InternalServerErrorException(string format, Exception innerException, params object[] args) - : base(string.Format(CultureInfo.InvariantCulture,format, args), innerException) - { - } - -#if !(NET8_0_OR_GREATER) - /// - /// - /// - /// - /// - /// - private InternalServerErrorException(SerializationInfo serializationInfo, StreamingContext streamingContext):base(serializationInfo, streamingContext) - { - - } -#endif - } -} \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/NameResolutionFailureException.cs b/src/redmine-net-api/Exceptions/NameResolutionFailureException.cs deleted file mode 100644 index da0350d0..00000000 --- a/src/redmine-net-api/Exceptions/NameResolutionFailureException.cs +++ /dev/null @@ -1,88 +0,0 @@ -/* - Copyright 2011 - 2025 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Globalization; -using System.Runtime.Serialization; - -namespace Redmine.Net.Api.Exceptions -{ - /// - /// - /// - [Serializable] - public sealed class NameResolutionFailureException : RedmineException - { - /// - /// Initializes a new instance of the class. - /// - public NameResolutionFailureException() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - public NameResolutionFailureException(string message) - : base(message) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - public NameResolutionFailureException(string format, params object[] args) - : base(string.Format(CultureInfo.InvariantCulture,format, args)) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - public NameResolutionFailureException(string message, Exception innerException) - : base(message, innerException) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - /// - public NameResolutionFailureException(string format, Exception innerException, params object[] args) - : base(string.Format(CultureInfo.InvariantCulture,format, args), innerException) - { - } - -#if !(NET8_0_OR_GREATER) - /// - /// - /// - /// - /// - private NameResolutionFailureException(SerializationInfo serializationInfo, StreamingContext streamingContext):base(serializationInfo, streamingContext) - { - - } -#endif - } -} \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/NotAcceptableException.cs b/src/redmine-net-api/Exceptions/NotAcceptableException.cs deleted file mode 100644 index bbae9b9c..00000000 --- a/src/redmine-net-api/Exceptions/NotAcceptableException.cs +++ /dev/null @@ -1,88 +0,0 @@ -/* - Copyright 2011 - 2025 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Globalization; -using System.Runtime.Serialization; - -namespace Redmine.Net.Api.Exceptions -{ - /// - /// - /// - [Serializable] - public sealed class NotAcceptableException : RedmineException - { - /// - /// Initializes a new instance of the class. - /// - public NotAcceptableException() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - public NotAcceptableException(string message) - : base(message) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - public NotAcceptableException(string format, params object[] args) - : base(string.Format(CultureInfo.InvariantCulture,format, args)) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - public NotAcceptableException(string message, Exception innerException) - : base(message, innerException) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - /// - public NotAcceptableException(string format, Exception innerException, params object[] args) - : base(string.Format(CultureInfo.InvariantCulture,format, args), innerException) - { - } - -#if !(NET8_0_OR_GREATER) - /// - /// - /// - /// - /// - private NotAcceptableException(SerializationInfo serializationInfo, StreamingContext streamingContext):base(serializationInfo, streamingContext) - { - - } -#endif - } -} \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/NotFoundException.cs b/src/redmine-net-api/Exceptions/NotFoundException.cs deleted file mode 100644 index d6c593dc..00000000 --- a/src/redmine-net-api/Exceptions/NotFoundException.cs +++ /dev/null @@ -1,89 +0,0 @@ -/* - Copyright 2011 - 2025 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Globalization; -using System.Runtime.Serialization; - -namespace Redmine.Net.Api.Exceptions -{ - /// - /// Thrown in case the objects requested for could not be found. - /// - /// - [Serializable] - public sealed class NotFoundException : RedmineException - { - /// - /// Initializes a new instance of the class. - /// - public NotFoundException() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - public NotFoundException(string message) - : base(message) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - public NotFoundException(string format, params object[] args) - : base(string.Format(CultureInfo.InvariantCulture,format, args)) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - public NotFoundException(string message, Exception innerException) - : base(message, innerException) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// - /// - /// - public NotFoundException(string format, Exception innerException, params object[] args) - : base(string.Format(CultureInfo.InvariantCulture,format, args), innerException) - { - } - -#if !(NET8_0_OR_GREATER) - /// - /// - /// - /// - /// - private NotFoundException(SerializationInfo serializationInfo, StreamingContext streamingContext):base(serializationInfo, streamingContext) - { - - } -#endif - } -} \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/RedmineApiException.cs b/src/redmine-net-api/Exceptions/RedmineApiException.cs index a858e304..fbe168ed 100644 --- a/src/redmine-net-api/Exceptions/RedmineApiException.cs +++ b/src/redmine-net-api/Exceptions/RedmineApiException.cs @@ -1,5 +1,11 @@ using System; +using System.Net; +#if NET40_OR_GREATER || NET +using System.Net.Http; +#endif +using System.Net.Sockets; using System.Runtime.Serialization; +using Redmine.Net.Api.Http.Constants; namespace Redmine.Net.Api.Exceptions { @@ -7,81 +13,196 @@ namespace Redmine.Net.Api.Exceptions /// /// [Serializable] - public sealed class RedmineApiException : RedmineException + public class RedmineApiException : RedmineException { /// - /// + /// Gets the error code parameter. /// - public RedmineApiException() - : this(errorCode: null, false) { } - + /// The error code associated with the exception. + public override string ErrorCode => "REDMINE-API-002"; + + /// + /// Gets a value indicating whether gets exception is Transient and operation can be retried. + /// + /// Value indicating whether the exception is transient or not. + public bool IsTransient { get; protected set; } + /// /// /// - /// - public RedmineApiException(string message) - : this(message, errorCode: null, false) { } - + public string Url { get; protected set; } + /// /// /// - /// - /// - public RedmineApiException(string message, Exception innerException) - : this(message, innerException, errorCode: null, false) { } - + public int? HttpStatusCode { get; protected set; } + /// /// /// - /// - /// - public RedmineApiException(string errorCode, bool isTransient) - : this(string.Empty, errorCode, isTransient) { } - + public RedmineApiException() + { + } + /// /// /// /// - /// - /// - public RedmineApiException(string message, string errorCode, bool isTransient) - : this(message, null, errorCode, isTransient) { } - + public RedmineApiException(string message) : base(message) + { + } + /// /// /// /// - /// - /// - /// - public RedmineApiException(string message, Exception inner, string errorCode, bool isTransient) - : base(message, inner) + /// + public RedmineApiException(string message, Exception innerException) : base(message, innerException) { - this.ErrorCode = errorCode ?? "UNKNOWN"; - this.IsTransient = isTransient; + var transientErrorResult = IsTransientError(InnerException); + IsTransient = transientErrorResult.IsTransient; + HttpStatusCode = transientErrorResult.StatusCode; } - + /// - /// Gets the error code parameter. + /// /// - /// The error code associated with the exception. - public string ErrorCode { get; } + /// + /// + /// + /// + public RedmineApiException(string message, string url, int? httpStatusCode = null, + Exception innerException = null) + : base(message, innerException) + { + Url = url; + var transientErrorResult = IsTransientError(InnerException); + IsTransient = transientErrorResult.IsTransient; + HttpStatusCode = httpStatusCode ?? transientErrorResult.StatusCode; + } /// - /// Gets a value indicating whether gets exception is Transient and operation can be retried. + /// /// - /// Value indicating whether the exception is transient or not. - public bool IsTransient { get; } - - #if !(NET8_0_OR_GREATER) + /// + /// + /// + /// + public RedmineApiException(string message, Uri requestUri, int? httpStatusCode = null, Exception innerException = null) + : base(message, innerException) + { + Url = requestUri?.ToString(); + var transientErrorResult = IsTransientError(InnerException); + IsTransient = transientErrorResult.IsTransient; + HttpStatusCode = httpStatusCode ?? transientErrorResult.StatusCode; + } + +#if !(NET8_0_OR_GREATER) /// + protected RedmineApiException(SerializationInfo info, StreamingContext context) : base(info, context) + { + Url = info.GetString(nameof(Url)); + HttpStatusCode = info.GetInt32(nameof(HttpStatusCode)); + } + + /// + /// + /// + /// + /// public override void GetObjectData(SerializationInfo info, StreamingContext context) { base.GetObjectData(info, context); + info.AddValue(nameof(Url), Url); + info.AddValue(nameof(HttpStatusCode), HttpStatusCode); + } +#endif + + internal struct TransientErrorResult(bool isTransient, int? statusCode) + { + public bool IsTransient = isTransient; + public int? StatusCode = statusCode; + } + + internal static TransientErrorResult IsTransientError(Exception exception) + { + return exception switch + { + null => new TransientErrorResult(false, null), + WebException webEx => HandleWebException(webEx), +#if NET40_OR_GREATER || NET + HttpRequestException httpRequestEx => HandleHttpRequestException(httpRequestEx), +#endif + _ => new TransientErrorResult(false, null) + }; + } + + private static TransientErrorResult HandleWebException(WebException webEx) + { + switch (webEx.Status) + { + case WebExceptionStatus.ProtocolError when webEx.Response is HttpWebResponse response: + var statusCode = (int)response.StatusCode; + return new TransientErrorResult(IsTransientStatusCode(statusCode), statusCode); + + case WebExceptionStatus.Timeout: + return new TransientErrorResult(true, HttpConstants.StatusCodes.RequestTimeout); + + case WebExceptionStatus.NameResolutionFailure: + case WebExceptionStatus.ConnectFailure: + return new TransientErrorResult(true, HttpConstants.StatusCodes.ServiceUnavailable); + + default: + return new TransientErrorResult(false, null); + } + } + + private static bool IsTransientStatusCode(int statusCode) + { + return statusCode == HttpConstants.StatusCodes.TooManyRequests || + statusCode == HttpConstants.StatusCodes.InternalServerError || + statusCode == HttpConstants.StatusCodes.BadGateway || + statusCode == HttpConstants.StatusCodes.ServiceUnavailable || + statusCode == HttpConstants.StatusCodes.GatewayTimeout; + } + +#if NET40_OR_GREATER || NET + private static TransientErrorResult HandleHttpRequestException( + HttpRequestException httpRequestEx) + { + if (httpRequestEx.InnerException is WebException innerWebEx) + { + return HandleWebException(innerWebEx); + } + + if (httpRequestEx.InnerException is SocketException socketEx) + { + switch (socketEx.SocketErrorCode) + { + case SocketError.HostNotFound: + case SocketError.HostUnreachable: + case SocketError.NetworkUnreachable: + case SocketError.ConnectionRefused: + return new TransientErrorResult(true, HttpConstants.StatusCodes.ServiceUnavailable); + + case SocketError.TimedOut: + return new TransientErrorResult(true, HttpConstants.StatusCodes.RequestTimeout); + + default: + return new TransientErrorResult(false, null); + } + } + +#if NET + if (httpRequestEx.StatusCode.HasValue) + { + var statusCode = (int)httpRequestEx.StatusCode.Value; + return new TransientErrorResult(IsTransientStatusCode(statusCode), statusCode); + } +#endif - info.AddValue(nameof(this.ErrorCode), this.ErrorCode); - info.AddValue(nameof(this.IsTransient), this.IsTransient); + return new TransientErrorResult(false, null); } - #endif +#endif } } \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/RedmineException.cs b/src/redmine-net-api/Exceptions/RedmineException.cs index ee2cc07a..95bff717 100644 --- a/src/redmine-net-api/Exceptions/RedmineException.cs +++ b/src/redmine-net-api/Exceptions/RedmineException.cs @@ -15,8 +15,8 @@ limitations under the License. */ using System; +using System.Collections.Generic; using System.Diagnostics; -using System.Globalization; using System.Runtime.Serialization; namespace Redmine.Net.Api.Exceptions @@ -30,63 +30,53 @@ namespace Redmine.Net.Api.Exceptions public class RedmineException : Exception { /// - /// Initializes a new instance of the class. + /// /// - public RedmineException() - { - } + public virtual string ErrorCode => "REDMINE-GEN-001"; /// - /// Initializes a new instance of the class. + /// /// - /// The message that describes the error. - public RedmineException(string message) - : base(message) - { - } + public Dictionary ErrorDetails { get; private set; } /// - /// Initializes a new instance of the class. + /// /// - /// The format. - /// The arguments. - public RedmineException(string format, params object[] args) - : base(string.Format(CultureInfo.InvariantCulture,format, args)) - { - } - + public RedmineException() { } /// - /// Initializes a new instance of the class. + /// /// - /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if no inner exception is specified. - public RedmineException(string message, Exception innerException) - : base(message, innerException) - { - } - + /// + public RedmineException(string message) : base(message) { } /// - /// Initializes a new instance of the class. + /// /// - /// The format. - /// The inner exception. - /// The arguments. - public RedmineException(string format, Exception innerException, params object[] args) - : base(string.Format(CultureInfo.InvariantCulture,format, args), innerException) - { - } - - #if !(NET8_0_OR_GREATER) + /// + /// + public RedmineException(string message, Exception innerException) : base(message, innerException) { } + +#if !(NET8_0_OR_GREATER) + /// + /// + /// + /// + /// + protected RedmineException(SerializationInfo info, StreamingContext context) : base(info, context) { } +#endif + /// /// /// - /// - /// - protected RedmineException(SerializationInfo serializationInfo, StreamingContext streamingContext):base(serializationInfo, streamingContext) + /// + /// + /// + public RedmineException AddErrorDetail(string key, string value) { + ErrorDetails ??= new Dictionary(); + ErrorDetails[key] = value; + return this; } - #endif private string DebuggerDisplay => $"[{Message}]"; } diff --git a/src/redmine-net-api/Exceptions/RedmineForbiddenException.cs b/src/redmine-net-api/Exceptions/RedmineForbiddenException.cs new file mode 100644 index 00000000..33e3d53d --- /dev/null +++ b/src/redmine-net-api/Exceptions/RedmineForbiddenException.cs @@ -0,0 +1,82 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System; +using Redmine.Net.Api.Http.Constants; + +namespace Redmine.Net.Api.Exceptions +{ + + /// + /// Represents an exception thrown when a Redmine API request is forbidden (HTTP 403). + /// + [Serializable] + public sealed class RedmineForbiddenException : RedmineApiException + { + /// + /// Gets the error code for this exception. + /// + public override string ErrorCode => "REDMINE-FORBIDDEN-005"; + + /// + /// Initializes a new instance of the class. + /// + public RedmineForbiddenException() + : base("Access to the Redmine API resource is forbidden.", (string)null, HttpConstants.StatusCodes.Forbidden, null) + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The error message that explains the reason for the exception. + /// Thrown when is null. + public RedmineForbiddenException(string message) + : base(message, (string)null, HttpConstants.StatusCodes.Forbidden, null) + { } + + /// + /// Initializes a new instance of the class with a specified error message and URL. + /// + /// The error message that explains the reason for the exception. + /// The URL of the Redmine API resource that caused the exception. + /// Thrown when is null. + public RedmineForbiddenException(string message, string url) + : base(message, url, HttpConstants.StatusCodes.Forbidden, null) + { } + + /// + /// Initializes a new instance of the class with a specified error message and inner exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception. + /// Thrown when or is null. + public RedmineForbiddenException(string message, Exception innerException) + : base(message, (string)null, HttpConstants.StatusCodes.Forbidden, innerException) + { } + + /// + /// Initializes a new instance of the class with a specified error message, URL, and inner exception. + /// + /// The error message that explains the reason for the exception. + /// The URL of the Redmine API resource that caused the exception. + /// The exception that is the cause of the current exception. + /// Thrown when or is null. + public RedmineForbiddenException(string message, string url, Exception innerException) + : base(message, url, HttpConstants.StatusCodes.Forbidden, innerException) + { } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/RedmineNotAcceptableException.cs b/src/redmine-net-api/Exceptions/RedmineNotAcceptableException.cs new file mode 100644 index 00000000..8fa39cc3 --- /dev/null +++ b/src/redmine-net-api/Exceptions/RedmineNotAcceptableException.cs @@ -0,0 +1,56 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System; + +namespace Redmine.Net.Api.Exceptions +{ + /// + /// + /// + [Serializable] + public sealed class RedmineNotAcceptableException : RedmineApiException + { + /// + public override string ErrorCode => "REDMINE-NOT-ACCEPTABLE-010"; + + /// + /// Initializes a new instance of the class. + /// + public RedmineNotAcceptableException() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + public RedmineNotAcceptableException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// + public RedmineNotAcceptableException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/RedmineNotFoundException.cs b/src/redmine-net-api/Exceptions/RedmineNotFoundException.cs new file mode 100644 index 00000000..b6a6e7bc --- /dev/null +++ b/src/redmine-net-api/Exceptions/RedmineNotFoundException.cs @@ -0,0 +1,80 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System; +using Redmine.Net.Api.Http.Constants; + +namespace Redmine.Net.Api.Exceptions +{ + /// + /// Thrown in case the objects requested for could not be found. + /// + /// + [Serializable] + public sealed class RedmineNotFoundException : RedmineApiException + { + /// + public override string ErrorCode => "REDMINE-NOTFOUND-006"; + + /// + /// Initializes a new instance of the class. + /// + public RedmineNotFoundException() + : base("The requested Redmine API resource was not found.", (string)null, HttpConstants.StatusCodes.NotFound, null) + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The error message that explains the reason for the exception. + /// Thrown when is null. + public RedmineNotFoundException(string message) + : base(message, (string)null, HttpConstants.StatusCodes.NotFound, null) + { } + + /// + /// Initializes a new instance of the class with a specified error message and URL. + /// + /// The error message that explains the reason for the exception. + /// The URL of the Redmine API resource that was not found. + /// Thrown when is null. + public RedmineNotFoundException(string message, string url) + : base(message, url, HttpConstants.StatusCodes.NotFound, null) + { } + + /// + /// Initializes a new instance of the class with a specified error message and inner exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception. + /// Thrown when or is null. + public RedmineNotFoundException(string message, Exception innerException) + : base(message, (string)null, HttpConstants.StatusCodes.NotFound, innerException) + { } + + /// + /// Initializes a new instance of the class with a specified error message, URL, and inner exception. + /// + /// The error message that explains the reason for the exception. + /// The URL of the Redmine API resource that was not found. + /// The exception that is the cause of the current exception. + /// Thrown when or is null. + public RedmineNotFoundException(string message, string url, Exception innerException) + : base(message, url, HttpConstants.StatusCodes.NotFound, innerException) + { } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/RedmineOperationCanceledException.cs b/src/redmine-net-api/Exceptions/RedmineOperationCanceledException.cs new file mode 100644 index 00000000..f3dcc39e --- /dev/null +++ b/src/redmine-net-api/Exceptions/RedmineOperationCanceledException.cs @@ -0,0 +1,85 @@ +using System; +using Redmine.Net.Api.Http.Constants; + +namespace Redmine.Net.Api.Exceptions; + +/// +/// Represents an exception thrown when a Redmine API operation is canceled, typically due to a CancellationToken. +/// +[Serializable] +public sealed class RedmineOperationCanceledException : RedmineException +{ + /// + /// Gets the error code for this exception. + /// + public override string ErrorCode => "REDMINE-CANCEL-003"; + + /// + /// Gets the URL of the Redmine API resource associated with the canceled operation, if applicable. + /// + public string Url { get; private set; } + + /// + /// Initializes a new instance of the class. + /// + public RedmineOperationCanceledException() + : base("The Redmine API operation was canceled.") + { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The error message that explains the reason for the exception. + /// Thrown when is null. + public RedmineOperationCanceledException(string message) + : base(message) + { } + + /// + /// Initializes a new instance of the class with a specified error message and URL. + /// + /// The error message that explains the reason for the exception. + /// The URL of the Redmine API resource associated with the canceled operation. + /// Thrown when or is null. + public RedmineOperationCanceledException(string message, string url) + : base(message) + { + Url = url; + } + + /// + /// Initializes a new instance of the class with a specified error message and inner exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception. + /// Thrown when or is null. + public RedmineOperationCanceledException(string message, Exception innerException) + : base(message, innerException) + { } + + /// + /// Initializes a new instance of the class with a specified error message, URL, and inner exception. + /// + /// The error message that explains the reason for the exception. + /// The URL of the Redmine API resource associated with the canceled operation. + /// The exception that is the cause of the current exception, or null if none. + /// Thrown when or is null. + public RedmineOperationCanceledException(string message, string url, Exception innerException) + : base(message, innerException) + { + Url = url; + } + + /// + /// Initializes a new instance of the class with a specified error message, URL, and inner exception. + /// + /// The error message that explains the reason for the exception. + /// The URL of the Redmine API resource associated with the canceled operation. + /// The exception that is the cause of the current exception, or null if none. + /// Thrown when or is null. + public RedmineOperationCanceledException(string message, Uri url, Exception innerException) + : base(message, innerException) + { + Url = url?.ToString(); + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/RedmineSerializationException.cs b/src/redmine-net-api/Exceptions/RedmineSerializationException.cs index 0f6b779f..e864ef40 100644 --- a/src/redmine-net-api/Exceptions/RedmineSerializationException.cs +++ b/src/redmine-net-api/Exceptions/RedmineSerializationException.cs @@ -3,14 +3,24 @@ namespace Redmine.Net.Api.Exceptions; /// -/// Represents an error that occurs during JSON serialization or deserialization. +/// Represents an exception thrown when a serialization or deserialization error occurs in the Redmine API client. /// -public class RedmineSerializationException : RedmineException +[Serializable] +public sealed class RedmineSerializationException : RedmineException { + /// + public override string ErrorCode => "REDMINE-SERIALIZATION-008"; + + /// + /// Gets the name of the parameter that caused the serialization or deserialization error, if applicable. + /// + public string ParamName { get; private set; } + /// /// Initializes a new instance of the class. /// public RedmineSerializationException() + : base("A serialization or deserialization error occurred in the Redmine API client.") { } @@ -18,31 +28,43 @@ public RedmineSerializationException() /// Initializes a new instance of the class with a specified error message. /// /// The error message that explains the reason for the exception. - public RedmineSerializationException(string message) : base(message) - { - } - + /// Thrown when is null. + public RedmineSerializationException(string message) + : base(message) + { } + /// - /// Initializes a new instance of the class with a specified error message. + /// Initializes a new instance of the class with a specified error message and parameter name. /// /// The error message that explains the reason for the exception. - /// /// The name of the parameter that caused the exception. - public RedmineSerializationException(string message, string paramName) : base(message) + /// The name of the parameter that caused the serialization or deserialization error. + /// Thrown when or is null. + public RedmineSerializationException(string message, string paramName) + : base(message) { - ParamName = paramName; + ParamName = paramName ?? throw new ArgumentNullException(nameof(paramName)); } /// - /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// Initializes a new instance of the class with a specified error message and inner exception. /// /// The error message that explains the reason for the exception. - /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. - public RedmineSerializationException(string message, Exception innerException) : base(message, innerException) - { - } + /// The exception that is the cause of the current exception. + /// Thrown when or is null. + public RedmineSerializationException(string message, Exception innerException) + : base(message, innerException) + { } /// - /// Gets the name of the parameter that caused the current exception. + /// Initializes a new instance of the class with a specified error message, parameter name, and inner exception. /// - public string ParamName { get; } + /// The error message that explains the reason for the exception. + /// The name of the parameter that caused the serialization or deserialization error. + /// The exception that is the cause of the current exception. + /// Thrown when , , or is null. + public RedmineSerializationException(string message, string paramName, Exception innerException) + : base(message, innerException) + { + ParamName = paramName ?? throw new ArgumentNullException(nameof(paramName)); + } } \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/RedmineTimeoutException.cs b/src/redmine-net-api/Exceptions/RedmineTimeoutException.cs index 31ec968f..038a44df 100644 --- a/src/redmine-net-api/Exceptions/RedmineTimeoutException.cs +++ b/src/redmine-net-api/Exceptions/RedmineTimeoutException.cs @@ -15,77 +15,76 @@ limitations under the License. */ using System; -using System.Globalization; -using System.Runtime.Serialization; +using Redmine.Net.Api.Http.Constants; namespace Redmine.Net.Api.Exceptions { /// + /// Represents an exception thrown when a Redmine API request times out (HTTP 408). /// - /// [Serializable] - public sealed class RedmineTimeoutException : RedmineException + public sealed class RedmineTimeoutException : RedmineApiException { + /// + public override string ErrorCode => "REDMINE-TIMEOUT-004"; + /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public RedmineTimeoutException() + : base("The Redmine API request timed out.", (string)null, HttpConstants.StatusCodes.RequestTimeout, null) { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class with a specified error message. /// - /// The message that describes the error. + /// The error message that explains the reason for the exception. + /// Thrown when is null. public RedmineTimeoutException(string message) - : base(message) - { - } + : base(message, (string)null, HttpConstants.StatusCodes.RequestTimeout, null) + { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class with a specified error message and URL. /// - /// The format. - /// The arguments. - public RedmineTimeoutException(string format, params object[] args) - : base(string.Format(CultureInfo.InvariantCulture,format, args)) - { - } + /// The error message that explains the reason for the exception. + /// The URL of the Redmine API resource that timed out. + /// Thrown when or is null. + public RedmineTimeoutException(string message, string url) + : base(message, url, HttpConstants.StatusCodes.RequestTimeout, null) + { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class with a specified error message and inner exception. /// /// The error message that explains the reason for the exception. - /// - /// The exception that is the cause of the current exception, or a null reference (Nothing in - /// Visual Basic) if no inner exception is specified. - /// + /// The exception that is the cause of the current exception. + /// Thrown when or is null. public RedmineTimeoutException(string message, Exception innerException) - : base(message, innerException) - { - } + : base(message, (string)null, HttpConstants.StatusCodes.RequestTimeout, innerException) + { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class with a specified error message, URL, and inner exception. /// - /// The format. - /// The inner exception. - /// The arguments. - public RedmineTimeoutException(string format, Exception innerException, params object[] args) - : base(string.Format(CultureInfo.InvariantCulture,format, args), innerException) - { - } + /// The error message that explains the reason for the exception. + /// The URL of the Redmine API resource that timed out. + /// The exception that is the cause of the current exception, or null if none. + /// Thrown when or is null. + public RedmineTimeoutException(string message, string url, Exception innerException) + : base(message, url, HttpConstants.StatusCodes.RequestTimeout, innerException) + { } -#if !(NET8_0_OR_GREATER) /// - /// + /// Initializes a new instance of the class with a specified error message, URL, and inner exception. /// - /// - /// - private RedmineTimeoutException(SerializationInfo serializationInfo, StreamingContext streamingContext):base(serializationInfo, streamingContext) - { - - } -#endif + /// The error message that explains the reason for the exception. + /// The URL of the Redmine API resource that timed out. + /// The exception that is the cause of the current exception, or null if none. + /// Thrown when or is null. + public RedmineTimeoutException(string message, Uri url, Exception innerException) + : base(message, url?.ToString(), HttpConstants.StatusCodes.RequestTimeout, innerException) + { } } } \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/RedmineUnauthorizedException.cs b/src/redmine-net-api/Exceptions/RedmineUnauthorizedException.cs new file mode 100644 index 00000000..824d7727 --- /dev/null +++ b/src/redmine-net-api/Exceptions/RedmineUnauthorizedException.cs @@ -0,0 +1,79 @@ +/* + Copyright 2011 - 2025 Adrian Popescu + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +using System; +using Redmine.Net.Api.Http.Constants; + +namespace Redmine.Net.Api.Exceptions +{ + /// + /// Represents an exception thrown when a Redmine API request is unauthorized (HTTP 401). + /// + [Serializable] + public sealed class RedmineUnauthorizedException : RedmineApiException + { + /// + public override string ErrorCode => "REDMINE-UNAUTHORIZED-007"; + + /// + /// Initializes a new instance of the class. + /// + public RedmineUnauthorizedException() + : base("The Redmine API request is unauthorized.", (string)null, HttpConstants.StatusCodes.Unauthorized, null) + { + } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The error message that explains the reason for the exception. + /// Thrown when is null. + public RedmineUnauthorizedException(string message) + : base(message, (string)null, HttpConstants.StatusCodes.Unauthorized, null) + { } + + /// + /// Initializes a new instance of the class with a specified error message and URL. + /// + /// The error message that explains the reason for the exception. + /// The URL of the Redmine API resource that caused the unauthorized access. + /// Thrown when is null. + public RedmineUnauthorizedException(string message, string url) + : base(message, url, HttpConstants.StatusCodes.Unauthorized, null) + { } + + /// + /// Initializes a new instance of the class with a specified error message and inner exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception. + /// Thrown when or is null. + public RedmineUnauthorizedException(string message, Exception innerException) + : base(message, (string)null, HttpConstants.StatusCodes.Unauthorized, innerException) + { } + + /// + /// Initializes a new instance of the class with a specified error message, URL, and inner exception. + /// + /// The error message that explains the reason for the exception. + /// The URL of the Redmine API resource that caused the unauthorized access. + /// The exception that is the cause of the current exception. + /// Thrown when or is null. + public RedmineUnauthorizedException(string message, string url, Exception innerException) + : base(message, url, HttpConstants.StatusCodes.Unauthorized, innerException) + { } + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/RedmineUnprocessableEntityException.cs b/src/redmine-net-api/Exceptions/RedmineUnprocessableEntityException.cs new file mode 100644 index 00000000..c37dd063 --- /dev/null +++ b/src/redmine-net-api/Exceptions/RedmineUnprocessableEntityException.cs @@ -0,0 +1,63 @@ +using System; +using Redmine.Net.Api.Http.Constants; + +namespace Redmine.Net.Api.Exceptions; + +/// +/// Represents an exception thrown when a Redmine API request cannot be processed due to semantic errors (HTTP 422). +/// +[Serializable] +public class RedmineUnprocessableEntityException : RedmineApiException +{ + /// + /// Gets the error code for this exception. + /// + public override string ErrorCode => "REDMINE-UNPROCESSABLE-009"; + + /// + /// Initializes a new instance of the class. + /// + public RedmineUnprocessableEntityException() + : base("The Redmine API request cannot be processed due to invalid data.", (string)null, HttpConstants.StatusCodes.UnprocessableEntity, null) + { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The error message that explains the reason for the exception. + /// Thrown when is null. + public RedmineUnprocessableEntityException(string message) + : base(message, (string)null, HttpConstants.StatusCodes.UnprocessableEntity, null) + { } + + /// + /// Initializes a new instance of the class with a specified error message and URL. + /// + /// The error message that explains the reason for the exception. + /// The URL of the Redmine API resource that caused the error. + /// Thrown when is null. + public RedmineUnprocessableEntityException(string message, string url) + : base(message, url, HttpConstants.StatusCodes.UnprocessableEntity, null) + { } + + /// + /// Initializes a new instance of the class with a specified error message and inner exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception. + /// Thrown when or is null. + public RedmineUnprocessableEntityException(string message, Exception innerException) + : base(message, (string)null, HttpConstants.StatusCodes.UnprocessableEntity, innerException) + { } + + /// + /// Initializes a new instance of the class with a specified error message, URL, and inner exception. + /// + /// The error message that explains the reason for the exception. + /// The URL of the Redmine API resource that caused the error. + /// The exception that is the cause of the current exception. + /// Thrown when or is null. + public RedmineUnprocessableEntityException(string message, string url, Exception innerException) + : base(message, url, HttpConstants.StatusCodes.UnprocessableEntity, innerException) + { } +} \ No newline at end of file diff --git a/src/redmine-net-api/Exceptions/UnauthorizedException.cs b/src/redmine-net-api/Exceptions/UnauthorizedException.cs deleted file mode 100644 index 214f7b84..00000000 --- a/src/redmine-net-api/Exceptions/UnauthorizedException.cs +++ /dev/null @@ -1,92 +0,0 @@ -/* - Copyright 2011 - 2025 Adrian Popescu - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. -*/ - -using System; -using System.Globalization; -using System.Runtime.Serialization; - -namespace Redmine.Net.Api.Exceptions -{ - /// - /// Thrown in case something went wrong while trying to login. - /// - /// - [Serializable] - public sealed class UnauthorizedException : RedmineException - { - /// - /// Initializes a new instance of the class. - /// - public UnauthorizedException() - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The message that describes the error. - public UnauthorizedException(string message) - : base(message) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The format. - /// The arguments. - public UnauthorizedException(string format, params object[] args) - : base(string.Format(CultureInfo.InvariantCulture,format, args)) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The error message that explains the reason for the exception. - /// - /// The exception that is the cause of the current exception, or a null reference (Nothing in - /// Visual Basic) if no inner exception is specified. - /// - public UnauthorizedException(string message, Exception innerException) - : base(message, innerException) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The format. - /// The inner exception. - /// The arguments. - public UnauthorizedException(string format, Exception innerException, params object[] args) - : base(string.Format(CultureInfo.InvariantCulture,format, args), innerException) - { - } -#if !(NET8_0_OR_GREATER) - /// - /// - /// - /// - /// - /// - private UnauthorizedException(SerializationInfo serializationInfo, StreamingContext streamingContext):base(serializationInfo, streamingContext) - { - - } -#endif - } -} \ No newline at end of file diff --git a/src/redmine-net-api/Http/Clients/HttpClient/InternalRedmineApiHttpClient.Async.cs b/src/redmine-net-api/Http/Clients/HttpClient/InternalRedmineApiHttpClient.Async.cs index 77186037..25beece1 100644 --- a/src/redmine-net-api/Http/Clients/HttpClient/InternalRedmineApiHttpClient.Async.cs +++ b/src/redmine-net-api/Http/Clients/HttpClient/InternalRedmineApiHttpClient.Async.cs @@ -16,11 +16,14 @@ limitations under the License. */ using System; +using System.IO; using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Extensions; +using Redmine.Net.Api.Http.Constants; using Redmine.Net.Api.Http.Helpers; using Redmine.Net.Api.Http.Messages; @@ -32,10 +35,9 @@ protected override async Task HandleRequestAsync(string addr object content = null, IProgress progress = null, CancellationToken cancellationToken = default) { var httpMethod = GetHttpMethod(verb); - using (var requestMessage = CreateRequestMessage(address, httpMethod, requestOptions, content as HttpContent)) - { - return await SendAsync(requestMessage, progress: progress, cancellationToken: cancellationToken).ConfigureAwait(false); - } + using var requestMessage = CreateRequestMessage(address, httpMethod, requestOptions, content as HttpContent); + var response = await SendAsync(requestMessage, progress: progress, cancellationToken: cancellationToken).ConfigureAwait(false); + return response; } private async Task SendAsync(HttpRequestMessage requestMessage, IProgress progress = null, CancellationToken cancellationToken = default) @@ -73,36 +75,45 @@ private async Task SendAsync(HttpRequestMessage requestMessa using (var stream = await httpResponseMessage.Content.ReadAsStreamAsync(cancellationToken) .ConfigureAwait(false)) { - RedmineExceptionHelper.MapStatusCodeToException(statusCode, stream, null, Serializer); + var url = requestMessage.RequestUri?.ToString(); + var message = httpResponseMessage.ReasonPhrase; + + throw statusCode switch + { + HttpConstants.StatusCodes.NotFound => new RedmineNotFoundException(message, url), + HttpConstants.StatusCodes.Unauthorized => new RedmineUnauthorizedException(message, url), + HttpConstants.StatusCodes.Forbidden => new RedmineForbiddenException(message, url), + HttpConstants.StatusCodes.UnprocessableEntity => RedmineExceptionHelper.CreateUnprocessableEntityException(url, stream, null, Serializer), + HttpConstants.StatusCodes.NotAcceptable => new RedmineNotAcceptableException(message), + _ => new RedmineApiException(message, url, statusCode), + }; } } } catch (OperationCanceledException ex) when (cancellationToken.IsCancellationRequested) { - throw new RedmineApiException("Token has been cancelled", ex); + throw new RedmineOperationCanceledException(ex.Message, requestMessage.RequestUri, ex); } catch (OperationCanceledException ex) when (ex.InnerException is TimeoutException tex) { - throw new RedmineApiException("Operation has timed out", ex); + throw new RedmineTimeoutException(tex.Message, requestMessage.RequestUri, tex); } catch (TaskCanceledException tcex) when (cancellationToken.IsCancellationRequested) { - throw new RedmineApiException("Operation ahs been cancelled by user", tcex); + throw new RedmineOperationCanceledException(tcex.Message, requestMessage.RequestUri, tcex); } catch (TaskCanceledException tce) { - throw new RedmineApiException(tce.Message, tce); + throw new RedmineTimeoutException(tce.Message, requestMessage.RequestUri, tce); } catch (HttpRequestException ex) { - throw new RedmineApiException(ex.Message, ex); + throw new RedmineApiException(ex.Message, requestMessage.RequestUri, HttpConstants.StatusCodes.Unknown, ex); } catch (Exception ex) when (ex is not RedmineException) { - throw new RedmineApiException(ex.Message, ex); + throw new RedmineApiException(ex.Message, requestMessage.RequestUri, HttpConstants.StatusCodes.Unknown, ex); } - - return null; } private static async Task DownloadWithProgressAsync(HttpContent httpContent, IProgress progress = null, CancellationToken cancellationToken = default) diff --git a/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.Async.cs b/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.Async.cs index a2678963..525e6014 100644 --- a/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.Async.cs +++ b/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.Async.cs @@ -84,26 +84,33 @@ private async Task SendAsync(RedmineApiRequest requestMessag payload = EmptyBytes; } - response = await webClient.UploadDataTaskAsync(requestMessage.RequestUri, requestMessage.Method, payload) + response = await webClient + .UploadDataTaskAsync(requestMessage.RequestUri, requestMessage.Method, payload) .ConfigureAwait(false); } - + responseHeaders = webClient.ResponseHeaders; - if (webClient is InternalWebClient iwc) - { - statusCode = iwc.StatusCode; - } + statusCode = webClient.GetStatusCode(); } catch (WebException ex) when (ex.Status == WebExceptionStatus.RequestCanceled) { - if (cancellationToken.IsCancellationRequested) - { - throw new RedmineApiException("The operation was canceled by the user.", ex); - } + throw new RedmineOperationCanceledException(ex.Message, requestMessage.RequestUri, ex); + } + catch (WebException ex) when (ex.Status == WebExceptionStatus.Timeout) + { + throw new RedmineTimeoutException(ex.Message, requestMessage.RequestUri, ex); + } + catch (WebException webException)when (webException.Status == WebExceptionStatus.ProtocolError) + { + HandleResponseException(webException, requestMessage.RequestUri, Serializer); + } + catch (OperationCanceledException ex) + { + throw new RedmineOperationCanceledException(ex.Message, requestMessage.RequestUri, ex); } - catch (WebException webException) + catch (Exception ex) { - HandleWebException(webException, Serializer); + throw new RedmineApiException(ex.Message, requestMessage.RequestUri, null, ex); } finally { diff --git a/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.cs b/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.cs index 775b7bed..59fbae62 100644 --- a/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.cs +++ b/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.cs @@ -16,17 +16,13 @@ limitations under the License. using System; using System.Collections.Specialized; -using System.Globalization; using System.Net; using System.Text; -using Redmine.Net.Api.Authentication; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Http.Constants; -using Redmine.Net.Api.Http.Extensions; using Redmine.Net.Api.Http.Helpers; using Redmine.Net.Api.Http.Messages; -using Redmine.Net.Api.Logging; using Redmine.Net.Api.Options; using Redmine.Net.Api.Serialization; @@ -63,29 +59,14 @@ protected override object CreateContentFromBytes(byte[] data) protected override RedmineApiResponse HandleRequest(string address, string verb, RequestOptions requestOptions = null, object content = null, IProgress progress = null) { - var requestMessage = CreateRequestMessage(address, verb, requestOptions, content as RedmineApiRequestContent); + var requestMessage = CreateRequestMessage(address, verb, Serializer, requestOptions, content as RedmineApiRequestContent); - if (Options.LoggingOptions?.IncludeHttpDetails == true) - { - Options.Logger.Debug($"Request HTTP {verb} {address}"); - - if (requestOptions?.QueryString != null) - { - Options.Logger.Debug($"Query parameters: {requestOptions.QueryString.ToQueryString()}"); - } - } - var responseMessage = Send(requestMessage, progress); - if (Options.LoggingOptions?.IncludeHttpDetails == true) - { - Options.Logger.Debug($"Response status: {responseMessage.StatusCode}"); - } - return responseMessage; } - private static RedmineApiRequest CreateRequestMessage(string address, string verb, RequestOptions requestOptions = null, RedmineApiRequestContent content = null) + private static RedmineApiRequest CreateRequestMessage(string address, string verb, IRedmineSerializer serializer, RequestOptions requestOptions = null, RedmineApiRequestContent content = null) { var req = new RedmineApiRequest() { @@ -117,13 +98,18 @@ private RedmineApiResponse Send(RedmineApiRequest requestMessage, IProgress try { webClient = _webClientFunc(); - - SetWebClientHeaders(webClient, requestMessage); + + if (requestMessage.QueryString != null) + { + webClient.QueryString = requestMessage.QueryString; + } + + webClient.ApplyHeaders(requestMessage, Credentials); if (IsGetOrDownload(requestMessage.Method)) { - response = requestMessage.Method == HttpConstants.HttpVerbs.DOWNLOAD - ? DownloadWithProgress(requestMessage.RequestUri, webClient, progress) + response = requestMessage.Method == HttpConstants.HttpVerbs.DOWNLOAD + ? webClient.DownloadWithProgress(requestMessage.RequestUri, progress) : webClient.DownloadData(requestMessage.RequestUri); } else @@ -143,14 +129,30 @@ private RedmineApiResponse Send(RedmineApiRequest requestMessage, IProgress } responseHeaders = webClient.ResponseHeaders; - if (webClient is InternalWebClient iwc) + statusCode = webClient.GetStatusCode(); + } + catch (WebException webException) when (webException.Status == WebExceptionStatus.ProtocolError) + { + HandleResponseException(webException, requestMessage.RequestUri, Serializer); + } + catch (WebException webException) + { + if (webException.Status == WebExceptionStatus.RequestCanceled) + { + throw new RedmineOperationCanceledException(webException.Message, requestMessage.RequestUri, webException.InnerException); + } + + if (webException.Status == WebExceptionStatus.Timeout) { - statusCode = iwc.StatusCode; + throw new RedmineTimeoutException(webException.Message, requestMessage.RequestUri, webException.InnerException); } + + var errStatusCode = GetExceptionStatusCode(webException); + throw new RedmineApiException(webException.Message, requestMessage.RequestUri, errStatusCode, webException.InnerException); } - catch (WebException webException) + catch (Exception ex) { - HandleWebException(webException, Serializer); + throw new RedmineApiException(ex.Message, requestMessage.RequestUri, HttpConstants.StatusCodes.Unknown, ex.InnerException); } finally { @@ -164,118 +166,36 @@ private RedmineApiResponse Send(RedmineApiRequest requestMessage, IProgress StatusCode = statusCode ?? HttpStatusCode.OK, }; } - - private void SetWebClientHeaders(System.Net.WebClient webClient, RedmineApiRequest requestMessage) + + private static void HandleResponseException(WebException exception, string url, IRedmineSerializer serializer) { - if (requestMessage.QueryString != null) - { - webClient.QueryString = requestMessage.QueryString; - } - - switch (Credentials) - { - case RedmineApiKeyAuthentication: - webClient.Headers.Add(RedmineConstants.API_KEY_AUTHORIZATION_HEADER_KEY,Credentials.Token); - break; - case RedmineBasicAuthentication: - webClient.Headers.Add(RedmineConstants.AUTHORIZATION_HEADER_KEY, Credentials.Token); - break; - } - - if (!requestMessage.ImpersonateUser.IsNullOrWhiteSpace()) - { - webClient.Headers.Add(RedmineConstants.IMPERSONATE_HEADER_KEY, requestMessage.ImpersonateUser); - } - } - - private static byte[] DownloadWithProgress(string url, System.Net.WebClient webClient, IProgress progress) - { - var contentLength = GetContentLength(webClient); - byte[] data; - if (contentLength > 0) - { - using (var respStream = webClient.OpenRead(url)) - { - data = new byte[contentLength]; - var buffer = new byte[4096]; - int bytesRead; - var totalBytesRead = 0; + var innerException = exception.InnerException ?? exception; - while ((bytesRead = respStream.Read(buffer, 0, buffer.Length)) > 0) - { - Buffer.BlockCopy(buffer, 0, data, totalBytesRead, bytesRead); - totalBytesRead += bytesRead; - - ReportProgress(progress, contentLength, totalBytesRead); - } - } - } - else + if (exception.Response == null) { - data = webClient.DownloadData(url); - progress?.Report(100); + throw new RedmineApiException(exception.Message, url, null, innerException); } - return data; - } - - private static int GetContentLength(System.Net.WebClient webClient) - { - var total = -1; - if (webClient.ResponseHeaders == null) - { - return total; - } - - var contentLengthAsString = webClient.ResponseHeaders[HttpRequestHeader.ContentLength]; - total = Convert.ToInt32(contentLengthAsString, CultureInfo.InvariantCulture); - - return total; + var statusCode = GetExceptionStatusCode(exception); + + using var responseStream = exception.Response.GetResponseStream(); + throw statusCode switch + { + HttpConstants.StatusCodes.NotFound => new RedmineNotFoundException(exception.Message, url, innerException), + HttpConstants.StatusCodes.Unauthorized => new RedmineUnauthorizedException(exception.Message, url, innerException), + HttpConstants.StatusCodes.Forbidden => new RedmineForbiddenException(exception.Message, url, innerException), + HttpConstants.StatusCodes.UnprocessableEntity => RedmineExceptionHelper.CreateUnprocessableEntityException(url, responseStream, innerException, serializer), + HttpConstants.StatusCodes.NotAcceptable => new RedmineNotAcceptableException(exception.Message, innerException), + _ => new RedmineApiException(exception.Message, url, statusCode, innerException), + }; } - /// - /// Handles the web exception. - /// - /// The exception. - /// - /// Timeout! - /// Bad domain name! - /// - /// - /// - /// - /// The page that you are trying to update is staled! - /// - /// - public static void HandleWebException(WebException exception, IRedmineSerializer serializer) + private static int? GetExceptionStatusCode(WebException webException) { - if (exception == null) - { - return; - } - - var innerException = exception.InnerException ?? exception; - - switch (exception.Status) - { - case WebExceptionStatus.Timeout: - throw new RedmineTimeoutException(nameof(WebExceptionStatus.Timeout), innerException); - case WebExceptionStatus.NameResolutionFailure: - throw new NameResolutionFailureException("Bad domain name.", innerException); - case WebExceptionStatus.ProtocolError: - if (exception.Response != null) - { - var statusCode = exception.Response is HttpWebResponse httpResponse - ? (int)httpResponse.StatusCode - : (int)HttpStatusCode.InternalServerError; - - using var responseStream = exception.Response.GetResponseStream(); - RedmineExceptionHelper.MapStatusCodeToException(statusCode, responseStream, innerException, serializer); - } - - break; - } - throw new RedmineException(exception.Message, innerException); + var statusCode = webException.Response is HttpWebResponse httpResponse + ? (int)httpResponse.StatusCode + : HttpConstants.StatusCodes.Unknown; + return statusCode; } } } diff --git a/src/redmine-net-api/Http/Clients/WebClient/WebClientExtensions.cs b/src/redmine-net-api/Http/Clients/WebClient/WebClientExtensions.cs index ceb2a2cc..f959c4e9 100644 --- a/src/redmine-net-api/Http/Clients/WebClient/WebClientExtensions.cs +++ b/src/redmine-net-api/Http/Clients/WebClient/WebClientExtensions.cs @@ -28,5 +28,13 @@ public static void ApplyHeaders(this System.Net.WebClient client, RequestOptions { client.Headers.Add(header.Key, header.Value); } + internal static HttpStatusCode? GetStatusCode(this System.Net.WebClient webClient) + { + if (webClient is InternalWebClient iwc) + { + return iwc.StatusCode; + } + + return null; } } \ No newline at end of file diff --git a/src/redmine-net-api/Http/Constants/HttpConstants.cs b/src/redmine-net-api/Http/Constants/HttpConstants.cs index d1666c4b..77ad1f4c 100644 --- a/src/redmine-net-api/Http/Constants/HttpConstants.cs +++ b/src/redmine-net-api/Http/Constants/HttpConstants.cs @@ -18,10 +18,12 @@ internal static class StatusCodes public const int Conflict = 409; public const int UnprocessableEntity = 422; public const int TooManyRequests = 429; + public const int ClientCloseRequest = 499; public const int InternalServerError = 500; public const int BadGateway = 502; public const int ServiceUnavailable = 503; public const int GatewayTimeout = 504; + public const int Unknown = 999; } /// diff --git a/src/redmine-net-api/Http/Helpers/RedmineExceptionHelper.cs b/src/redmine-net-api/Http/Helpers/RedmineExceptionHelper.cs index 4e0aa252..2d51e16e 100644 --- a/src/redmine-net-api/Http/Helpers/RedmineExceptionHelper.cs +++ b/src/redmine-net-api/Http/Helpers/RedmineExceptionHelper.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.IO; -using System.Net; using System.Text; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; @@ -16,60 +15,39 @@ namespace Redmine.Net.Api.Http.Helpers; /// internal static class RedmineExceptionHelper { - /// - /// Maps an HTTP status code to an appropriate Redmine exception. - /// - /// The HTTP status code. - /// The response stream containing error details. - /// The inner exception, if any. - /// The serializer to use for deserializing error messages. - /// A specific Redmine exception based on the status code. - internal static void MapStatusCodeToException(int statusCode, Stream responseStream, Exception inner, IRedmineSerializer serializer) - { - switch (statusCode) - { - case HttpConstants.StatusCodes.NotFound: - throw new NotFoundException(HttpConstants.ErrorMessages.NotFound, inner); - - case HttpConstants.StatusCodes.Unauthorized: - throw new UnauthorizedException(HttpConstants.ErrorMessages.Unauthorized, inner); - - case HttpConstants.StatusCodes.Forbidden: - throw new ForbiddenException(HttpConstants.ErrorMessages.Forbidden, inner); - - case HttpConstants.StatusCodes.Conflict: - throw new ConflictException(HttpConstants.ErrorMessages.Conflict, inner); - - case HttpConstants.StatusCodes.UnprocessableEntity: - throw CreateUnprocessableEntityException(responseStream, inner, serializer); - - case HttpConstants.StatusCodes.NotAcceptable: - throw new NotAcceptableException(HttpConstants.ErrorMessages.NotAcceptable, inner); - - case HttpConstants.StatusCodes.InternalServerError: - throw new InternalServerErrorException(HttpConstants.ErrorMessages.InternalServerError, inner); - - default: - throw new RedmineException($"HTTP {statusCode} – {(HttpStatusCode)statusCode}", inner); - } - } - /// /// Creates an exception for a 422 Unprocessable Entity response. /// + /// /// The response stream containing error details. /// The inner exception, if any. /// The serializer to use for deserializing error messages. - /// A RedmineException with details about the validation errors. - private static RedmineException CreateUnprocessableEntityException(Stream responseStream, Exception inner, IRedmineSerializer serializer) + /// A RedmineApiException with details about the validation errors. + internal static RedmineApiException CreateUnprocessableEntityException(string uri, Stream responseStream, Exception inner, IRedmineSerializer serializer) { var errors = GetRedmineErrors(responseStream, serializer); if (errors is null) { - return new RedmineException(HttpConstants.ErrorMessages.UnprocessableEntity, inner); + return new RedmineUnprocessableEntityException(HttpConstants.ErrorMessages.UnprocessableEntity ,uri, inner); } + var message = BuildUnprocessableContentMessage(errors); + + return new RedmineUnprocessableEntityException(message, url: uri, inner); + } + + internal static RedmineApiException CreateUnprocessableEntityException(string url, string content, IRedmineSerializer serializer) + { + var paged = serializer.DeserializeToPagedResults(content); + + var message = BuildUnprocessableContentMessage(paged.Items); + + return new RedmineApiException(message: message, url: url, httpStatusCode: HttpConstants.StatusCodes.UnprocessableEntity, innerException: null); + } + + internal static string BuildUnprocessableContentMessage(List errors) + { var sb = new StringBuilder(); foreach (var error in errors) { @@ -82,7 +60,7 @@ private static RedmineException CreateUnprocessableEntityException(Stream respon sb.Length -= 1; } - return new RedmineException($"Unprocessable Content: {sb}", inner); + return sb.ToString(); } /// @@ -100,40 +78,15 @@ private static List GetRedmineErrors(Stream responseStream, IRedmineSeria using (responseStream) { - try - { - using var reader = new StreamReader(responseStream); - var content = reader.ReadToEnd(); - return GetRedmineErrors(content, serializer); - } - catch(Exception ex) + using var reader = new StreamReader(responseStream); + var content = reader.ReadToEnd(); + if (content.IsNullOrWhiteSpace()) { - throw new RedmineApiException(ex.Message, ex); + return null; } - } - } - - /// - /// Gets the Redmine errors from response content. - /// - /// The response content as a string. - /// The serializer to use for deserializing error messages. - /// A list of error objects or null if unable to parse errors. - private static List GetRedmineErrors(string content, IRedmineSerializer serializer) - { - if (content.IsNullOrWhiteSpace()) - { - return null; - } - try - { var paged = serializer.DeserializeToPagedResults(content); - return (List)paged.Items; - } - catch(Exception ex) - { - throw new RedmineException(ex.Message, ex); + return paged.Items; } } } From 8ceac85d228f2ade5adc97786a464ee931e4babd Mon Sep 17 00:00:00 2001 From: Padi Date: Mon, 2 Jun 2025 13:18:11 +0300 Subject: [PATCH 541/549] Remove unused list extensions --- .../Extensions/ListExtensions.cs | 52 ------------------- 1 file changed, 52 deletions(-) diff --git a/src/redmine-net-api/Extensions/ListExtensions.cs b/src/redmine-net-api/Extensions/ListExtensions.cs index 48ef0705..1064b3f4 100755 --- a/src/redmine-net-api/Extensions/ListExtensions.cs +++ b/src/redmine-net-api/Extensions/ListExtensions.cs @@ -23,30 +23,6 @@ namespace Redmine.Net.Api.Extensions /// public static class ListExtensions { - /// - /// Creates a deep clone of the specified list. - /// - /// The type of elements in the list. Must implement . - /// The list to be cloned. - /// Specifies whether to reset the ID for each cloned item. - /// A new list containing cloned copies of the elements from the original list. Returns null if the original list is null. - public static IList Clone(this IList listToClone, bool resetId) where T : ICloneable - { - if (listToClone == null) - { - return null; - } - - var clonedList = new List(listToClone.Count); - - foreach (var item in listToClone) - { - clonedList.Add(item.Clone(resetId)); - } - - return clonedList; - } - /// /// Creates a deep clone of the specified list. /// @@ -70,34 +46,6 @@ public static List Clone(this List listToClone, bool resetId) where T : return clonedList; } - /// - /// Compares two lists for equality by checking if they contain the same elements in the same order. - /// - /// The type of elements in the lists. Must be a reference type. - /// The first list to be compared. - /// The second list to be compared. - /// True if both lists contain the same elements in the same order; otherwise, false. Returns false if either list is null. - public static bool Equals(this IList list, IList listToCompare) where T : class - { - if (list == null || listToCompare == null) - { - return false; - } - - if (list.Count != listToCompare.Count) - { - return false; - } - - var index = 0; - while (index < list.Count && list[index].Equals(listToCompare[index])) - { - index++; - } - - return index == list.Count; - } - /// /// Compares two lists for equality based on their elements. /// From 99a46b63fac1ae9d6f977812129e5b4541606f53 Mon Sep 17 00:00:00 2001 From: Padi Date: Mon, 2 Jun 2025 13:18:41 +0300 Subject: [PATCH 542/549] Exceptions --- .../Extensions/IdentifiableNameExtensions.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/redmine-net-api/Extensions/IdentifiableNameExtensions.cs b/src/redmine-net-api/Extensions/IdentifiableNameExtensions.cs index beed972a..b461a8e4 100644 --- a/src/redmine-net-api/Extensions/IdentifiableNameExtensions.cs +++ b/src/redmine-net-api/Extensions/IdentifiableNameExtensions.cs @@ -1,6 +1,8 @@ +using System; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Types; - +using Version = Redmine.Net.Api.Types.Version; + namespace Redmine.Net.Api.Extensions { /// @@ -57,7 +59,7 @@ public static IdentifiableName ToIdentifier(this int val) { if (val <= 0) { - throw new RedmineException(nameof(val), "Value must be greater than zero"); + throw new ArgumentException("Value must be greater than zero", nameof(val)); } return new IdentifiableName(val, null); @@ -73,7 +75,7 @@ public static IssueStatus ToIssueStatusIdentifier(this int val) { if (val <= 0) { - throw new RedmineException(nameof(val), "Value must be greater than zero"); + throw new ArgumentException("Value must be greater than zero", nameof(val)); } return new IssueStatus(val, null); From f9412d1911d0dd67fac717248592d56771cf61d8 Mon Sep 17 00:00:00 2001 From: Padi Date: Mon, 2 Jun 2025 13:19:46 +0300 Subject: [PATCH 543/549] Add accept, UserAgent & Headers --- .../Http/Messages/RedmineApiRequest.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/redmine-net-api/Http/Messages/RedmineApiRequest.cs b/src/redmine-net-api/Http/Messages/RedmineApiRequest.cs index ad42592d..bce2a22f 100644 --- a/src/redmine-net-api/Http/Messages/RedmineApiRequest.cs +++ b/src/redmine-net-api/Http/Messages/RedmineApiRequest.cs @@ -14,6 +14,7 @@ You may obtain a copy of the License at limitations under the License. */ +using System.Collections.Generic; using System.Collections.Specialized; using Redmine.Net.Api.Http.Clients.WebClient; using Redmine.Net.Api.Http.Constants; @@ -22,11 +23,46 @@ namespace Redmine.Net.Api.Http.Messages; internal sealed class RedmineApiRequest { + /// + /// + /// public RedmineApiRequestContent Content { get; set; } + + /// + /// + /// public string Method { get; set; } = HttpConstants.HttpVerbs.GET; + + /// + /// + /// public string RequestUri { get; set; } + + /// + /// + /// public NameValueCollection QueryString { get; set; } + /// + /// + /// public string ImpersonateUser { get; set; } + /// + /// + /// public string ContentType { get; set; } + + /// + /// + /// + public string Accept { get; set; } + /// + /// + /// + public string UserAgent { get; set; } + + /// + /// + /// + public Dictionary Headers { get; set; } } \ No newline at end of file From 66420ae8cbf87fd786c4a8eead5e77f8f70ada76 Mon Sep 17 00:00:00 2001 From: Padi Date: Mon, 2 Jun 2025 13:21:54 +0300 Subject: [PATCH 544/549] Apply the new headers --- .../InternalRedmineApiHttpClient.Async.cs | 2 +- .../InternalRedmineApiHttpClient.cs | 36 ++++++-- .../InternalRedmineApiWebClient.Async.cs | 26 ++++-- .../WebClient/InternalRedmineApiWebClient.cs | 19 ++++ .../Clients/WebClient/WebClientExtensions.cs | 88 +++++++++++++++++-- .../Http/Helpers/ClientHelper.cs | 16 ++++ .../Http/Helpers/RedmineHttpMethodHelper.cs | 2 +- src/redmine-net-api/Http/RedmineApiClient.cs | 45 +++++----- 8 files changed, 180 insertions(+), 54 deletions(-) create mode 100644 src/redmine-net-api/Http/Helpers/ClientHelper.cs diff --git a/src/redmine-net-api/Http/Clients/HttpClient/InternalRedmineApiHttpClient.Async.cs b/src/redmine-net-api/Http/Clients/HttpClient/InternalRedmineApiHttpClient.Async.cs index 25beece1..6f2f7e10 100644 --- a/src/redmine-net-api/Http/Clients/HttpClient/InternalRedmineApiHttpClient.Async.cs +++ b/src/redmine-net-api/Http/Clients/HttpClient/InternalRedmineApiHttpClient.Async.cs @@ -139,7 +139,7 @@ private static async Task DownloadWithProgressAsync(HttpContent httpCont var progressPercentage = (int)(totalBytesRead * 100 / contentLength); progress?.Report(progressPercentage); - ReportProgress(progress, contentLength, totalBytesRead); + ClientHelper.ReportProgress(progress, contentLength, totalBytesRead); } } } diff --git a/src/redmine-net-api/Http/Clients/HttpClient/InternalRedmineApiHttpClient.cs b/src/redmine-net-api/Http/Clients/HttpClient/InternalRedmineApiHttpClient.cs index a68897d6..c785145c 100644 --- a/src/redmine-net-api/Http/Clients/HttpClient/InternalRedmineApiHttpClient.cs +++ b/src/redmine-net-api/Http/Clients/HttpClient/InternalRedmineApiHttpClient.cs @@ -31,7 +31,6 @@ namespace Redmine.Net.Api.Http.Clients.HttpClient; internal sealed partial class InternalRedmineApiHttpClient : RedmineApiClient { private static readonly HttpMethod PatchMethod = new HttpMethod("PATCH"); - private static readonly HttpMethod DownloadMethod = new HttpMethod("DOWNLOAD"); private static readonly Encoding DefaultEncoding = Encoding.UTF8; private readonly System.Net.Http.HttpClient _httpClient; @@ -68,19 +67,14 @@ protected override RedmineApiResponse HandleRequest(string address, string verb, var httpMethod = GetHttpMethod(verb); using (var requestMessage = CreateRequestMessage(address, httpMethod, requestOptions, content as HttpContent)) { - // LogRequest(verb, address, requestOptions); - var response = Send(requestMessage, progress); - - // LogResponse(response.StatusCode); - return response; } } private RedmineApiResponse Send(HttpRequestMessage requestMessage, IProgress progress = null) { - return TaskExtensions.Synchronize(()=>SendAsync(requestMessage, progress)); + return TaskExtensions.Synchronize(() => SendAsync(requestMessage, progress)); } private HttpRequestMessage CreateRequestMessage(string address, HttpMethod method, @@ -136,11 +130,35 @@ private HttpRequestMessage CreateRequestMessage(string address, HttpMethod metho { httpRequest.Headers.Add(RedmineConstants.IMPERSONATE_HEADER_KEY, requestOptions.ImpersonateUser); } + + if (!requestOptions.Accept.IsNullOrWhiteSpace()) + { + httpRequest.Headers.Accept.ParseAdd(requestOptions.Accept); + } + + if (!requestOptions.UserAgent.IsNullOrWhiteSpace()) + { + httpRequest.Headers.UserAgent.ParseAdd(requestOptions.UserAgent); + } + + if (requestOptions.Headers != null) + { + foreach (var header in requestOptions.Headers) + { + httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + } } - if (content != null) + if (content == null) + { + return httpRequest; + } + + httpRequest.Content = content; + if (requestOptions?.ContentType != null) { - httpRequest.Content = content ; + content.Headers.ContentType = new MediaTypeHeaderValue(requestOptions.ContentType); } return httpRequest; diff --git a/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.Async.cs b/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.Async.cs index 525e6014..94277181 100644 --- a/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.Async.cs +++ b/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.Async.cs @@ -34,7 +34,13 @@ internal sealed partial class InternalRedmineApiWebClient protected override async Task HandleRequestAsync(string address, string verb, RequestOptions requestOptions = null, object content = null, IProgress progress = null, CancellationToken cancellationToken = default) { - return await SendAsync(CreateRequestMessage(address, verb, requestOptions, content as RedmineApiRequestContent), progress, cancellationToken: cancellationToken).ConfigureAwait(false); + LogRequest(verb, address, requestOptions); + + var response = await SendAsync(CreateRequestMessage(address, verb, Serializer, requestOptions, content as RedmineApiRequestContent), progress, cancellationToken: cancellationToken).ConfigureAwait(false); + + LogResponse((int)response.StatusCode); + + return response; } private async Task SendAsync(RedmineApiRequest requestMessage, IProgress progress = null, CancellationToken cancellationToken = default) @@ -44,7 +50,7 @@ private async Task SendAsync(RedmineApiRequest requestMessag HttpStatusCode? statusCode = null; NameValueCollection responseHeaders = null; CancellationTokenRegistration cancellationTokenRegistration = default; - + try { webClient = _webClientFunc(); @@ -53,20 +59,22 @@ private async Task SendAsync(RedmineApiRequest requestMessag static state => ((System.Net.WebClient)state).CancelAsync(), webClient ); - + cancellationToken.ThrowIfCancellationRequested(); if (progress != null) { - webClient.DownloadProgressChanged += (_, e) => - { - progress.Report(e.ProgressPercentage); - }; + webClient.DownloadProgressChanged += (_, e) => { progress.Report(e.ProgressPercentage); }; + } + + if (requestMessage.QueryString != null) + { + webClient.QueryString = requestMessage.QueryString; } - SetWebClientHeaders(webClient, requestMessage); + webClient.ApplyHeaders(requestMessage, Credentials); - if(IsGetOrDownload(requestMessage.Method)) + if (IsGetOrDownload(requestMessage.Method)) { response = await webClient.DownloadDataTaskAsync(requestMessage.RequestUri) .ConfigureAwait(false); diff --git a/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.cs b/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.cs index 59fbae62..b4eb7a7b 100644 --- a/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.cs +++ b/src/redmine-net-api/Http/Clients/WebClient/InternalRedmineApiWebClient.cs @@ -78,11 +78,30 @@ private static RedmineApiRequest CreateRequestMessage(string address, string ver { req.QueryString = requestOptions.QueryString; req.ImpersonateUser = requestOptions.ImpersonateUser; + if (!requestOptions.Accept.IsNullOrWhiteSpace()) + { + req.Accept = requestOptions.Accept; + } + + if (requestOptions.Headers != null) + { + req.Headers = requestOptions.Headers; + } + + if (!requestOptions.UserAgent.IsNullOrWhiteSpace()) + { + req.UserAgent = requestOptions.UserAgent; + } } if (content != null) { req.Content = content; + req.ContentType = content.ContentType; + } + else + { + req.ContentType = serializer.ContentType; } return req; diff --git a/src/redmine-net-api/Http/Clients/WebClient/WebClientExtensions.cs b/src/redmine-net-api/Http/Clients/WebClient/WebClientExtensions.cs index f959c4e9..3a831655 100644 --- a/src/redmine-net-api/Http/Clients/WebClient/WebClientExtensions.cs +++ b/src/redmine-net-api/Http/Clients/WebClient/WebClientExtensions.cs @@ -1,33 +1,103 @@ +using System; +using System.Globalization; +using System.Net; +using Redmine.Net.Api.Authentication; using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Serialization; +using Redmine.Net.Api.Http.Helpers; +using Redmine.Net.Api.Http.Messages; namespace Redmine.Net.Api.Http.Clients.WebClient; internal static class WebClientExtensions { - public static void ApplyHeaders(this System.Net.WebClient client, RequestOptions options, IRedmineSerializer serializer) + public static void ApplyHeaders(this System.Net.WebClient client, RedmineApiRequest request, IRedmineAuthentication authentication) { - client.Headers.Add(RedmineConstants.CONTENT_TYPE_HEADER_KEY, options.ContentType ?? serializer.ContentType); + switch (authentication) + { + case RedmineApiKeyAuthentication: + client.Headers.Add(RedmineConstants.API_KEY_AUTHORIZATION_HEADER_KEY, authentication.Token); + break; + case RedmineBasicAuthentication: + client.Headers.Add(RedmineConstants.AUTHORIZATION_HEADER_KEY, authentication.Token); + break; + } + + client.Headers.Add(RedmineConstants.CONTENT_TYPE_HEADER_KEY, request.ContentType); - if (!options.UserAgent.IsNullOrWhiteSpace()) + if (!request.UserAgent.IsNullOrWhiteSpace()) { - client.Headers.Add(RedmineConstants.USER_AGENT_HEADER_KEY, options.UserAgent); + client.Headers.Add(RedmineConstants.USER_AGENT_HEADER_KEY, request.UserAgent); } - if (!options.ImpersonateUser.IsNullOrWhiteSpace()) + if (!request.ImpersonateUser.IsNullOrWhiteSpace()) { - client.Headers.Add(RedmineConstants.IMPERSONATE_HEADER_KEY, options.ImpersonateUser); + client.Headers.Add(RedmineConstants.IMPERSONATE_HEADER_KEY, request.ImpersonateUser); } - if (options.Headers is not { Count: > 0 }) + if (request.Headers is not { Count: > 0 }) { return; } - foreach (var header in options.Headers) + foreach (var header in request.Headers) { client.Headers.Add(header.Key, header.Value); } + + if (!request.Accept.IsNullOrWhiteSpace()) + { + client.Headers.Add(HttpRequestHeader.Accept, request.Accept); + } + } + + internal static byte[] DownloadWithProgress(this System.Net.WebClient webClient, string url, IProgress progress) + { + var contentLength = GetContentLength(webClient); + byte[] data; + if (contentLength > 0) + { + using (var respStream = webClient.OpenRead(url)) + { + data = new byte[contentLength]; + var buffer = new byte[4096]; + int bytesRead; + var totalBytesRead = 0; + + while ((bytesRead = respStream.Read(buffer, 0, buffer.Length)) > 0) + { + Buffer.BlockCopy(buffer, 0, data, totalBytesRead, bytesRead); + totalBytesRead += bytesRead; + + ClientHelper.ReportProgress(progress, contentLength, totalBytesRead); + } + } + } + else + { + data = webClient.DownloadData(url); + progress?.Report(100); + } + + return data; + } + + internal static long GetContentLength(this System.Net.WebClient webClient) + { + var total = -1L; + if (webClient.ResponseHeaders == null) + { + return total; + } + + var contentLengthAsString = webClient.ResponseHeaders[HttpRequestHeader.ContentLength]; + if (!string.IsNullOrEmpty(contentLengthAsString)) + { + total = Convert.ToInt64(contentLengthAsString, CultureInfo.InvariantCulture); + } + + return total; + } + internal static HttpStatusCode? GetStatusCode(this System.Net.WebClient webClient) { if (webClient is InternalWebClient iwc) diff --git a/src/redmine-net-api/Http/Helpers/ClientHelper.cs b/src/redmine-net-api/Http/Helpers/ClientHelper.cs new file mode 100644 index 00000000..6ffbef3b --- /dev/null +++ b/src/redmine-net-api/Http/Helpers/ClientHelper.cs @@ -0,0 +1,16 @@ +using System; + +namespace Redmine.Net.Api.Http.Helpers; + +internal static class ClientHelper +{ + internal static void ReportProgress(IProgressprogress, long total, long bytesRead) + { + if (progress == null || total <= 0) + { + return; + } + var percent = (int)(bytesRead * 100L / total); + progress.Report(percent); + } +} \ No newline at end of file diff --git a/src/redmine-net-api/Http/Helpers/RedmineHttpMethodHelper.cs b/src/redmine-net-api/Http/Helpers/RedmineHttpMethodHelper.cs index c3fc7e10..6adee604 100644 --- a/src/redmine-net-api/Http/Helpers/RedmineHttpMethodHelper.cs +++ b/src/redmine-net-api/Http/Helpers/RedmineHttpMethodHelper.cs @@ -47,7 +47,7 @@ public static bool IsGetOrDownload(string method) /// /// The HTTP response status code. /// True if the status code represents a transient error; otherwise, false. - private static bool IsTransientError(int statusCode) + internal static bool IsTransientError(int statusCode) { return statusCode switch { diff --git a/src/redmine-net-api/Http/RedmineApiClient.cs b/src/redmine-net-api/Http/RedmineApiClient.cs index 0f2889e4..93b56aef 100644 --- a/src/redmine-net-api/Http/RedmineApiClient.cs +++ b/src/redmine-net-api/Http/RedmineApiClient.cs @@ -1,7 +1,11 @@ using System; +using System.Net; using Redmine.Net.Api.Authentication; +using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Http.Constants; +using Redmine.Net.Api.Http.Extensions; using Redmine.Net.Api.Http.Messages; +using Redmine.Net.Api.Logging; using Redmine.Net.Api.Options; using Redmine.Net.Api.Serialization; @@ -77,35 +81,26 @@ protected static bool IsGetOrDownload(string method) return method is HttpConstants.HttpVerbs.GET or HttpConstants.HttpVerbs.DOWNLOAD; } - protected static void ReportProgress(IProgressprogress, long total, long bytesRead) + protected void LogRequest(string verb, string address, RequestOptions requestOptions) { - if (progress == null || total <= 0) + if (Options.LoggingOptions?.IncludeHttpDetails != true) { return; } - var percent = (int)(bytesRead * 100L / total); - progress.Report(percent); + + Options.Logger.Info($"Request HTTP {verb} {address}"); + + if (requestOptions?.QueryString != null) + { + Options.Logger.Info($"Query parameters: {requestOptions.QueryString.ToQueryString()}"); + } } - // protected void LogRequest(string verb, string address, RequestOptions requestOptions) - // { - // if (_options.LoggingOptions?.IncludeHttpDetails == true) - // { - // _options.Logger.Debug($"Request HTTP {verb} {address}"); - // - // if (requestOptions?.QueryString != null) - // { - // _options.Logger.Debug($"Query parameters: {requestOptions.QueryString.ToQueryString()}"); - // } - // } - // } - // - // protected void LogResponse(HttpStatusCode statusCode) - // { - // if (_options.LoggingOptions?.IncludeHttpDetails == true) - // { - // _options.Logger.Debug($"Response status: {statusCode}"); - // } - // } - + protected void LogResponse(int statusCode) + { + if (Options.LoggingOptions?.IncludeHttpDetails == true) + { + Options.Logger.Info($"Response status: {statusCode.ToInvariantString()}"); + } + } } \ No newline at end of file From 22ee383d2a0880e9e1f1fb8edeebbc4ce7582b0b Mon Sep 17 00:00:00 2001 From: Padi Date: Mon, 2 Jun 2025 13:24:59 +0300 Subject: [PATCH 545/549] Move config to appsettings --- .../RedmineTestContainerCollection.cs | 0 .../Fixtures/RedmineTestContainerFixture.cs | 148 +++++++++--------- .../Infrastructure/ClientType.cs | 7 + .../Infrastructure/ConfigurationHelper.cs | 1 + .../Options/AuthenticationMode.cs | 8 + .../Options/AuthenticationOptions.cs | 8 + .../Options/BasicAuthenticationOptions.cs | 7 + .../Infrastructure/Options/PostgresOptions.cs | 10 ++ .../Infrastructure/Options/RedmineOptions.cs | 15 ++ .../Options/TestContainerOptions.cs | 10 ++ .../Infrastructure/RedmineConfiguration.cs | 3 + .../Infrastructure/RedmineOptions.cs | 35 ----- .../Infrastructure/SerializationType.cs | 7 + .../Infrastructure/TestContainerMode.cs | 13 ++ .../Tests/Common/TestConstants.cs | 2 +- .../appsettings.json | 28 +++- .../appsettings.local.json | 10 +- 17 files changed, 194 insertions(+), 118 deletions(-) rename tests/redmine-net-api.Integration.Tests/{Fixtures => Collections}/RedmineTestContainerCollection.cs (100%) create mode 100644 tests/redmine-net-api.Integration.Tests/Infrastructure/ClientType.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Infrastructure/Options/AuthenticationMode.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Infrastructure/Options/AuthenticationOptions.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Infrastructure/Options/BasicAuthenticationOptions.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Infrastructure/Options/PostgresOptions.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Infrastructure/Options/RedmineOptions.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Infrastructure/Options/TestContainerOptions.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Infrastructure/RedmineConfiguration.cs delete mode 100644 tests/redmine-net-api.Integration.Tests/Infrastructure/RedmineOptions.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Infrastructure/SerializationType.cs create mode 100644 tests/redmine-net-api.Integration.Tests/Infrastructure/TestContainerMode.cs diff --git a/tests/redmine-net-api.Integration.Tests/Fixtures/RedmineTestContainerCollection.cs b/tests/redmine-net-api.Integration.Tests/Collections/RedmineTestContainerCollection.cs similarity index 100% rename from tests/redmine-net-api.Integration.Tests/Fixtures/RedmineTestContainerCollection.cs rename to tests/redmine-net-api.Integration.Tests/Collections/RedmineTestContainerCollection.cs diff --git a/tests/redmine-net-api.Integration.Tests/Fixtures/RedmineTestContainerFixture.cs b/tests/redmine-net-api.Integration.Tests/Fixtures/RedmineTestContainerFixture.cs index a840849b..06fc5750 100644 --- a/tests/redmine-net-api.Integration.Tests/Fixtures/RedmineTestContainerFixture.cs +++ b/tests/redmine-net-api.Integration.Tests/Fixtures/RedmineTestContainerFixture.cs @@ -4,6 +4,7 @@ using DotNet.Testcontainers.Networks; using Npgsql; using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure.Options; using Redmine.Net.Api; using Redmine.Net.Api.Options; using Testcontainers.PostgreSql; @@ -13,19 +14,11 @@ namespace Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; public class RedmineTestContainerFixture : IAsyncLifetime { - private const int RedminePort = 3000; - private const int PostgresPort = 5432; - private const string PostgresImage = "postgres:17.4-alpine"; - private const string RedmineImage = "redmine:6.0.5-alpine"; - private const string PostgresDb = "postgres"; - private const string PostgresUser = "postgres"; - private const string PostgresPassword = "postgres"; - private const string RedmineSqlFilePath = "TestData/init-redmine.sql"; - - private readonly string RedmineNetworkAlias = Guid.NewGuid().ToString(); + private readonly RedmineConfiguration _configuration; + private readonly string _redmineNetworkAlias = Guid.NewGuid().ToString(); private readonly ITestOutputHelper _output; - private readonly TestContainerOptions _redmineOptions; + private readonly TestContainerOptions _testContainerOptions; private INetwork Network { get; set; } private PostgreSqlContainer PostgresContainer { get; set; } @@ -35,9 +28,10 @@ public class RedmineTestContainerFixture : IAsyncLifetime public RedmineTestContainerFixture() { - _redmineOptions = ConfigurationHelper.GetConfiguration(); + //_configuration = configuration; + _testContainerOptions = ConfigurationHelper.GetConfiguration(); - if (_redmineOptions.Mode != TestContainerMode.UseExisting) + if (_testContainerOptions.Mode != TestContainerMode.UseExisting) { BuildContainers(); } @@ -60,52 +54,50 @@ private void BuildContainers() .Build(); var postgresBuilder = new PostgreSqlBuilder() - .WithImage(PostgresImage) + .WithImage(_testContainerOptions.Postgres.Image) .WithNetwork(Network) - .WithNetworkAliases(RedmineNetworkAlias) - .WithPortBinding(PostgresPort, assignRandomHostPort: true) + .WithNetworkAliases(_redmineNetworkAlias) .WithEnvironment(new Dictionary { - { "POSTGRES_DB", PostgresDb }, - { "POSTGRES_USER", PostgresUser }, - { "POSTGRES_PASSWORD", PostgresPassword }, + { "POSTGRES_DB", _testContainerOptions.Postgres.Database }, + { "POSTGRES_USER", _testContainerOptions.Postgres.User }, + { "POSTGRES_PASSWORD", _testContainerOptions.Postgres.Password }, }) - .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(PostgresPort)); + .WithWaitStrategy(Wait.ForUnixContainer().UntilPortIsAvailable(_testContainerOptions.Postgres.Port)); - if (_redmineOptions.Mode == TestContainerMode.CreateNewWithRandomPorts) + if (_testContainerOptions.Mode == TestContainerMode.CreateNewWithRandomPorts) { - postgresBuilder.WithPortBinding(PostgresPort, assignRandomHostPort: true); + postgresBuilder.WithPortBinding(_testContainerOptions.Postgres.Port, assignRandomHostPort: true); } else { - postgresBuilder.WithPortBinding(PostgresPort, PostgresPort); + postgresBuilder.WithPortBinding(_testContainerOptions.Postgres.Port, _testContainerOptions.Postgres.Port); } PostgresContainer = postgresBuilder.Build(); var redmineBuilder = new ContainerBuilder() - .WithImage(RedmineImage) + .WithImage(_testContainerOptions.Redmine.Image) .WithNetwork(Network) - .WithPortBinding(RedminePort, assignRandomHostPort: true) .WithEnvironment(new Dictionary { - { "REDMINE_DB_POSTGRES", RedmineNetworkAlias }, - { "REDMINE_DB_PORT", PostgresPort.ToString() }, - { "REDMINE_DB_DATABASE", PostgresDb }, - { "REDMINE_DB_USERNAME", PostgresUser }, - { "REDMINE_DB_PASSWORD", PostgresPassword }, + { "REDMINE_DB_POSTGRES", _redmineNetworkAlias }, + { "REDMINE_DB_PORT", _testContainerOptions.Redmine.Port.ToString() }, + { "REDMINE_DB_DATABASE", _testContainerOptions.Postgres.Database }, + { "REDMINE_DB_USERNAME", _testContainerOptions.Postgres.User }, + { "REDMINE_DB_PASSWORD", _testContainerOptions.Postgres.Password }, }) .DependsOn(PostgresContainer) .WithWaitStrategy(Wait.ForUnixContainer() - .UntilHttpRequestIsSucceeded(request => request.ForPort(RedminePort).ForPath("/"))); + .UntilHttpRequestIsSucceeded(request => request.ForPort((ushort)_testContainerOptions.Redmine.Port).ForPath("/"))); - if (_redmineOptions.Mode == TestContainerMode.CreateNewWithRandomPorts) + if (_testContainerOptions.Mode == TestContainerMode.CreateNewWithRandomPorts) { - redmineBuilder.WithPortBinding(RedminePort, assignRandomHostPort: true); + redmineBuilder.WithPortBinding(_testContainerOptions.Redmine.Port, assignRandomHostPort: true); } else { - redmineBuilder.WithPortBinding(RedminePort, RedminePort); + redmineBuilder.WithPortBinding(_testContainerOptions.Redmine.Port, _testContainerOptions.Redmine.Port); } RedmineContainer = redmineBuilder.Build(); @@ -115,22 +107,22 @@ public async Task InitializeAsync() { var rmgBuilder = new RedmineManagerOptionsBuilder(); - switch (_redmineOptions.AuthenticationMode) + switch (_testContainerOptions.Redmine.AuthenticationMode) { case AuthenticationMode.ApiKey: - var apiKey = _redmineOptions.Authentication.ApiKey; + var apiKey = _testContainerOptions.Redmine.Authentication.ApiKey; rmgBuilder.WithApiKeyAuthentication(apiKey); break; case AuthenticationMode.Basic: - var username = _redmineOptions.Authentication.Basic.Username; - var password = _redmineOptions.Authentication.Basic.Password; + var username = _testContainerOptions.Redmine.Authentication.Basic.Username; + var password = _testContainerOptions.Redmine.Authentication.Basic.Password; rmgBuilder.WithBasicAuthentication(username, password); break; } - if (_redmineOptions.Mode == TestContainerMode.UseExisting) + if (_testContainerOptions.Mode == TestContainerMode.UseExisting) { - RedmineHost = _redmineOptions.Url; + RedmineHost = _testContainerOptions.Redmine.Url; } else { @@ -142,32 +134,60 @@ public async Task InitializeAsync() await SeedTestDataAsync(PostgresContainer, CancellationToken.None); - RedmineHost = $"http://{RedmineContainer.Hostname}:{RedmineContainer.GetMappedPublicPort(RedminePort)}"; + RedmineHost = $"http://{RedmineContainer.Hostname}:{RedmineContainer.GetMappedPublicPort(_testContainerOptions.Redmine.Port)}"; + } + + rmgBuilder.WithHost(RedmineHost); + + if (_configuration != null) + { + switch (_configuration.Client) + { + case ClientType.Http: + rmgBuilder.UseHttpClient(); + break; + case ClientType.Web: + rmgBuilder.UseWebClient(); + break; + } + + switch (_configuration.Serialization) + { + case SerializationType.Xml: + rmgBuilder.WithXmlSerialization(); + break; + case SerializationType.Json: + rmgBuilder.WithJsonSerialization(); + break; + } + } + else + { + rmgBuilder + .UseHttpClient() + // .UseWebClient() + .WithXmlSerialization(); } - rmgBuilder - .WithHost(RedmineHost) - .UseHttpClient() - //.UseWebClient() - .WithXmlSerialization(); - RedmineManager = new RedmineManager(rmgBuilder); } public async Task DisposeAsync() { var exceptions = new List(); - - if (_redmineOptions.Mode != TestContainerMode.UseExisting) + + if (_testContainerOptions.Mode == TestContainerMode.UseExisting) { - await SafeDisposeAsync(() => RedmineContainer.StopAsync()); - await SafeDisposeAsync(() => PostgresContainer.StopAsync()); - await SafeDisposeAsync(() => Network.DisposeAsync().AsTask()); + return; + } + + await SafeDisposeAsync(() => RedmineContainer.StopAsync()); + await SafeDisposeAsync(() => PostgresContainer.StopAsync()); + await SafeDisposeAsync(() => Network.DisposeAsync().AsTask()); - if (exceptions.Count > 0) - { - throw new AggregateException(exceptions); - } + if (exceptions.Count > 0) + { + throw new AggregateException(exceptions); } return; @@ -207,23 +227,11 @@ private async Task SeedTestDataAsync(PostgreSqlContainer container, Cancellation await Task.Delay(dbRetryDelay, ct); } } - var sql = await System.IO.File.ReadAllTextAsync(RedmineSqlFilePath, ct); + var sql = await System.IO.File.ReadAllTextAsync(_testContainerOptions.Redmine.SqlFilePath, ct); var res = await container.ExecScriptAsync(sql, ct); if (!string.IsNullOrWhiteSpace(res.Stderr)) { _output.WriteLine(res.Stderr); } } -} - -/// -/// Enum defining how containers should be managed -/// -public enum TestContainerMode -{ - /// Use existing running containers at specified URL - UseExisting, - - /// Create new containers with random ports (CI-friendly) - CreateNewWithRandomPorts -} +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Infrastructure/ClientType.cs b/tests/redmine-net-api.Integration.Tests/Infrastructure/ClientType.cs new file mode 100644 index 00000000..62dff405 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Infrastructure/ClientType.cs @@ -0,0 +1,7 @@ +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; + +public enum ClientType +{ + Http, + Web +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Infrastructure/ConfigurationHelper.cs b/tests/redmine-net-api.Integration.Tests/Infrastructure/ConfigurationHelper.cs index 19a92aa9..8d1214f3 100644 --- a/tests/redmine-net-api.Integration.Tests/Infrastructure/ConfigurationHelper.cs +++ b/tests/redmine-net-api.Integration.Tests/Infrastructure/ConfigurationHelper.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Configuration; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure.Options; namespace Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure { diff --git a/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/AuthenticationMode.cs b/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/AuthenticationMode.cs new file mode 100644 index 00000000..45b5d786 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/AuthenticationMode.cs @@ -0,0 +1,8 @@ +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure.Options; + +public enum AuthenticationMode +{ + None, + ApiKey, + Basic +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/AuthenticationOptions.cs b/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/AuthenticationOptions.cs new file mode 100644 index 00000000..bed34dff --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/AuthenticationOptions.cs @@ -0,0 +1,8 @@ +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure.Options; + +public sealed class AuthenticationOptions +{ + public string ApiKey { get; set; } + + public BasicAuthenticationOptions Basic { get; set; } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/BasicAuthenticationOptions.cs b/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/BasicAuthenticationOptions.cs new file mode 100644 index 00000000..9aa9f28a --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/BasicAuthenticationOptions.cs @@ -0,0 +1,7 @@ +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure.Options; + +public sealed class BasicAuthenticationOptions +{ + public string Username { get; set; } + public string Password { get; set; } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/PostgresOptions.cs b/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/PostgresOptions.cs new file mode 100644 index 00000000..77093b4c --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/PostgresOptions.cs @@ -0,0 +1,10 @@ +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure.Options; + +public sealed class PostgresOptions +{ + public int Port { get; set; } + public string Image { get; set; } = string.Empty; + public string Database { get; set; } = string.Empty; + public string User { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/RedmineOptions.cs b/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/RedmineOptions.cs new file mode 100644 index 00000000..8e7cb6b2 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/RedmineOptions.cs @@ -0,0 +1,15 @@ +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure.Options +{ + public sealed class RedmineOptions + { + public string Url { get; set; } + + public AuthenticationMode AuthenticationMode { get; set; } + + public AuthenticationOptions Authentication { get; set; } + + public int Port { get; set; } + public string Image { get; set; } = string.Empty; + public string SqlFilePath { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/TestContainerOptions.cs b/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/TestContainerOptions.cs new file mode 100644 index 00000000..c26821c6 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Infrastructure/Options/TestContainerOptions.cs @@ -0,0 +1,10 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure.Options; + +public sealed class TestContainerOptions +{ + public RedmineOptions Redmine { get; set; } + public PostgresOptions Postgres { get; set; } + public TestContainerMode Mode { get; set; } +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Infrastructure/RedmineConfiguration.cs b/tests/redmine-net-api.Integration.Tests/Infrastructure/RedmineConfiguration.cs new file mode 100644 index 00000000..4a82568a --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Infrastructure/RedmineConfiguration.cs @@ -0,0 +1,3 @@ +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; + +public record RedmineConfiguration(SerializationType Serialization, ClientType Client); \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Infrastructure/RedmineOptions.cs b/tests/redmine-net-api.Integration.Tests/Infrastructure/RedmineOptions.cs deleted file mode 100644 index 2c5f5d09..00000000 --- a/tests/redmine-net-api.Integration.Tests/Infrastructure/RedmineOptions.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; - -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure -{ - public sealed class TestContainerOptions - { - public string Url { get; set; } - - public AuthenticationMode AuthenticationMode { get; set; } - - public Authentication Authentication { get; set; } - - public TestContainerMode Mode { get; set; } - } - - public sealed class Authentication - { - public string ApiKey { get; set; } - - public BasicAuthentication Basic { get; set; } - } - - public sealed class BasicAuthentication - { - public string Username { get; set; } - public string Password { get; set; } - } - - public enum AuthenticationMode - { - None, - ApiKey, - Basic - } -} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Infrastructure/SerializationType.cs b/tests/redmine-net-api.Integration.Tests/Infrastructure/SerializationType.cs new file mode 100644 index 00000000..8ba1ed61 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Infrastructure/SerializationType.cs @@ -0,0 +1,7 @@ +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; + +public enum SerializationType +{ + Xml, + Json +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Infrastructure/TestContainerMode.cs b/tests/redmine-net-api.Integration.Tests/Infrastructure/TestContainerMode.cs new file mode 100644 index 00000000..03a2443a --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Infrastructure/TestContainerMode.cs @@ -0,0 +1,13 @@ +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; + +/// +/// Enum defining how containers should be managed +/// +public enum TestContainerMode +{ + /// Use existing running containers at specified URL + UseExisting, + + /// Create new containers with random ports (CI-friendly) + CreateNewWithRandomPorts +} \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Common/TestConstants.cs b/tests/redmine-net-api.Integration.Tests/Tests/Common/TestConstants.cs index bf16727b..12c4f636 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Common/TestConstants.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Common/TestConstants.cs @@ -1,7 +1,7 @@ using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Types; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; public static class TestConstants { diff --git a/tests/redmine-net-api.Integration.Tests/appsettings.json b/tests/redmine-net-api.Integration.Tests/appsettings.json index 0c75cfe4..585ef671 100644 --- a/tests/redmine-net-api.Integration.Tests/appsettings.json +++ b/tests/redmine-net-api.Integration.Tests/appsettings.json @@ -1,14 +1,26 @@ { "TestContainer": { "Mode": "CreateNewWithRandomPorts", - "Url": "$Url", - "AuthenticationMode": "ApiKey", - "Authentication": { - "Basic":{ - "Username": "$Username", - "Password": "$Password" - }, - "ApiKey": "$ApiKey" + "Redmine":{ + "Url": "$Url", + "Port": 3000, + "Image": "redmine:6.0.5-alpine", + "SqlFilePath": "TestData/init-redmine.sql", + "AuthenticationMode": "ApiKey", + "Authentication": { + "Basic":{ + "Username": "$Username", + "Password": "$Password" + }, + "ApiKey": "$ApiKey" + } + }, + "Postgres": { + "Port": 5432, + "Image": "postgres:17.4-alpine", + "Database": "postgres", + "User": "postgres", + "Password": "postgres" } } } diff --git a/tests/redmine-net-api.Integration.Tests/appsettings.local.json b/tests/redmine-net-api.Integration.Tests/appsettings.local.json index 9c508d1e..65fce933 100644 --- a/tests/redmine-net-api.Integration.Tests/appsettings.local.json +++ b/tests/redmine-net-api.Integration.Tests/appsettings.local.json @@ -1,10 +1,12 @@ { "TestContainer": { "Mode": "UseExisting", - "Url": "http://localhost:8089/", - "AuthenticationMode": "ApiKey", - "Authentication": { - "ApiKey": "61d6fa45ca2c570372b08b8c54b921e5fc39335a" + "Redmine":{ + "Url": "http://localhost:8089/", + "AuthenticationMode": "ApiKey", + "Authentication": { + "ApiKey": "61d6fa45ca2c570372b08b8c54b921e5fc39335a" + } } } } From db659bd6d07e84af606af1d69f4c249e21e931ad Mon Sep 17 00:00:00 2001 From: Padi Date: Mon, 2 Jun 2025 13:25:59 +0300 Subject: [PATCH 546/549] [IntegrationTests] Add & improve --- .../Tests/Common/EmailNotificationType.cs | 2 +- .../Tests/Common/IssueTestHelper.cs | 2 +- .../Tests/Common/TestEntityFactory.cs | 3 +- .../Entities/Attachment/AttachmentTests.cs | 1 + .../Attachment/AttachmentTestsAsync.cs | 1 + .../Tests/Entities/File/FileTests.cs | 3 +- .../Tests/Entities/File/FileTestsAsync.cs | 16 +++- .../Tests/Entities/Group/GroupTests.cs | 9 +-- .../Tests/Entities/Group/GroupTestsAsync.cs | 3 +- .../Issue/IssueAttachmentUploadTests.cs | 1 + .../Issue/IssueAttachmentUploadTestsAsync.cs | 2 +- .../Tests/Entities/Issue/IssueTests.cs | 5 +- .../Tests/Entities/Issue/IssueTestsAsync.cs | 2 +- .../Tests/Entities/Issue/IssueWatcherTests.cs | 2 +- .../Entities/Issue/IssueWatcherTestsAsync.cs | 2 +- .../IssueCategory/IssueCategoryTests.cs | 31 +++++--- .../IssueCategory/IssueCategoryTestsAsync.cs | 59 ++++++++------- .../IssueRelation/IssueRelationTests.cs | 7 +- .../IssueRelation/IssueRelationTestsAsync.cs | 8 +- .../Entities/IssueStatus/IssueStatusTests.cs | 5 +- .../IssueStatus/IssueStatusTestsAsync.cs | 5 +- .../Tests/Entities/Journal/JournalTests.cs | 4 +- .../Entities/Journal/JournalTestsAsync.cs | 5 +- .../Tests/Entities/News/NewsTests.cs | 4 +- .../Tests/Entities/News/NewsTestsAsync.cs | 4 +- .../Entities/Project/ProjectTestsAsync.cs | 2 +- .../ProjectMembershipTests.cs | 4 +- .../ProjectMembershipTestsAsync.cs | 4 +- .../Entities/TimeEntry/TimeEntryTestsAsync.cs | 6 +- .../Tests/Entities/User/UserTestsAsync.cs | 3 +- .../Entities/Version/VersionTestsAsync.cs | 3 +- .../Tests/Entities/Wiki/WikiTestsAsync.cs | 3 +- .../Tests/RedmineApiWebClientTests.cs | 75 +++++++++++++++++++ 33 files changed, 195 insertions(+), 91 deletions(-) create mode 100644 tests/redmine-net-api.Integration.Tests/Tests/RedmineApiWebClientTests.cs diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Common/EmailNotificationType.cs b/tests/redmine-net-api.Integration.Tests/Tests/Common/EmailNotificationType.cs index 2c046dad..e77fef76 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Common/EmailNotificationType.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Common/EmailNotificationType.cs @@ -1,4 +1,4 @@ -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; public sealed record EmailNotificationType { diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Common/IssueTestHelper.cs b/tests/redmine-net-api.Integration.Tests/Tests/Common/IssueTestHelper.cs index 1be46ec3..fe1fa744 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Common/IssueTestHelper.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Common/IssueTestHelper.cs @@ -1,7 +1,7 @@ using Redmine.Net.Api; using Redmine.Net.Api.Types; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; internal static class IssueTestHelper { diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Common/TestEntityFactory.cs b/tests/redmine-net-api.Integration.Tests/Tests/Common/TestEntityFactory.cs index bb721f70..0ff28752 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Common/TestEntityFactory.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Common/TestEntityFactory.cs @@ -1,7 +1,8 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Types; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; public static class TestEntityFactory { diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Attachment/AttachmentTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Attachment/AttachmentTests.cs index d0b7e133..567e586e 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Attachment/AttachmentTests.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Attachment/AttachmentTests.cs @@ -1,6 +1,7 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; using Redmine.Net.Api; using Redmine.Net.Api.Http; diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Attachment/AttachmentTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Attachment/AttachmentTestsAsync.cs index e5bc284b..d9335df4 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Attachment/AttachmentTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Attachment/AttachmentTestsAsync.cs @@ -1,6 +1,7 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; using Redmine.Net.Api; using Redmine.Net.Api.Http; diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/File/FileTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/File/FileTests.cs index 010677f9..00bba896 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Entities/File/FileTests.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/File/FileTests.cs @@ -1,6 +1,7 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; using Redmine.Net.Api.Extensions; namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.File; @@ -71,7 +72,6 @@ public void CreateFile_With_Version_Should_Succeed() Filename = fileName, Description = RandomHelper.GenerateText(9), ContentType = "text/plain", - Version = 1.ToIdentifier(), }; _ = fixture.RedmineManager.Create(filePayload, TestConstants.Projects.DefaultProjectIdentifier); @@ -85,7 +85,6 @@ public void CreateFile_With_Version_Should_Succeed() Assert.Equal(filePayload.Description, file.Description); Assert.Equal(filePayload.ContentType, file.ContentType); Assert.EndsWith($"/attachments/download/{file.Id}/{fileName}", file.ContentUrl); - Assert.Equal(filePayload.Version.Id, file.Version.Id); } private (string fileName, string token) UploadFile() diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/File/FileTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/File/FileTestsAsync.cs index 5363e416..2d38d3d9 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Entities/File/FileTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/File/FileTestsAsync.cs @@ -1,6 +1,7 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; using Redmine.Net.Api; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; @@ -38,13 +39,14 @@ public async Task CreateFile_Should_Succeed() [Fact] public async Task CreateFile_Without_Token_Should_Fail() { - await Assert.ThrowsAsync(() => fixture.RedmineManager.CreateAsync( + await Assert.ThrowsAsync(() => fixture.RedmineManager.CreateAsync( new Redmine.Net.Api.Types.File { Filename = "VBpMc.txt" }, PROJECT_ID)); } [Fact] public async Task CreateFile_With_OptionalParameters_Should_Succeed() { + // Arrange var (fileName, token) = await UploadFileAsync(); var filePayload = new Redmine.Net.Api.Types.File @@ -55,6 +57,7 @@ public async Task CreateFile_With_OptionalParameters_Should_Succeed() ContentType = "text/plain", }; + // Act _ = await fixture.RedmineManager.CreateAsync(filePayload, PROJECT_ID); var files = await fixture.RedmineManager.GetProjectFilesAsync(PROJECT_ID); var file = files.Items.FirstOrDefault(x => x.Filename == fileName); @@ -71,6 +74,11 @@ public async Task CreateFile_With_OptionalParameters_Should_Succeed() [Fact] public async Task CreateFile_With_Version_Should_Succeed() { + // Arrange + var versionPayload = TestEntityFactory.CreateRandomVersionPayload(); + var version = await fixture.RedmineManager.CreateAsync(versionPayload, TestConstants.Projects.DefaultProjectIdentifier); + Assert.NotNull(version); + var (fileName, token) = await UploadFileAsync(); var filePayload = new Redmine.Net.Api.Types.File @@ -79,13 +87,15 @@ public async Task CreateFile_With_Version_Should_Succeed() Filename = fileName, Description = RandomHelper.GenerateText(9), ContentType = "text/plain", - Version = 1.ToIdentifier(), + Version = version }; + // Act _ = await fixture.RedmineManager.CreateAsync(filePayload, PROJECT_ID); var files = await fixture.RedmineManager.GetProjectFilesAsync(PROJECT_ID); var file = files.Items.FirstOrDefault(x => x.Filename == fileName); - + + // Assert Assert.NotNull(file); Assert.True(file.Id > 0); Assert.NotEmpty(file.Digest); diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Group/GroupTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Group/GroupTests.cs index d2a4c4d1..6354553e 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Group/GroupTests.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Group/GroupTests.cs @@ -1,6 +1,7 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; using Redmine.Net.Api; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; @@ -73,7 +74,7 @@ public void DeleteGroup_Should_Succeed() fixture.RedmineManager.Delete(groupId); - Assert.Throws(() => + Assert.Throws(() => fixture.RedmineManager.Get(groupId)); } @@ -84,14 +85,12 @@ public void AddUserToGroup_Should_Succeed() var group = fixture.RedmineManager.Create(groupPayload); Assert.NotNull(group); - var userId = 1; - - fixture.RedmineManager.AddUserToGroup(group.Id, userId); + fixture.RedmineManager.AddUserToGroup(group.Id, userId: 1); var updatedGroup = fixture.RedmineManager.Get(group.Id.ToString(), RequestOptions.Include(RedmineKeys.USERS)); Assert.NotNull(updatedGroup); Assert.NotNull(updatedGroup.Users); - Assert.Contains(updatedGroup.Users, u => u.Id == userId); + Assert.Contains(updatedGroup.Users, u => u.Id == 1); } [Fact] diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Group/GroupTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Group/GroupTestsAsync.cs index d67a2e0c..3471cd4d 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Group/GroupTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Group/GroupTestsAsync.cs @@ -1,6 +1,7 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; using Redmine.Net.Api; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; @@ -87,7 +88,7 @@ public async Task DeleteGroup_Should_Succeed() await fixture.RedmineManager.DeleteAsync(groupId); // Assert - await Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetAsync(groupId)); } diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueAttachmentUploadTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueAttachmentUploadTests.cs index 527a5135..e74b62da 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueAttachmentUploadTests.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueAttachmentUploadTests.cs @@ -1,6 +1,7 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; using Redmine.Net.Api; using Redmine.Net.Api.Http; diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueAttachmentUploadTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueAttachmentUploadTestsAsync.cs index be0dc85e..342400a0 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueAttachmentUploadTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueAttachmentUploadTestsAsync.cs @@ -1,9 +1,9 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; using Redmine.Net.Api; using Redmine.Net.Api.Http; -using Redmine.Net.Api.Types; namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Issue; diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueTests.cs index 2c1fb974..27e3033e 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueTests.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueTests.cs @@ -1,6 +1,7 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Http; @@ -90,7 +91,7 @@ public void DeleteIssue_Should_Succeed() fixture.RedmineManager.Delete(issueId); //Assert - Assert.Throws(() => fixture.RedmineManager.Get(issueId)); + Assert.Throws(() => fixture.RedmineManager.Get(issueId)); } [Fact] @@ -113,7 +114,7 @@ public void GetIssue_With_Watchers_And_Relations_Should_Succeed() customFields: [TestEntityFactory.CreateRandomIssueCustomFieldWithMultipleValuesPayload()], watchers: [new Watcher() { Id = 1 }, new Watcher() { Id = userId }]); - var issueRelation = new IssueRelation() + var issueRelation = new Redmine.Net.Api.Types.IssueRelation() { Type = IssueRelationType.Relates, IssueToId = firstIssue.Id, diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueTestsAsync.cs index 1e39a2fc..9118390c 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueTestsAsync.cs @@ -132,7 +132,7 @@ public async Task DeleteIssue_Should_Succeed() await fixture.RedmineManager.DeleteAsync(issueId); //Assert - await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetAsync(issueId)); + await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetAsync(issueId)); } [Fact] diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueWatcherTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueWatcherTests.cs index c46dc941..fb62e53c 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueWatcherTests.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueWatcherTests.cs @@ -1,6 +1,6 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; using Redmine.Net.Api; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Http; diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueWatcherTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueWatcherTestsAsync.cs index fb96377f..b9d5c567 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueWatcherTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Issue/IssueWatcherTestsAsync.cs @@ -16,7 +16,7 @@ public class IssueWatcherTestsAsync(RedmineTestContainerFixture fixture) { Project = new IdentifiableName { Id = 1 }, Tracker = new IdentifiableName { Id = 1 }, - Status = new IssueStatus { Id = 1 }, + Status = new Redmine.Net.Api.Types.IssueStatus { Id = 1 }, Priority = new IdentifiableName { Id = 4 }, Subject = $"Test issue subject {Guid.NewGuid()}", Description = "Test issue description" diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueCategory/IssueCategoryTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueCategory/IssueCategoryTests.cs index 166cfc6f..78744ae5 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueCategory/IssueCategoryTests.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueCategory/IssueCategoryTests.cs @@ -1,31 +1,40 @@ +using System.Collections.Specialized; using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; +using Redmine.Net.Api; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; +using Redmine.Net.Api.Http; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Issue; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.IssueCategory; [Collection(Constants.RedmineTestContainerCollection)] public class IssueCategoryTests(RedmineTestContainerFixture fixture) { - private const string PROJECT_ID = "1"; + private const string PROJECT_ID = TestConstants.Projects.DefaultProjectIdentifier; - private IssueCategory CreateCategory() + private Redmine.Net.Api.Types.IssueCategory CreateCategory() { return fixture.RedmineManager.Create( - new IssueCategory { Name = $"Test Category {Guid.NewGuid()}" }, + new Redmine.Net.Api.Types.IssueCategory { Name = $"Test Category {Guid.NewGuid()}" }, PROJECT_ID); } [Fact] public void GetProjectIssueCategories_Should_Succeed() => - Assert.NotNull(fixture.RedmineManager.Get(PROJECT_ID)); + Assert.NotNull(fixture.RedmineManager.Get(new RequestOptions() + { + QueryString = new NameValueCollection() + { + {RedmineKeys.PROJECT_ID, PROJECT_ID} + } + })); [Fact] public void CreateIssueCategory_Should_Succeed() { - var cat = new IssueCategory { Name = $"Cat {Guid.NewGuid()}" }; + var cat = new Redmine.Net.Api.Types.IssueCategory { Name = $"Cat {Guid.NewGuid()}" }; var created = fixture.RedmineManager.Create(cat, PROJECT_ID); Assert.True(created.Id > 0); @@ -36,7 +45,7 @@ public void CreateIssueCategory_Should_Succeed() public void GetIssueCategory_Should_Succeed() { var created = CreateCategory(); - var retrieved = fixture.RedmineManager.Get(created.Id.ToInvariantString()); + var retrieved = fixture.RedmineManager.Get(created.Id.ToInvariantString()); Assert.Equal(created.Id, retrieved.Id); Assert.Equal(created.Name, retrieved.Name); @@ -49,7 +58,7 @@ public void UpdateIssueCategory_Should_Succeed() created.Name = $"Updated {Guid.NewGuid()}"; fixture.RedmineManager.Update(created.Id.ToInvariantString(), created); - var retrieved = fixture.RedmineManager.Get(created.Id.ToInvariantString()); + var retrieved = fixture.RedmineManager.Get(created.Id.ToInvariantString()); Assert.Equal(created.Name, retrieved.Name); } @@ -60,8 +69,8 @@ public void DeleteIssueCategory_Should_Succeed() var created = CreateCategory(); var id = created.Id.ToInvariantString(); - fixture.RedmineManager.Delete(id); + fixture.RedmineManager.Delete(id); - Assert.Throws(() => fixture.RedmineManager.Get(id)); + Assert.Throws(() => fixture.RedmineManager.Get(id)); } } \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueCategory/IssueCategoryTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueCategory/IssueCategoryTestsAsync.cs index d96361b7..1b0601c1 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueCategory/IssueCategoryTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueCategory/IssueCategoryTestsAsync.cs @@ -1,21 +1,25 @@ +using System.Collections.Specialized; using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; +using Redmine.Net.Api; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; -using Redmine.Net.Api.Types; +using Redmine.Net.Api.Http; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Issue; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.IssueCategory; [Collection(Constants.RedmineTestContainerCollection)] public class IssueCategoryTestsAsync(RedmineTestContainerFixture fixture) { - private const string PROJECT_ID = "1"; + private const string PROJECT_ID = TestConstants.Projects.DefaultProjectIdentifier; - private async Task CreateTestIssueCategoryAsync() + private async Task CreateRandomIssueCategoryAsync() { - var category = new IssueCategory + var category = new Redmine.Net.Api.Types.IssueCategory { - Name = $"Test Category {Guid.NewGuid()}" + Name = RandomHelper.GenerateText(5) }; return await fixture.RedmineManager.CreateAsync(category, PROJECT_ID); @@ -24,8 +28,18 @@ private async Task CreateTestIssueCategoryAsync() [Fact] public async Task GetProjectIssueCategories_Should_Succeed() { + // Arrange + var category = await CreateRandomIssueCategoryAsync(); + Assert.NotNull(category); + // Act - var categories = await fixture.RedmineManager.GetAsync(PROJECT_ID); + var categories = await fixture.RedmineManager.GetAsync(new RequestOptions() + { + QueryString = new NameValueCollection() + { + {RedmineKeys.PROJECT_ID, PROJECT_ID} + } + }); // Assert Assert.NotNull(categories); @@ -34,30 +48,23 @@ public async Task GetProjectIssueCategories_Should_Succeed() [Fact] public async Task CreateIssueCategory_Should_Succeed() { - // Arrange - var category = new IssueCategory - { - Name = $"Test Category {Guid.NewGuid()}" - }; - - // Act - var createdCategory = await fixture.RedmineManager.CreateAsync(category, PROJECT_ID); + // Arrange & Act + var category = await CreateRandomIssueCategoryAsync(); // Assert - Assert.NotNull(createdCategory); - Assert.True(createdCategory.Id > 0); - Assert.Equal(category.Name, createdCategory.Name); + Assert.NotNull(category); + Assert.True(category.Id > 0); } [Fact] public async Task GetIssueCategory_Should_Succeed() { // Arrange - var createdCategory = await CreateTestIssueCategoryAsync(); + var createdCategory = await CreateRandomIssueCategoryAsync(); Assert.NotNull(createdCategory); // Act - var retrievedCategory = await fixture.RedmineManager.GetAsync(createdCategory.Id.ToInvariantString()); + var retrievedCategory = await fixture.RedmineManager.GetAsync(createdCategory.Id.ToInvariantString()); // Assert Assert.NotNull(retrievedCategory); @@ -69,7 +76,7 @@ public async Task GetIssueCategory_Should_Succeed() public async Task UpdateIssueCategory_Should_Succeed() { // Arrange - var createdCategory = await CreateTestIssueCategoryAsync(); + var createdCategory = await CreateRandomIssueCategoryAsync(); Assert.NotNull(createdCategory); var updatedName = $"Updated Test Category {Guid.NewGuid()}"; @@ -77,7 +84,7 @@ public async Task UpdateIssueCategory_Should_Succeed() // Act await fixture.RedmineManager.UpdateAsync(createdCategory.Id.ToInvariantString(), createdCategory); - var retrievedCategory = await fixture.RedmineManager.GetAsync(createdCategory.Id.ToInvariantString()); + var retrievedCategory = await fixture.RedmineManager.GetAsync(createdCategory.Id.ToInvariantString()); // Assert Assert.NotNull(retrievedCategory); @@ -89,16 +96,16 @@ public async Task UpdateIssueCategory_Should_Succeed() public async Task DeleteIssueCategory_Should_Succeed() { // Arrange - var createdCategory = await CreateTestIssueCategoryAsync(); + var createdCategory = await CreateRandomIssueCategoryAsync(); Assert.NotNull(createdCategory); var categoryId = createdCategory.Id.ToInvariantString(); // Act - await fixture.RedmineManager.DeleteAsync(categoryId); + await fixture.RedmineManager.DeleteAsync(categoryId); // Assert - await Assert.ThrowsAsync(async () => - await fixture.RedmineManager.GetAsync(categoryId)); + await Assert.ThrowsAsync(async () => + await fixture.RedmineManager.GetAsync(categoryId)); } } \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueRelation/IssueRelationTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueRelation/IssueRelationTests.cs index d2f72d88..11efdfce 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueRelation/IssueRelationTests.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueRelation/IssueRelationTests.cs @@ -1,11 +1,10 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; using Redmine.Net.Api; using Redmine.Net.Api.Http; -using Redmine.Net.Api.Types; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Issue; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.IssueRelation; [Collection(Constants.RedmineTestContainerCollection)] public class IssueRelationTests(RedmineTestContainerFixture fixture) @@ -25,7 +24,7 @@ public void CreateIssueRelation_Should_Succeed() public void DeleteIssueRelation_Should_Succeed() { var (rel, _, _) = IssueTestHelper.CreateRandomIssueRelation(fixture.RedmineManager); - fixture.RedmineManager.Delete(rel.Id.ToString()); + fixture.RedmineManager.Delete(rel.Id.ToString()); var issue = fixture.RedmineManager.Get( rel.IssueId.ToString(), diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueRelation/IssueRelationTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueRelation/IssueRelationTestsAsync.cs index 5adbd6d5..ac1b5839 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueRelation/IssueRelationTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueRelation/IssueRelationTestsAsync.cs @@ -1,11 +1,11 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; using Redmine.Net.Api; using Redmine.Net.Api.Http; using Redmine.Net.Api.Types; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Issue; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.IssueRelation; [Collection(Constants.RedmineTestContainerCollection)] public class IssueRelationTestsAsync(RedmineTestContainerFixture fixture) @@ -16,7 +16,7 @@ public async Task CreateIssueRelation_Should_Succeed() // Arrange var (issue1, issue2) = await IssueTestHelper.CreateRandomTwoIssuesAsync(fixture.RedmineManager); - var relation = new IssueRelation + var relation = new Redmine.Net.Api.Types.IssueRelation { IssueId = issue1.Id, IssueToId = issue2.Id, @@ -42,7 +42,7 @@ public async Task DeleteIssueRelation_Should_Succeed() Assert.NotNull(relation); // Act & Assert - await fixture.RedmineManager.DeleteAsync(relation.Id.ToString()); + await fixture.RedmineManager.DeleteAsync(relation.Id.ToString()); var issue = await fixture.RedmineManager.GetAsync(relation.IssueId.ToString(), RequestOptions.Include(RedmineKeys.RELATIONS)); diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueStatus/IssueStatusTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueStatus/IssueStatusTests.cs index 94e099c1..b18abf13 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueStatus/IssueStatusTests.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueStatus/IssueStatusTests.cs @@ -1,8 +1,7 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; -using Redmine.Net.Api.Types; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Issue; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.IssueStatus; [Collection(Constants.RedmineTestContainerCollection)] public class IssueStatusTests(RedmineTestContainerFixture fixture) @@ -10,7 +9,7 @@ public class IssueStatusTests(RedmineTestContainerFixture fixture) [Fact] public void GetAllIssueStatuses_Should_Succeed() { - var statuses = fixture.RedmineManager.Get(); + var statuses = fixture.RedmineManager.Get(); Assert.NotNull(statuses); Assert.NotEmpty(statuses); } diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueStatus/IssueStatusTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueStatus/IssueStatusTestsAsync.cs index 5422befa..a1c7bd5c 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueStatus/IssueStatusTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/IssueStatus/IssueStatusTestsAsync.cs @@ -1,8 +1,7 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; -using Redmine.Net.Api.Types; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Issue; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.IssueStatus; [Collection(Constants.RedmineTestContainerCollection)] public class IssueStatusAsyncTests(RedmineTestContainerFixture fixture) @@ -11,7 +10,7 @@ public class IssueStatusAsyncTests(RedmineTestContainerFixture fixture) public async Task GetAllIssueStatuses_Should_Succeed() { // Act - var statuses = await fixture.RedmineManager.GetAsync(); + var statuses = await fixture.RedmineManager.GetAsync(); // Assert Assert.NotNull(statuses); diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Journal/JournalTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Journal/JournalTests.cs index ed59ccae..b3923d8e 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Journal/JournalTests.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Journal/JournalTests.cs @@ -1,11 +1,11 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; using Redmine.Net.Api; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Http; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Issue; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Journal; [Collection(Constants.RedmineTestContainerCollection)] public class JournalTests(RedmineTestContainerFixture fixture) diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Journal/JournalTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Journal/JournalTestsAsync.cs index 608bf1a7..b208a84d 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Journal/JournalTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Journal/JournalTestsAsync.cs @@ -1,12 +1,11 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; using Redmine.Net.Api; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Http; -using Redmine.Net.Api.Types; -namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Issue; +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.Journal; [Collection(Constants.RedmineTestContainerCollection)] public class JournalTestsAsync(RedmineTestContainerFixture fixture) diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/News/NewsTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/News/NewsTests.cs index 190d25b7..fc8f2050 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Entities/News/NewsTests.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/News/NewsTests.cs @@ -1,6 +1,6 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; using Redmine.Net.Api.Extensions; namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.News; @@ -13,7 +13,7 @@ public void GetAllNews_Should_Succeed() { _ = fixture.RedmineManager.AddProjectNews(TestConstants.Projects.DefaultProjectIdentifier, TestEntityFactory.CreateRandomNewsPayload()); - _ = fixture.RedmineManager.AddProjectNews("2", TestEntityFactory.CreateRandomNewsPayload()); + _ = fixture.RedmineManager.AddProjectNews(TestConstants.Projects.DefaultProjectIdentifier, TestEntityFactory.CreateRandomNewsPayload()); var news = fixture.RedmineManager.Get(); diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/News/NewsTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/News/NewsTestsAsync.cs index 88945ab2..8002e556 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Entities/News/NewsTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/News/NewsTestsAsync.cs @@ -1,6 +1,6 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; using Redmine.Net.Api.Extensions; namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Entities.News; @@ -14,7 +14,7 @@ public async Task GetAllNews_Should_Succeed() // Arrange _ = await fixture.RedmineManager.AddProjectNewsAsync(TestConstants.Projects.DefaultProjectIdentifier, TestEntityFactory.CreateRandomNewsPayload()); - _ = await fixture.RedmineManager.AddProjectNewsAsync("2", TestEntityFactory.CreateRandomNewsPayload()); + _ = await fixture.RedmineManager.AddProjectNewsAsync(TestConstants.Projects.DefaultProjectIdentifier, TestEntityFactory.CreateRandomNewsPayload()); // Act var news = await fixture.RedmineManager.GetAsync(); diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Project/ProjectTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Project/ProjectTestsAsync.cs index aaec371b..fd3d4137 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Project/ProjectTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Project/ProjectTestsAsync.cs @@ -74,7 +74,7 @@ public async Task DeleteIssue_Should_Succeed() await Task.Delay(200); //Assert - await Assert.ThrowsAsync(TestCode); + await Assert.ThrowsAsync(TestCode); return; async Task TestCode() diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/ProjectMembership/ProjectMembershipTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/ProjectMembership/ProjectMembershipTests.cs index c9cca3ba..2b2b2f52 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Entities/ProjectMembership/ProjectMembershipTests.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/ProjectMembership/ProjectMembershipTests.cs @@ -1,6 +1,6 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Types; @@ -79,6 +79,6 @@ public void DeleteProjectMembership_WithValidId_ShouldSucceed() fixture.RedmineManager.Delete(membership.Id.ToString()); // Assert - Assert.Throws(() => fixture.RedmineManager.Get(membership.Id.ToString())); + Assert.Throws(() => fixture.RedmineManager.Get(membership.Id.ToString())); } } \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/ProjectMembership/ProjectMembershipTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/ProjectMembership/ProjectMembershipTestsAsync.cs index ee17f90f..e8c36536 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Entities/ProjectMembership/ProjectMembershipTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/ProjectMembership/ProjectMembershipTestsAsync.cs @@ -1,6 +1,6 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Types; @@ -87,7 +87,7 @@ public async Task DeleteProjectMembership_WithValidId_ShouldSucceed() await fixture.RedmineManager.DeleteAsync(membershipId); // Assert - await Assert.ThrowsAsync(() => fixture.RedmineManager.GetAsync(membershipId)); + await Assert.ThrowsAsync(() => fixture.RedmineManager.GetAsync(membershipId)); } [Fact] diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/TimeEntry/TimeEntryTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/TimeEntry/TimeEntryTestsAsync.cs index 6a273b4d..db3719e1 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Entities/TimeEntry/TimeEntryTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/TimeEntry/TimeEntryTestsAsync.cs @@ -1,6 +1,6 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; -using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; @@ -13,7 +13,7 @@ public class TimeEntryTestsAsync(RedmineTestContainerFixture fixture) { var (issue, _) = await IssueTestHelper.CreateRandomIssueAsync(fixture.RedmineManager); - var timeEntry = TestEntityFactory.CreateRandomTimeEntryPayload(TestConstants.Projects.DefaultProjectId, issue.Id); + var timeEntry = TestEntityFactory.CreateRandomTimeEntryPayload(TestConstants.Projects.DefaultProjectId, issue.Id, activityId: 8); return (await fixture.RedmineManager.CreateAsync(timeEntry), timeEntry); } @@ -86,6 +86,6 @@ public async Task DeleteTimeEntry_Should_Succeed() await fixture.RedmineManager.DeleteAsync(timeEntryId); //Assert - await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetAsync(timeEntryId)); + await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetAsync(timeEntryId)); } } \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/User/UserTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/User/UserTestsAsync.cs index 833f7566..cd63378b 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Entities/User/UserTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/User/UserTestsAsync.cs @@ -1,6 +1,7 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; using Redmine.Net.Api; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; @@ -83,7 +84,7 @@ public async Task DeleteUser_WithValidId_ShouldSucceed() await fixture.RedmineManager.DeleteAsync(userId); //Assert - await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetAsync(userId)); + await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetAsync(userId)); } [Fact] diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Version/VersionTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Version/VersionTestsAsync.cs index 0a325719..e292bf4d 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Version/VersionTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Version/VersionTestsAsync.cs @@ -1,6 +1,7 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; using Redmine.Net.Api.Types; @@ -80,7 +81,7 @@ public async Task DeleteVersion_Should_Succeed() await fixture.RedmineManager.DeleteAsync(version); // Assert - await Assert.ThrowsAsync(async () => + await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetAsync(version)); } } \ No newline at end of file diff --git a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Wiki/WikiTestsAsync.cs b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Wiki/WikiTestsAsync.cs index d54a28a1..c5033eb3 100644 --- a/tests/redmine-net-api.Integration.Tests/Tests/Entities/Wiki/WikiTestsAsync.cs +++ b/tests/redmine-net-api.Integration.Tests/Tests/Entities/Wiki/WikiTestsAsync.cs @@ -1,6 +1,7 @@ using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; using Padi.DotNet.RedmineAPI.Integration.Tests.Helpers; using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Padi.DotNet.RedmineAPI.Integration.Tests.Tests.Common; using Redmine.Net.Api; using Redmine.Net.Api.Exceptions; using Redmine.Net.Api.Extensions; @@ -78,7 +79,7 @@ public async Task DeleteWikiPage_WithValidTitle_ShouldSucceed() await fixture.RedmineManager.DeleteWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, wikiPage.Title); // Assert - await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, wikiPage.Title)); + await Assert.ThrowsAsync(async () => await fixture.RedmineManager.GetWikiPageAsync(TestConstants.Projects.DefaultProjectIdentifier, wikiPage.Title)); } [Fact] diff --git a/tests/redmine-net-api.Integration.Tests/Tests/RedmineApiWebClientTests.cs b/tests/redmine-net-api.Integration.Tests/Tests/RedmineApiWebClientTests.cs new file mode 100644 index 00000000..ebed01e9 --- /dev/null +++ b/tests/redmine-net-api.Integration.Tests/Tests/RedmineApiWebClientTests.cs @@ -0,0 +1,75 @@ +using Padi.DotNet.RedmineAPI.Integration.Tests.Fixtures; +using Padi.DotNet.RedmineAPI.Integration.Tests.Infrastructure; +using Redmine.Net.Api.Exceptions; +using Redmine.Net.Api.Types; + +namespace Padi.DotNet.RedmineAPI.Integration.Tests.Tests; + +[Collection(Constants.RedmineTestContainerCollection)] +public class RedmineApiWebClientTests(RedmineTestContainerFixture fixture) +{ + [Fact] + public async Task SendAsync_WhenRequestCanceled_ThrowsRedmineOperationCanceledException() + { + using var cts = new CancellationTokenSource(); + // Arrange + cts.CancelAfter(TimeSpan.FromMilliseconds(100)); + + // Act & Assert + _ = await Assert.ThrowsAsync(async () => + await fixture.RedmineManager.GetAsync(cancellationToken: cts.Token)); + } + + [Fact] + public async Task SendAsync_WhenWebExceptionOccurs_ThrowsRedmineApiException() + { + // Act & Assert + var exception = await Assert.ThrowsAnyAsync(async () => + await fixture.RedmineManager.GetAsync("xyz")); + + Assert.NotNull(exception.InnerException); + } + + [Fact] + public async Task SendAsync_WhenOperationCanceled_ThrowsRedmineOperationCanceledException() + { + using var cts = new CancellationTokenSource(); + // Arrange + await cts.CancelAsync(); + + // Act & Assert + _ = await Assert.ThrowsAsync(async () => + await fixture.RedmineManager.GetAsync(cancellationToken: cts.Token)); + } + + [Fact] + public async Task SendAsync_WhenOperationTimedOut_ThrowsRedmineOperationCanceledException() + { + // Arrange + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(1)); + + // Act & Assert + _ = await Assert.ThrowsAsync(async () => + await fixture.RedmineManager.GetAsync(cancellationToken: timeoutCts.Token)); + } + + [Fact] + public async Task SendAsync_WhenTaskCanceled_ThrowsRedmineOperationCanceledException() + { + using var cts = new CancellationTokenSource(); + // Arrange + cts.CancelAfter(TimeSpan.FromMilliseconds(50)); + + // Act & Assert + _ = await Assert.ThrowsAsync(async () => + await fixture.RedmineManager.GetAsync(cancellationToken: cts.Token)); + } + + [Fact] + public async Task SendAsync_WhenGeneralException_ThrowsRedmineException() + { + // Act & Assert + _ = await Assert.ThrowsAsync(async () => + await fixture.RedmineManager.CreateAsync(null)); + } +} \ No newline at end of file From caaefe444c02ceb355cf56476f179de1ca9fb816 Mon Sep 17 00:00:00 2001 From: Padi Date: Mon, 2 Jun 2025 13:26:41 +0300 Subject: [PATCH 547/549] [New] ParameterValidator --- .../Internals/ParameterValidator.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/redmine-net-api/Internals/ParameterValidator.cs diff --git a/src/redmine-net-api/Internals/ParameterValidator.cs b/src/redmine-net-api/Internals/ParameterValidator.cs new file mode 100644 index 00000000..55a4f944 --- /dev/null +++ b/src/redmine-net-api/Internals/ParameterValidator.cs @@ -0,0 +1,34 @@ +using System; + +namespace Redmine.Net.Api.Internals; + +/// +/// +/// +internal static class ParameterValidator +{ + public static void ValidateNotNull(T parameter, string parameterName) + where T : class + { + if (parameter is null) + { + throw new ArgumentNullException(parameterName); + } + } + + public static void ValidateNotNullOrEmpty(string parameter, string parameterName) + { + if (string.IsNullOrEmpty(parameter)) + { + throw new ArgumentException("Value cannot be null or empty", parameterName); + } + } + + public static void ValidateId(int id, string parameterName) + { + if (id <= 0) + { + throw new ArgumentException("Id must be greater than 0", parameterName); + } + } +} \ No newline at end of file From 9faded78f909cabf988ed6b876a25c498afdf251 Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 17 Sep 2025 13:39:11 +0300 Subject: [PATCH 548/549] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 39 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 28 ++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..87bc8a46 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,39 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: 'type: bug' +assignees: zapadi + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Call the Redmine API endpoint '...' +2. Call the client method: `...` +3. Inspect the returned response/data +4. Notice the incorrect behavior or error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Sample Code / Request** +If applicable, add a minimal code snippet or HTTP request that reproduces the issue. + +**API Response / Error** +If applicable, paste the response body, error message, or stack trace. + +**Environment (please complete the following information):** +- Library version: [e.g. 4.50.0] +- .NET runtime: [e.g. .NET 8.0, .NET Framework 4.8] +- Redmine version: [e.g. 5.1.2] +- OS: [e.g. Windows 11, Ubuntu 22.04] + +**Screenshots** +If applicable, add screenshots to help explain the problem. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..87bd1b13 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,28 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[Feature]" +labels: 'type: enhancement, type: feature' +assignees: zapadi + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. +Ex: "Add support for calling the `...` endpoint so that I can [...]" + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. +Ex: "Right now I make a raw HTTP request using `HttpClient`, but it would be better if the client library exposed [...]" + +**Proposed API / Usage Example (if applicable)** +If relevant, provide a code snippet, method signature, or example API request/response. +```csharp +var issue = await client.Issues.GetByIdAsync(123, includeChildren: true); +``` + +**Additional context** +Add any other context or screenshots about the feature request here. From e23b2d2cdb9730f3374c3fb0f0e4c38a9523b4ea Mon Sep 17 00:00:00 2001 From: Padi Date: Wed, 17 Sep 2025 13:50:24 +0300 Subject: [PATCH 549/549] Update PULL_REQUEST_TEMPLATE.md --- PULL_REQUEST_TEMPLATE.md | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index 83503568..a3f3ba2e 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -2,8 +2,32 @@ We welcome contributions! Here's how you can help: -1. Fork the repository -2. Create a new branch (git checkout -b feature/my-feature) -3. Make your changes and commit (git commit -m 'Add some feature') -4. Push to your fork (git push origin feature/my-feature) -5. Open a Pull Request +## Description + + +## Related Issues + +- Closes #ISSUE_ID +- Fixes #ISSUE_ID + +## Type of Change + +- [ ] 🐛 Bug fix (non-breaking change which fixes an issue) +- [ ] ✨ New feature (non-breaking change which adds functionality) +- [ ] 💥 Breaking change (fix or feature that would cause existing functionality to change) +- [ ] 🧹 Code cleanup / refactor +- [ ] 📖 Documentation update + +## Checklist + +- [ ] I have tested my changes locally against a Redmine instance. +- [ ] I have added/updated unit tests if applicable. +- [ ] I have updated documentation (README / XML comments / wiki). +- [ ] I followed the project’s coding style. +- [ ] New and existing tests pass with my changes. + +## API Changes (if applicable) + +```csharp +// Example of a new/updated method signature +Task GetByIdAsync(int id, bool includeChildren = false);

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�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 Date: Tue, 19 Nov 2019 15:28:19 +0200 Subject: [PATCH 009/549] Add Resharper logo --- logo-resharper.gif | Bin 0 -> 1143 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 logo-resharper.gif diff --git a/logo-resharper.gif b/logo-resharper.gif new file mode 100644 index 0000000000000000000000000000000000000000..739dcb4d52c937d0ae551a44bb8d53241adb325a GIT binary patch literal 1143 zcmZ?wbhEHb>|>B+xXQrr{>iI4)#x_k)Oj&2%QE}cm(SeZy7=7MgV%STxqsr?^J@?K z915oSRqdO!_SpRG-#>o+I#uM`9OZ9|48N~*__4wN$F{hidvkssss44U^Y?`rzppR< zb8qXPCrAFizWndgqklg>{Qv)-ftElAWH`t#3~c`$CKP!12(UD!Bs{1PFrA{|s=UWT zQ+83=>9KO=Vi6^xr72PEjDOuQewJ#mN6wb>Rv21v3-Q+)Ftbu6|}F6R(s6x51~==l+{q3t>yx*?v#nlf&eI zAk#Zud;N4hreua^4U-soc?=eCIcWsieqmudbUmj@pf5kKvD1p>z$ryeDVYswE~+cH zl}>H2xUhh8B0EQsSp`S9V#f|nIgJ{LiVtE8d__WR3oRN=_r<;Ny!GsvZ;Qs+dmbH| zdbpz%xtQ*RDwuUP@A0hS6Z|E-RA6-`2hYvU-HvM*cpS2nlZ4K0JM78v=1h(ti_aF> zsgArGJ}@>iyx73{p_hSk>3vTLo>{tGvY(QjoESgpu4a6r>J`^~{dViU^FQQXxRjg8 zrZYD3SS0pWZ)aT~%^|6}@qm+)U_g(MpMuBr(gRDEw=gjC6e}{_UFrCLrV#%_w^KC- z4cpmNIarz|1^wRpv9*Bl2HPRY!zLn+1^7~&I|KY0!sjwGZ!x>l!{x?tuVP1_`l1AZ zCiexMUOFy{QXxAP8QCPB6md;kI@3*-gCXYlRGlc950mraI0aQX+*y+zmKCi1nJ83v za?u2#5=YPF0=JeX9$*)YP-tN-O;I?OdCPW6OF~c8%Bb)!mfM#{a9ccJs9k9DaWeh6Zsq2ZbdToowtgnT}hS)piQbId-D#tgAwRN9e3cLDQpzuFbGYSyrfKcyH;R zE{R0JiLYL+5;(eW;ceNsJ1Smm3fq|M-w?fI2ZNt|ZgcjO-Fe?1L?7T{oX5as;yvdS zvscKh24+?p6)lM+L26H;6--zgBQM8oS-p1^=dlCMXBR0pG)!DqslTk7P5Y$N5x$ap z6XLI&PLnEc4B(b&`X(*lpTM=9jf3w_G(+tDcQ@6_cn)XgdAvQ Date: Tue, 19 Nov 2019 15:28:47 +0200 Subject: [PATCH 010/549] Add solution folder with yml files --- redmine-net-api.sln | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/redmine-net-api.sln b/redmine-net-api.sln index 8f0cd011..6a6da6ee 100644 --- a/redmine-net-api.sln +++ b/redmine-net-api.sln @@ -12,6 +12,10 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "redmine-net-api.Tests", "tests\redmine-net-api.Tests\redmine-net-api.Tests.csproj", "{900EF0B3-0233-45DA-811F-4C59483E8452}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionFolder", "SolutionFolder", "{E8C35EC2-DD90-46E8-9B63-84EFD5F2FDE3}" +ProjectSection(SolutionItems) = preProject + appveyor.yml = appveyor.yml + docker-compose.yml = docker-compose.yml +EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution From 8591c42ed47cff25fc362e860a7a8fc99dde09e3 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Tue, 19 Nov 2019 15:37:01 +0200 Subject: [PATCH 011/549] Update docker-compose --- docker-compose.yml | 63 +++++++++++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 18 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 1d017917..78e1e2f4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,30 +1,57 @@ -version: '2' +version: '3.7' services: redmine: ports: - '8089:3000' image: 'redmine:4.0.4' - container_name: 'redmine-web-404' + container_name: 'redmine-web' depends_on: - - postgres - links: - - postgres - restart: always - environment: - POSTGRES_PORT_5432_TCP: 5432 - POSTGRES_ENV_POSTGRES_USER: redmine - POSTGRES_ENV_POSTGRES_PASSWORD: redmine-pswd + - db-postgres + # healthcheck: + # test: ["CMD", "curl", "-f", "http://localhost:8089/"] + # interval: 1m30s + # timeout: 10s + # retries: 3 + # start_period: 40s + restart: unless-stopped + environment: + REDMINE_DB_POSTGRES: db-postgres + REDMINE_DB_PORT: 5432 + REDMINE_DB_DATABASE: redmine + REDMINE_DB_USERNAME: redmine-usr + REDMINE_DB_PASSWORD: redmine-pswd + networks: + - redmine-network + stop_grace_period: 30s volumes: - - ~/docker-vols/redmine/files/:/usr/src/redmine/files/ - postgres: + - redmine-data:/usr/src/redmine/files + + db-postgres: environment: - POSTGRES_USER: redmine + POSTGRES_DB: redmine + POSTGRES_USER: redmine-usr POSTGRES_PASSWORD: redmine-pswd - container_name: 'redmine-db-111' + container_name: 'redmine-db' image: 'postgres:11.1' - #restart: always - ports: + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 20s + timeout: 20s + retries: 5 + restart: unless-stopped + ports: - '5432:5432' - volumes: - - ~/docker-vols/redmine/postgres:/var/lib/postgresql/data \ No newline at end of file + volumes: + - postgres-data:/var/lib/postgresql/data + networks: + - redmine-network + stop_grace_period: 30s + +volumes: + postgres-data: + redmine-data: + +networks: + redmine-network: + driver: bridge \ No newline at end of file From 0c2a08e1904d9830a5ad5dbfdde4cb0687b7e3ed Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Tue, 19 Nov 2019 17:20:19 +0200 Subject: [PATCH 012/549] Add Package property group --- src/redmine-net-api/redmine-net-api.csproj | 31 ++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 762bbb62..d8d6ca9e 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -25,6 +25,37 @@ + + + 1.0.0.0 + Adrian Popescu + + Redmine Api is a .NET rest client for Redmine. + + padi + Copyright © Adrian Popescu. All rights Reserved + + 1.0.0.0 + 1.0.0 + redmine-net-api + https://github.com/zapadi/redmine-net-api/blob/master/logo.png + https://github.com/zapadi/redmine-net-api/blob/master/LICENSE + https://github.com/zapadi/redmine-net-api + true + Changed to new csproj format. + Redmine; REST; API; Client; .NET; Adrian Popescu; + 1.0.0 + Redmine .NET API Client + + git + https://github.com/zapadi/redmine-net-api + + 1.0.0 + 2.0.0 + $(VersionSuffix) + + + NET20;NETFULL From 7e6c7bd46ef36ff731af3b663eb717f18ff82a48 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Tue, 19 Nov 2019 17:41:53 +0200 Subject: [PATCH 013/549] Remove csproj Platform tag --- src/redmine-net-api/redmine-net-api.csproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index d8d6ca9e..3e25add6 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -14,8 +14,6 @@ TRACE - x64 or x86 - Debug;Release PackageReference From 727c531329f5850a692a7ad74554b958d98e72d3 Mon Sep 17 00:00:00 2001 From: Adrian Popescu Date: Wed, 20 Nov 2019 18:22:48 +0200 Subject: [PATCH 014/549] Remove TargetFramework --- src/redmine-net-api/redmine-net-api.csproj | 24 +++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/redmine-net-api/redmine-net-api.csproj b/src/redmine-net-api/redmine-net-api.csproj index 3e25add6..30f8bea1 100644 --- a/src/redmine-net-api/redmine-net-api.csproj +++ b/src/redmine-net-api/redmine-net-api.csproj @@ -2,39 +2,35 @@ - net48 + net20;net40;net45;net451;net452;net46;net461;net462;net47;net471;net472;net48; - false - Redmine.Net.Api redmine-net-api - true - TRACE - Debug;Release - PackageReference - AnyCPU;x64 - - 1.0.0.0 + Adrian Popescu Redmine Api is a .NET rest client for Redmine. - padi - Copyright © Adrian Popescu. All rights Reserved + p.adi + + Adrian Popescu, 2011-2020 1.0.0.0 + 1.0.0 + + en-US redmine-net-api https://github.com/zapadi/redmine-net-api/blob/master/logo.png https://github.com/zapadi/redmine-net-api/blob/master/LICENSE @@ -43,11 +39,15 @@ Changed to new csproj format. Redmine; REST; API; Client; .NET; Adrian Popescu; 1.0.0 + Redmine .NET API Client git + https://github.com/zapadi/redmine-net-api +