Skip to content

Commit

Permalink
#20 get comments, query, tests, migration
Browse files Browse the repository at this point in the history
  • Loading branch information
mgroves committed Sep 22, 2023
1 parent 639b80f commit 89268a9
Show file tree
Hide file tree
Showing 14 changed files with 345 additions and 15 deletions.
33 changes: 33 additions & 0 deletions Conduit/Conduit.Migrations/008_CreateIndexForGetComments.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using NoSqlMigrator.Infrastructure;

namespace Conduit.Migrations;

// Manual alternative: CREATE INDEX `idx_comments_author_follows2` ON `Conduit`.`_default`.`Comments`((meta().`id`),(distinct (array (`c`.`authorUsername`) for `c` in ((`Conduit`.`_default`).`Comments`) end)))
[Migration(8)]
public class CreateIndexForGetComments : MigrateBase
{
private readonly string? _scopeName;

public CreateIndexForGetComments()
{
_scopeName = _config["Couchbase:ScopeName"];
}

public override void Up()
{
// there are other options for more complex and/or covering indexes
// but this index is the simplest for the List Articles SQL++ query
Create.Index("ix_get_comments")
.OnScope(_scopeName)
.OnCollection("Comments")
.OnFieldRaw("META().`id`")
.OnFieldRaw("DISTINCT (array (`c`.`authorUsername`) for `c` in ((`Conduit`.`_default`).`Comments`");
}

public override void Down()
{
Delete.Index("ix_get_comments")
.FromScope(_scopeName)
.FromCollection("Comments");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ public async Task Comment_gets_saved_to_database()

// assert
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
await _commentsCollectionProvider.AssertExists($"{article.ArticleKey}::comments", x =>
await _commentsCollectionProvider.AssertExists(article.Slug, x =>
{
Assert.That(x.Any(c => c.AuthorUsername == _user.Username && c.Body == expectedBody));
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using System.Net;
using System.Net.Http.Headers;
using Conduit.Tests.TestHelpers;
using Conduit.Tests.TestHelpers.Data;
using Conduit.Web.DataAccess.Models;
using Conduit.Web.DataAccess.Providers;
using Conduit.Web.Extensions;
using Microsoft.Extensions.DependencyInjection;

namespace Conduit.Tests.Functional.Articles.Controllers.CommentsControllerTests;

[TestFixture]
public class GetCommentsTests : FunctionalTestBase
{
private IConduitArticlesCollectionProvider _articleCollectionProvider;
private IConduitUsersCollectionProvider _usersCollectionProvider;
private User _user;
private Random _random;
private IConduitCommentsCollectionProvider _commentsCollectionProvider;
private IConduitFollowsCollectionProvider _followCollectionProvider;

[SetUp]
public async Task Setup()
{
await base.Setup();

// setup database objects for arranging
var service = WebAppFactory.Services;
_articleCollectionProvider = service.GetRequiredService<IConduitArticlesCollectionProvider>();
_usersCollectionProvider = service.GetRequiredService<IConduitUsersCollectionProvider>();
_commentsCollectionProvider = service.GetRequiredService<IConduitCommentsCollectionProvider>();
_followCollectionProvider = service.GetRequiredService<IConduitFollowsCollectionProvider>();

// setup an authorized header
_user = await _usersCollectionProvider.CreateUserInDatabase();
var jwtToken = AuthSvc.GenerateJwtToken(_user.Email, _user.Username);
WebClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Token", jwtToken);

_random = new Random();
}

[Test]
public async Task Comments_for_an_article_are_returned()
{
// arrange - an article
var author = await _usersCollectionProvider.CreateUserInDatabase();
var article = await _articleCollectionProvider.CreateArticleInDatabase(authorUsername: author.Username);
// arrange - two comments from two different users
var commenter1 = await _usersCollectionProvider.CreateUserInDatabase();
var commenter2 = await _usersCollectionProvider.CreateUserInDatabase();
var commentBody1 = _random.String(64);
var commentBody2 = _random.String(64);
await _commentsCollectionProvider.CreateCommentInDatabase(article.Slug, commenter1.Username, commentBody1);
await _commentsCollectionProvider.CreateCommentInDatabase(article.Slug, commenter2.Username, commentBody2);

// act
var response = await WebClient.GetAsync($"/api/articles/{article.Slug}/comments");
var responseString = await response.Content.ReadAsStringAsync();
var articleViewModel = responseString.SubDoc<List<CommentListDataView>>("comments");

// assert
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
Assert.That(articleViewModel.Count, Is.EqualTo(2));
await _commentsCollectionProvider.AssertExists(article.Slug, x =>
{
Assert.That(x.Any(c => c.Body == commentBody1));
Assert.That(x.Any(c => c.Body == commentBody2));
});
}

[TestCase(true)]
[TestCase(false)]
public async Task Comments_include_correct_following_information(bool doesFollowAuthor)
{
// arrange - an article
var author = await _usersCollectionProvider.CreateUserInDatabase();
var article = await _articleCollectionProvider.CreateArticleInDatabase(authorUsername: author.Username);
// arrange - comment
var commenter = await _usersCollectionProvider.CreateUserInDatabase();
await _commentsCollectionProvider.CreateCommentInDatabase(article.Slug, commenter.Username, _random.String(64));
// arrange - follow (or not) the comment author
if (doesFollowAuthor)
await _followCollectionProvider.CreateFollow(commenter.Username, _user.Username);

// act
var response = await WebClient.GetAsync($"/api/articles/{article.Slug}/comments");
var responseString = await response.Content.ReadAsStringAsync();
var articleViewModel = responseString.SubDoc<List<CommentListDataView>>("comments");

// assert
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
Assert.That(articleViewModel.Count, Is.EqualTo(1));
Assert.That(articleViewModel[0].Author.Following, Is.EqualTo(doesFollowAuthor));
}
}
4 changes: 3 additions & 1 deletion Conduit/Conduit.Tests/Functional/FunctionalTestBase.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using Conduit.Web;
using Conduit.Web.Users.Services;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;

namespace Conduit.Tests.Functional;

Expand All @@ -23,4 +25,4 @@ public async Task TearDown()
{
// cleanup happens in GlobalFunctionalSetUp
}
}
}
34 changes: 32 additions & 2 deletions Conduit/Conduit.Tests/TestHelpers/Data/CommentHelper.cs
Original file line number Diff line number Diff line change
@@ -1,20 +1,50 @@
using Conduit.Web.DataAccess.Models;
using Conduit.Web.Articles.Services;
using Conduit.Web.DataAccess.Models;
using Conduit.Web.DataAccess.Providers;
using Conduit.Web.Extensions;
using Couchbase.KeyValue;

namespace Conduit.Tests.TestHelpers.Data;

public static class CommentHelper
{
public static async Task AssertExists(this IConduitCommentsCollectionProvider @this, string key, Action<IList<Comment>>? assertions = null)
public static async Task AssertExists(this IConduitCommentsCollectionProvider @this, string slug, Action<IList<Comment>>? assertions = null)
{
var collection = await @this.GetCollectionAsync();

var key = CommentsDataService.GetCommentsKey(slug.GetArticleKey());
var doesExist = await collection.ExistsAsync(key);
Assert.That(doesExist.Exists, Is.True);

var listOfComments = collection.List<Comment>(key);
if (assertions != null)
assertions(listOfComments);
}

public static async Task<Comment> CreateCommentInDatabase(this IConduitCommentsCollectionProvider @this,
string slug,
string username,
string? body = null,
ulong? id = null)
{
var random = new Random();

body ??= random.String(64);
id ??= (ulong)random.Next(100000, 200000);

var collection = await @this.GetCollectionAsync();

var key = CommentsDataService.GetCommentsKey(slug.GetArticleKey());

var comments = collection.List<Comment>(key);
var comment = new Comment
{
AuthorUsername = username,
Body = body,
CreatedAt = DateTimeOffset.Now,
Id = id.Value
};
await comments.AddAsync(comment);
return comment;
}
}
28 changes: 28 additions & 0 deletions Conduit/Conduit.Web/Articles/Controllers/CommentsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,32 @@ public async Task<IActionResult> AddComment([FromRoute] string slug, [FromBody]

return Ok(new { comment = viewModel });
}

[HttpGet]
[AllowAnonymous]
[Route("/api/articles/{slug}/comments")]
public async Task<IActionResult> GetComments([FromRoute] string slug)
{
// get (optional) auth info
string username = null;
var headers = Request.Headers["Authorization"];
var isUserAnonymous = headers.All(string.IsNullOrEmpty);

if (!isUserAnonymous)
{
var claims = _authService.GetAllAuthInfo(Request.Headers["Authorization"]);
username = claims.Username.Value;
}

// make request to mediator
var request = new GetCommentsRequest(slug, username);
var response = await _mediator.Send(request);

if (response.IsArticleNotFound)
return NotFound($"Article {slug} not found.");
if (response.IsFailed)
return StatusCode(500, "There was a problem adding that comment.");

return Ok(new { comments = response.CommentsView });
}
}
51 changes: 51 additions & 0 deletions Conduit/Conduit.Web/Articles/Handlers/GetCommentsHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using Conduit.Web.Articles.Services;
using Conduit.Web.Articles.ViewModels;
using Conduit.Web.Users.ViewModels;
using MediatR;

namespace Conduit.Web.Articles.Handlers;

public class GetCommentsHandler : IRequestHandler<GetCommentsRequest, GetCommentsResponse>
{
private readonly IArticlesDataService _articlesDataService;
private readonly ICommentsDataService _commentsDataService;

public GetCommentsHandler(IArticlesDataService articlesDataService, ICommentsDataService commentsDataService)
{
_articlesDataService = articlesDataService;
_commentsDataService = commentsDataService;
}

public async Task<GetCommentsResponse> Handle(GetCommentsRequest request, CancellationToken cancellationToken)
{
// article must exist
var doesArticleExist = await _articlesDataService.Exists(request.Slug);
if (!doesArticleExist)
{
return new GetCommentsResponse
{
IsArticleNotFound = true
};
}

var results = await _commentsDataService.Get(request.Slug, request.Username);

return new GetCommentsResponse
{
CommentsView = results.DataResult.Select(x => new CommentViewModel
{
Id = x.Id,
Body = x.Body,
CreatedAt = x.CreatedAt,
UpdatedAt = x.CreatedAt,
Author = new ProfileViewModel
{
Bio = x.Author.Bio,
Following = x.Author.Following,
Image = x.Author.Image,
Username = x.Author.Username
}
}).ToList()
};
}
}
16 changes: 16 additions & 0 deletions Conduit/Conduit.Web/Articles/Handlers/GetCommentsRequest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Conduit.Web.DataAccess.Models;
using MediatR;

namespace Conduit.Web.Articles.Handlers;

public class GetCommentsRequest : IRequest<GetCommentsResponse>
{
public string? Username { get; set; }
public string Slug { get; set; }

public GetCommentsRequest(string slug, string? username)
{
Slug = slug;
Username = username;
}
}
10 changes: 10 additions & 0 deletions Conduit/Conduit.Web/Articles/Handlers/GetCommentsResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Conduit.Web.Articles.ViewModels;

namespace Conduit.Web.Articles.Handlers;

public class GetCommentsResponse
{
public bool IsArticleNotFound { get; set; }
public bool IsFailed { get; set; }
public List<CommentViewModel> CommentsView { get; set; }
}
3 changes: 1 addition & 2 deletions Conduit/Conduit.Web/Articles/Services/ArticlesDataService.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Conduit.Web.Articles.Handlers;
using Conduit.Web.Articles.ViewModels;
using Conduit.Web.Articles.ViewModels;
using Conduit.Web.DataAccess.Dto;
using Conduit.Web.DataAccess.Dto.Articles;
using Conduit.Web.DataAccess.Models;
Expand Down
Loading

0 comments on commit 89268a9

Please sign in to comment.