Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ jobs:
gitlab:
# Keep in sync with the version in GitLabDockerContainer.cs
# Available tags: https://hub.docker.com/r/gitlab/gitlab-ee/tags?name=-ee.0
- "gitlab/gitlab-ee:16.11.10-ee.0"
- "gitlab/gitlab-ee:17.1.8-ee.0"
- "gitlab/gitlab-ee:18.1.6-ee.0"
configuration: [Release]
fail-fast: false
services:
Expand Down
87 changes: 77 additions & 10 deletions NGitLab.Tests/Docker/GitLabDockerContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public class GitLabDockerContainer
/// <para>Keep in sync with .github/workflows/ci.yml, use the lowest supported version</para>
/// <para>List of available versions: https://hub.docker.com/r/gitlab/gitlab-ee/tags/</para>
/// </remarks>
private const string LocalGitLabDockerVersion = "17.1.8-ee.0";
private const string LocalGitLabDockerVersion = "18.1.6-ee.0";

/// <summary>
/// Resolved GitLab version taken from the help page once logged in
Expand Down Expand Up @@ -208,8 +208,41 @@ private async Task SpawnDockerContainerAsync()
{
{ HttpPort.ToString(CultureInfo.InvariantCulture) + "/tcp", new List<PortBinding> { new PortBinding { HostPort = HttpPort.ToString(CultureInfo.InvariantCulture) } } },
},

// Update size of /dev/shm to to 512mb (default: 64mb)
// Avoids intermittent crashes of GitLab
ShmSize = 512 * 1024 * 1024,
};

// Disables non-useful features
// See https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-config-template/gitlab.rb.template
string[] omnibusConfig =
[
$"external_url 'http://localhost:{HttpPort.ToString(CultureInfo.InvariantCulture)}/'",
"gitlab_rails['gitlab_email_enabled'] = false",
"gitlab_rails['incoming_email_enabled'] = false",
"gitlab_rails['lfs_enabled'] = false",
"gitlab_rails['terraform_state_enabled'] = false",
"gitlab_rails['pages_object_store_enabled'] = false",
"gitlab_rails['usage_ping_enabled'] = false",
"gitlab_rails['registry_enabled'] = false",
"registry['enable'] = false",
"sidekiq['metrics_enabled'] = false",
"logrotate['enable'] = false",
"gitlab_pages['enable'] = false",
"gitlab_rails['gitlab_kas_enabled'] = false",
"mattermost['enable'] = false",
"alertmanager['enable'] = false",
"node_exporter['enable'] = false",
"redis_exporter['enable'] = false",
"postgres_exporter['enable'] = false",
"pgbouncer_exporter['enable'] = false",
"gitlab_exporter['enable'] = false",
"gitlab_rails['kerberos_enabled'] = false",
"gitlab_rails['packages_enabled'] = false",
"gitlab_rails['dependency_proxy_enabled'] = false",
];

var response = await client.Containers.CreateContainerAsync(new CreateContainerParameters
{
Hostname = "localhost",
Expand All @@ -223,8 +256,8 @@ private async Task SpawnDockerContainerAsync()
},
Env =
[
"GITLAB_OMNIBUS_CONFIG=external_url 'http://localhost:" + HttpPort.ToString(CultureInfo.InvariantCulture) + "/'",
"GITLAB_ROOT_PASSWORD=" + AdminPassword,
$"GITLAB_ROOT_PASSWORD={AdminPassword}",
$"GITLAB_OMNIBUS_CONFIG={string.Join("; ", omnibusConfig)}",
],
}).ConfigureAwait(false);

Expand Down Expand Up @@ -303,21 +336,28 @@ async Task GenerateAdminToken(GitLabCredential credentials)
var gitLabVersionAsNuGetVersion = NuGetVersion.Parse(ResolvedGitLabVersion);
var isMajorVersion15 = VersionRange.Parse("[15.0,16.0)").Satisfies(gitLabVersionAsNuGetVersion);
var isMajorVersionAtLeast16 = VersionRange.Parse("[16.0,)").Satisfies(gitLabVersionAsNuGetVersion);
var isMajorVersionAtLeast18 = VersionRange.Parse("[18.0,)").Satisfies(gitLabVersionAsNuGetVersion);

TestContext.Progress.WriteLine("Creating root token");

var accessTokenRelativeUri = "/-/profile/personal_access_tokens";
if (isMajorVersionAtLeast18)
{
accessTokenRelativeUri = "/-/user_settings/personal_access_tokens";
}

var page = await browserContext.NewPageAsync();
await page.GotoAsync(GitLabUrl + "/-/profile/personal_access_tokens");
await page.GotoAsync(new Uri(GitLabUrl, accessTokenRelativeUri).ToString());

var formLocator = page.Locator("main#content-body form");

var tokenName = "GitLabClientTest-" + DateTime.UtcNow.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture);

if (isMajorVersion15)
if (isMajorVersionAtLeast18)
{
// Try the "old" 15.x.y way
formLocator = page.Locator("main#content-body form");
await formLocator.GetByLabel("Token name").FillAsync(tokenName);
await page.Locator("main[id='content-body'] button[data-testid='add-new-token-button']").ClickAsync(new LocatorClickOptions { Timeout = 5_000 });
formLocator = page.Locator("form[id='token-create-form']");
await formLocator.Locator("input[data-testid='access-token-name-field']").FillAsync(tokenName);
}
else if (isMajorVersionAtLeast16)
{
Expand All @@ -327,6 +367,12 @@ async Task GenerateAdminToken(GitLabCredential credentials)
formLocator = page.Locator("main[id='content-body'] form[id='js-new-access-token-form']");
await formLocator.Locator("input[data-testid='access-token-name-field']").FillAsync(tokenName);
}
else if (isMajorVersion15)
{
// Try the "old" 15.x.y way
formLocator = page.Locator("main#content-body form");
await formLocator.GetByLabel("Token name").FillAsync(tokenName);
}
else
{
s_creationErrorMessage = $"Unable to generate an admin token: resolved GitLab version '{ResolvedGitLabVersion}' doesn't match any supported range in '{nameof(GenerateCredentialsAsync)}'.";
Expand All @@ -338,9 +384,19 @@ async Task GenerateAdminToken(GitLabCredential credentials)
await checkbox.CheckAsync(new LocatorCheckOptions { Force = true });
}

await formLocator.GetByRole(AriaRole.Button, new() { Name = "Create personal access token" }).ClickAsync();
string token = null;
if (isMajorVersionAtLeast18)
{
await formLocator.GetByTestId("create-token-button").ClickAsync();
await page.GetByRole(AriaRole.Alert).GetByLabel("Click to reveal").ClickAsync();
token = await page.GetByTestId("created-access-token-field").InputValueAsync();
}
else
{
await formLocator.GetByRole(AriaRole.Button, new() { Name = "Create personal access token" }).ClickAsync();
token = await page.Locator("button[title='Copy personal access token']").GetAttributeAsync("data-clipboard-text");
}

var token = await page.Locator("button[title='Copy personal access token']").GetAttributeAsync("data-clipboard-text");
credentials.AdminUserToken = token;

// Get admin login cookie
Expand Down Expand Up @@ -508,6 +564,8 @@ private async Task ResolveGitLabVersionAsync(IBrowserContext browserContext)

ResolvedGitLabVersion = version.Trim().TrimStart('v');
Console.WriteLine($"GitLab resolved version is '{ResolvedGitLabVersion}'");

await CloseRedesignModal(page);
}

private async Task LoginAsync(IBrowserContext browserContext)
Expand Down Expand Up @@ -552,5 +610,14 @@ await page.RunAndWaitForResponseAsync(async () =>
}, response => response.Status == 200);
}

private async Task CloseRedesignModal(IPage page)
{
var isModalVisible = await page.IsVisibleAsync("div#dap_welcome_modal button[aria-label='Close']");
if (isModalVisible)
{
await page.Locator("div#dap_welcome_modal button[aria-label='Close']").ClickAsync();
}
}

private static Task<string> GetCurrentUrl(IPage page) => page.EvaluateAsync<string>("window.location.pathname");
}
34 changes: 32 additions & 2 deletions NGitLab.Tests/ProjectsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -535,11 +535,18 @@ public async Task UpdateAsync_WhenProjectNotFound_ItThrows()
Assert.That(ex.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
}

/// <remarks>
/// <para>On versions prior to v18, projects where deleted immediately.</para>
/// <para>On v18 and above, it is by default "marked for deletion" and deleted after 7 days.</para>
/// <para>Although the default behavior can be changed (admin settings), a new test has been created to validate the "mark for deletion behavior". See <see cref="DeleteAsync_WhenProjectExists_ItIsMarkedForDeletion"/>.</para>
/// </remarks>
[Test]
[NGitLabRetry]
public async Task DeleteAsync_WhenProjectExists_ItIsDeleted()
{
using var context = await GitLabTestContext.CreateAsync();
context.IgnoreTestIfGitLabVersionOutOfRange(VersionRange.Parse("[,18.0)"));

var group = context.CreateGroup();
var project = context.CreateProject(group.Id);
var projectClient = context.Client.Projects;
Expand All @@ -551,6 +558,26 @@ public async Task DeleteAsync_WhenProjectExists_ItIsDeleted()
Assert.ThrowsAsync<GitLabException>(() => projectClient.GetAsync(project.Id));
}

[Test]
[NGitLabRetry]
public async Task DeleteAsync_WhenProjectExists_ItIsMarkedForDeletion()
{
using var context = await GitLabTestContext.CreateAsync();
context.IgnoreTestIfGitLabVersionOutOfRange(VersionRange.Parse("[18.0,)"));

var group = context.CreateGroup();
var project = context.CreateProject(group.Id);
var projectClient = context.Client.Projects;

// Act
await projectClient.DeleteAsync(project.Id);

// Assert
var projectMarkedForDeletion = await projectClient.GetAsync(project.Id);
Assert.That(projectMarkedForDeletion.MarkedForDeletionOn, Is.Not.Null);
Assert.That(projectMarkedForDeletion.MarkedForDeletionOn.Value.Date, Is.EqualTo(DateTime.UtcNow.Date));
}

[Test]
[NGitLabRetry]
public async Task DeleteAsync_WhenProjectNotFound_ItThrows()
Expand Down Expand Up @@ -791,14 +818,17 @@ public async Task Test_project_groups_query_returns_ancestor_groups()
Assert.That(groups.Select(g => g.Id), Is.EquivalentTo(new[] { group.Id, subgroup.Id }));
}

/// <remarks>
/// <para>On v18 and above, Job Token Permissions are enforced by default.</para>
/// <para>Although the default behavior can be changed (admin settings), we should create a new test to toggle the job token allow list on admin section to validate the old behavior (<see href="https://github.com/ubisoft/NGitLab/issues/1051")/>.</para>
/// </remarks>
[Test]
[NGitLabRetry]
public async Task GetAndSetProjectJobTokenScope()
{
// Arrange
using var context = await GitLabTestContext.CreateAsync();

context.IgnoreTestIfGitLabVersionOutOfRange(VersionRange.Parse("[16.1,)"));
context.IgnoreTestIfGitLabVersionOutOfRange(VersionRange.Parse("[16.1,18.0)"));

var project = context.CreateProject();
var gitLabClient = context.Client;
Expand Down
24 changes: 22 additions & 2 deletions NGitLab.Tests/RepositoryClient/RepositoryClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using NGitLab.Models;
using NGitLab.Tests.Docker;
using NuGet.Versioning;
using NUnit.Framework;

namespace NGitLab.Tests.RepositoryClient;
Expand Down Expand Up @@ -268,16 +270,34 @@ public async Task GetAllTreeObjectsInPathWith100ElementsByPage()
Assert.That(treeObjects, Is.Not.Empty);
}

/// <remarks>
/// See <see href="https://docs.gitlab.com/update/deprecations/#gitlab-177"/>.
/// </remarks>
[Test]
[NGitLabRetry]
public async Task GetAllTreeObjectsAtInvalidPath()
public async Task GetAllTreeObjectsAtInvalidPathReturnsEmpty()
{
using var context = await RepositoryClientTestsContext.CreateAsync(commitCount: 2);
context.Context.IgnoreTestIfGitLabVersionOutOfRange(VersionRange.Parse("[,17.7)"));

var treeObjects = context.RepositoryClient.GetTree("Fakepath");
var treeObjects = context.RepositoryClient.GetTree("Fakepath").ToArray();
Assert.That(treeObjects, Is.Empty);
}

/// <remarks>
/// See <see href="https://docs.gitlab.com/update/deprecations/#gitlab-177"/>.
/// </remarks>
[Test]
[NGitLabRetry]
public async Task GetAllTreeObjectsAtInvalidPathReturnsNotFound()
{
using var context = await RepositoryClientTestsContext.CreateAsync(commitCount: 2);
context.Context.IgnoreTestIfGitLabVersionOutOfRange(VersionRange.Parse("[17.7,)"));

var exception = Assert.Throws<GitLabException>(() => context.RepositoryClient.GetTree("Fakepath").ToArray());
Assert.That(exception.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
}

[TestCase(CommitRefType.All)]
[TestCase(CommitRefType.Branch)]
[TestCase(CommitRefType.Tag)]
Expand Down
68 changes: 68 additions & 0 deletions NGitLab.Tests/RunnerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Threading.Tasks;
using NGitLab.Models;
using NGitLab.Tests.Docker;
using NuGet.Versioning;
using NUnit.Framework;
using Polly;
using Polly.Retry;
Expand All @@ -12,12 +13,21 @@ namespace NGitLab.Tests;

public class RunnerTests
{
/// <remarks>
/// <para>Project runner ownership is enforced on v18.</para>
/// <para>It is not possible to unassign a runner from the owner project. Runner should be deleted instead.</para>
/// <para>See <see href="https://docs.gitlab.com/ci/runners/runners_scope/#project-runner-ownership"/>.</para>
/// <para>See <see cref="Test_can_enable_disable_and_delete_a_runner_on_projects"/> for a scenario with the runner deletion on owner project.</para>
/// <para>See <see cref="Test_cannot_disable_runner_on_owner_project"/> for a scenario that validates the restriction on owner project.</para>
/// </remarks>
[Test]
[NGitLabRetry]
public async Task Test_can_enable_and_disable_a_runner_on_a_project()
{
// We need 2 projects associated to a runner to disable a runner
using var context = await GitLabTestContext.CreateAsync();
context.IgnoreTestIfGitLabVersionOutOfRange(VersionRange.Parse("[,18.0)"));

var project1 = context.CreateProject(initializeWithCommits: true);
var project2 = context.CreateProject(initializeWithCommits: true);

Expand All @@ -34,6 +44,57 @@ public async Task Test_can_enable_and_disable_a_runner_on_a_project()
bool IsEnabled() => runnersClient[runner.Id].Projects.Any(x => x.Id == project1.Id);
}

[Test]
[NGitLabRetry]
public async Task Test_can_enable_disable_and_delete_a_runner_on_projects()
{
using var context = await GitLabTestContext.CreateAsync();

var project1 = context.CreateProject(initializeWithCommits: true);
var project2 = context.CreateProject(initializeWithCommits: true);

var runnersClient = context.Client.Runners;

// Register a runner on project 1 (owner of the runner)
var runner = runnersClient.Register(new RunnerRegister { Token = project1.RunnersToken });

// It Should be enabled by default
Assert.That(IsEnabledOnProject(project1), Is.True);

runnersClient.EnableRunner(project2.Id, new RunnerId(runner.Id));
Assert.That(IsEnabledOnProject(project2), Is.True);

// Runner can be disabled on projects that does not owns it
runnersClient.DisableRunner(project2.Id, new RunnerId(runner.Id));
Assert.That(IsEnabledOnProject(project2), Is.False);

// And the only way to unregister it from the owner project is to delete it
runnersClient.Delete(runner.Id);
var ex = Assert.Throws<GitLabException>(() => IsRegistered(runner.Id));
Assert.That(ex.StatusCode, Is.EqualTo(HttpStatusCode.NotFound));

bool IsEnabledOnProject(Project project) => runnersClient[runner.Id].Projects.Any(x => x.Id == project.Id);
bool IsRegistered(long runnerId) => GetRetryPolicy().Execute(() => runnersClient[runnerId].Projects.Length != 0);
}

[Test]
[NGitLabRetry]
public async Task Test_cannot_disable_runner_on_owner_project()
{
using var context = await GitLabTestContext.CreateAsync();
context.IgnoreTestIfGitLabVersionOutOfRange(VersionRange.Parse("[18.0,)"));

var project = context.CreateProject(initializeWithCommits: true);

var runnersClient = context.Client.Runners;
var runner = runnersClient.Register(new RunnerRegister { Token = project.RunnersToken });
Assert.That(IsEnabledOnProject(project), Is.True);

var exception = Assert.Throws<GitLabException>(() => runnersClient.DisableRunner(project.Id, new RunnerId(runner.Id)));
Assert.That(exception.StatusCode, Is.EqualTo(HttpStatusCode.Forbidden));
bool IsEnabledOnProject(Project project) => runnersClient[runner.Id].Projects.Any(x => x.Id == project.Id);
}

[Test]
[NGitLabRetry]
public async Task Test_can_register_and_delete_a_runner_on_a_group()
Expand All @@ -56,11 +117,18 @@ public async Task Test_can_register_and_delete_a_runner_on_a_group()
bool IsRegistered() => GetRetryPolicy().Execute(() => runnersClient[runner.Id].Groups.Any(x => x.Id == createdGroup1.Id));
}

/// <remarks>
/// <para>Project runner ownership is enforced on v18.</para>
/// <para>It is not possible to unassign a runner from the owner project. Delete the runner instead.</para>
/// <para>See <see href="https://docs.gitlab.com/ci/runners/runners_scope/#project-runner-ownership"/>.</para>
/// </remarks>
[Test]
[NGitLabRetry]
public async Task Test_can_find_a_runner_on_a_project()
{
using var context = await GitLabTestContext.CreateAsync();
context.IgnoreTestIfGitLabVersionOutOfRange(VersionRange.Parse("[,18.0)"));

var project = context.CreateProject(initializeWithCommits: true);
var project2 = context.CreateProject(initializeWithCommits: true);
var runnersClient = context.Client.Runners;
Expand Down
3 changes: 3 additions & 0 deletions NGitLab/Models/Project.cs
Original file line number Diff line number Diff line change
Expand Up @@ -227,4 +227,7 @@ public class Project

[JsonPropertyName("ci_default_git_depth")]
public int? CiDefaultGitDepth { get; set; }

[JsonPropertyName("marked_for_deletion_on")]
public DateTime? MarkedForDeletionOn { get; set; }
}
Loading
Loading