From 17ab59ebf1be3b1d2737adfab82ac9846e5d82fa Mon Sep 17 00:00:00 2001 From: Martin Hey Date: Tue, 7 Jan 2025 15:27:16 +0100 Subject: [PATCH 001/136] 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 002/136] 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 003/136] 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 004/136] 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 005/136] 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 006/136] 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 007/136] [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 008/136] [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 009/136] 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 010/136] [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 011/136] [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 012/136] 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 013/136] [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 014/136] 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 015/136] 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 016/136] 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 017/136] 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 018/136] 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 019/136] 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 020/136] 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 021/136] 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 022/136] 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 023/136] [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 024/136] 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 025/136] [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 026/136] 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 027/136] 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 028/136] 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 029/136] 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 030/136] 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 031/136] [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 032/136] 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 033/136] 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 034/136] [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 035/136] [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 036/136] 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 037/136] 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 038/136] [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 039/136] [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 040/136] [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 041/136] [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 042/136] 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 043/136] 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 044/136] 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 045/136] [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 046/136] [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 047/136] [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 048/136] [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 049/136] [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 050/136] [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 051/136] [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 052/136] [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 053/136] 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 054/136] 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 055/136] 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 056/136] 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 057/136] 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 058/136] 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 059/136] 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 060/136] 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 061/136] 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 062/136] 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 063/136] 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 064/136] 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 065/136] 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 066/136] 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 067/136] 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 068/136] 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 069/136] 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 070/136] 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 071/136] 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 072/136] 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 073/136] 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 074/136] 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 075/136] 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 076/136] 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 077/136] 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 078/136] 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 079/136] 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 080/136] 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 081/136] 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 082/136] 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 083/136] 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 084/136] 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 085/136] 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 086/136] 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 087/136] 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 088/136] 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 089/136] 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 090/136] [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 091/136] 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 092/136] 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 093/136] 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 094/136] 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 095/136] 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 096/136] 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 097/136] [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 098/136] 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 099/136] 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 100/136] 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 101/136] 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 102/136] [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 103/136] 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 104/136] 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 105/136] 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 106/136] 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 107/136] [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 108/136] [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 109/136] 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 110/136] 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 111/136] 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 112/136] 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 113/136] 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 114/136] 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 115/136] 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 116/136] 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 117/136] 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 118/136] 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 119/136] 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 120/136] 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 121/136] ... --- 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 122/136] 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 123/136] 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 124/136] 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 125/136] 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 126/136] 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 127/136] 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 128/136] 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 129/136] 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 130/136] 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 131/136] 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 132/136] 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 133/136] [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 134/136] [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 135/136] 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 136/136] 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);