Skip to content

Commit

Permalink
#21 delete comment endpoint and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
mgroves committed Sep 26, 2023
1 parent 480f5f9 commit efe80c6
Show file tree
Hide file tree
Showing 10 changed files with 244 additions and 9 deletions.
1 change: 1 addition & 0 deletions Conduit/Conduit.Tests/Conduit.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
<Folder Include="Integration\Articles\Handlers\" />
<Folder Include="Extensions\" />
<Folder Include="Integration\Articles\Services\ArticlesDataService\" />
<Folder Include="Integration\Articles\Services\CommentsDataService\" />
<Folder Include="Integration\Users\Services\UserDataServiceTests\" />
<Folder Include="Unit\Articles\Handlers\" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<IConduitCommentsCollectionProvider>();
_articlesCollectionProvider = ServiceProvider.GetRequiredService<IConduitArticlesCollectionProvider>();
_userCollectionProvider = ServiceProvider.GetRequiredService<IConduitUsersCollectionProvider>();

var couchbaseOptions = new OptionsWrapper<CouchbaseOptions>(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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ public async Task RunBeforeAllTests()
b
.AddScope(_config["Couchbase:ScopeName"])
.AddCollection<IConduitFollowsCollectionProvider>("Follows");
b
.AddScope(_config["Couchbase:ScopeName"])
.AddCollection<IConduitCommentsCollectionProvider>("Comments");
});

ServiceCollection = services;
Expand Down
25 changes: 25 additions & 0 deletions Conduit/Conduit.Web/Articles/Controllers/CommentsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,4 +106,29 @@ public async Task<IActionResult> GetComments([FromRoute] string slug)

return Ok(new { comments = response.CommentsView });
}

[HttpDelete]
[Route("/api/articles/{slug}/comments/{commentId}")]
[Authorize]
public async Task<IActionResult> 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();
}
}
44 changes: 44 additions & 0 deletions Conduit/Conduit.Web/Articles/Handlers/DeleteCommentHandler.cs
Original file line number Diff line number Diff line change
@@ -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<DeleteCommentRequest, DeleteCommentResponse>
{
private readonly IValidator<DeleteCommentRequest> _validator;
private readonly IArticlesDataService _articlesDataService;
private readonly ICommentsDataService _commentsDataService;

public DeleteCommentHandler(IValidator<DeleteCommentRequest> validator, IArticlesDataService articlesDataService, ICommentsDataService commentsDataService)
{
_validator = validator;
_articlesDataService = articlesDataService;
_commentsDataService = commentsDataService;
}

public async Task<DeleteCommentResponse> 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();
}
}
10 changes: 10 additions & 0 deletions Conduit/Conduit.Web/Articles/Handlers/DeleteCommentRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using MediatR;

namespace Conduit.Web.Articles.Handlers;

public class DeleteCommentRequest : IRequest<DeleteCommentResponse>
{
public string Slug { get; init; }
public ulong CommentId { get; init; }
public string Username { get; init; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using FluentValidation;

namespace Conduit.Web.Articles.Handlers;

public class DeleteCommentRequestValidator : AbstractValidator<DeleteCommentRequest>
{
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");
}
}
12 changes: 12 additions & 0 deletions Conduit/Conduit.Web/Articles/Handlers/DeleteCommentResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using FluentValidation.Results;

namespace Conduit.Web.Articles.Handlers;

public class DeleteCommentResponse
{
public List<ValidationFailure> ValidationErrors { get; set; }
public bool IsArticleNotFound { get; set; }
public bool IsNotAuthorized { get; set; }
public bool IsFailed { get; set; }
public bool IsCommentNotFound { get; set; }
}
54 changes: 46 additions & 8 deletions Conduit/Conduit.Web/Articles/Services/CommentsDataService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public interface ICommentsDataService
{
Task<DataServiceResult<Comment>> Add(Comment newComment, string slug);
Task<DataServiceResult<List<CommentListDataView>>> Get(string slug, string? currentUsername = null);
Task<DataResultStatus> Delete(ulong commentId, string slug, string username);
}

public class CommentsDataService : ICommentsDataService
Expand Down Expand Up @@ -87,21 +88,58 @@ public async Task<DataServiceResult<List<CommentListDataView>>> Get(string slug,
{loggedInJoin} ;";

var cluster = collection.Scope.Bucket.Cluster;
// try
// {
//
try
{

var results = await cluster.QueryAsync<CommentListDataView>(sql, options =>
{
options.Parameter("commentsKey", GetCommentsKey(slug.GetArticleKey()));
options.Parameter("currentUsername", currentUsername);
options.ScanConsistency(_couchbaseOptions.Value.ScanConsistency);
});
return new DataServiceResult<List<CommentListDataView>>(await results.Rows.ToListAsync(), DataResultStatus.Ok);
// }
// catch (Exception ex)
// {
// return new DataServiceResult<List<CommentListDataView>>(null, DataResultStatus.Error);
// }
}
catch (Exception ex)
{
return new DataServiceResult<List<CommentListDataView>>(null, DataResultStatus.Error);
}
}

public async Task<DataResultStatus> Delete(ulong commentId, string slug, string username)
{
var collection = await _commentsCollectionProvider.GetCollectionAsync();

try
{
var commentsList = collection.List<Comment>(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<ulong> GetNextCommentId(string slug)
Expand Down
3 changes: 2 additions & 1 deletion Conduit/Conduit.Web/DataAccess/Dto/DataResultStatus.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ public enum DataResultStatus
NotFound = 0,
Ok = 1,
FailedToInsert = 2,
Error = 3
Error = 3,
Unauthorized = 4
}

0 comments on commit efe80c6

Please sign in to comment.