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
120 changes: 120 additions & 0 deletions NGitLab.Mock.Tests/MergeRequestCommentsMockTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
using System;
using System.Linq;
using System.Net;
using NGitLab.Models;
using NUnit.Framework;

namespace NGitLab.Mock.Tests;

public class MergeRequestCommentsMockTests
{
[Test]
public void AddComment_CreatesIndividualNoteWithSyntheticDiscussionId()
{
var (server, project, mr, user) = MergeRequestMockTestHelper.CreateProjectWithMergeRequest();
using (server)
{
var client = server.CreateClient(user).GetMergeRequest(project.Id);

client.Comments(mr.Iid).Add(new MergeRequestCommentCreate { Body = "Plain note" });

var discussion = client.Comments(mr.Iid).Discussions.Single(d => string.Equals(d.Notes[0].Body, "Plain note", StringComparison.Ordinal));

Assert.That(discussion.IndividualNote, Is.True, "a comment added outside the discussions endpoint has no thread id and is reported as an individual note");
Assert.That(discussion.Id, Is.Not.Null.And.Not.Empty);
Assert.That(discussion.Notes, Has.Length.EqualTo(1));
}
}

[Test]
public void EditComment_UpdatesBodyAndPersists()
{
var (server, project, mr, user) = MergeRequestMockTestHelper.CreateProjectWithMergeRequest();
using (server)
{
var client = server.CreateClient(user).GetMergeRequest(project.Id);

var comment = client.Comments(mr.Iid).Add(new MergeRequestCommentCreate { Body = "Original body" });
var edited = client.Comments(mr.Iid).Edit(comment.Id, new MergeRequestCommentEdit { Body = "Edited body" });

Assert.That(edited.Body, Is.EqualTo("Edited body"));

var reread = client.Comments(mr.Iid).All.Single(c => c.Id == comment.Id);
Assert.That(reread.Body, Is.EqualTo("Edited body"));
}
}

[Test]
public void DeleteComment_RemovesCommentFromMergeRequest()
{
var (server, project, mr, user) = MergeRequestMockTestHelper.CreateProjectWithMergeRequest();
using (server)
{
var client = server.CreateClient(user).GetMergeRequest(project.Id);

var comment = client.Comments(mr.Iid).Add(new MergeRequestCommentCreate { Body = "To be deleted" });
client.Comments(mr.Iid).Delete(comment.Id);

Assert.That(client.Comments(mr.Iid).All.Any(c => c.Id == comment.Id), Is.False);
}
}

[Test]
public void Reply_AppendsNoteToExistingDiscussionThread()
{
var (server, project, mr, user) = MergeRequestMockTestHelper.CreateProjectWithMergeRequest();
using (server)
{
var client = server.CreateClient(user).GetMergeRequest(project.Id);

var discussion = client.Discussions(mr.Iid).Add(new MergeRequestDiscussionCreate { Body = "Original" });
client.Comments(mr.Iid).Add(discussion.Id, new MergeRequestCommentCreate { Body = "A reply" });

var reread = client.Discussions(mr.Iid).Get(discussion.Id);
Assert.That(reread.IndividualNote, Is.False);
Assert.That(reread.Notes.Select(n => n.Body), Is.EqualTo(new[] { "Original", "A reply" }));

var discussions = client.Comments(mr.Iid).Discussions.ToArray();
Assert.That(discussions, Has.Length.EqualTo(1), "the reply should join the existing thread instead of creating a new one");
Assert.That(discussions[0].Notes, Has.Length.EqualTo(2));
}
}

[Test]
public void Reply_UnknownDiscussionId_ThrowsNotFound()
{
var (server, project, mr, user) = MergeRequestMockTestHelper.CreateProjectWithMergeRequest();
using (server)
{
var client = server.CreateClient(user).GetMergeRequest(project.Id);

Assert.That(Assert.Throws<GitLabException>((Action)(() => client.Comments(mr.Iid).Add("unknown-id", new MergeRequestCommentCreate { Body = "x" }))).StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
}
}

[Test]
public void Comments_UnknownId_ThrowNotFound()
{
var (server, project, mr, user) = MergeRequestMockTestHelper.CreateProjectWithMergeRequest();
using (server)
{
var client = server.CreateClient(user).GetMergeRequest(project.Id);

Assert.That(Assert.Throws<GitLabException>((Action)(() => client.Comments(mr.Iid).Edit(99999, new MergeRequestCommentEdit { Body = "x" }))).StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
Assert.That(Assert.Throws<GitLabException>((Action)(() => client.Comments(mr.Iid).Delete(99999))).StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
}
}

[Test]
public void ArchivedProject_AddComment_ThrowsForbidden()
{
var (server, project, mr, user) = MergeRequestMockTestHelper.CreateProjectWithMergeRequest();
using (server)
{
project.Archived = true;
var client = server.CreateClient(user).GetMergeRequest(project.Id);

Assert.That(Assert.Throws<GitLabException>((Action)(() => client.Comments(mr.Iid).Add(new MergeRequestCommentCreate { Body = "Should fail" }))).StatusCode, Is.EqualTo(HttpStatusCode.Forbidden));
}
}
}
188 changes: 188 additions & 0 deletions NGitLab.Mock.Tests/MergeRequestDiscussionsMockTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
using System;
using System.Linq;
using System.Net;
using NGitLab.Models;
using NUnit.Framework;

namespace NGitLab.Mock.Tests;

public class MergeRequestDiscussionsMockTests
{
[Test]
public void AddDiscussion_GeneralComment_IsRetrievableThroughBothCommentsAndDiscussionsClients()
{
var (server, project, mr, user) = MergeRequestMockTestHelper.CreateProjectWithMergeRequest();
using (server)
{
var client = server.CreateClient(user).GetMergeRequest(project.Id);

var discussion = client.Discussions(mr.Iid).Add(new MergeRequestDiscussionCreate
{
Body = "General comment",
});

Assert.That(discussion.Notes[0].Body, Is.EqualTo("General comment"));
Assert.That(discussion.Notes[0].Position, Is.Null);
Assert.That(discussion.Id, Is.Not.Null.And.Not.Empty);
Assert.That(discussion.IndividualNote, Is.False, "a discussion created via the discussions endpoint is not an individual note, even with a single comment");

var comments = client.Comments(mr.Iid).All.ToArray();
Assert.That(comments.Any(c => string.Equals(c.Body, "General comment", StringComparison.Ordinal)), Is.True);

var rereadFromCommentsClient = client.Comments(mr.Iid).Discussions.Single(d => string.Equals(d.Notes[0].Body, "General comment", StringComparison.Ordinal));
Assert.That(rereadFromCommentsClient.Id, Is.EqualTo(discussion.Id));
Assert.That(rereadFromCommentsClient.IndividualNote, Is.False);

var rereadById = client.Discussions(mr.Iid).Get(discussion.Id);
Assert.That(rereadById.Notes[0].Body, Is.EqualTo("General comment"));
}
}

[Test]
public void AddDiscussion_InlineComment_RoundtripsPositionAndHeadSha()
{
var (server, project, mr, user) = MergeRequestMockTestHelper.CreateProjectWithMergeRequest();
using (server)
{
var client = server.CreateClient(user).GetMergeRequest(project.Id);
var version = client.GetVersionsAsync(mr.Iid).First();
var position = MergeRequestMockTestHelper.CreateTextPosition(version);

var discussion = client.Discussions(mr.Iid).Add(new MergeRequestDiscussionCreate
{
Body = "Inline comment",
Position = position,
});

Assert.That(discussion.Notes[0].Position, Is.Not.Null);
Assert.That(discussion.Notes[0].Position.NewPath, Is.EqualTo("file.txt"));
Assert.That(discussion.Notes[0].Position.NewLine, Is.EqualTo(1));
Assert.That(discussion.Notes[0].Position.HeadSha.ToString(), Is.EqualTo(new Sha1(version.HeadCommitSha).ToString()));

var discussions = client.Comments(mr.Iid).Discussions.ToArray();
var reread = discussions.Single(d => string.Equals(d.Notes[0].Body, "Inline comment", StringComparison.Ordinal));
Assert.That(reread.Notes[0].Position, Is.Not.Null);
Assert.That(reread.Notes[0].Position.NewPath, Is.EqualTo("file.txt"));
Assert.That(reread.Notes[0].Position.NewLine, Is.EqualTo(1));
Assert.That(reread.Id, Is.EqualTo(discussion.Id));
}
}

[Test]
public void DeleteDiscussion_RemovesBothNoteAndDiscussion()
{
var (server, project, mr, user) = MergeRequestMockTestHelper.CreateProjectWithMergeRequest();
using (server)
{
var client = server.CreateClient(user).GetMergeRequest(project.Id);

var discussion = client.Discussions(mr.Iid).Add(new MergeRequestDiscussionCreate { Body = "To be deleted" });
client.Discussions(mr.Iid).Delete(discussion.Id, discussion.Notes[0].Id);

Assert.That(client.Comments(mr.Iid).All.Any(c => c.Id == discussion.Notes[0].Id), Is.False);
Assert.That(client.Discussions(mr.Iid).All.Any(d => string.Equals(d.Id, discussion.Id, StringComparison.Ordinal)), Is.False);
}
}

[Test]
public void Resolve_PersistsResolvedAndUnresolvedState_OnReread()
{
var (server, project, mr, user) = MergeRequestMockTestHelper.CreateProjectWithMergeRequest();
using (server)
{
var client = server.CreateClient(user).GetMergeRequest(project.Id);

var discussion = client.Discussions(mr.Iid).Add(new MergeRequestDiscussionCreate { Body = "Resolvable comment" });
Assert.That(discussion.Notes[0].Resolved, Is.False);

var resolved = client.Discussions(mr.Iid).Resolve(new MergeRequestDiscussionResolve { Id = discussion.Id, Resolved = true });
Assert.That(resolved.Notes[0].Resolved, Is.True, "Resolve() returns notes marked as resolved");

var rereadResolved = client.Discussions(mr.Iid).Get(discussion.Id);
Assert.That(rereadResolved.Notes[0].Resolved, Is.True, "Resolve() must persist resolution onto the stored comment");

var unresolved = client.Discussions(mr.Iid).Resolve(new MergeRequestDiscussionResolve { Id = discussion.Id, Resolved = false });
Assert.That(unresolved.Notes[0].Resolved, Is.False);

var rereadUnresolved = client.Discussions(mr.Iid).Get(discussion.Id);
Assert.That(rereadUnresolved.Notes[0].Resolved, Is.False, "Resolve() with Resolved=false must persist the unresolved state");
}
}

[Test]
public void AddDiscussion_MarksNoteAsResolvable()
{
var (server, project, mr, user) = MergeRequestMockTestHelper.CreateProjectWithMergeRequest();
using (server)
{
var client = server.CreateClient(user).GetMergeRequest(project.Id);

var discussion = client.Discussions(mr.Iid).Add(new MergeRequestDiscussionCreate { Body = "Resolvable comment" });

Assert.That(discussion.Notes[0].Resolvable, Is.True, "discussions created via the discussions endpoint are resolvable threads, matching real GitLab");
}
}

[Test]
public void Resolve_TogglesBlockingDiscussionsResolved()
{
var (server, project, mr, user) = MergeRequestMockTestHelper.CreateProjectWithMergeRequest();
using (server)
{
project.AllThreadsMustBeResolvedToMerge = true;
var client = server.CreateClient(user).GetMergeRequest(project.Id);

var discussion = client.Discussions(mr.Iid).Add(new MergeRequestDiscussionCreate { Body = "Blocking comment" });
Assert.That(client[mr.Iid].BlockingDiscussionsResolved, Is.False, "an unresolved resolvable discussion should block merging");

client.Discussions(mr.Iid).Resolve(new MergeRequestDiscussionResolve { Id = discussion.Id, Resolved = true });
Assert.That(client[mr.Iid].BlockingDiscussionsResolved, Is.True, "resolving the discussion should clear the block");
}
}

[Test]
public void AddDiscussion_CalledTwice_CreatesSeparateSingleNoteThreads()
{
var (server, project, mr, user) = MergeRequestMockTestHelper.CreateProjectWithMergeRequest();
using (server)
{
var client = server.CreateClient(user).GetMergeRequest(project.Id);

var first = client.Discussions(mr.Iid).Add(new MergeRequestDiscussionCreate { Body = "First reply" });
var second = client.Discussions(mr.Iid).Add(new MergeRequestDiscussionCreate { Body = "Second reply" });

Assert.That(first.Id, Is.Not.EqualTo(second.Id), "every Discussions.Add() call starts a new thread; appending to an existing thread is done via Comments().Add(discussionId, ...)");

var discussions = client.Comments(mr.Iid).Discussions.ToArray();
Assert.That(discussions, Has.Length.EqualTo(2));
Assert.That(discussions.All(d => d.Notes.Length == 1), Is.True);
}
}

[Test]
public void Discussions_UnknownId_ThrowNotFound()
{
var (server, project, mr, user) = MergeRequestMockTestHelper.CreateProjectWithMergeRequest();
using (server)
{
var client = server.CreateClient(user).GetMergeRequest(project.Id);

Assert.That(Assert.Throws<GitLabException>((Action)(() => client.Discussions(mr.Iid).Get("unknown-id"))).StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
Assert.That(Assert.Throws<GitLabException>((Action)(() => client.Discussions(mr.Iid).Delete("unknown-id", 1))).StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
Assert.That(Assert.Throws<GitLabException>((Action)(() => client.Discussions(mr.Iid).Resolve(new MergeRequestDiscussionResolve { Id = "unknown-id", Resolved = true }))).StatusCode, Is.EqualTo(HttpStatusCode.NotFound));
}
}

[Test]
public void ArchivedProject_AddDiscussion_ThrowsForbidden()
{
var (server, project, mr, user) = MergeRequestMockTestHelper.CreateProjectWithMergeRequest();
using (server)
{
project.Archived = true;
var client = server.CreateClient(user).GetMergeRequest(project.Id);

Assert.That(Assert.Throws<GitLabException>((Action)(() => client.Discussions(mr.Iid).Add(new MergeRequestDiscussionCreate { Body = "Should fail" }))).StatusCode, Is.EqualTo(HttpStatusCode.Forbidden));
}
}
}
38 changes: 38 additions & 0 deletions NGitLab.Mock.Tests/MergeRequestMockTestHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using NGitLab.Models;

namespace NGitLab.Mock.Tests;

internal static class MergeRequestMockTestHelper
{
public static Position CreateTextPosition(MergeRequestVersion version)
{
return new Position
{
NewPath = "file.txt",
NewLine = 1,
PositionType = new DynamicEnum<PositionType>(PositionType.Text),
BaseSha = new Sha1(version.BaseCommitSha),
StartSha = new Sha1(version.StartCommitSha),
HeadSha = new Sha1(version.HeadCommitSha),
};
}

public static (GitLabServer Server, Project Project, MergeRequest MergeRequest, User User) CreateProjectWithMergeRequest()
{
var server = new GitLabServer();
var user = server.Users.AddNew("maintainer");
var group = new Group("TestGroup");
server.Groups.Add(group);
var project = new Project("Test") { Visibility = VisibilityLevel.Internal };
group.Projects.Add(project);
project.Permissions.Add(new Permission(user, AccessLevel.Maintainer));

project.Repository.Commit(user, "Initial commit");
project.Repository.CreateAndCheckoutBranch("feature");
project.Repository.Commit(user, "add file", new[] { File.CreateFromText("file.txt", "new content") });

var mr = project.CreateMergeRequest(user, "A title", "A description", project.DefaultBranch, "feature");

return (server, project, mr, user);
}
}
Loading
Loading