diff --git a/src/GitLabApiClient/EnvironmentClient.cs b/src/GitLabApiClient/EnvironmentClient.cs new file mode 100644 index 00000000..1bcc2f36 --- /dev/null +++ b/src/GitLabApiClient/EnvironmentClient.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using GitLabApiClient.Internal.Http; +using GitLabApiClient.Internal.Paths; +using GitLabApiClient.Internal.Queries; +using GitLabApiClient.Internal.Utilities; +using GitLabApiClient.Models.Projects.Responses; +using GitLabApiClient.Models.Environments.Requests; +using GitLabApiClient.Models.Environments.Responses; +using Environment = GitLabApiClient.Models.Environments.Responses.Environment; + +namespace GitLabApiClient +{ + public sealed class EnvironmentClient : IEnvironmentsClient + { + private readonly GitLabHttpFacade _httpFacade; + private readonly EnvironmentsQueryBuilder _environmentQueryBuilder; + + internal EnvironmentClient( + GitLabHttpFacade httpFacade, + EnvironmentsQueryBuilder environmentQueryBuilder) + { + _httpFacade = httpFacade; + _environmentQueryBuilder = environmentQueryBuilder; + } + + /// + /// Retrieves an environment by its name + /// + /// The ID, path or of the project. + /// The ID of the environment to retrieve. + /// + public async Task GetAsync(ProjectId projectId, int environmentId) => + await _httpFacade.Get($"projects/{projectId}/environments/{environmentId}"); + + /// + /// Get a list of environments + /// + /// The ID, path or of the project. + /// Query options . + /// + public async Task> GetAsync(ProjectId projectId, Action options = null) + { + var queryOptions = new EnvironmentsQueryOptions(); + options?.Invoke(queryOptions); + + string url = _environmentQueryBuilder.Build($"projects/{projectId}/environments", queryOptions); + return await _httpFacade.GetPagedList(url); + } + + /// + /// Create an environment + /// + /// The ID, path or of the project. + /// Create environment request. + /// + public async Task CreateAsync(ProjectId projectId, CreateEnvironmentRequest request) => + await _httpFacade.Post($"projects/{projectId}/environments", request); + + /// + /// Update an environment + /// + /// The ID, path or of the project. + /// Update environment request + /// + public async Task UpdateAsync(ProjectId projectId, UpdateEnvironmentRequest request) => + await _httpFacade.Put($"projects/{projectId}/environments/{request.EnvironmentId}", request); + + /// + /// Stop an environment + /// + /// The ID, path or of the project. + /// The ID of the environment to stop. + /// + public async Task StopAsync(ProjectId projectId, int environmentId) => + await _httpFacade.Post($"projects/{projectId}/environments/{environmentId}/stop"); + + /// + /// Delete an environment + /// + /// The ID, path or of the project. + /// The ID of the environment to delete. + /// + public async Task DeleteAsync(ProjectId projectId, int environmentId) => + await _httpFacade.Delete($"projects/{projectId}/environments/{environmentId}"); + } +} diff --git a/src/GitLabApiClient/GitLabApiClient.csproj b/src/GitLabApiClient/GitLabApiClient.csproj index 374b1209..ae3b6e98 100644 --- a/src/GitLabApiClient/GitLabApiClient.csproj +++ b/src/GitLabApiClient/GitLabApiClient.csproj @@ -19,7 +19,7 @@ - true + true true diff --git a/src/GitLabApiClient/GitLabClient.cs b/src/GitLabApiClient/GitLabClient.cs index d64f7c34..7a746d25 100644 --- a/src/GitLabApiClient/GitLabClient.cs +++ b/src/GitLabApiClient/GitLabClient.cs @@ -60,6 +60,7 @@ public GitLabClient(string hostUrl, string authenticationToken = "", HttpMessage var jobQueryBuilder = new JobQueryBuilder(); var toDoListBuilder = new ToDoListQueryBuilder(); var iterationsBuilder = new IterationsQueryBuilder(); + var environmentBuilder = new EnvironmentsQueryBuilder(); Issues = new IssuesClient(_httpFacade, issuesQueryBuilder, projectIssueNotesQueryBuilder); Uploads = new UploadsClient(_httpFacade); @@ -80,6 +81,7 @@ public GitLabClient(string hostUrl, string authenticationToken = "", HttpMessage ToDoList = new ToDoListClient(_httpFacade, toDoListBuilder); Iterations = new IterationsClient(_httpFacade, iterationsBuilder); Connection = new ConnectionClient(_httpFacade); + Environments = new EnvironmentClient(_httpFacade, environmentBuilder); } /// @@ -175,6 +177,11 @@ public GitLabClient(string hostUrl, string authenticationToken = "", HttpMessage /// public IIterationsClient Iterations { get; } + /// + /// Access GitLab's Environments API. + /// + public IEnvironmentsClient Environments { get; } + /// /// Host address of GitLab instance. For example https://gitlab.example.com or https://gitlab.example.com/api/v4/. /// diff --git a/src/GitLabApiClient/IEnvironmentsClient.cs b/src/GitLabApiClient/IEnvironmentsClient.cs new file mode 100644 index 00000000..8c0f4938 --- /dev/null +++ b/src/GitLabApiClient/IEnvironmentsClient.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using GitLabApiClient.Internal.Paths; +using GitLabApiClient.Models.Projects.Responses; +using GitLabApiClient.Models.Environments.Requests; +using GitLabApiClient.Models.Environments.Responses; +using Environment = GitLabApiClient.Models.Environments.Responses.Environment; + +namespace GitLabApiClient +{ + public interface IEnvironmentsClient + { + /// + /// Get a list of environments + /// + /// The ID, path or of the project. + /// The ID of the environment to retrieve. + /// + Task GetAsync(ProjectId projectId, int environmentId); + + /// + /// Get a list of environments + /// + /// The ID, path or of the project. + /// Query options . + /// + Task> GetAsync(ProjectId projectId, Action options = null); + + /// + /// Create an environment + /// + /// The ID, path or of the project. + /// Create environment request. + /// + Task CreateAsync(ProjectId projectId, CreateEnvironmentRequest request); + + /// + /// Update an environment + /// + /// The ID, path or of the project. + /// Update environment request + /// + Task UpdateAsync(ProjectId projectId, UpdateEnvironmentRequest request); + + /// + /// Stop an environment + /// + /// The ID, path or of the project. + /// The ID of the environment to stop. + /// + Task StopAsync(ProjectId projectId, int environmentId); + + /// + /// Delete an environment + /// + /// The ID, path or of the project. + /// The ID of the environment to delete. + /// + Task DeleteAsync(ProjectId projectId, int environmentId); + } +} diff --git a/src/GitLabApiClient/Internal/Queries/EnvironmentsQueryBuilder.cs b/src/GitLabApiClient/Internal/Queries/EnvironmentsQueryBuilder.cs new file mode 100644 index 00000000..efdea90e --- /dev/null +++ b/src/GitLabApiClient/Internal/Queries/EnvironmentsQueryBuilder.cs @@ -0,0 +1,23 @@ +using System; +using GitLabApiClient.Models.Environments.Requests; + +namespace GitLabApiClient.Internal.Queries +{ + class EnvironmentsQueryBuilder : QueryBuilder + { + protected override void BuildCore(Query query, EnvironmentsQueryOptions options) + { + if (!string.IsNullOrEmpty(options.Name) && !string.IsNullOrEmpty(options.Search)) + throw new InvalidOperationException("Environment queries for 'name' and 'search' are mutually exclusive."); + + if (!string.IsNullOrEmpty(options.Name)) + query.Add("name", options.Name); + + if (!string.IsNullOrEmpty(options.Search)) + query.Add("search", options.Search); + + if (options.States != null) + query.Add("states", options.States.ToString().ToLowerInvariant()); + } + } +} diff --git a/src/GitLabApiClient/Models/Environments/Requests/CreateEnvironmentRequest.cs b/src/GitLabApiClient/Models/Environments/Requests/CreateEnvironmentRequest.cs new file mode 100644 index 00000000..33c420f2 --- /dev/null +++ b/src/GitLabApiClient/Models/Environments/Requests/CreateEnvironmentRequest.cs @@ -0,0 +1,28 @@ +using System; +using GitLabApiClient.Internal.Utilities; +using GitLabApiClient.Models.Environments.Responses; +using Newtonsoft.Json; +using Environment = GitLabApiClient.Models.Environments.Responses.Environment; + +namespace GitLabApiClient.Models.Environments.Requests +{ + /// + /// Used to create an environment in a project. + /// + public sealed class CreateEnvironmentRequest : Environment + { + /// + /// Initializes a new instance of the class. + /// + /// The name of the environment. + /// The external URL for the environment. + public CreateEnvironmentRequest(string name, Uri externalUrl) + { + Guard.NotEmpty(name, nameof(name)); + Guard.NotNullOrDefault(externalUrl, nameof(externalUrl)); + + Name = name; + ExternalUrl = externalUrl; + } + } +} diff --git a/src/GitLabApiClient/Models/Environments/Requests/EnvironmentsQueryOptions.cs b/src/GitLabApiClient/Models/Environments/Requests/EnvironmentsQueryOptions.cs new file mode 100644 index 00000000..387e8f82 --- /dev/null +++ b/src/GitLabApiClient/Models/Environments/Requests/EnvironmentsQueryOptions.cs @@ -0,0 +1,17 @@ +using GitLabApiClient.Models.Environments.Responses; + +namespace GitLabApiClient.Models.Environments.Requests +{ + public sealed class EnvironmentsQueryOptions + { + public string Name { get; set; } + + public string Search { get; set; } + + public EnvironmentState? States { get; set; } + + internal EnvironmentsQueryOptions() + { + } + } +} diff --git a/src/GitLabApiClient/Models/Environments/Requests/UpdateEnvironmentRequest.cs b/src/GitLabApiClient/Models/Environments/Requests/UpdateEnvironmentRequest.cs new file mode 100644 index 00000000..ac7d330c --- /dev/null +++ b/src/GitLabApiClient/Models/Environments/Requests/UpdateEnvironmentRequest.cs @@ -0,0 +1,32 @@ +using System; +using GitLabApiClient.Internal.Utilities; +using Newtonsoft.Json; + +namespace GitLabApiClient.Models.Environments.Requests +{ + /// + /// Used to update an environment in a project + /// + public sealed class UpdateEnvironmentRequest + { + [JsonProperty("environment_id")] + public int EnvironmentId { get; set; } + + [JsonProperty("external_url")] + public Uri ExternalUrl { get; set; } + + /// + /// Initializes a new instance of the class + /// + /// The ID of the environment + /// The new external URL + public UpdateEnvironmentRequest(int environmentId, Uri externalUrl) + { + Guard.NotNullOrDefault(environmentId, nameof(environmentId)); + Guard.NotNullOrDefault(externalUrl, nameof(externalUrl)); + + EnvironmentId = environmentId; + ExternalUrl = externalUrl; + } + } +} diff --git a/src/GitLabApiClient/Models/Environments/Responses/Environment.cs b/src/GitLabApiClient/Models/Environments/Responses/Environment.cs new file mode 100644 index 00000000..5f562c79 --- /dev/null +++ b/src/GitLabApiClient/Models/Environments/Responses/Environment.cs @@ -0,0 +1,30 @@ +using System; +using GitLabApiClient.Models.Environments.Responses; +using Newtonsoft.Json; + +namespace GitLabApiClient.Models.Environments.Responses +{ + public class Environment + { + [JsonProperty("id")] + public int Id { get; set; } + + [JsonProperty("name")] + public string Name { get; set; } + + [JsonProperty("slug")] + public string Slug { get; set; } + + [JsonProperty("external_url")] + public Uri ExternalUrl { get; set; } + + [JsonProperty("state")] + public EnvironmentState State { get; set; } + + [JsonProperty("created_at")] + public DateTime? CreatedAt { get; set; } + + [JsonProperty("updated_at")] + public DateTime? UpdatedAt { get; set; } + } +} diff --git a/src/GitLabApiClient/Models/Environments/Responses/EnvironmentState.cs b/src/GitLabApiClient/Models/Environments/Responses/EnvironmentState.cs new file mode 100644 index 00000000..4e2ac8b7 --- /dev/null +++ b/src/GitLabApiClient/Models/Environments/Responses/EnvironmentState.cs @@ -0,0 +1,8 @@ +namespace GitLabApiClient.Models.Environments.Responses +{ + public enum EnvironmentState + { + Available, + Stopped + } +} diff --git a/test/GitLabApiClient.Test/EnvironmentsTest.cs b/test/GitLabApiClient.Test/EnvironmentsTest.cs new file mode 100644 index 00000000..92ddc7d3 --- /dev/null +++ b/test/GitLabApiClient.Test/EnvironmentsTest.cs @@ -0,0 +1,112 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +using GitLabApiClient.Internal.Queries; +using GitLabApiClient.Models.Environments.Requests; +using GitLabApiClient.Models.Environments.Responses; +using Xunit; +using static GitLabApiClient.Test.Utilities.GitLabApiHelper; +using Environment = GitLabApiClient.Models.Environments.Responses.Environment; + +namespace GitLabApiClient.Test +{ + [Trait("Category", "LinuxIntegration")] + [Collection("GitLabContainerFixture")] + public class EnvironmentsTest + { + private readonly EnvironmentClient _sut = new EnvironmentClient(GetFacade(), new EnvironmentsQueryBuilder()); + + [Fact] + public async Task CreatedEnvironmentCanBeUpdated() + { + //arrange + const string testEnvironment = "Test Env Name"; + var externalUrl = new Uri("https://dev.testingthis.com"); + var createdEnvironment = await _sut.CreateAsync(TestProjectTextId, + new CreateEnvironmentRequest(testEnvironment, externalUrl)); + + //act + var updatedExternalUrl = new Uri("https://beta.testingthis.com"); + var updatedEnvironment = await _sut.UpdateAsync(TestProjectTextId, + new UpdateEnvironmentRequest(createdEnvironment.Id, updatedExternalUrl)); + + //assert + updatedEnvironment.Should().Match(i => + i.Name == testEnvironment && + i.ExternalUrl == updatedExternalUrl); + } + + [Fact] + public async Task CreatedEnvironmentCanBeFetched() + { + //arrange + const string testEnvironment = "Test Env Name"; + var externalUrl = new Uri("https://dev.testingthis.com"); + var createdEnvironment = await _sut.CreateAsync(TestProjectTextId, + new CreateEnvironmentRequest(testEnvironment, externalUrl)); + + //act + var fetchedEnvironment = await _sut.GetAsync(TestProjectTextId, createdEnvironment.Id); + + //assert + fetchedEnvironment.Should().Match(i => + i.Name == testEnvironment && + i.ExternalUrl == externalUrl); + } + + [Fact] + public async Task CreatedEnvironmentCanBeListed() + { + //arrange + const string testEnvironment = "Test Env Name"; + var externalUrl = new Uri("https://dev.testingthis.com"); + var createdEnvironment = await _sut.CreateAsync(TestProjectTextId, + new CreateEnvironmentRequest(testEnvironment, externalUrl)); + + //act + var environmentList = await _sut.GetAsync(TestProjectTextId); + + //assert + environmentList.Should().Contain(i => + i.Name == testEnvironment && + i.ExternalUrl == externalUrl); + } + + [Fact] + public async Task CreatedEnvironmentCanBeStopped() + { + //arrange + const string testEnvironment = "Test Env Name"; + var externalUrl = new Uri("https://dev.testingthis.com"); + var createdEnvironment = await _sut.CreateAsync(TestProjectTextId, + new CreateEnvironmentRequest(testEnvironment, externalUrl)); + + //act + await _sut.StopAsync(TestProjectTextId, createdEnvironment.Id); + + //assert + var fetchedEnvironment = await _sut.GetAsync(TestProjectTextId); + fetchedEnvironment.Should().Contain(i => + i.Name == testEnvironment && + i.ExternalUrl == externalUrl && + i.State == EnvironmentState.Stopped); + } + + [Fact] + public async Task CreatedEnvironmentCanBeDeleted() + { + //arrange + const string testEnvironment = "Test Env Name"; + var externalUrl = new Uri("https://dev.testingthis.com"); + var createdEnvironment = await _sut.CreateAsync(TestProjectTextId, + new CreateEnvironmentRequest(testEnvironment, externalUrl)); + + //act + await _sut.DeleteAsync(TestProjectTextId, createdEnvironment.Id); + + //assert + var fetchedEnvironment = await _sut.GetAsync(TestProjectTextId); + fetchedEnvironment.Should().BeEmpty(); + } + } +} diff --git a/test/GitLabApiClient.Test/Internal/Queries/EnvironmentsQueryBuilderTest.cs b/test/GitLabApiClient.Test/Internal/Queries/EnvironmentsQueryBuilderTest.cs new file mode 100644 index 00000000..61aa4738 --- /dev/null +++ b/test/GitLabApiClient.Test/Internal/Queries/EnvironmentsQueryBuilderTest.cs @@ -0,0 +1,64 @@ +using System; +using FluentAssertions; +using GitLabApiClient.Internal.Queries; +using GitLabApiClient.Models.Environments.Requests; +using GitLabApiClient.Models.Environments.Responses; +using Xunit; + +namespace GitLabApiClient.Test.Internal.Queries +{ + public class EnvironmentsQueryBuilderTest + { + [Fact] + public void NameQueryBuilt() + { + var sut = new EnvironmentsQueryBuilder(); + + string query = sut.Build( + "https://gitlab.com/api/v4/projects/projectId/environments", + new EnvironmentsQueryOptions() + { + Name = "Test Env Name", + States = EnvironmentState.Available + }); + + query.Should().Be("https://gitlab.com/api/v4/projects/projectId/environments?" + + "name=Test%20Env%20Name&" + + "states=available"); + } + + [Fact] + public void SearchQueryBuilt() + { + var sut = new EnvironmentsQueryBuilder(); + + string query = sut.Build( + "https://gitlab.com/api/v4/projects/projectId/environments", + new EnvironmentsQueryOptions() + { + States = EnvironmentState.Available, + Search = "filter env" + }); + + query.Should().Be("https://gitlab.com/api/v4/projects/projectId/environments?" + + "search=filter%20env&" + + "states=available"); + } + + [Fact] + public void NameAndSearchMutuallyExclusive() + { + var sut = new EnvironmentsQueryBuilder(); + + Assert.Throws(()=> + sut.Build( + "https://gitlab.com/api/v4/projects/projectId/environments", + new EnvironmentsQueryOptions() + { + Name = "Test Env Name", + States = EnvironmentState.Available, + Search = "filter env" + })); + } + } +}