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": "