From c8d0d6ac3d8a000bf322e40c71d45244531e348e Mon Sep 17 00:00:00 2001 From: StanR Date: Sat, 28 May 2022 01:03:28 +0300 Subject: [PATCH] Setup unit testing project, add couple of redis unit tests --- .../CircleForms.Tests.csproj | 32 ++++ .../Services/RedisCacheRepositoryTests.cs | 180 ++++++++++++++++++ .../CircleForms.Tests/Utils/MappingHelper.cs | 17 ++ .../CircleForms/Database/Models/Users/User.cs | 8 +- .../Database/Services/RedisCacheRepository.cs | 11 +- backend/CircleForms/UnitTestDetector.cs | 25 +++ 6 files changed, 269 insertions(+), 4 deletions(-) create mode 100644 backend/CircleForms.Tests/CircleForms.Tests.csproj create mode 100644 backend/CircleForms.Tests/Unit/Database/Services/RedisCacheRepositoryTests.cs create mode 100644 backend/CircleForms.Tests/Utils/MappingHelper.cs create mode 100644 backend/CircleForms/UnitTestDetector.cs diff --git a/backend/CircleForms.Tests/CircleForms.Tests.csproj b/backend/CircleForms.Tests/CircleForms.Tests.csproj new file mode 100644 index 00000000..e5793e24 --- /dev/null +++ b/backend/CircleForms.Tests/CircleForms.Tests.csproj @@ -0,0 +1,32 @@ + + + + net6.0 + enable + + false + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + diff --git a/backend/CircleForms.Tests/Unit/Database/Services/RedisCacheRepositoryTests.cs b/backend/CircleForms.Tests/Unit/Database/Services/RedisCacheRepositoryTests.cs new file mode 100644 index 00000000..11b1e0d4 --- /dev/null +++ b/backend/CircleForms.Tests/Unit/Database/Services/RedisCacheRepositoryTests.cs @@ -0,0 +1,180 @@ + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using CircleForms.Database.Models.Users; +using CircleForms.Database.Services; +using CircleForms.Database.Services.Extensions; +using CircleForms.Tests.Utils; +using Microsoft.Extensions.Logging.Abstractions; +using MongoDB.Bson; +using Moq; +using StackExchange.Redis; +using Xunit; +using JsonConvert = Newtonsoft.Json.JsonConvert; + +namespace CircleForms.Tests.Unit.Database.Services +{ + public class RedisCacheRepositoryTests + { + private readonly RedisCacheRepository _cache; + private readonly Mock _redisDatabaseMock; + + public RedisCacheRepositoryTests() + { + _redisDatabaseMock = new Mock(MockBehavior.Strict); + + var redisMock = new Mock(); + redisMock.Setup(x => x.GetDatabase(It.IsAny(), It.IsAny())).Returns(_redisDatabaseMock.Object); + + _cache = new RedisCacheRepository(redisMock.Object, MappingHelper.CreateMapper(), + new NullLogger()); + } + + [Fact] + public async Task IncrementAnswersUsesCorrectId() + { + const string id = "123456"; + + _redisDatabaseMock + .Setup(x => x.StringIncrementAsync(It.Is(y => y == id.ToPostAnswersCount()), It.IsAny(), + It.IsAny())) + .ReturnsAsync(1); + + await _cache.IncrementAnswers(id); + } + + [Fact] + public async Task PinPostAddsPostToTheSet() + { + var postId = new Guid().ToString(); + + _redisDatabaseMock + .Setup(x => x.KeyExists(It.Is(y => y == postId.ToPostId()), It.IsAny())) + .Returns(true); + + _redisDatabaseMock + .Setup(x => x.SetAddAsync(It.Is(y => y == "posts:pinned"), + It.Is(y => y == postId.ToPostId()), It.IsAny())) + .ReturnsAsync(true); + + var result = await _cache.PinPost(postId); + + Assert.True(result); + _redisDatabaseMock.Verify(); + } + + [Fact] + public async Task PinPostDoesntAddPostToTheSetWhenItDoesntExist() + { + var postId = new Guid().ToString(); + + _redisDatabaseMock + .Setup(x => x.KeyExists(It.Is(y => y == postId.ToPostId()), It.IsAny())) + .Returns(false); + + var result = await _cache.PinPost(postId); + + Assert.False(result); + _redisDatabaseMock.Verify(); + } + + [Fact] + public async Task AddUserAddsCorrectData() + { + var user = new User + { + ID = "123456", + Discord = "Test#7270", + Osu = new BsonDocument(new Dictionary + { + { "Username", "VsevolodVolkov" }, + { "AvatarUrl", "https://a.ppy.sh/123456" } + }) + }; + + var expectedRedisUser = JsonConvert.SerializeObject(new UserMinimalRedis + { + ID = user.ID, + Discord = user.Discord, + Username = user.Osu["Username"].ToString(), + AvatarUrl = user.Osu["AvatarUrl"].ToString() + }); + + _redisDatabaseMock + .Setup(x => x.SetAddAsync(It.Is(y => y == "user_ids"), It.Is(y => y == user.ID), + It.IsAny())) + .ReturnsAsync(true); + + _redisDatabaseMock + .Setup(x => x.StringSetAsync(It.Is(y => y == user.ID.ToUserId()), + It.Is(y => y == expectedRedisUser), + It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + + await _cache.AddUser(user); + + _redisDatabaseMock.Verify(); + } + + [Fact] + public async Task GetMinimalUserReturnsUserOnCorrectId() + { + var user = new UserMinimalRedis + { + ID = "123456", + Username = "VsevolodVolkov", + AvatarUrl = "https://a.ppy.sh/123456", + Discord = "Test#7270" + }; + + _redisDatabaseMock + .Setup(x => x.StringGetAsync(It.Is(y => y == user.ID.ToUserId()), It.IsAny())) + .ReturnsAsync(new RedisValue(JsonConvert.SerializeObject(user))); + + var result = await _cache.GetMinimalUser(user.ID); + + Assert.NotNull(result); + Assert.Equal(user.ID, result.ID); + Assert.Equal(user.Username, result.Username); + Assert.Equal(user.AvatarUrl, result.AvatarUrl); + Assert.Equal(user.Discord, result.Discord); + } + + [Fact] + public async Task GetMinimalUserReturnsNullOnIncorrectId() + { + var user = new UserMinimalRedis + { + ID = "123456", + Username = "VsevolodVolkov", + AvatarUrl = "https://a.ppy.sh/123456", + Discord = "Test#7270" + }; + + _redisDatabaseMock + .Setup(x => x.StringGetAsync(It.Is(y => y == user.ID.ToUserId()), It.IsAny())) + .ReturnsAsync(new RedisValue()); + + var result = await _cache.GetMinimalUser(user.ID); + + Assert.Null(result); + } + + [Fact] + public async Task RemoveUserUsesCorrectIds() + { + const string id = "123456"; + + _redisDatabaseMock + .Setup(x => x.KeyDeleteAsync(It.Is(y => y == id.ToUserId()), It.IsAny())) + .ReturnsAsync(true); + + _redisDatabaseMock + .Setup(x => x.SetRemoveAsync(It.Is(y => y == "user_ids"), It.Is(y => y == id), It.IsAny())) + .ReturnsAsync(true); + + await _cache.RemoveUser(id); + } + } +} diff --git a/backend/CircleForms.Tests/Utils/MappingHelper.cs b/backend/CircleForms.Tests/Utils/MappingHelper.cs new file mode 100644 index 00000000..83bbd310 --- /dev/null +++ b/backend/CircleForms.Tests/Utils/MappingHelper.cs @@ -0,0 +1,17 @@ +using FastExpressionCompiler; +using Mapster; +using MapsterMapper; + +namespace CircleForms.Tests.Utils +{ + internal static class MappingHelper + { + public static IMapper CreateMapper() + { + TypeAdapterConfig.GlobalSettings.Scan(typeof(Program).Assembly); + TypeAdapterConfig.GlobalSettings.Compiler = x => x.CompileFast(); + + return new Mapper(); + } + } +} diff --git a/backend/CircleForms/Database/Models/Users/User.cs b/backend/CircleForms/Database/Models/Users/User.cs index a8feb7b4..57c8bd71 100644 --- a/backend/CircleForms/Database/Models/Users/User.cs +++ b/backend/CircleForms/Database/Models/Users/User.cs @@ -12,8 +12,12 @@ public class User : IEntity { public User() { - this.InitOneToMany(() => PostsRelation); - this.InitOneToMany(() => Answers); + // DB inits can't be done in unit testing due to how MongoDB.Entities database initialization works + if (!UnitTestDetector.IsRunningFromXUnit) + { + this.InitOneToMany(() => PostsRelation); + this.InitOneToMany(() => Answers); + } } [Field("token")] diff --git a/backend/CircleForms/Database/Services/RedisCacheRepository.cs b/backend/CircleForms/Database/Services/RedisCacheRepository.cs index eb192a3b..3ecd38fe 100644 --- a/backend/CircleForms/Database/Services/RedisCacheRepository.cs +++ b/backend/CircleForms/Database/Services/RedisCacheRepository.cs @@ -70,9 +70,16 @@ public async Task UserExists(string id) public async Task AddUser(User user) { - await _redis.SetAddAsync(_userIds, user.ID); + if (!await _redis.SetAddAsync(_userIds, user.ID)) + { + _logger.LogError("Could not post {@User} to the redis userid cache", user.ID); + } + var cachedUser = _mapper.Map(user); - await _redis.StringSetAsync(user.ID.ToUserId(), JsonConvert.SerializeObject(cachedUser, Formatting.None)); + if (!await _redis.StringSetAsync(user.ID.ToUserId(), JsonConvert.SerializeObject(cachedUser, Formatting.None))) + { + _logger.LogError("Could not post {@User} to the redis user cache", user); + } } public async Task GetMinimalUser(string id) diff --git a/backend/CircleForms/UnitTestDetector.cs b/backend/CircleForms/UnitTestDetector.cs new file mode 100644 index 00000000..3f2d0b1a --- /dev/null +++ b/backend/CircleForms/UnitTestDetector.cs @@ -0,0 +1,25 @@ +using System; + +namespace CircleForms; + +/// +/// This exists ONLY for MongoDB.Entities-based models and MUST NOT be used anywhere else. +/// Doing workarounds for unit testing is a very very bad design, but unfortunately we can't avoid it here. +/// +public static class UnitTestDetector +{ + static UnitTestDetector() + { + foreach (var assem in AppDomain.CurrentDomain.GetAssemblies()) + { + if (assem.FullName != null && + assem.FullName.ToLowerInvariant().StartsWith("xunit")) + { + IsRunningFromXUnit = true; + break; + } + } + } + + public static bool IsRunningFromXUnit { get; } +}