diff --git a/Conduit/Conduit.Migrations/005_CreateArticlesCollection.cs b/Conduit/Conduit.Migrations/005_CreateArticlesCollection.cs new file mode 100644 index 0000000000..7447373715 --- /dev/null +++ b/Conduit/Conduit.Migrations/005_CreateArticlesCollection.cs @@ -0,0 +1,28 @@ +using NoSqlMigrator.Infrastructure; + +namespace Conduit.Migrations; + +[Migration(5)] +public class CreateArticlesCollection : MigrateBase +{ + private readonly string? _collectionName; + private readonly string? _scopeName; + + public CreateArticlesCollection() + { + _collectionName = _config["Couchbase:ArticlesCollectionName"]; + _scopeName = _config["Couchbase:ScopeName"]; + } + + public override void Up() + { + Create.Collection(_collectionName) + .InScope(_scopeName); + } + + public override void Down() + { + Delete.Collection(_collectionName) + .FromScope(_scopeName); + } +} \ No newline at end of file diff --git a/Conduit/Conduit.Tests/Conduit.Tests.csproj b/Conduit/Conduit.Tests/Conduit.Tests.csproj index ec72a952f8..07b6cd4603 100644 --- a/Conduit/Conduit.Tests/Conduit.Tests.csproj +++ b/Conduit/Conduit.Tests/Conduit.Tests.csproj @@ -46,6 +46,7 @@ + diff --git a/Conduit/Conduit.Tests/Integration/Articles/Handlers/GetTagsHandlerTests.cs b/Conduit/Conduit.Tests/Integration/Articles/Handlers/GetTagsHandlerTests.cs index 328e0770de..f67a589b96 100644 --- a/Conduit/Conduit.Tests/Integration/Articles/Handlers/GetTagsHandlerTests.cs +++ b/Conduit/Conduit.Tests/Integration/Articles/Handlers/GetTagsHandlerTests.cs @@ -1,6 +1,6 @@ using Conduit.Web.Articles.Handlers; using Conduit.Web.Articles.Services; -using Conduit.Web.Models; +using Conduit.Web.DataAccess.Providers; using Couchbase.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection; diff --git a/Conduit/Conduit.Tests/Integration/Follows/Handlers/FollowUserHandlerTests.cs b/Conduit/Conduit.Tests/Integration/Follows/Handlers/FollowUserHandlerTests.cs index d5a36b19e4..20872bcbdd 100644 --- a/Conduit/Conduit.Tests/Integration/Follows/Handlers/FollowUserHandlerTests.cs +++ b/Conduit/Conduit.Tests/Integration/Follows/Handlers/FollowUserHandlerTests.cs @@ -1,8 +1,8 @@ using Conduit.Tests.TestHelpers; using Conduit.Tests.TestHelpers.Data; +using Conduit.Web.DataAccess.Providers; using Conduit.Web.Follows.Handlers; using Conduit.Web.Follows.Services; -using Conduit.Web.Models; using Conduit.Web.Users.Services; using Couchbase.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection; diff --git a/Conduit/Conduit.Tests/Integration/Follows/Handlers/UnfollowUserHandlerTests.cs b/Conduit/Conduit.Tests/Integration/Follows/Handlers/UnfollowUserHandlerTests.cs index d53ef70e3a..0475cbc20e 100644 --- a/Conduit/Conduit.Tests/Integration/Follows/Handlers/UnfollowUserHandlerTests.cs +++ b/Conduit/Conduit.Tests/Integration/Follows/Handlers/UnfollowUserHandlerTests.cs @@ -1,8 +1,8 @@ using Conduit.Tests.TestHelpers; using Conduit.Tests.TestHelpers.Data; +using Conduit.Web.DataAccess.Providers; using Conduit.Web.Follows.Handlers; using Conduit.Web.Follows.Services; -using Conduit.Web.Models; using Conduit.Web.Users.Services; using Couchbase.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection; diff --git a/Conduit/Conduit.Tests/Integration/Users/Controllers/UserController/LoginTests.cs b/Conduit/Conduit.Tests/Integration/Users/Controllers/UserController/LoginTests.cs index f7d70b2eba..7927109cf5 100644 --- a/Conduit/Conduit.Tests/Integration/Users/Controllers/UserController/LoginTests.cs +++ b/Conduit/Conduit.Tests/Integration/Users/Controllers/UserController/LoginTests.cs @@ -4,7 +4,6 @@ using Conduit.Web; using System.Net; using Conduit.Tests.TestHelpers; -using Conduit.Web.Models; using Conduit.Web.Users.ViewModels; using Couchbase.Extensions.DependencyInjection; using Microsoft.AspNetCore.Mvc.Testing; @@ -12,6 +11,7 @@ using Newtonsoft.Json; using Microsoft.Extensions.Configuration; using Newtonsoft.Json.Linq; +using Conduit.Web.DataAccess.Providers; namespace Conduit.Tests.Integration.Users.Controllers.UserController; diff --git a/Conduit/Conduit.Tests/Integration/Users/Handlers/GetCurrentUserRequestHandlerTests.cs b/Conduit/Conduit.Tests/Integration/Users/Handlers/GetCurrentUserRequestHandlerTests.cs index 7bdab7cb2c..4fa40e8543 100644 --- a/Conduit/Conduit.Tests/Integration/Users/Handlers/GetCurrentUserRequestHandlerTests.cs +++ b/Conduit/Conduit.Tests/Integration/Users/Handlers/GetCurrentUserRequestHandlerTests.cs @@ -1,6 +1,6 @@ using Conduit.Tests.TestHelpers; using Conduit.Tests.TestHelpers.Data; -using Conduit.Web.Models; +using Conduit.Web.DataAccess.Providers; using Conduit.Web.Users.Handlers; using Conduit.Web.Users.Services; using Couchbase.Extensions.DependencyInjection; diff --git a/Conduit/Conduit.Tests/Integration/Users/Handlers/GetProfileHandlerTests.cs b/Conduit/Conduit.Tests/Integration/Users/Handlers/GetProfileHandlerTests.cs index 90f5bcf12f..cda0f8afaa 100644 --- a/Conduit/Conduit.Tests/Integration/Users/Handlers/GetProfileHandlerTests.cs +++ b/Conduit/Conduit.Tests/Integration/Users/Handlers/GetProfileHandlerTests.cs @@ -1,7 +1,7 @@ using Conduit.Tests.TestHelpers; using Conduit.Tests.TestHelpers.Data; +using Conduit.Web.DataAccess.Providers; using Conduit.Web.Follows.Services; -using Conduit.Web.Models; using Conduit.Web.Users.Handlers; using Conduit.Web.Users.Services; using Couchbase.Extensions.DependencyInjection; diff --git a/Conduit/Conduit.Tests/Integration/Users/Handlers/LoginRequestHandlerTests.cs b/Conduit/Conduit.Tests/Integration/Users/Handlers/LoginRequestHandlerTests.cs index a6479106c1..8c76cfabfe 100644 --- a/Conduit/Conduit.Tests/Integration/Users/Handlers/LoginRequestHandlerTests.cs +++ b/Conduit/Conduit.Tests/Integration/Users/Handlers/LoginRequestHandlerTests.cs @@ -1,7 +1,7 @@ using Conduit.Tests.TestHelpers; using Conduit.Tests.TestHelpers.Data; using Conduit.Tests.TestHelpers.Dto; -using Conduit.Web.Models; +using Conduit.Web.DataAccess.Providers; using Conduit.Web.Users.Handlers; using Conduit.Web.Users.Services; using Couchbase.Extensions.DependencyInjection; diff --git a/Conduit/Conduit.Tests/Integration/Users/Handlers/RegistrationRequestHandlerTests.cs b/Conduit/Conduit.Tests/Integration/Users/Handlers/RegistrationRequestHandlerTests.cs index 630bd1ef75..7453aebe59 100644 --- a/Conduit/Conduit.Tests/Integration/Users/Handlers/RegistrationRequestHandlerTests.cs +++ b/Conduit/Conduit.Tests/Integration/Users/Handlers/RegistrationRequestHandlerTests.cs @@ -1,5 +1,4 @@ using Conduit.Tests.TestHelpers.Dto; -using Conduit.Web.Models; using Conduit.Web.Users.Handlers; using Conduit.Web.Users.Services; using Conduit.Web.Users.ViewModels; @@ -7,6 +6,7 @@ using Microsoft.Extensions.DependencyInjection; using Conduit.Tests.TestHelpers.Data; using Conduit.Tests.TestHelpers; +using Conduit.Web.DataAccess.Providers; namespace Conduit.Tests.Integration.Users.Handlers; diff --git a/Conduit/Conduit.Tests/Integration/Users/Handlers/UpdateUserHandlerTests.cs b/Conduit/Conduit.Tests/Integration/Users/Handlers/UpdateUserHandlerTests.cs index bf360c0248..d9aa192c10 100644 --- a/Conduit/Conduit.Tests/Integration/Users/Handlers/UpdateUserHandlerTests.cs +++ b/Conduit/Conduit.Tests/Integration/Users/Handlers/UpdateUserHandlerTests.cs @@ -1,7 +1,7 @@ using Conduit.Tests.TestHelpers; using Conduit.Tests.TestHelpers.Data; using Conduit.Tests.TestHelpers.Dto; -using Conduit.Web.Models; +using Conduit.Web.DataAccess.Providers; using Conduit.Web.Users.Handlers; using Conduit.Web.Users.Services; using Conduit.Web.Users.ViewModels; diff --git a/Conduit/Conduit.Tests/Integration/Users/Services/UserDataServiceTests/RegisterNewUserTests.cs b/Conduit/Conduit.Tests/Integration/Users/Services/UserDataServiceTests/RegisterNewUserTests.cs index 54d54792a4..b4078f67b2 100644 --- a/Conduit/Conduit.Tests/Integration/Users/Services/UserDataServiceTests/RegisterNewUserTests.cs +++ b/Conduit/Conduit.Tests/Integration/Users/Services/UserDataServiceTests/RegisterNewUserTests.cs @@ -1,6 +1,6 @@ using Conduit.Tests.TestHelpers; using Conduit.Tests.TestHelpers.Data; -using Conduit.Web.Models; +using Conduit.Web.DataAccess.Providers; using Conduit.Web.Users.Services; using Couchbase.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection; diff --git a/Conduit/Conduit.Tests/TestHelpers/Data/FollowHelper.cs b/Conduit/Conduit.Tests/TestHelpers/Data/FollowHelper.cs index a79e491745..3d8c547c55 100644 --- a/Conduit/Conduit.Tests/TestHelpers/Data/FollowHelper.cs +++ b/Conduit/Conduit.Tests/TestHelpers/Data/FollowHelper.cs @@ -1,4 +1,4 @@ -using Conduit.Web.Models; +using Conduit.Web.DataAccess.Providers; using Couchbase.KeyValue; namespace Conduit.Tests.TestHelpers.Data; diff --git a/Conduit/Conduit.Tests/TestHelpers/Data/UserHelper.cs b/Conduit/Conduit.Tests/TestHelpers/Data/UserHelper.cs index ae8b0e908b..1ce5d75832 100644 --- a/Conduit/Conduit.Tests/TestHelpers/Data/UserHelper.cs +++ b/Conduit/Conduit.Tests/TestHelpers/Data/UserHelper.cs @@ -1,4 +1,5 @@ -using Conduit.Web.Models; +using Conduit.Web.DataAccess.Models; +using Conduit.Web.DataAccess.Providers; using Conduit.Web.Users.Services; namespace Conduit.Tests.TestHelpers.Data; diff --git a/Conduit/Conduit.Tests/TestHelpers/Dto/CreateArticleRequestHelper.cs b/Conduit/Conduit.Tests/TestHelpers/Dto/CreateArticleRequestHelper.cs new file mode 100644 index 0000000000..4d3ce5a381 --- /dev/null +++ b/Conduit/Conduit.Tests/TestHelpers/Dto/CreateArticleRequestHelper.cs @@ -0,0 +1,35 @@ +using Conduit.Web.Articles.Handlers; +using Conduit.Web.Articles.ViewModels; + +namespace Conduit.Tests.TestHelpers.Dto; + +public static class CreateArticleRequestHelper +{ + public static CreateArticleRequest Create( + string? body = null, + string? description = null, + string? title = null, + List? tags = null, + string? username = null, + bool makeTagsNull = false) + { + var random = new Random(); + + body ??= random.String(1000); + description ??= random.String(100); + title ??= random.String(60); + tags ??= new List { "Couchbase", "cruising" }; + username ??= Path.GetRandomFileName(); + if (makeTagsNull) tags = null; + + var article = new CreateArticleSubmitModel + { + Body = body, + Description = description, + Title = title, + Tags = tags + }; + + return new CreateArticleRequest(article, username); + } +} \ No newline at end of file diff --git a/Conduit/Conduit.Tests/TestHelpers/RandomHelpers.cs b/Conduit/Conduit.Tests/TestHelpers/RandomHelpers.cs new file mode 100644 index 0000000000..0f9580165e --- /dev/null +++ b/Conduit/Conduit.Tests/TestHelpers/RandomHelpers.cs @@ -0,0 +1,13 @@ +namespace Conduit.Tests.TestHelpers; + +public static class RandomHelpers +{ + const string _defaultCharacterSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + public static string String(this Random @this, int length, string? characterPool = null) + { + var chars = characterPool ?? _defaultCharacterSet; + return new string(Enumerable.Repeat(chars, length) + .Select(s => s[@this.Next(s.Length)]).ToArray()); + } +} \ No newline at end of file diff --git a/Conduit/Conduit.Tests/Unit/Articles/Handlers/CreateArticleHandlerTests.cs b/Conduit/Conduit.Tests/Unit/Articles/Handlers/CreateArticleHandlerTests.cs new file mode 100644 index 0000000000..7388eeb9d4 --- /dev/null +++ b/Conduit/Conduit.Tests/Unit/Articles/Handlers/CreateArticleHandlerTests.cs @@ -0,0 +1,226 @@ +using Conduit.Tests.TestHelpers; +using Conduit.Tests.TestHelpers.Dto; +using Conduit.Web.Articles.Handlers; +using Conduit.Web.Articles.Services; +using Moq; + +namespace Conduit.Tests.Unit.Articles.Handlers; + +[TestFixture] +public class CreateArticleHandlerTests +{ + private CreateArticleHandler _handler; + private Mock _tagsDataServiceMock; + private List _allTags; + private Random _random; + private Mock _articleDataServiceMock; + private Mock _slugServiceMock; + + [SetUp] + public async Task Setup() + { + _tagsDataServiceMock = new Mock(); + _articleDataServiceMock = new Mock(); + _slugServiceMock = new Mock(); + _allTags = new List { "Couchbase", "cruising" }; + _random = new Random(); + + _handler = new CreateArticleHandler( + new CreateArticleRequestValidator(_tagsDataServiceMock.Object), + _articleDataServiceMock.Object, + _slugServiceMock.Object); + _tagsDataServiceMock.Setup(m => m.GetAllTags()).ReturnsAsync(_allTags); + } + + [TestCase("", "Title is required.")] + [TestCase(null, "Title is required.")] + [TestCase("aaa", "Title must be at least 10 characters.")] + [TestCase("00000000001111111111222222222233333333334444444444555555555566666666667777777778888888888999999999900000000001111111111", "Title must be 100 characters or less.")] + public async Task Title_must_be_valid(string title, string expectedErrorMessage) + { + // arrange + var request = CreateArticleRequestHelper.Create(); + request.ArticleSubmission.Title = title; + + // act + var result = await _handler.Handle(request, CancellationToken.None); + + // assert + Assert.That(result, Is.Not.Null); + Assert.That(result.ValidationErrors, Is.Not.Null); + Assert.That(result.ValidationErrors.Any(e => e.ErrorMessage == expectedErrorMessage)); + } + + // kinda hacky, but I'm using -1 here to indicate "null" so I can use TestCase + [TestCase(-1, "Description is required.")] + [TestCase(0, "Description is required.")] + [TestCase(5, "Description must be 10 characters or more.")] + [TestCase(201, "Description must be 200 characters or less.")] + public async Task Description_must_be_valid(int strLength, string expectedErrorMessage) + { + // arrange + var request = CreateArticleRequestHelper.Create(); + request.ArticleSubmission.Description = + strLength == -1 + ? null + : _random.String(strLength); + + // act + var result = await _handler.Handle(request, CancellationToken.None); + + // assert + Assert.That(result, Is.Not.Null); + Assert.That(result.ValidationErrors, Is.Not.Null); + Assert.That(result.ValidationErrors.Any(e => e.ErrorMessage == expectedErrorMessage)); + } + + // kinda hacky, but I'm using -1 here to indicate "null" so I can use TestCase + [TestCase(-1, "Body is required.")] + [TestCase(0, "Body is required.")] + [TestCase(5, "Body must be 10 characters or more.")] + [TestCase(20000000, "Body must be 15,000,000 characters or less.")] + public async Task Body_must_be_valid(int strLength, string expectedErrorMessage) + { + // arrange + var request = CreateArticleRequestHelper.Create(); + request.ArticleSubmission.Body = + strLength == -1 + ? null + : _random.String(strLength); + + // act + var result = await _handler.Handle(request, CancellationToken.None); + + // assert + Assert.That(result, Is.Not.Null); + Assert.That(result.ValidationErrors, Is.Not.Null); + Assert.That(result.ValidationErrors.Any(e => e.ErrorMessage == expectedErrorMessage)); + } + + [Test] + public async Task Tags_must_be_on_the_approved_list() + { + // arrange + var request = CreateArticleRequestHelper.Create(); + request.ArticleSubmission.Tags = new List + { + "not-approved-tag-" + Path.GetRandomFileName() + }; + + // act + var result = await _handler.Handle(request, CancellationToken.None); + + // assert + Assert.That(result, Is.Not.Null); + Assert.That(result.ValidationErrors, Is.Not.Null); + Assert.That(result.ValidationErrors.Any(e => e.ErrorMessage == "At least one of those tags isn't allowed.")); + } + + [Test] + public async Task Title_is_trimmed() + { + // arrange + var request = CreateArticleRequestHelper.Create(); + request.ArticleSubmission.Title = " this has spaces that need trimmed "; + + // act + var result = await _handler.Handle(request, CancellationToken.None); + + // assert + Assert.That(result,Is.Not.Null); + Assert.That(result.Article.Title, Is.EqualTo("this has spaces that need trimmed")); + } + + [Test] + public async Task Body_is_trimmed() + { + // arrange + var request = CreateArticleRequestHelper.Create(); + request.ArticleSubmission.Body = " this body has spaces that need trimmed "; + + // act + var result = await _handler.Handle(request, CancellationToken.None); + + // assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Article.Body, Is.EqualTo("this body has spaces that need trimmed")); + } + + [Test] + public async Task Description_is_trimmed() + { + // arrange + var request = CreateArticleRequestHelper.Create(); + request.ArticleSubmission.Description = " this desc has spaces that need trimmed "; + + // act + var result = await _handler.Handle(request, CancellationToken.None); + + // assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Article.Description, Is.EqualTo("this desc has spaces that need trimmed")); + } + + [Test] + public async Task Slug_service_is_used_to_generate_from_title() + { + // arrange + var request = CreateArticleRequestHelper.Create(); + request.ArticleSubmission.Title = "slugify this title"; + _slugServiceMock.Setup(m => m.GenerateSlug(request.ArticleSubmission.Title)) + .ReturnsAsync("slugified-title-a8d9a8ef"); + + // act + var result = await _handler.Handle(request, CancellationToken.None); + + // assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Article.Slug, Is.EqualTo("slugified-title-a8d9a8ef")); + } + + [Test] + public async Task Tag_list_is_passed_through() + { + // arrange + var request = CreateArticleRequestHelper.Create(); + request.ArticleSubmission.Tags = _allTags.Take(1).ToList(); + + // act + var result = await _handler.Handle(request, CancellationToken.None); + + // assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Article.TagList.Contains(request.ArticleSubmission.Tags.Single()), Is.True); + } + + [Test] + public async Task CreatedAt_date_is_set() + { + // arrange + var request = CreateArticleRequestHelper.Create(); + + // act + var result = await _handler.Handle(request, CancellationToken.None); + + // assert - as long as the CreatedAt is within 30 seconds, I'd say that's close enough for a test + Assert.That(result, Is.Not.Null); + Assert.That((new DateTimeOffset(DateTime.Now) - result.Article.CreatedAt).TotalSeconds, Is.LessThanOrEqualTo(30)); + } + + + [Test] + public async Task Favorited_is_initially_false_FavoritesCount_is_zero() + { + // arrange + var request = CreateArticleRequestHelper.Create(); + request.ArticleSubmission.Tags = _allTags.Take(1).ToList(); + + // act + var result = await _handler.Handle(request, CancellationToken.None); + + // assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Article.Favorited, Is.False); + Assert.That(result.Article.FavoritesCount, Is.EqualTo(0)); + } +} \ No newline at end of file diff --git a/Conduit/Conduit.Tests/Unit/Follows/Handlers/FollowUserHandlerTests.cs b/Conduit/Conduit.Tests/Unit/Follows/Handlers/FollowUserHandlerTests.cs index fd2aefaa42..11e4069345 100644 --- a/Conduit/Conduit.Tests/Unit/Follows/Handlers/FollowUserHandlerTests.cs +++ b/Conduit/Conduit.Tests/Unit/Follows/Handlers/FollowUserHandlerTests.cs @@ -2,12 +2,12 @@ using System.Threading; using System.Threading.Tasks; +using Conduit.Web.DataAccess.Dto; +using Conduit.Web.DataAccess.Models; using Conduit.Web.Follows.Handlers; using Conduit.Web.Follows.Services; -using Conduit.Web.Models; using Conduit.Web.Users.Services; -using FluentValidation; -using global::Conduit.Web.Users.ViewModels; +using Conduit.Web.Users.ViewModels; using Moq; using NUnit.Framework; diff --git a/Conduit/Conduit.Tests/Unit/Follows/Handlers/UnfollowUserHandlerTests.cs b/Conduit/Conduit.Tests/Unit/Follows/Handlers/UnfollowUserHandlerTests.cs index 6e378a109a..c0e030490a 100644 --- a/Conduit/Conduit.Tests/Unit/Follows/Handlers/UnfollowUserHandlerTests.cs +++ b/Conduit/Conduit.Tests/Unit/Follows/Handlers/UnfollowUserHandlerTests.cs @@ -1,6 +1,7 @@ -using Conduit.Web.Follows.Handlers; +using Conduit.Web.DataAccess.Dto; +using Conduit.Web.DataAccess.Models; +using Conduit.Web.Follows.Handlers; using Conduit.Web.Follows.Services; -using Conduit.Web.Models; using Conduit.Web.Users.Services; using Conduit.Web.Users.ViewModels; using Moq; diff --git a/Conduit/Conduit.Tests/Unit/Users/Handlers/GetCurrentUserRequestHandlerTests.cs b/Conduit/Conduit.Tests/Unit/Users/Handlers/GetCurrentUserRequestHandlerTests.cs index 6a70f71695..bc094513ca 100644 --- a/Conduit/Conduit.Tests/Unit/Users/Handlers/GetCurrentUserRequestHandlerTests.cs +++ b/Conduit/Conduit.Tests/Unit/Users/Handlers/GetCurrentUserRequestHandlerTests.cs @@ -1,5 +1,6 @@ using Conduit.Tests.TestHelpers; -using Conduit.Web.Models; +using Conduit.Web.DataAccess.Dto; +using Conduit.Web.DataAccess.Models; using Conduit.Web.Users.Handlers; using Conduit.Web.Users.Services; using Moq; diff --git a/Conduit/Conduit.Tests/Unit/Users/Handlers/GetProfileHandlerTests.cs b/Conduit/Conduit.Tests/Unit/Users/Handlers/GetProfileHandlerTests.cs index f38b0380cc..f152613b91 100644 --- a/Conduit/Conduit.Tests/Unit/Users/Handlers/GetProfileHandlerTests.cs +++ b/Conduit/Conduit.Tests/Unit/Users/Handlers/GetProfileHandlerTests.cs @@ -1,7 +1,8 @@ using Conduit.Tests.TestHelpers; using Conduit.Tests.TestHelpers.Data; +using Conduit.Web.DataAccess.Dto; +using Conduit.Web.DataAccess.Models; using Conduit.Web.Follows.Services; -using Conduit.Web.Models; using Conduit.Web.Users.Handlers; using Conduit.Web.Users.Services; using Moq; diff --git a/Conduit/Conduit.Tests/Unit/Users/Handlers/LoginRequestHandlerTests.cs b/Conduit/Conduit.Tests/Unit/Users/Handlers/LoginRequestHandlerTests.cs index ffff76942e..15d6d9bbcc 100644 --- a/Conduit/Conduit.Tests/Unit/Users/Handlers/LoginRequestHandlerTests.cs +++ b/Conduit/Conduit.Tests/Unit/Users/Handlers/LoginRequestHandlerTests.cs @@ -1,4 +1,5 @@ -using Conduit.Web.Models; +using Conduit.Web.DataAccess.Dto; +using Conduit.Web.DataAccess.Models; using Conduit.Web.Users.Handlers; using Conduit.Web.Users.Services; using Conduit.Web.Users.ViewModels; diff --git a/Conduit/Conduit.Tests/Unit/Users/Handlers/RegistrationRequestHandlerTests.cs b/Conduit/Conduit.Tests/Unit/Users/Handlers/RegistrationRequestHandlerTests.cs index f07ace7320..0768f52aa5 100644 --- a/Conduit/Conduit.Tests/Unit/Users/Handlers/RegistrationRequestHandlerTests.cs +++ b/Conduit/Conduit.Tests/Unit/Users/Handlers/RegistrationRequestHandlerTests.cs @@ -1,4 +1,5 @@ -using Conduit.Web.Models; +using Conduit.Web.DataAccess.Dto; +using Conduit.Web.DataAccess.Models; using Conduit.Web.Users.Handlers; using Conduit.Web.Users.Services; using Conduit.Web.Users.ViewModels; diff --git a/Conduit/Conduit.Tests/secrets.json.template b/Conduit/Conduit.Tests/secrets.json.template index 916325e26e..8758a8d869 100644 --- a/Conduit/Conduit.Tests/secrets.json.template +++ b/Conduit/Conduit.Tests/secrets.json.template @@ -13,6 +13,8 @@ "BucketName": "ConduitIntegrationTests", "ScopeName": "_default", "UsersCollectionName": "Users", - "FollowsCollectionName": "Follows" + "FollowsCollectionName": "Follows", + "TagsCollectionName": "Tags", + "ArticlesCollectionName": "Articles" } } diff --git a/Conduit/Conduit.Web/Articles/Controllers/ArticlesController.cs b/Conduit/Conduit.Web/Articles/Controllers/ArticlesController.cs new file mode 100644 index 0000000000..4c9f562515 --- /dev/null +++ b/Conduit/Conduit.Web/Articles/Controllers/ArticlesController.cs @@ -0,0 +1,52 @@ +using Conduit.Web.Articles.Handlers; +using Conduit.Web.Articles.ViewModels; +using Conduit.Web.Users.Handlers; +using Conduit.Web.Users.Services; +using MediatR; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Conduit.Web.Articles.Controllers; + +[ApiController] +[Authorize] +public class ArticlesController : ControllerBase +{ + private readonly IMediator _mediator; + private readonly IAuthService _authService; + + public ArticlesController(IMediator mediator, IAuthService authService) + { + _mediator = mediator; + _authService = authService; + } + + [Authorize] + [HttpPost("/api/articles")] + public async Task CreateArticle([FromBody] CreateArticleSubmitModel articleSubmission) + { + // TODO: make these 3 lines into a convenience method in authservice + var authHeader = Request.Headers["Authorization"]; + var bearerToken = _authService.GetTokenFromHeader(authHeader); + var authorUsername = _authService.GetUsernameClaim(bearerToken).Value; + + var request = new CreateArticleRequest(articleSubmission, authorUsername); + + var article = await _mediator.Send(request); + + // TODO: check validation errors for article + + var authorProfile = await _mediator.Send(new GetProfileRequest(authorUsername, bearerToken)); + + // TODO: check validation errors for profile + + // using dynamic here to meet requirements of Conduit + // spec without creating yet another DTO class + dynamic returnArticle = new + { + article = article.Article + }; + returnArticle.article.author = authorProfile.ProfileView; + return Ok(returnArticle); + } +} diff --git a/Conduit/Conduit.Web/Articles/Handlers/CreateArticleHandler.cs b/Conduit/Conduit.Web/Articles/Handlers/CreateArticleHandler.cs new file mode 100644 index 0000000000..20bf201f25 --- /dev/null +++ b/Conduit/Conduit.Web/Articles/Handlers/CreateArticleHandler.cs @@ -0,0 +1,54 @@ +using Conduit.Web.Articles.Services; +using Conduit.Web.DataAccess.Models; +using FluentValidation; +using MediatR; + +namespace Conduit.Web.Articles.Handlers; + +public class CreateArticleHandler : IRequestHandler +{ + private readonly IValidator _validator; + private readonly IArticlesDataService _articleDataService; + private readonly ISlugService _slugService; + + public CreateArticleHandler(IValidator validator, IArticlesDataService articleDataService, ISlugService slugService) + { + _validator = validator; + _articleDataService = articleDataService; + _slugService = slugService; + } + + public async Task Handle(CreateArticleRequest request, CancellationToken cancellationToken) + { + // validation + var validationResult = await _validator.ValidateAsync(request, cancellationToken); + if (!validationResult.IsValid) + { + return new CreateArticleResponse + { + ValidationErrors = validationResult.Errors + }; + } + + var articleToInsert = new Article + { + Title = request.ArticleSubmission.Title.Trim(), + Description = request.ArticleSubmission.Description.Trim(), + Body = request.ArticleSubmission.Body.Trim(), + Slug = await _slugService.GenerateSlug(request.ArticleSubmission.Title.Trim()), + TagList = request.ArticleSubmission.Tags, + CreatedAt = new DateTimeOffset(DateTime.Now), + Favorited = false, // brand new article, no one can have favorited it yet + FavoritesCount = 0, // brand new article, no one can have favorited it yet + AuthorUsername = request.AuthorUsername + }; + + await _articleDataService.Create(articleToInsert); + + var response = new CreateArticleResponse(); + response.Article = articleToInsert; + response.Article.Slug = articleToInsert.Slug; + + return response; + } +} \ No newline at end of file diff --git a/Conduit/Conduit.Web/Articles/Handlers/CreateArticleRequest.cs b/Conduit/Conduit.Web/Articles/Handlers/CreateArticleRequest.cs new file mode 100644 index 0000000000..e303e91c6d --- /dev/null +++ b/Conduit/Conduit.Web/Articles/Handlers/CreateArticleRequest.cs @@ -0,0 +1,16 @@ +using Conduit.Web.Articles.ViewModels; +using MediatR; + +namespace Conduit.Web.Articles.Handlers; + +public class CreateArticleRequest : IRequest +{ + public CreateArticleSubmitModel ArticleSubmission { get; } + public string AuthorUsername { get; } + + public CreateArticleRequest(CreateArticleSubmitModel articleSubmission, string authorUsername) + { + ArticleSubmission = articleSubmission; + AuthorUsername = authorUsername; + } +} \ No newline at end of file diff --git a/Conduit/Conduit.Web/Articles/Handlers/CreateArticleRequestValidator.cs b/Conduit/Conduit.Web/Articles/Handlers/CreateArticleRequestValidator.cs new file mode 100644 index 0000000000..e46a215b22 --- /dev/null +++ b/Conduit/Conduit.Web/Articles/Handlers/CreateArticleRequestValidator.cs @@ -0,0 +1,46 @@ +using Conduit.Web.Articles.Services; +using FluentValidation; + +namespace Conduit.Web.Articles.Handlers; + +public class CreateArticleRequestValidator : AbstractValidator +{ + private readonly ITagsDataService _tagsDataService; + + public CreateArticleRequestValidator(ITagsDataService tagsDataService) + { + _tagsDataService = tagsDataService; + + RuleFor(x => x.ArticleSubmission.Title) + .Cascade(CascadeMode.Stop) + .NotEmpty().WithMessage("Title is required.") + .MaximumLength(100).WithMessage("Title must be 100 characters or less.") + .MinimumLength(10).WithMessage("Title must be at least 10 characters."); + + RuleFor(x => x.ArticleSubmission.Description) + .Cascade(CascadeMode.Stop) + .NotEmpty().WithMessage("Description is required.") + .MaximumLength(200).WithMessage("Description must be 200 characters or less.") + .MinimumLength(10).WithMessage("Description must be 10 characters or more."); + + RuleFor(x => x.ArticleSubmission.Body) + .Cascade(CascadeMode.Stop) + .NotEmpty().WithMessage("Body is required.") + .MaximumLength(15000000).WithMessage("Body must be 15,000,000 characters or less.") + .MinimumLength(10).WithMessage("Body must be 10 characters or more."); + + RuleFor(x => x.ArticleSubmission.Tags) + .Cascade(CascadeMode.Stop) + .MustAsync(BeAllowedTags).WithMessage("At least one of those tags isn't allowed."); + } + + private async Task BeAllowedTags(List? submittedTags, CancellationToken arg2) + { + if (submittedTags == null || !submittedTags.Any()) + return true; + + var allAllowedTags = await _tagsDataService.GetAllTags(); + + return submittedTags.All(tag => allAllowedTags.Contains(tag)); + } +} \ No newline at end of file diff --git a/Conduit/Conduit.Web/Articles/Handlers/CreateArticleResponse.cs b/Conduit/Conduit.Web/Articles/Handlers/CreateArticleResponse.cs new file mode 100644 index 0000000000..49e7bf064c --- /dev/null +++ b/Conduit/Conduit.Web/Articles/Handlers/CreateArticleResponse.cs @@ -0,0 +1,11 @@ +using Conduit.Web.DataAccess.Models; +using Conduit.Web.Users.ViewModels; +using FluentValidation.Results; + +namespace Conduit.Web.Articles.Handlers; + +public class CreateArticleResponse +{ + public List ValidationErrors { get; set; } + public Article Article { get; set; } +} \ No newline at end of file diff --git a/Conduit/Conduit.Web/Articles/Services/ArticlesDataService.cs b/Conduit/Conduit.Web/Articles/Services/ArticlesDataService.cs new file mode 100644 index 0000000000..18ca999d93 --- /dev/null +++ b/Conduit/Conduit.Web/Articles/Services/ArticlesDataService.cs @@ -0,0 +1,26 @@ +using Conduit.Web.DataAccess.Models; +using Conduit.Web.DataAccess.Providers; + +namespace Conduit.Web.Articles.Services; + +public interface IArticlesDataService +{ + Task Create(Article articleToInsert); +} + +public class ArticlesDataService : IArticlesDataService +{ + private readonly IConduitArticlesCollectionProvider _articlesCollectionProvider; + + public ArticlesDataService(IConduitArticlesCollectionProvider articlesCollectionProvider) + { + _articlesCollectionProvider = articlesCollectionProvider; + } + + public async Task Create(Article articleToInsert) + { + var collection = await _articlesCollectionProvider.GetCollectionAsync(); + + await collection.InsertAsync(articleToInsert.Slug, articleToInsert); + } +} \ No newline at end of file diff --git a/Conduit/Conduit.Web/Articles/Services/SlugService.cs b/Conduit/Conduit.Web/Articles/Services/SlugService.cs new file mode 100644 index 0000000000..e93a4516b2 --- /dev/null +++ b/Conduit/Conduit.Web/Articles/Services/SlugService.cs @@ -0,0 +1,36 @@ +using Conduit.Web.Extensions; +using Slugify; + +namespace Conduit.Web.Articles.Services; + +public interface ISlugService +{ + Task GenerateSlug(string title); +} + +public class SlugService : ISlugService +{ + private readonly ISlugHelper _slugHelper; + private readonly Random _random; + + public SlugService(ISlugHelper slugHelper) + { + _slugHelper = slugHelper; + _random = new Random(); + } + + /// + /// Create a slug of the form "foo-bar-baz-[random string]" + /// This method does NOT check the length or truncate anything + /// + /// Title like "foo bar baz" + /// Slugified string + public async Task GenerateSlug(string title) + { + var slug = _slugHelper.GenerateSlug(title); + + slug += _random.String(12); + + return slug; + } +} \ No newline at end of file diff --git a/Conduit/Conduit.Web/Articles/Services/TagsDataService.cs b/Conduit/Conduit.Web/Articles/Services/TagsDataService.cs index 5e649a5e20..62f168b4cd 100644 --- a/Conduit/Conduit.Web/Articles/Services/TagsDataService.cs +++ b/Conduit/Conduit.Web/Articles/Services/TagsDataService.cs @@ -1,4 +1,5 @@ -using Conduit.Web.Models; +using Conduit.Web.DataAccess.Models; +using Conduit.Web.DataAccess.Providers; namespace Conduit.Web.Articles.Services; diff --git a/Conduit/Conduit.Web/Articles/ViewModels/CreateArticleSubmitModel.cs b/Conduit/Conduit.Web/Articles/ViewModels/CreateArticleSubmitModel.cs new file mode 100644 index 0000000000..87b7390513 --- /dev/null +++ b/Conduit/Conduit.Web/Articles/ViewModels/CreateArticleSubmitModel.cs @@ -0,0 +1,9 @@ +namespace Conduit.Web.Articles.ViewModels; + +public class CreateArticleSubmitModel +{ + public string Title { get; set; } + public string Description { get; set; } + public string Body { get; set; } + public List Tags { get; set; } +} \ No newline at end of file diff --git a/Conduit/Conduit.Web/Conduit.Web.csproj b/Conduit/Conduit.Web/Conduit.Web.csproj index 7d0452c041..a59b0b140b 100644 --- a/Conduit/Conduit.Web/Conduit.Web.csproj +++ b/Conduit/Conduit.Web/Conduit.Web.csproj @@ -17,11 +17,11 @@ + - diff --git a/Conduit/Conduit.Web/DataAccess/Dto/DataResultStatus.cs b/Conduit/Conduit.Web/DataAccess/Dto/DataResultStatus.cs new file mode 100644 index 0000000000..fe9a1c0b15 --- /dev/null +++ b/Conduit/Conduit.Web/DataAccess/Dto/DataResultStatus.cs @@ -0,0 +1,8 @@ +namespace Conduit.Web.DataAccess.Dto; + +public enum DataResultStatus +{ + NotFound = 0, + Ok = 1, + FailedToInsert = 2 +} \ No newline at end of file diff --git a/Conduit/Conduit.Web/Models/DataServiceResult.cs b/Conduit/Conduit.Web/DataAccess/Dto/DataServiceResult.cs similarity index 86% rename from Conduit/Conduit.Web/Models/DataServiceResult.cs rename to Conduit/Conduit.Web/DataAccess/Dto/DataServiceResult.cs index 97be931644..4da38e1c89 100644 --- a/Conduit/Conduit.Web/Models/DataServiceResult.cs +++ b/Conduit/Conduit.Web/DataAccess/Dto/DataServiceResult.cs @@ -1,4 +1,4 @@ -namespace Conduit.Web.Models; +namespace Conduit.Web.DataAccess.Dto; public class DataServiceResult { diff --git a/Conduit/Conduit.Web/Models/KeyWrapper.cs b/Conduit/Conduit.Web/DataAccess/Dto/KeyWrapper.cs similarity index 91% rename from Conduit/Conduit.Web/Models/KeyWrapper.cs rename to Conduit/Conduit.Web/DataAccess/Dto/KeyWrapper.cs index 69554ddb67..39440e78db 100644 --- a/Conduit/Conduit.Web/Models/KeyWrapper.cs +++ b/Conduit/Conduit.Web/DataAccess/Dto/KeyWrapper.cs @@ -1,4 +1,4 @@ -namespace Conduit.Web.Models; +namespace Conduit.Web.DataAccess.Dto; /// /// This should only be used to return documents from SQL++ queries diff --git a/Conduit/Conduit.Web/DataAccess/Models/Article.cs b/Conduit/Conduit.Web/DataAccess/Models/Article.cs new file mode 100644 index 0000000000..ae3e6b399a --- /dev/null +++ b/Conduit/Conduit.Web/DataAccess/Models/Article.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; + +namespace Conduit.Web.DataAccess.Models; + +public class Article +{ + [JsonIgnore] // ignoring this because the document ID is the slug, don't store duplicated data + public string Slug { get; set; } + + public string Title { get; set; } + public string Description { get; set; } + public string Body { get; set; } + public List TagList { get; set; } + public DateTimeOffset CreatedAt { get; set; } + public bool Favorited { get; set; } + public int FavoritesCount { get; set; } + public string AuthorUsername { get; set; } +} \ No newline at end of file diff --git a/Conduit/Conduit.Web/Models/TagData.cs b/Conduit/Conduit.Web/DataAccess/Models/TagData.cs similarity index 60% rename from Conduit/Conduit.Web/Models/TagData.cs rename to Conduit/Conduit.Web/DataAccess/Models/TagData.cs index c7377a3867..0b2e961cc7 100644 --- a/Conduit/Conduit.Web/Models/TagData.cs +++ b/Conduit/Conduit.Web/DataAccess/Models/TagData.cs @@ -1,4 +1,4 @@ -namespace Conduit.Web.Models; +namespace Conduit.Web.DataAccess.Models; public class TagData { diff --git a/Conduit/Conduit.Web/Models/User.cs b/Conduit/Conduit.Web/DataAccess/Models/User.cs similarity index 91% rename from Conduit/Conduit.Web/Models/User.cs rename to Conduit/Conduit.Web/DataAccess/Models/User.cs index aeab2c3070..69285fa90e 100644 --- a/Conduit/Conduit.Web/Models/User.cs +++ b/Conduit/Conduit.Web/DataAccess/Models/User.cs @@ -1,7 +1,7 @@ using System.Runtime.Serialization; using Newtonsoft.Json; -namespace Conduit.Web.Models; +namespace Conduit.Web.DataAccess.Models; public class User { diff --git a/Conduit/Conduit.Web/DataAccess/Providers/IConduitArticlesCollectionProvider.cs b/Conduit/Conduit.Web/DataAccess/Providers/IConduitArticlesCollectionProvider.cs new file mode 100644 index 0000000000..1ed98d17b1 --- /dev/null +++ b/Conduit/Conduit.Web/DataAccess/Providers/IConduitArticlesCollectionProvider.cs @@ -0,0 +1,8 @@ +using Couchbase.Extensions.DependencyInjection; + +namespace Conduit.Web.DataAccess.Providers; + +public interface IConduitArticlesCollectionProvider : INamedCollectionProvider +{ + +} \ No newline at end of file diff --git a/Conduit/Conduit.Web/Models/IConduitBucketProvider.cs b/Conduit/Conduit.Web/DataAccess/Providers/IConduitBucketProvider.cs similarity index 72% rename from Conduit/Conduit.Web/Models/IConduitBucketProvider.cs rename to Conduit/Conduit.Web/DataAccess/Providers/IConduitBucketProvider.cs index f99d19b745..46c7f0fcd4 100644 --- a/Conduit/Conduit.Web/Models/IConduitBucketProvider.cs +++ b/Conduit/Conduit.Web/DataAccess/Providers/IConduitBucketProvider.cs @@ -1,8 +1,8 @@ using Couchbase.Extensions.DependencyInjection; -namespace Conduit.Web.Models; +namespace Conduit.Web.DataAccess.Providers; public interface IConduitBucketProvider : INamedBucketProvider { - + } \ No newline at end of file diff --git a/Conduit/Conduit.Web/Models/IConduitFollowsCollectionProvider.cs b/Conduit/Conduit.Web/DataAccess/Providers/IConduitFollowsCollectionProvider.cs similarity index 75% rename from Conduit/Conduit.Web/Models/IConduitFollowsCollectionProvider.cs rename to Conduit/Conduit.Web/DataAccess/Providers/IConduitFollowsCollectionProvider.cs index e35b6f4a31..4564f5fae6 100644 --- a/Conduit/Conduit.Web/Models/IConduitFollowsCollectionProvider.cs +++ b/Conduit/Conduit.Web/DataAccess/Providers/IConduitFollowsCollectionProvider.cs @@ -1,6 +1,6 @@ using Couchbase.Extensions.DependencyInjection; -namespace Conduit.Web.Models; +namespace Conduit.Web.DataAccess.Providers; public interface IConduitFollowsCollectionProvider : INamedCollectionProvider { diff --git a/Conduit/Conduit.Web/Models/IConduitTagsCollectionProvider.cs b/Conduit/Conduit.Web/DataAccess/Providers/IConduitTagsCollectionProvider.cs similarity index 75% rename from Conduit/Conduit.Web/Models/IConduitTagsCollectionProvider.cs rename to Conduit/Conduit.Web/DataAccess/Providers/IConduitTagsCollectionProvider.cs index 1f12163313..b9562899df 100644 --- a/Conduit/Conduit.Web/Models/IConduitTagsCollectionProvider.cs +++ b/Conduit/Conduit.Web/DataAccess/Providers/IConduitTagsCollectionProvider.cs @@ -1,6 +1,6 @@ using Couchbase.Extensions.DependencyInjection; -namespace Conduit.Web.Models; +namespace Conduit.Web.DataAccess.Providers; public interface IConduitTagsCollectionProvider : INamedCollectionProvider { diff --git a/Conduit/Conduit.Web/Models/IConduitUsersCollectionProvider.cs b/Conduit/Conduit.Web/DataAccess/Providers/IConduitUsersCollectionProvider.cs similarity index 74% rename from Conduit/Conduit.Web/Models/IConduitUsersCollectionProvider.cs rename to Conduit/Conduit.Web/DataAccess/Providers/IConduitUsersCollectionProvider.cs index 317a4feb55..71bc70d0f0 100644 --- a/Conduit/Conduit.Web/Models/IConduitUsersCollectionProvider.cs +++ b/Conduit/Conduit.Web/DataAccess/Providers/IConduitUsersCollectionProvider.cs @@ -1,8 +1,8 @@ using Couchbase.Extensions.DependencyInjection; -namespace Conduit.Web.Models; +namespace Conduit.Web.DataAccess.Providers; public interface IConduitUsersCollectionProvider : INamedCollectionProvider { - + } \ No newline at end of file diff --git a/Conduit/Conduit.Web/Extensions/RandomExtensions.cs b/Conduit/Conduit.Web/Extensions/RandomExtensions.cs new file mode 100644 index 0000000000..f17a5652b1 --- /dev/null +++ b/Conduit/Conduit.Web/Extensions/RandomExtensions.cs @@ -0,0 +1,13 @@ +namespace Conduit.Web.Extensions; + +public static class RandomExtensions +{ + const string _defaultCharacterSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + + public static string String(this Random @this, int length, string? characterPool = null) + { + var chars = characterPool ?? _defaultCharacterSet; + return new string(Enumerable.Repeat(chars, length) + .Select(s => s[@this.Next(s.Length)]).ToArray()); + } +} \ No newline at end of file diff --git a/Conduit/Conduit.Web/Follows/Handlers/FollowUserHandler.cs b/Conduit/Conduit.Web/Follows/Handlers/FollowUserHandler.cs index 79e83f7b54..2c3864b6df 100644 --- a/Conduit/Conduit.Web/Follows/Handlers/FollowUserHandler.cs +++ b/Conduit/Conduit.Web/Follows/Handlers/FollowUserHandler.cs @@ -1,9 +1,9 @@ using Conduit.Web.Users.Services; using Conduit.Web.Users.ViewModels; using Conduit.Web.Follows.Services; -using Conduit.Web.Models; using MediatR; using FluentValidation; +using Conduit.Web.DataAccess.Dto; namespace Conduit.Web.Follows.Handlers; diff --git a/Conduit/Conduit.Web/Follows/Handlers/FollowUserRequestValidator.cs b/Conduit/Conduit.Web/Follows/Handlers/FollowUserRequestValidator.cs index 67f3323e33..0f6ab63742 100644 --- a/Conduit/Conduit.Web/Follows/Handlers/FollowUserRequestValidator.cs +++ b/Conduit/Conduit.Web/Follows/Handlers/FollowUserRequestValidator.cs @@ -1,6 +1,4 @@ -using Conduit.Web.Models; -using Conduit.Web.Users.Services; -using FluentValidation; +using FluentValidation; namespace Conduit.Web.Follows.Handlers; @@ -10,5 +8,13 @@ public FollowUserRequestValidator() { RuleFor(x => x.UserToFollow) .NotEmpty().WithMessage("Username is required."); + + RuleFor(x => x.UserToFollow) + .Must(NotBeMyself).WithMessage("You can't follow yourself."); + } + + private bool NotBeMyself(string username) + { + throw new NotImplementedException(); } } \ No newline at end of file diff --git a/Conduit/Conduit.Web/Follows/Handlers/UnfollowUserHandler.cs b/Conduit/Conduit.Web/Follows/Handlers/UnfollowUserHandler.cs index 6ac7e8ed10..b1148415d7 100644 --- a/Conduit/Conduit.Web/Follows/Handlers/UnfollowUserHandler.cs +++ b/Conduit/Conduit.Web/Follows/Handlers/UnfollowUserHandler.cs @@ -1,9 +1,9 @@ using Conduit.Web.Users.Services; using Conduit.Web.Users.ViewModels; using Conduit.Web.Follows.Services; -using Conduit.Web.Models; using MediatR; using FluentValidation; +using Conduit.Web.DataAccess.Dto; namespace Conduit.Web.Follows.Handlers; diff --git a/Conduit/Conduit.Web/Follows/Services/FollowsDataService.cs b/Conduit/Conduit.Web/Follows/Services/FollowsDataService.cs index 396c609730..4c52f1810d 100644 --- a/Conduit/Conduit.Web/Follows/Services/FollowsDataService.cs +++ b/Conduit/Conduit.Web/Follows/Services/FollowsDataService.cs @@ -1,4 +1,4 @@ -using Conduit.Web.Models; +using Conduit.Web.DataAccess.Providers; using Conduit.Web.Users.Services; using Couchbase.KeyValue; @@ -46,6 +46,8 @@ public async Task UnfollowUser(string userToUnfollow, string followerUsername) public async Task IsCurrentUserFollowing(string currentUserBearerToken, string username) { + // TODO: can't follow yourself + var currentUserUsername = _authService.GetUsernameClaim(currentUserBearerToken); var collection = await _followsCollectionProvider.GetCollectionAsync(); diff --git a/Conduit/Conduit.Web/Models/DataResultStatus.cs b/Conduit/Conduit.Web/Models/DataResultStatus.cs deleted file mode 100644 index 8ae50d1cd1..0000000000 --- a/Conduit/Conduit.Web/Models/DataResultStatus.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Conduit.Web.Models; - -// TODO: move this out of Users slice, since it's likely to be used -// by multiple slices -// maybe this goes into Models? -public enum DataResultStatus -{ - NotFound = 0, - Ok = 1, - FailedToInsert = 2 -} \ No newline at end of file diff --git a/Conduit/Conduit.Web/Program.cs b/Conduit/Conduit.Web/Program.cs index bee153d209..92971df362 100644 --- a/Conduit/Conduit.Web/Program.cs +++ b/Conduit/Conduit.Web/Program.cs @@ -3,13 +3,13 @@ using System.Text; using Conduit.Web.Articles.Services; using Conduit.Web.Follows.Services; -using Conduit.Web.Models; using Conduit.Web.Users.Handlers; using Conduit.Web.Users.Services; using Couchbase.Extensions.DependencyInjection; using FluentValidation; using Microsoft.OpenApi.Models; -using Microsoft.Extensions.Configuration; +using Slugify; +using Conduit.Web.DataAccess.Providers; namespace Conduit.Web { @@ -70,12 +70,14 @@ public static class ConduitServiceSetupExtension /// public static void AddConduitServiceDependencies(this IServiceCollection @this, ConfigurationManager configManager) { + @this.AddTransient(); @this.AddValidatorsFromAssemblyContaining(); @this.AddTransient(typeof(SharedUserValidator<>)); @this.AddTransient(); @this.AddTransient(); @this.AddTransient(); @this.AddTransient(); + @this.AddTransient(); @this.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining()); @this.AddCouchbase(configManager.GetSection("Couchbase")); @this.AddCouchbaseBucket(configManager["Couchbase:BucketName"], b => @@ -89,6 +91,9 @@ public static void AddConduitServiceDependencies(this IServiceCollection @this, b .AddScope(configManager["Couchbase:ScopeName"]) .AddCollection(configManager["Couchbase:TagsCollectionName"]); + b + .AddScope(configManager["Couchbase:ScopeName"]) + .AddCollection(configManager["Couchbase:ArticlesCollectionName"]); }); } } diff --git a/Conduit/Conduit.Web/Users/Handlers/GetCurrentUserHandler.cs b/Conduit/Conduit.Web/Users/Handlers/GetCurrentUserHandler.cs index 095409bf73..f98d697a7a 100644 --- a/Conduit/Conduit.Web/Users/Handlers/GetCurrentUserHandler.cs +++ b/Conduit/Conduit.Web/Users/Handlers/GetCurrentUserHandler.cs @@ -1,4 +1,4 @@ -using Conduit.Web.Models; +using Conduit.Web.DataAccess.Dto; using Conduit.Web.Users.Services; using Conduit.Web.Users.ViewModels; using MediatR; diff --git a/Conduit/Conduit.Web/Users/Handlers/GetProfileHandler.cs b/Conduit/Conduit.Web/Users/Handlers/GetProfileHandler.cs index b8c81b82e7..3e9637b147 100644 --- a/Conduit/Conduit.Web/Users/Handlers/GetProfileHandler.cs +++ b/Conduit/Conduit.Web/Users/Handlers/GetProfileHandler.cs @@ -3,7 +3,7 @@ using Conduit.Web.Users.ViewModels; using MediatR; using FluentValidation; -using Conduit.Web.Models; +using Conduit.Web.DataAccess.Dto; namespace Conduit.Web.Users.Handlers; diff --git a/Conduit/Conduit.Web/Users/Handlers/LoginRequestHandler.cs b/Conduit/Conduit.Web/Users/Handlers/LoginRequestHandler.cs index 366ee4af3e..187002015f 100644 --- a/Conduit/Conduit.Web/Users/Handlers/LoginRequestHandler.cs +++ b/Conduit/Conduit.Web/Users/Handlers/LoginRequestHandler.cs @@ -1,4 +1,4 @@ -using Conduit.Web.Models; +using Conduit.Web.DataAccess.Dto; using Conduit.Web.Users.Services; using Conduit.Web.Users.ViewModels; using FluentValidation; diff --git a/Conduit/Conduit.Web/Users/Handlers/RegistrationRequestHandler.cs b/Conduit/Conduit.Web/Users/Handlers/RegistrationRequestHandler.cs index 6c89456e36..e002cff657 100644 --- a/Conduit/Conduit.Web/Users/Handlers/RegistrationRequestHandler.cs +++ b/Conduit/Conduit.Web/Users/Handlers/RegistrationRequestHandler.cs @@ -1,4 +1,5 @@ -using Conduit.Web.Models; +using Conduit.Web.DataAccess.Dto; +using Conduit.Web.DataAccess.Models; using Conduit.Web.Users.Services; using Conduit.Web.Users.ViewModels; using FluentValidation; diff --git a/Conduit/Conduit.Web/Users/Handlers/RegistrationRequestValidator.cs b/Conduit/Conduit.Web/Users/Handlers/RegistrationRequestValidator.cs index 25738bda6f..dbf94b9ce9 100644 --- a/Conduit/Conduit.Web/Users/Handlers/RegistrationRequestValidator.cs +++ b/Conduit/Conduit.Web/Users/Handlers/RegistrationRequestValidator.cs @@ -1,4 +1,4 @@ -using Conduit.Web.Models; +using Conduit.Web.DataAccess.Dto; using Conduit.Web.Users.Services; using Conduit.Web.Users.ViewModels; using Couchbase.Query; diff --git a/Conduit/Conduit.Web/Users/Handlers/UpdateUserRequestValidator.cs b/Conduit/Conduit.Web/Users/Handlers/UpdateUserRequestValidator.cs index c8e6107fd9..8947bc8963 100644 --- a/Conduit/Conduit.Web/Users/Handlers/UpdateUserRequestValidator.cs +++ b/Conduit/Conduit.Web/Users/Handlers/UpdateUserRequestValidator.cs @@ -1,7 +1,5 @@ -using Conduit.Web.Models; -using Conduit.Web.Users.Services; +using Conduit.Web.Users.Services; using Conduit.Web.Users.ViewModels; -using Couchbase.Query; using FluentValidation; namespace Conduit.Web.Users.Handlers; diff --git a/Conduit/Conduit.Web/Users/Services/UserDataService.cs b/Conduit/Conduit.Web/Users/Services/UserDataService.cs index c4e45b9e29..1ffb0b9c74 100644 --- a/Conduit/Conduit.Web/Users/Services/UserDataService.cs +++ b/Conduit/Conduit.Web/Users/Services/UserDataService.cs @@ -1,9 +1,11 @@ -using Conduit.Web.Models; -using Couchbase.Core.Exceptions.KeyValue; +using Couchbase.Core.Exceptions.KeyValue; using Couchbase.KeyValue; using Couchbase.Query; using Conduit.Web.Users.ViewModels; using Microsoft.AspNetCore.Mvc.Formatters; +using Conduit.Web.DataAccess.Providers; +using Conduit.Web.DataAccess.Models; +using Conduit.Web.DataAccess.Dto; namespace Conduit.Web.Users.Services; diff --git a/Conduit/Conduit.Web/appsettings.json b/Conduit/Conduit.Web/appsettings.json index 53f46e7b47..bcfd2b3722 100644 --- a/Conduit/Conduit.Web/appsettings.json +++ b/Conduit/Conduit.Web/appsettings.json @@ -12,7 +12,7 @@ "SecurityKey": "" }, "Couchbase": { - "//I would recommend using User Secrets and environment variables instead of appsettings.json" : "comment", + "//I would recommend using User Secrets and environment variables instead of appsettings.json": "comment", "ConnectionString": "couchbases://cb..cloud.couchbase.com", "Username": "", "Password": "