From 5ac8552abbaec0c7185522f657370279ed743aeb Mon Sep 17 00:00:00 2001 From: mchanyeechoy <96125017+mchanyeechoy@users.noreply.github.com> Date: Wed, 7 Jun 2023 10:35:03 -0400 Subject: [PATCH] Add support for Lint endpoints (#460) * Add Lint Client * Update PublicAPI.Unshipped.txt * Update 7 files * Update PublicAPI.Unshipped.txt * Put the tests yamls in constants --- NGitLab.Mock/Clients/GitLabClient.cs | 2 + NGitLab.Mock/Clients/LintClient.cs | 27 +++++++ NGitLab.Tests/LintClientTests.cs | 106 +++++++++++++++++++++++++++ NGitLab/GitLabClient.cs | 3 + NGitLab/IGitLabClient.cs | 2 + NGitLab/ILintClient.cs | 16 ++++ NGitLab/Impl/LintClient.cs | 46 ++++++++++++ NGitLab/Models/LintCI.cs | 21 ++++++ NGitLab/Models/LintCIOptions.cs | 20 +++++ NGitLab/PublicAPI.Unshipped.txt | 28 +++++++ 10 files changed, 271 insertions(+) create mode 100644 NGitLab.Mock/Clients/LintClient.cs create mode 100644 NGitLab.Tests/LintClientTests.cs create mode 100644 NGitLab/ILintClient.cs create mode 100644 NGitLab/Impl/LintClient.cs create mode 100644 NGitLab/Models/LintCI.cs create mode 100644 NGitLab/Models/LintCIOptions.cs diff --git a/NGitLab.Mock/Clients/GitLabClient.cs b/NGitLab.Mock/Clients/GitLabClient.cs index 137474ef..b5363a23 100644 --- a/NGitLab.Mock/Clients/GitLabClient.cs +++ b/NGitLab.Mock/Clients/GitLabClient.cs @@ -51,6 +51,8 @@ public IGraphQLClient GraphQL public ISearchClient AdvancedSearch => new AdvancedSearchClient(Context); + public ILintClient Lint => new LintClient(Context); + public IEventClient GetEvents() => new EventClient(Context); public IEventClient GetUserEvents(int userId) => new EventClient(Context, userId: userId); diff --git a/NGitLab.Mock/Clients/LintClient.cs b/NGitLab.Mock/Clients/LintClient.cs new file mode 100644 index 00000000..300c7781 --- /dev/null +++ b/NGitLab.Mock/Clients/LintClient.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using NGitLab.Models; + +namespace NGitLab.Mock.Clients +{ + internal class LintClient : ILintClient + { + private readonly ClientContext _context; + + public LintClient(ClientContext context) + { + _context = context; + } + + public Task ValidateCIYamlContentAsync(string projectId, string yamlContent, LintCIOptions options, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public Task ValidateProjectCIConfigurationAsync(string projectId, LintCIOptions options, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + } +} diff --git a/NGitLab.Tests/LintClientTests.cs b/NGitLab.Tests/LintClientTests.cs new file mode 100644 index 00000000..215a9879 --- /dev/null +++ b/NGitLab.Tests/LintClientTests.cs @@ -0,0 +1,106 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using NGitLab.Models; +using NGitLab.Tests.Docker; +using NUnit.Framework; + +namespace NGitLab.Tests +{ + public class LintClientTests + { + private const string ValidCIYaml = @" +variables: + CI_DEBUG_TRACE: ""true"" +build: + script: + - echo test +"; + + private const string InvalidCIYaml = @" +variables: + CI_DEBUG_TRACE: ""true"" +build: + script: + - echo test + this_key_should_not_exist: + - this should fail the linting +"; + + [Test] + [NGitLabRetry] + public async Task LintValidCIYaml() + { + using var context = await GitLabTestContext.CreateAsync(); + var project = context.CreateProject(); + var lintClient = context.Client.Lint; + + var result = await context.Client.Lint.ValidateCIYamlContentAsync(project.Id.ToString(), ValidCIYaml, new(), CancellationToken.None); + + Assert.True(result.Valid); + Assert.False(result.Errors.Any()); + Assert.False(result.Warnings.Any()); + } + + [Test] + [NGitLabRetry] + public async Task LintInvalidCIYaml() + { + using var context = await GitLabTestContext.CreateAsync(); + var project = context.CreateProject(); + var lintClient = context.Client.Lint; + + var result = await context.Client.Lint.ValidateCIYamlContentAsync(project.Id.ToString(), InvalidCIYaml, new(), CancellationToken.None); + + Assert.False(result.Valid); + Assert.True(result.Errors.Any()); + Assert.False(result.Warnings.Any()); + } + + [Test] + [NGitLabRetry] + public async Task LintValidCIProjectYaml() + { + using var context = await GitLabTestContext.CreateAsync(); + var project = context.CreateProject(); + var lintClient = context.Client.Lint; + + context.Client.GetRepository(project.Id).Files.Create(new FileUpsert + { + Branch = project.DefaultBranch, + CommitMessage = "test", + Path = ".gitlab-ci.yml", + Content = ValidCIYaml, + }); + + var result = await context.Client.Lint.ValidateProjectCIConfigurationAsync(project.Id.ToString(), new(), CancellationToken.None); + + Assert.True(result.Valid); + Assert.False(result.Errors.Any()); + Assert.False(result.Warnings.Any()); + } + + [Test] + [NGitLabRetry] + public async Task LintInvalidProjectCIYaml() + { + using var context = await GitLabTestContext.CreateAsync(); + var project = context.CreateProject(); + var lintClient = context.Client.Lint; + + context.Client.GetRepository(project.Id).Files.Create(new FileUpsert + { + Branch = project.DefaultBranch, + CommitMessage = "test", + Path = ".gitlab-ci.yml", + Content = InvalidCIYaml, + }); + + var result = await context.Client.Lint.ValidateProjectCIConfigurationAsync(project.Id.ToString(), new(), CancellationToken.None); + + Assert.False(result.Valid); + Assert.True(result.Errors.Any()); + Assert.False(result.Warnings.Any()); + } + } +} diff --git a/NGitLab/GitLabClient.cs b/NGitLab/GitLabClient.cs index 6ad4cf75..9bb61b5e 100644 --- a/NGitLab/GitLabClient.cs +++ b/NGitLab/GitLabClient.cs @@ -42,6 +42,8 @@ public class GitLabClient : IGitLabClient public IGlobalJobClient Jobs { get; } + public ILintClient Lint { get; } + public RequestOptions Options { get => _api.RequestOptions; @@ -88,6 +90,7 @@ private GitLabClient(GitLabCredentials credentials, RequestOptions options) GraphQL = new GraphQLClient(_api); AdvancedSearch = new SearchClient(_api, "/search"); Jobs = new GlobalJobsClient(_api); + Lint = new LintClient(_api); } [Obsolete("Use GitLabClient constructor instead")] diff --git a/NGitLab/IGitLabClient.cs b/NGitLab/IGitLabClient.cs index fb2994d8..fd60a0da 100644 --- a/NGitLab/IGitLabClient.cs +++ b/NGitLab/IGitLabClient.cs @@ -18,6 +18,8 @@ public interface IGitLabClient IMergeRequestClient MergeRequests { get; } + ILintClient Lint { get; } + /// /// All the user events of GitLab (can be scoped for the current user). /// diff --git a/NGitLab/ILintClient.cs b/NGitLab/ILintClient.cs new file mode 100644 index 00000000..2dc84cdb --- /dev/null +++ b/NGitLab/ILintClient.cs @@ -0,0 +1,16 @@ +using System.Threading; +using System.Threading.Tasks; +using NGitLab.Models; + +namespace NGitLab +{ + /// + /// https://github.com/gitlabhq/gitlabhq/blob/master/doc/api/lint.md + /// + public interface ILintClient + { + Task ValidateCIYamlContentAsync(string projectId, string yamlContent, LintCIOptions options, CancellationToken cancellationToken = default); + + Task ValidateProjectCIConfigurationAsync(string projectId, LintCIOptions options, CancellationToken cancellationToken = default); + } +} diff --git a/NGitLab/Impl/LintClient.cs b/NGitLab/Impl/LintClient.cs new file mode 100644 index 00000000..b97071ae --- /dev/null +++ b/NGitLab/Impl/LintClient.cs @@ -0,0 +1,46 @@ +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using NGitLab.Models; + +namespace NGitLab.Impl +{ + public class LintClient : ILintClient + { + private readonly API _api; + + public LintClient(API api) + { + _api = api; + } + + public Task ValidateCIYamlContentAsync(string projectId, string yamlContent, LintCIOptions options, CancellationToken cancellationToken = default) + { + var url = BuildLintCIUrl(projectId, options); + var data = new + { + content = yamlContent, + }; + + return _api.Post().With(data).ToAsync(url, cancellationToken); + } + + public Task ValidateProjectCIConfigurationAsync(string projectId, LintCIOptions options, CancellationToken cancellationToken = default) + { + var url = BuildLintCIUrl(projectId, options); + + return _api.Get().ToAsync(url, cancellationToken); + } + + private static string BuildLintCIUrl(string projectId, LintCIOptions options) + { + var url = Project.Url + "/" + WebUtility.UrlEncode(projectId) + LintCI.Url; + + url = Utils.AddParameter(url, "dry_run", options.DryRun); + url = Utils.AddParameter(url, "ref", options.Ref); + url = Utils.AddParameter(url, "include_jobs", options.IncludeJobs); + + return url; + } + } +} diff --git a/NGitLab/Models/LintCI.cs b/NGitLab/Models/LintCI.cs new file mode 100644 index 00000000..1ecdd942 --- /dev/null +++ b/NGitLab/Models/LintCI.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace NGitLab.Models +{ + public class LintCI + { + public const string Url = "/ci/lint"; + + [JsonPropertyName("valid")] + public bool Valid { get; set; } + + [JsonPropertyName("merged_yaml")] + public string MergedYaml { get; set; } + + [JsonPropertyName("errors")] + public string[] Errors { get; set; } + + [JsonPropertyName("warnings")] + public string[] Warnings { get; set; } + } +} diff --git a/NGitLab/Models/LintCIOptions.cs b/NGitLab/Models/LintCIOptions.cs new file mode 100644 index 00000000..c02300b2 --- /dev/null +++ b/NGitLab/Models/LintCIOptions.cs @@ -0,0 +1,20 @@ +namespace NGitLab.Models +{ + public class LintCIOptions + { + /// + /// Run pipeline creation simulation, or only do static check. + /// + public bool? DryRun { get; set; } + + /// + /// If the list of jobs that would exist in a static check or pipeline simulation should be included in the response. + /// + public bool? IncludeJobs { get; set; } + + /// + /// When dry_run is true, sets the branch or tag to use. + /// + public string Ref { get; set; } + } +} diff --git a/NGitLab/PublicAPI.Unshipped.txt b/NGitLab/PublicAPI.Unshipped.txt index 753fc4ba..28e05a85 100644 --- a/NGitLab/PublicAPI.Unshipped.txt +++ b/NGitLab/PublicAPI.Unshipped.txt @@ -7,6 +7,7 @@ const NGitLab.Impl.NamespacesClient.Url = "/namespaces" -> string const NGitLab.Models.Commit.Url = "/commits" -> string const NGitLab.Models.Contributor.Url = "/contributors" -> string const NGitLab.Models.Group.Url = "/groups" -> string +const NGitLab.Models.LintCI.Url = "/ci/lint" -> string const NGitLab.Models.MergeRequest.Url = "/merge_requests" -> string const NGitLab.Models.Pipeline.Url = "/pipelines" -> string const NGitLab.Models.PipelineBasic.Url = "/pipelines" -> string @@ -72,6 +73,7 @@ NGitLab.GitLabClient.Groups.get -> NGitLab.IGroupsClient NGitLab.GitLabClient.Issues.get -> NGitLab.IIssueClient NGitLab.GitLabClient.Jobs.get -> NGitLab.IGlobalJobClient NGitLab.GitLabClient.Labels.get -> NGitLab.ILabelClient +NGitLab.GitLabClient.Lint.get -> NGitLab.ILintClient NGitLab.GitLabClient.Members.get -> NGitLab.IMembersClient NGitLab.GitLabClient.MergeRequests.get -> NGitLab.IMergeRequestClient NGitLab.GitLabClient.Namespaces.get -> NGitLab.INamespacesClient @@ -184,6 +186,7 @@ NGitLab.IGitLabClient.Groups.get -> NGitLab.IGroupsClient NGitLab.IGitLabClient.Issues.get -> NGitLab.IIssueClient NGitLab.IGitLabClient.Jobs.get -> NGitLab.IGlobalJobClient NGitLab.IGitLabClient.Labels.get -> NGitLab.ILabelClient +NGitLab.IGitLabClient.Lint.get -> NGitLab.ILintClient NGitLab.IGitLabClient.Members.get -> NGitLab.IMembersClient NGitLab.IGitLabClient.MergeRequests.get -> NGitLab.IMergeRequestClient NGitLab.IGitLabClient.Namespaces.get -> NGitLab.INamespacesClient @@ -297,6 +300,9 @@ NGitLab.ILabelClient.ForProject(int projectId) -> System.Collections.Generic.IEn NGitLab.ILabelClient.GetGroupLabel(int groupId, string name) -> NGitLab.Models.Label NGitLab.ILabelClient.GetLabel(int projectId, string name) -> NGitLab.Models.Label NGitLab.ILabelClient.GetProjectLabel(int projectId, string name) -> NGitLab.Models.Label +NGitLab.ILintClient +NGitLab.ILintClient.ValidateCIYamlContentAsync(string projectId, string yamlContent, NGitLab.Models.LintCIOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +NGitLab.ILintClient.ValidateProjectCIConfigurationAsync(string projectId, NGitLab.Models.LintCIOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task NGitLab.IMembersClient NGitLab.IMembersClient.AddMemberToGroup(string groupId, NGitLab.Models.GroupMemberCreate user) -> NGitLab.Models.Membership NGitLab.IMembersClient.AddMemberToProject(string projectId, NGitLab.Models.ProjectMemberCreate user) -> NGitLab.Models.Membership @@ -546,6 +552,10 @@ NGitLab.Impl.LabelClient.GetGroupLabel(int groupId, string name) -> NGitLab.Mode NGitLab.Impl.LabelClient.GetLabel(int projectId, string name) -> NGitLab.Models.Label NGitLab.Impl.LabelClient.GetProjectLabel(int projectId, string name) -> NGitLab.Models.Label NGitLab.Impl.LabelClient.LabelClient(NGitLab.Impl.API api) -> void +NGitLab.Impl.LintClient +NGitLab.Impl.LintClient.LintClient(NGitLab.Impl.API api) -> void +NGitLab.Impl.LintClient.ValidateCIYamlContentAsync(string projectId, string yamlContent, NGitLab.Models.LintCIOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task +NGitLab.Impl.LintClient.ValidateProjectCIConfigurationAsync(string projectId, NGitLab.Models.LintCIOptions options, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task NGitLab.Impl.MembersClient NGitLab.Impl.MembersClient.AddMemberToGroup(string groupId, NGitLab.Models.GroupMemberCreate user) -> NGitLab.Models.Membership NGitLab.Impl.MembersClient.AddMemberToProject(string projectId, NGitLab.Models.ProjectMemberCreate user) -> NGitLab.Models.Membership @@ -1999,6 +2009,24 @@ NGitLab.Models.LastActivityDate.LastActivityOn.get -> System.DateTimeOffset NGitLab.Models.LastActivityDate.LastActivityOn.set -> void NGitLab.Models.LastActivityDate.Username.get -> string NGitLab.Models.LastActivityDate.Username.set -> void +NGitLab.Models.LintCI +NGitLab.Models.LintCI.Errors.get -> string[] +NGitLab.Models.LintCI.Errors.set -> void +NGitLab.Models.LintCI.LintCI() -> void +NGitLab.Models.LintCI.MergedYaml.get -> string +NGitLab.Models.LintCI.MergedYaml.set -> void +NGitLab.Models.LintCI.Valid.get -> bool +NGitLab.Models.LintCI.Valid.set -> void +NGitLab.Models.LintCI.Warnings.get -> string[] +NGitLab.Models.LintCI.Warnings.set -> void +NGitLab.Models.LintCIOptions +NGitLab.Models.LintCIOptions.DryRun.get -> bool? +NGitLab.Models.LintCIOptions.DryRun.set -> void +NGitLab.Models.LintCIOptions.IncludeJobs.get -> bool? +NGitLab.Models.LintCIOptions.IncludeJobs.set -> void +NGitLab.Models.LintCIOptions.LintCIOptions() -> void +NGitLab.Models.LintCIOptions.Ref.get -> string +NGitLab.Models.LintCIOptions.Ref.set -> void NGitLab.Models.Membership NGitLab.Models.Membership.AccessLevel -> int NGitLab.Models.Membership.AvatarURL -> string