diff --git a/src/YouTrackSharp/ConnectionExtensions.cs b/src/YouTrackSharp/ConnectionExtensions.cs index 6958afeb..3d3b2f93 100644 --- a/src/YouTrackSharp/ConnectionExtensions.cs +++ b/src/YouTrackSharp/ConnectionExtensions.cs @@ -59,5 +59,15 @@ public static TimeTrackingManagementService CreateTimeTrackingManagementService( { return new TimeTrackingManagementService(connection); } + + /// + /// Creates a . + /// + /// The to create a service with. + /// for accessing custom project fields. + public static ProjectCustomFieldsService ProjectCustomFieldsService(this Connection connection) + { + return new ProjectCustomFieldsService(connection); + } } } \ No newline at end of file diff --git a/src/YouTrackSharp/Projects/CustomField.cs b/src/YouTrackSharp/Projects/CustomField.cs new file mode 100644 index 00000000..94aec834 --- /dev/null +++ b/src/YouTrackSharp/Projects/CustomField.cs @@ -0,0 +1,52 @@ +using Newtonsoft.Json; + +namespace YouTrackSharp.Projects +{ + /// + /// Custom field for a project + /// + public class CustomField + { + /// + /// Creates an instance of the class. + /// + public CustomField() + { + Name = string.Empty; + Url = string.Empty; + Type = string.Empty; + CanBeEmpty = false; + EmptyText = string.Empty; + } + + /// + /// Name of project custom field. + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// The Url of the custom field. + /// + [JsonProperty("url")] + public string Url { get; set; } + + /// + /// Type of this custom field. + /// + [JsonProperty("type")] + public string Type { get; set; } + + /// + /// Mandatory binary parameter defining if the field can have empty value or not. + /// + [JsonProperty("canBeEmpty")] + public bool CanBeEmpty { get; set; } + + /// + /// Text that is shown when the custom field has no value. + /// + [JsonProperty("emptyText")] + public string EmptyText { get; set; } + } +} \ No newline at end of file diff --git a/src/YouTrackSharp/Projects/ProjectCustomFieldsService.cs b/src/YouTrackSharp/Projects/ProjectCustomFieldsService.cs new file mode 100644 index 00000000..b6c533b3 --- /dev/null +++ b/src/YouTrackSharp/Projects/ProjectCustomFieldsService.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using YouTrackSharp.Management; + +namespace YouTrackSharp.Projects +{ + /// + /// A class that represents a REST API client for methods related to operations with custom fields of a project. + /// It uses a implementation to connect to the remote YouTrack server instance. + /// + public class ProjectCustomFieldsService + { + private readonly Connection _connection; + + /// + /// Creates an instance of the class. + /// + /// A instance that provides a connection to the remote YouTrack server instance. + public ProjectCustomFieldsService(Connection connection) + { + _connection = connection ?? throw new ArgumentNullException(nameof(connection)); + } + + /// + /// Get custom fields used in a project. + /// + /// Uses the REST API Get Project Custom Fields. + /// Id of the project to get the custom fields for. + /// A of that are accessible for currently logged in user. + /// When the call to the remote YouTrack server instance failed. + public async Task> GetProjectCustomFields(string projectId) + { + var client = await _connection.GetAuthenticatedHttpClient(); + var response = await client.GetAsync($"rest/admin/project/{projectId}/customfield"); + + response.EnsureSuccessStatusCode(); + + return JsonConvert.DeserializeObject>(await response.Content.ReadAsStringAsync()); + } + + /// + /// Get a project's custom field by its name. + /// + /// Uses the REST API Get Project Custom Field. + /// Id of the project to get the custom field for. + /// Name of the custom field to get. + /// . + /// When the call to the remote YouTrack server instance failed. + public async Task GetProjectCustomField(string projectId, string customFieldName) + { + var client = await _connection.GetAuthenticatedHttpClient(); + var response = await client.GetAsync($"rest/admin/project/{projectId}/customfield/{customFieldName}"); + + response.EnsureSuccessStatusCode(); + + return JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); + } + + /// + /// Remove specified custom field from a project. + /// + /// Uses the REST API Delete a project custom field. + /// Id of the project to delete the custom field for. + /// Name of the custom field to delete. + /// When the is null or empty. + /// When the is null or empty. + /// When the call to the remote YouTrack server instance failed. + public async Task DeleteProjectCustomField(string projectId, string customFieldName) + { + if (string.IsNullOrEmpty(projectId)) + { + throw new ArgumentNullException(nameof(projectId)); + } + if (string.IsNullOrEmpty(customFieldName)) + { + throw new ArgumentNullException(nameof(customFieldName)); + } + + var client = await _connection.GetAuthenticatedHttpClient(); + var response = await client.DeleteAsync($"rest/admin/project/{projectId}/customfield/{customFieldName}"); + + if (response.StatusCode == HttpStatusCode.NotFound) + { + return; + } + + response.EnsureSuccessStatusCode(); + } + + /// + /// Adds an existing custom field to a specific project. + /// + /// Uses the REST API Create a project custom field. + /// Id of the project to add a custom field. + /// to add to the project. + /// When the is null or empty. + /// When the is null or empty. + /// When the call to the remote YouTrack server instance failed and YouTrack reported an error message. + /// When the call to the remote YouTrack server instance failed. + public async Task CreateProjectCustomField(string projectId, CustomField customField) + { + if (string.IsNullOrEmpty(customField?.Name)) + { + throw new ArgumentNullException(nameof(customField)); + } + + string query = string.Empty; + if (!string.IsNullOrEmpty(customField.EmptyText)) + { + query = $"?emptyFieldText={WebUtility.UrlEncode(customField.EmptyText)}"; + } + + var client = await _connection.GetAuthenticatedHttpClient(); + var response = await client.PutAsync($"rest/admin/project/{projectId}/customfield/{customField.Name}{query}", new MultipartContent()); + + response.EnsureSuccessStatusCode(); + } + + /// + /// Updates a custom field to a specific project. + /// + /// Uses the REST API Updates a project custom field. + /// Id of the project. + /// to update in project. + /// When the is null or empty. + /// When the is null or empty. + /// When the call to the remote YouTrack server instance failed and YouTrack reported an error message. + /// When the call to the remote YouTrack server instance failed. + public async Task UpdateProjectCustomField(string projectId, CustomField customField) + { + if (string.IsNullOrEmpty(projectId)) + { + throw new ArgumentNullException(nameof(projectId)); + } + + if (string.IsNullOrEmpty(customField?.Name)) + { + throw new ArgumentNullException(nameof(customField)); + } + + string query = string.Empty; + if (!string.IsNullOrEmpty(customField.EmptyText)) + { + query = $"?emptyFieldText={WebUtility.UrlEncode(customField.EmptyText)}"; + } + + var client = await _connection.GetAuthenticatedHttpClient(); + var response = await client.PostAsync($"rest/admin/project/{projectId}/customfield/{customField.Name}{query}", new MultipartContent()); + + response.EnsureSuccessStatusCode(); + } + } +} \ No newline at end of file diff --git a/src/YouTrackSharp/TimeTracking/TimeTrackingService.cs b/src/YouTrackSharp/TimeTracking/TimeTrackingService.cs index b51407ba..949498ae 100644 --- a/src/YouTrackSharp/TimeTracking/TimeTrackingService.cs +++ b/src/YouTrackSharp/TimeTracking/TimeTrackingService.cs @@ -30,7 +30,7 @@ public TimeTrackingService(Connection connection) /// Get work types for a specific project from the server. /// /// Uses the REST API GET Work Types for a Project. - /// Id of the issue to get work items for. + /// Id of the project to get work items for. /// An of for the requested project . /// When the is null or empty. /// When the call to the remote YouTrack server instance failed. diff --git a/tests/YouTrackSharp.Tests/Integration/Projects/CreateProjectCustomField.cs b/tests/YouTrackSharp.Tests/Integration/Projects/CreateProjectCustomField.cs new file mode 100644 index 00000000..7a3c47f2 --- /dev/null +++ b/tests/YouTrackSharp.Tests/Integration/Projects/CreateProjectCustomField.cs @@ -0,0 +1,74 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Xunit; +using YouTrackSharp.Projects; +using YouTrackSharp.Tests.Infrastructure; + +namespace YouTrackSharp.Tests.Integration.Projects +{ + public partial class ProjectCustomFieldsServiceTests + { + public class CreateProjectCustomField + { + [Fact] + public async Task Valid_Connection_Creates_CustomField_For_Project() + { + // Arrange + var connection = Connections.Demo1Password; + var service = connection.ProjectCustomFieldsService(); + var customField = new CustomField {Name = "TestField"}; + var projectId = "DP1"; + + // Act + await service.CreateProjectCustomField(projectId, customField); + + var created = await service.GetProjectCustomField(projectId, customField.Name); + + // Assert + Assert.NotNull(created); + + Assert.Equal(customField.Name, created.Name); + Assert.Equal(customField.EmptyText, string.Empty); + + // cleanup + await service.DeleteProjectCustomField(projectId, customField.Name); + } + + [Fact] + public async Task Valid_Connection_Creates_CustomField_With_EmptyText_For_Project() + { + // Arrange + var connection = Connections.Demo1Password; + var service = connection.ProjectCustomFieldsService(); + var customField = new CustomField { Name = "TestField", EmptyText = "empty" }; + var projectId = "DP1"; + + // Act + await service.CreateProjectCustomField(projectId, customField); + + var created = await service.GetProjectCustomField(projectId, customField.Name); + + // Assert + Assert.NotNull(created); + + Assert.Equal(customField.Name, created.Name); + Assert.Equal(customField.EmptyText, created.EmptyText); + + // cleanup + await service.DeleteProjectCustomField(projectId, customField.Name); + } + + [Fact] + public async Task Invalid_Connection_Throws_UnauthorizedConnectionException() + { + // Arrange + var service = Connections.UnauthorizedConnection.ProjectCustomFieldsService(); + + // Act & Assert + await Assert.ThrowsAsync( + async () => await service.GetProjectCustomField("DP1", "TestField")); + } + } + } +} \ No newline at end of file diff --git a/tests/YouTrackSharp.Tests/Integration/Projects/DeleteProjectCustomField.cs b/tests/YouTrackSharp.Tests/Integration/Projects/DeleteProjectCustomField.cs new file mode 100644 index 00000000..da67abcc --- /dev/null +++ b/tests/YouTrackSharp.Tests/Integration/Projects/DeleteProjectCustomField.cs @@ -0,0 +1,40 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Xunit; +using YouTrackSharp.Tests.Infrastructure; + +namespace YouTrackSharp.Tests.Integration.Projects +{ + public partial class ProjectCustomFieldsServiceTests + { + public class DeleteProjectCustomField + { + [Fact] + public async Task Valid_Connection_Deletes_CustomField_For_Project() + { + // Arrange + var connection = Connections.Demo1Token; + var service = connection.ProjectCustomFieldsService(); + + // Act & Assert + var acted = false; + await service.DeleteProjectCustomField("DP1", " TestField"); + acted = true; + + Assert.True(acted); + } + + [Fact] + public async Task Invalid_Connection_Throws_UnauthorizedConnectionException() + { + // Arrange + var service = Connections.UnauthorizedConnection.ProjectCustomFieldsService(); + + // Act & Assert + await Assert.ThrowsAsync( + async () => await service.DeleteProjectCustomField("DP1", "TestField")); + } + } + } +} \ No newline at end of file diff --git a/tests/YouTrackSharp.Tests/Integration/Projects/GetProjectCustomField.cs b/tests/YouTrackSharp.Tests/Integration/Projects/GetProjectCustomField.cs new file mode 100644 index 00000000..153aaa18 --- /dev/null +++ b/tests/YouTrackSharp.Tests/Integration/Projects/GetProjectCustomField.cs @@ -0,0 +1,46 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Xunit; +using YouTrackSharp.Tests.Infrastructure; + +namespace YouTrackSharp.Tests.Integration.Projects +{ + public partial class ProjectCustomFieldsServiceTests + { + public class GetProjectCustomField + { + [Fact] + public async Task Valid_Connection_Gets_CustomFields_For_Project() + { + // Arrange + var connection = Connections.Demo1Token; + var service = connection.ProjectCustomFieldsService(); + string customFieldName = "Assignee"; + + // Act + var result = await service.GetProjectCustomField("DP1", customFieldName); + + // Assert + Assert.NotNull(result); + + Assert.Equal(customFieldName, result.Name); + Assert.Equal(string.Empty, result.Url); + Assert.Equal("user[1]", result.Type); + Assert.True(result.CanBeEmpty); + Assert.Equal("Unassigned", result.EmptyText); + } + + [Fact] + public async Task Invalid_Connection_Throws_UnauthorizedConnectionException() + { + // Arrange + var service = Connections.UnauthorizedConnection.ProjectCustomFieldsService(); + + // Act & Assert + await Assert.ThrowsAsync( + async () => await service.GetProjectCustomField("DP1", "Assignee")); + } + } + } +} \ No newline at end of file diff --git a/tests/YouTrackSharp.Tests/Integration/Projects/GetProjectCustomFields.cs b/tests/YouTrackSharp.Tests/Integration/Projects/GetProjectCustomFields.cs new file mode 100644 index 00000000..198c56c4 --- /dev/null +++ b/tests/YouTrackSharp.Tests/Integration/Projects/GetProjectCustomFields.cs @@ -0,0 +1,46 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Xunit; +using YouTrackSharp.Projects; +using YouTrackSharp.Tests.Infrastructure; + +namespace YouTrackSharp.Tests.Integration.Projects +{ + public partial class ProjectCustomFieldsServiceTests + { + public class GetProjectCustomFields + { + [Fact] + public async Task Valid_Connection_Gets_CustomFields_For_Project() + { + // Arrange + var connection = Connections.Demo1Token; + var service = connection.ProjectCustomFieldsService(); + + // Act + var result = await service.GetProjectCustomFields("DP1"); + + // Assert + Assert.NotEmpty(result); + foreach (var customField in result) + { + Assert.NotNull(customField); + Assert.NotEqual(string.Empty, customField.Name); + Assert.NotEqual(string.Empty, customField.Url); + } + } + + [Fact] + public async Task Invalid_Connection_Throws_UnauthorizedConnectionException() + { + // Arrange + var service = Connections.UnauthorizedConnection.ProjectCustomFieldsService(); + + // Act & Assert + await Assert.ThrowsAsync( + async () => await service.GetProjectCustomFields("DP1")); + } + } + } +} \ No newline at end of file diff --git a/tests/YouTrackSharp.Tests/Integration/Projects/UpdateProjectCustomField.cs b/tests/YouTrackSharp.Tests/Integration/Projects/UpdateProjectCustomField.cs new file mode 100644 index 00000000..d320339e --- /dev/null +++ b/tests/YouTrackSharp.Tests/Integration/Projects/UpdateProjectCustomField.cs @@ -0,0 +1,47 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Xunit; +using YouTrackSharp.Projects; +using YouTrackSharp.Tests.Infrastructure; + +namespace YouTrackSharp.Tests.Integration.Projects +{ + public partial class ProjectCustomFieldsServiceTests + { + public class UpdateProjectCustomField + { + [Fact] + public async Task Valid_Connection_Update_CustomField_For_Project() + { + // Arrange + var connection = Connections.Demo1Password; + var service = connection.ProjectCustomFieldsService(); + var customField = new CustomField {Name = "TestField", EmptyText = "my empty"}; + var projectId = "DP1"; + + // Act + await service.UpdateProjectCustomField(projectId, customField); + + var created = await service.GetProjectCustomField(projectId, customField.Name); + + // Assert + Assert.NotNull(created); + + Assert.Equal(customField.Name, created.Name); + Assert.Equal(customField.EmptyText, created.EmptyText); + } + + [Fact] + public async Task Invalid_Connection_Throws_UnauthorizedConnectionException() + { + // Arrange + var service = Connections.UnauthorizedConnection.ProjectCustomFieldsService(); + + // Act & Assert + await Assert.ThrowsAsync( + async () => await service.GetProjectCustomField("DP1", "TestField")); + } + } + } +} \ No newline at end of file