diff --git a/Conduit/Conduit.Tests/Conduit.Tests.csproj b/Conduit/Conduit.Tests/Conduit.Tests.csproj
index cb2b1d7774..e763056c72 100644
--- a/Conduit/Conduit.Tests/Conduit.Tests.csproj
+++ b/Conduit/Conduit.Tests/Conduit.Tests.csproj
@@ -47,6 +47,7 @@
+
diff --git a/Conduit/Conduit.Tests/Integration/Articles/Services/CommentsDataService/DeleteTests.cs b/Conduit/Conduit.Tests/Integration/Articles/Services/CommentsDataService/DeleteTests.cs
new file mode 100644
index 0000000000..c4307674dd
--- /dev/null
+++ b/Conduit/Conduit.Tests/Integration/Articles/Services/CommentsDataService/DeleteTests.cs
@@ -0,0 +1,85 @@
+using Conduit.Tests.TestHelpers.Data;
+using Conduit.Web.DataAccess;
+using Conduit.Web.DataAccess.Dto;
+using Conduit.Web.DataAccess.Providers;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+
+namespace Conduit.Tests.Integration.Articles.Services.CommentsDataService;
+
+[TestFixture]
+public class DeleteTests : CouchbaseIntegrationTest
+{
+ private IConduitCommentsCollectionProvider _commentsCollectionProvider;
+ private IConduitArticlesCollectionProvider _articlesCollectionProvider;
+ private Web.Articles.Services.CommentsDataService _commentsDataService;
+ private IConduitUsersCollectionProvider _userCollectionProvider;
+
+ [SetUp]
+ public override async Task Setup()
+ {
+ await base.Setup();
+
+ _commentsCollectionProvider = ServiceProvider.GetRequiredService();
+ _articlesCollectionProvider = ServiceProvider.GetRequiredService();
+ _userCollectionProvider = ServiceProvider.GetRequiredService();
+
+ var couchbaseOptions = new OptionsWrapper(new CouchbaseOptions());
+ _commentsDataService = new Web.Articles.Services.CommentsDataService(_commentsCollectionProvider, couchbaseOptions);
+ }
+
+ [Test]
+ public async Task Can_delete_a_comment_by_id()
+ {
+ // arrange an article
+ var author = await _userCollectionProvider.CreateUserInDatabase();
+ var article = await _articlesCollectionProvider.CreateArticleInDatabase(authorUsername: author.Username);
+ var commenter = await _userCollectionProvider.CreateUserInDatabase();
+ // arrange some comments on the article
+ var comment1 = await _commentsCollectionProvider.CreateCommentInDatabase(article.Slug, commenter.Username);
+ var comment2 = await _commentsCollectionProvider.CreateCommentInDatabase(article.Slug, commenter.Username);
+
+ // act
+ var result = await _commentsDataService.Delete(comment1.Id, article.Slug, commenter.Username);
+
+ // assert
+ Assert.That(result, Is.EqualTo(DataResultStatus.Ok));
+ await _commentsCollectionProvider.AssertExists(article.Slug, x =>
+ {
+ Assert.That(x.Count, Is.EqualTo(1));
+ Assert.That(x[0].Id, Is.EqualTo(comment2.Id));
+ });
+ }
+
+ [Test]
+ public async Task Id_must_exist()
+ {
+ // arrange an article
+ var author = await _userCollectionProvider.CreateUserInDatabase();
+ var article = await _articlesCollectionProvider.CreateArticleInDatabase(authorUsername: author.Username);
+ var comment1 = await _commentsCollectionProvider.CreateCommentInDatabase(article.Slug, author.Username);
+ var idDoesntExist = comment1.Id + 1;
+
+ // act
+ var result = await _commentsDataService.Delete(idDoesntExist, article.Slug, author.Username);
+
+ // assert
+ Assert.That(result, Is.EqualTo(DataResultStatus.NotFound));
+ }
+
+ [Test]
+ public async Task Cant_delete_comments_that_you_didnt_author()
+ {
+ // arrange an article
+ var author = await _userCollectionProvider.CreateUserInDatabase();
+ var article = await _articlesCollectionProvider.CreateArticleInDatabase(authorUsername: author.Username);
+ var comment1 = await _commentsCollectionProvider.CreateCommentInDatabase(article.Slug, author.Username);
+ var someOtherUser = await _userCollectionProvider.CreateUserInDatabase();
+
+ // act
+ var result = await _commentsDataService.Delete(comment1.Id, article.Slug, someOtherUser.Username);
+
+ // assert
+ Assert.That(result, Is.EqualTo(DataResultStatus.Unauthorized));
+ }
+}
\ No newline at end of file
diff --git a/Conduit/Conduit.Tests/Integration/GlobalCouchbaseIntegrationSetUp.cs b/Conduit/Conduit.Tests/Integration/GlobalCouchbaseIntegrationSetUp.cs
index 95ca482080..87baa2726c 100644
--- a/Conduit/Conduit.Tests/Integration/GlobalCouchbaseIntegrationSetUp.cs
+++ b/Conduit/Conduit.Tests/Integration/GlobalCouchbaseIntegrationSetUp.cs
@@ -78,6 +78,9 @@ public async Task RunBeforeAllTests()
b
.AddScope(_config["Couchbase:ScopeName"])
.AddCollection("Follows");
+ b
+ .AddScope(_config["Couchbase:ScopeName"])
+ .AddCollection("Comments");
});
ServiceCollection = services;
diff --git a/Conduit/Conduit.Web/Articles/Controllers/CommentsController.cs b/Conduit/Conduit.Web/Articles/Controllers/CommentsController.cs
index a3304a7431..09106cd08f 100644
--- a/Conduit/Conduit.Web/Articles/Controllers/CommentsController.cs
+++ b/Conduit/Conduit.Web/Articles/Controllers/CommentsController.cs
@@ -106,4 +106,29 @@ public async Task GetComments([FromRoute] string slug)
return Ok(new { comments = response.CommentsView });
}
+
+ [HttpDelete]
+ [Route("/api/articles/{slug}/comments/{commentId}")]
+ [Authorize]
+ public async Task DeleteComment([FromRoute] string slug, [FromRoute] ulong commentId)
+ {
+ var claims = _authService.GetAllAuthInfo(Request.Headers["Authorization"]);
+ var username = claims.Username.Value;
+
+ var deleteCommentRequest = new DeleteCommentRequest { Slug = slug, CommentId = commentId, Username = username };
+ var deleteCommentResponse = await _mediator.Send(deleteCommentRequest);
+
+ if (deleteCommentResponse.IsArticleNotFound)
+ return NotFound($"Article {slug} not found.");
+ if (deleteCommentResponse.IsCommentNotFound)
+ return NotFound($"Comment with ID {commentId} not found.");
+ if (deleteCommentResponse.IsNotAuthorized)
+ return Unauthorized("You aren't allowed to delete that comment.");
+ if (deleteCommentResponse.IsFailed)
+ return StatusCode(500, "There was a problem deleting that comment.");
+ if (deleteCommentResponse.ValidationErrors?.Any() ?? false)
+ return UnprocessableEntity(deleteCommentResponse.ValidationErrors.ToCsv());
+
+ return Ok();
+ }
}
\ No newline at end of file
diff --git a/Conduit/Conduit.Web/Articles/Handlers/DeleteCommentHandler.cs b/Conduit/Conduit.Web/Articles/Handlers/DeleteCommentHandler.cs
new file mode 100644
index 0000000000..37d126b4d5
--- /dev/null
+++ b/Conduit/Conduit.Web/Articles/Handlers/DeleteCommentHandler.cs
@@ -0,0 +1,44 @@
+using Conduit.Web.Articles.Services;
+using Conduit.Web.DataAccess.Dto;
+using FluentValidation;
+using MediatR;
+
+namespace Conduit.Web.Articles.Handlers;
+
+public class DeleteCommentHandler : IRequestHandler
+{
+ private readonly IValidator _validator;
+ private readonly IArticlesDataService _articlesDataService;
+ private readonly ICommentsDataService _commentsDataService;
+
+ public DeleteCommentHandler(IValidator validator, IArticlesDataService articlesDataService, ICommentsDataService commentsDataService)
+ {
+ _validator = validator;
+ _articlesDataService = articlesDataService;
+ _commentsDataService = commentsDataService;
+ }
+
+ public async Task Handle(DeleteCommentRequest request, CancellationToken cancellationToken)
+ {
+ // validation
+ var validationResult = await _validator.ValidateAsync(request, cancellationToken);
+ if (!validationResult.IsValid)
+ return new DeleteCommentResponse { ValidationErrors = validationResult.Errors };
+
+ // return error if article doesn't exist
+ var doesArticleExist = await _articlesDataService.Exists(request.Slug);
+ if (!doesArticleExist)
+ return new DeleteCommentResponse { IsArticleNotFound = true };
+
+ var dataResponse = await _commentsDataService.Delete(request.CommentId, request.Slug, request.Username);
+
+ if (dataResponse == DataResultStatus.NotFound)
+ return new DeleteCommentResponse { IsCommentNotFound = true };
+ if (dataResponse == DataResultStatus.Unauthorized)
+ return new DeleteCommentResponse { IsNotAuthorized = true };
+ if (dataResponse == DataResultStatus.Error)
+ return new DeleteCommentResponse { IsFailed = true };
+
+ return new DeleteCommentResponse();
+ }
+}
\ No newline at end of file
diff --git a/Conduit/Conduit.Web/Articles/Handlers/DeleteCommentRequest.cs b/Conduit/Conduit.Web/Articles/Handlers/DeleteCommentRequest.cs
new file mode 100644
index 0000000000..089eed93e4
--- /dev/null
+++ b/Conduit/Conduit.Web/Articles/Handlers/DeleteCommentRequest.cs
@@ -0,0 +1,10 @@
+using MediatR;
+
+namespace Conduit.Web.Articles.Handlers;
+
+public class DeleteCommentRequest : IRequest
+{
+ public string Slug { get; init; }
+ public ulong CommentId { get; init; }
+ public string Username { get; init; }
+}
\ No newline at end of file
diff --git a/Conduit/Conduit.Web/Articles/Handlers/DeleteCommentRequestValidator.cs b/Conduit/Conduit.Web/Articles/Handlers/DeleteCommentRequestValidator.cs
new file mode 100644
index 0000000000..15bd9d6199
--- /dev/null
+++ b/Conduit/Conduit.Web/Articles/Handlers/DeleteCommentRequestValidator.cs
@@ -0,0 +1,16 @@
+using FluentValidation;
+
+namespace Conduit.Web.Articles.Handlers;
+
+public class DeleteCommentRequestValidator : AbstractValidator
+{
+ public DeleteCommentRequestValidator()
+ {
+ RuleFor(x => x.Slug)
+ .Cascade(CascadeMode.Stop)
+ .NotEmpty().WithMessage("Slug is required.");
+
+ RuleFor(x => x.CommentId)
+ .Must(v => v > 0).WithMessage("Must be a valid CommentID");
+ }
+}
\ No newline at end of file
diff --git a/Conduit/Conduit.Web/Articles/Handlers/DeleteCommentResponse.cs b/Conduit/Conduit.Web/Articles/Handlers/DeleteCommentResponse.cs
new file mode 100644
index 0000000000..8bfe79cfab
--- /dev/null
+++ b/Conduit/Conduit.Web/Articles/Handlers/DeleteCommentResponse.cs
@@ -0,0 +1,12 @@
+using FluentValidation.Results;
+
+namespace Conduit.Web.Articles.Handlers;
+
+public class DeleteCommentResponse
+{
+ public List ValidationErrors { get; set; }
+ public bool IsArticleNotFound { get; set; }
+ public bool IsNotAuthorized { get; set; }
+ public bool IsFailed { get; set; }
+ public bool IsCommentNotFound { get; set; }
+}
\ No newline at end of file
diff --git a/Conduit/Conduit.Web/Articles/Services/CommentsDataService.cs b/Conduit/Conduit.Web/Articles/Services/CommentsDataService.cs
index 735653fd4f..db3ba8db76 100644
--- a/Conduit/Conduit.Web/Articles/Services/CommentsDataService.cs
+++ b/Conduit/Conduit.Web/Articles/Services/CommentsDataService.cs
@@ -14,6 +14,7 @@ public interface ICommentsDataService
{
Task> Add(Comment newComment, string slug);
Task>> Get(string slug, string? currentUsername = null);
+ Task Delete(ulong commentId, string slug, string username);
}
public class CommentsDataService : ICommentsDataService
@@ -87,9 +88,9 @@ UNNEST c2 AS c
{loggedInJoin} ;";
var cluster = collection.Scope.Bucket.Cluster;
- // try
- // {
- //
+ try
+ {
+
var results = await cluster.QueryAsync(sql, options =>
{
options.Parameter("commentsKey", GetCommentsKey(slug.GetArticleKey()));
@@ -97,11 +98,48 @@ UNNEST c2 AS c
options.ScanConsistency(_couchbaseOptions.Value.ScanConsistency);
});
return new DataServiceResult>(await results.Rows.ToListAsync(), DataResultStatus.Ok);
- // }
- // catch (Exception ex)
- // {
- // return new DataServiceResult>(null, DataResultStatus.Error);
- // }
+ }
+ catch (Exception ex)
+ {
+ return new DataServiceResult>(null, DataResultStatus.Error);
+ }
+ }
+
+ public async Task Delete(ulong commentId, string slug, string username)
+ {
+ var collection = await _commentsCollectionProvider.GetCollectionAsync();
+
+ try
+ {
+ var commentsList = collection.List(GetCommentsKey(slug.GetArticleKey()));
+
+ // comment with ID must exist
+ var comment = commentsList.SingleOrDefault(c => c.Id == commentId);
+ if (comment == null)
+ return DataResultStatus.NotFound;
+
+ // comment must be authored by the given user
+ var isAuthorized = comment.AuthorUsername == username;
+ if (!isAuthorized)
+ return DataResultStatus.Unauthorized;
+
+ // delete comment
+ // this is a hacky workaround, see NCBC-3498 https://issues.couchbase.com/browse/NCBC-3498
+ var list = await commentsList.ToListAsync();
+ for (var i = 0; i < list.Count; i++)
+ {
+ if (list[i].Id == commentId)
+ {
+ await commentsList.RemoveAtAsync(i);
+ }
+ }
+ }
+ catch
+ {
+ return DataResultStatus.Error;
+ }
+
+ return DataResultStatus.Ok;
}
private async Task GetNextCommentId(string slug)
diff --git a/Conduit/Conduit.Web/DataAccess/Dto/DataResultStatus.cs b/Conduit/Conduit.Web/DataAccess/Dto/DataResultStatus.cs
index 657fd2249e..e485b3c387 100644
--- a/Conduit/Conduit.Web/DataAccess/Dto/DataResultStatus.cs
+++ b/Conduit/Conduit.Web/DataAccess/Dto/DataResultStatus.cs
@@ -5,5 +5,6 @@ public enum DataResultStatus
NotFound = 0,
Ok = 1,
FailedToInsert = 2,
- Error = 3
+ Error = 3,
+ Unauthorized = 4
}
\ No newline at end of file