From 181b53e270ff7c0558edec75b8b255d487e796c3 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Fri, 25 Oct 2019 23:03:36 +0800 Subject: Add user detail service. --- Timeline.Tests/DatabaseTest.cs | 17 ++ Timeline.Tests/Services/UserAvatarServiceTest.cs | 281 ++++++++++++++++++++ Timeline.Tests/Services/UserDetailServiceTest.cs | 108 ++++++++ Timeline.Tests/UserAvatarServiceTest.cs | 284 --------------------- Timeline/Entities/DatabaseContext.cs | 1 + Timeline/Entities/User.cs | 2 + Timeline/Entities/UserDetail.cs | 21 ++ .../Services/UserDetailService.Designer.cs | 99 +++++++ Timeline/Resources/Services/UserDetailService.resx | 132 ++++++++++ Timeline/Services/DatabaseExtensions.cs | 6 +- Timeline/Services/UserAvatarService.cs | 9 +- Timeline/Services/UserDetailService.cs | 102 ++++++++ Timeline/Timeline.csproj | 9 + 13 files changed, 779 insertions(+), 292 deletions(-) create mode 100644 Timeline.Tests/Services/UserAvatarServiceTest.cs create mode 100644 Timeline.Tests/Services/UserDetailServiceTest.cs delete mode 100644 Timeline.Tests/UserAvatarServiceTest.cs create mode 100644 Timeline/Entities/UserDetail.cs create mode 100644 Timeline/Resources/Services/UserDetailService.Designer.cs create mode 100644 Timeline/Resources/Services/UserDetailService.resx create mode 100644 Timeline/Services/UserDetailService.cs diff --git a/Timeline.Tests/DatabaseTest.cs b/Timeline.Tests/DatabaseTest.cs index b5681491..fc153c24 100644 --- a/Timeline.Tests/DatabaseTest.cs +++ b/Timeline.Tests/DatabaseTest.cs @@ -42,5 +42,22 @@ namespace Timeline.Tests _context.SaveChanges(); _context.UserAvatars.Count().Should().Be(0); } + + [Fact] + public void DeleteUserShouldAlsoDeleteDetail() + { + var user = _context.Users.First(); + _context.UserDetails.Count().Should().Be(0); + _context.UserDetails.Add(new UserDetail + { + Nickname = null, + UserId = user.Id + }); + _context.SaveChanges(); + _context.UserDetails.Count().Should().Be(1); + _context.Users.Remove(user); + _context.SaveChanges(); + _context.UserDetails.Count().Should().Be(0); + } } } diff --git a/Timeline.Tests/Services/UserAvatarServiceTest.cs b/Timeline.Tests/Services/UserAvatarServiceTest.cs new file mode 100644 index 00000000..cf3d2a0a --- /dev/null +++ b/Timeline.Tests/Services/UserAvatarServiceTest.cs @@ -0,0 +1,281 @@ +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging.Abstractions; +using Moq; +using SixLabors.ImageSharp.Formats.Png; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Timeline.Entities; +using Timeline.Services; +using Timeline.Tests.Helpers; +using Timeline.Tests.Mock.Data; +using Xunit; + +namespace Timeline.Tests.Services +{ + public class UserAvatarValidatorTest : IClassFixture + { + private readonly UserAvatarValidator _validator; + + public UserAvatarValidatorTest(UserAvatarValidator validator) + { + _validator = validator; + } + + [Fact] + public void CantDecode() + { + Avatar avatar = new Avatar + { + Data = Encoding.ASCII.GetBytes("This is not a image."), + Type = "image/png" + }; + _validator.Awaiting(v => v.Validate(avatar)) + .Should().Throw() + .Where(e => e.Avatar == avatar && e.Error == AvatarFormatException.ErrorReason.CantDecode); + } + + [Fact] + public void UnmatchedFormat() + { + Avatar avatar = new Avatar + { + Data = ImageHelper.CreatePngWithSize(100, 100), + Type = "image/jpeg" + }; + _validator.Awaiting(v => v.Validate(avatar)) + .Should().Throw() + .Where(e => e.Avatar == avatar && e.Error == AvatarFormatException.ErrorReason.UnmatchedFormat); + } + + [Fact] + public void BadSize() + { + Avatar avatar = new Avatar + { + Data = ImageHelper.CreatePngWithSize(100, 200), + Type = PngFormat.Instance.DefaultMimeType + }; + _validator.Awaiting(v => v.Validate(avatar)) + .Should().Throw() + .Where(e => e.Avatar == avatar && e.Error == AvatarFormatException.ErrorReason.BadSize); + } + + [Fact] + public void Success() + { + Avatar avatar = new Avatar + { + Data = ImageHelper.CreatePngWithSize(100, 100), + Type = PngFormat.Instance.DefaultMimeType + }; + _validator.Awaiting(v => v.Validate(avatar)) + .Should().NotThrow(); + } + } + + public class UserAvatarServiceTest : IDisposable + { + private UserAvatar CreateMockAvatarEntity(string key) => new UserAvatar + { + Type = $"image/test{key}", + Data = Encoding.ASCII.GetBytes($"mock{key}"), + ETag = $"etag{key}", + LastModified = DateTime.Now + }; + + private AvatarInfo CreateMockAvatarInfo(string key) => new AvatarInfo + { + Avatar = new Avatar + { + Type = $"image/test{key}", + Data = Encoding.ASCII.GetBytes($"mock{key}") + }, + LastModified = DateTime.Now + }; + + private Avatar CreateMockAvatar(string key) => new Avatar + { + Type = $"image/test{key}", + Data = Encoding.ASCII.GetBytes($"mock{key}") + }; + + private static Avatar ToAvatar(UserAvatar entity) + { + return new Avatar + { + Data = entity.Data, + Type = entity.Type + }; + } + + private static AvatarInfo ToAvatarInfo(UserAvatar entity) + { + return new AvatarInfo + { + Avatar = ToAvatar(entity), + LastModified = entity.LastModified + }; + } + + private readonly Mock _mockDefaultAvatarProvider; + private readonly Mock _mockValidator; + private readonly Mock _mockETagGenerator; + private readonly Mock _mockClock; + + private readonly TestDatabase _database; + + private readonly UserAvatarService _service; + + public UserAvatarServiceTest() + { + _mockDefaultAvatarProvider = new Mock(); + _mockValidator = new Mock(); + _mockETagGenerator = new Mock(); + _mockClock = new Mock(); + + _database = new TestDatabase(); + + _service = new UserAvatarService(NullLogger.Instance, _database.DatabaseContext, _mockDefaultAvatarProvider.Object, _mockValidator.Object, _mockETagGenerator.Object, _mockClock.Object); + } + + public void Dispose() + { + _database.Dispose(); + } + + [Theory] + [InlineData(null, typeof(ArgumentNullException))] + [InlineData("", typeof(UsernameBadFormatException))] + [InlineData("a!a", typeof(UsernameBadFormatException))] + [InlineData("usernotexist", typeof(UserNotExistException))] + public async Task GetAvatarETag_ShouldThrow(string username, Type exceptionType) + { + await _service.Awaiting(s => s.GetAvatarETag(username)).Should().ThrowAsync(exceptionType); + } + + [Fact] + public async Task GetAvatarETag_ShouldReturn_Default() + { + const string etag = "aaaaaa"; + _mockDefaultAvatarProvider.Setup(p => p.GetDefaultAvatarETag()).ReturnsAsync(etag); + (await _service.GetAvatarETag(MockUser.User.Username)).Should().Be(etag); + } + + [Fact] + public async Task GetAvatarETag_ShouldReturn_Data() + { + string username = MockUser.User.Username; + var mockAvatarEntity = CreateMockAvatarEntity("aaa"); + { + var context = _database.DatabaseContext; + var user = await context.Users.Where(u => u.Name == username).Include(u => u.Avatar).SingleAsync(); + user.Avatar = mockAvatarEntity; + await context.SaveChangesAsync(); + } + (await _service.GetAvatarETag(username)).Should().BeEquivalentTo(mockAvatarEntity.ETag); + } + + [Theory] + [InlineData(null, typeof(ArgumentNullException))] + [InlineData("", typeof(UsernameBadFormatException))] + [InlineData("a!a", typeof(UsernameBadFormatException))] + [InlineData("usernotexist", typeof(UserNotExistException))] + public async Task GetAvatar_ShouldThrow(string username, Type exceptionType) + { + await _service.Awaiting(s => s.GetAvatar(username)).Should().ThrowAsync(exceptionType); + + } + + [Fact] + public async Task GetAvatar_ShouldReturn_Default() + { + var mockAvatar = CreateMockAvatarInfo("aaa"); + _mockDefaultAvatarProvider.Setup(p => p.GetDefaultAvatar()).ReturnsAsync(mockAvatar); + string username = MockUser.User.Username; + (await _service.GetAvatar(username)).Should().BeEquivalentTo(mockAvatar); + } + + [Fact] + public async Task GetAvatar_ShouldReturn_Data() + { + string username = MockUser.User.Username; + var mockAvatarEntity = CreateMockAvatarEntity("aaa"); + { + var context = _database.DatabaseContext; + var user = await context.Users.Where(u => u.Name == username).Include(u => u.Avatar).SingleAsync(); + user.Avatar = mockAvatarEntity; + await context.SaveChangesAsync(); + } + + (await _service.GetAvatar(username)).Should().BeEquivalentTo(ToAvatarInfo(mockAvatarEntity)); + } + + public static IEnumerable SetAvatar_ShouldThrow_Data() + { + yield return new object[] { null, null, typeof(ArgumentNullException) }; + yield return new object[] { "", null, typeof(UsernameBadFormatException) }; + yield return new object[] { "u!u", null, typeof(UsernameBadFormatException) }; + yield return new object[] { null, new Avatar { Type = null, Data = new[] { (byte)0x00 } }, typeof(ArgumentException) }; + yield return new object[] { null, new Avatar { Type = "", Data = new[] { (byte)0x00 } }, typeof(ArgumentException) }; + yield return new object[] { null, new Avatar { Type = "aaa", Data = null }, typeof(ArgumentException) }; + yield return new object[] { "usernotexist", null, typeof(UserNotExistException) }; + } + + [Theory] + [MemberData(nameof(SetAvatar_ShouldThrow_Data))] + public async Task SetAvatar_ShouldThrow(string username, Avatar avatar, Type exceptionType) + { + await _service.Awaiting(s => s.SetAvatar(username, avatar)).Should().ThrowAsync(exceptionType); + } + + [Fact] + public async Task SetAvatar_Should_Work() + { + string username = MockUser.User.Username; + + var user = await _database.DatabaseContext.Users.Where(u => u.Name == username).Include(u => u.Avatar).SingleAsync(); + + var avatar1 = CreateMockAvatar("aaa"); + var avatar2 = CreateMockAvatar("bbb"); + + string etag1 = "etagaaa"; + string etag2 = "etagbbb"; + + DateTime dateTime1 = DateTime.Now.AddSeconds(2); + DateTime dateTime2 = DateTime.Now.AddSeconds(10); + DateTime dateTime3 = DateTime.Now.AddSeconds(20); + + // create + _mockETagGenerator.Setup(g => g.Generate(avatar1.Data)).ReturnsAsync(etag1); + _mockClock.Setup(c => c.GetCurrentTime()).Returns(dateTime1); + await _service.SetAvatar(username, avatar1); + user.Avatar.Should().NotBeNull(); + user.Avatar.Type.Should().Be(avatar1.Type); + user.Avatar.Data.Should().Equal(avatar1.Data); + user.Avatar.ETag.Should().Be(etag1); + user.Avatar.LastModified.Should().Be(dateTime1); + + // modify + _mockETagGenerator.Setup(g => g.Generate(avatar2.Data)).ReturnsAsync(etag2); + _mockClock.Setup(c => c.GetCurrentTime()).Returns(dateTime2); + await _service.SetAvatar(username, avatar2); + user.Avatar.Should().NotBeNull(); + user.Avatar.Type.Should().Be(avatar2.Type); + user.Avatar.Data.Should().Equal(avatar2.Data); + user.Avatar.ETag.Should().Be(etag2); + user.Avatar.LastModified.Should().Be(dateTime2); + + // delete + _mockClock.Setup(c => c.GetCurrentTime()).Returns(dateTime3); + await _service.SetAvatar(username, null); + user.Avatar.Type.Should().BeNull(); + user.Avatar.Data.Should().BeNull(); + user.Avatar.ETag.Should().BeNull(); + user.Avatar.LastModified.Should().Be(dateTime3); + } + } +} diff --git a/Timeline.Tests/Services/UserDetailServiceTest.cs b/Timeline.Tests/Services/UserDetailServiceTest.cs new file mode 100644 index 00000000..c7037c6e --- /dev/null +++ b/Timeline.Tests/Services/UserDetailServiceTest.cs @@ -0,0 +1,108 @@ +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging.Abstractions; +using System; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Entities; +using Timeline.Services; +using Timeline.Tests.Helpers; +using Timeline.Tests.Mock.Data; +using Xunit; + +namespace Timeline.Tests.Services +{ + public class UserDetailServiceTest : IDisposable + { + private readonly TestDatabase _testDatabase; + + private readonly UserDetailService _service; + + public UserDetailServiceTest() + { + _testDatabase = new TestDatabase(); + _service = new UserDetailService(_testDatabase.DatabaseContext, NullLogger.Instance); + } + + public void Dispose() + { + _testDatabase.Dispose(); + } + + [Theory] + [InlineData(null, typeof(ArgumentNullException))] + [InlineData("", typeof(UsernameBadFormatException))] + [InlineData("a!a", typeof(UsernameBadFormatException))] + [InlineData("usernotexist", typeof(UserNotExistException))] + public async Task GetNickname_ShouldThrow(string username, Type exceptionType) + { + await _service.Awaiting(s => s.GetNickname(username)).Should().ThrowAsync(exceptionType); + } + + [Fact] + public async Task GetNickname_ShouldReturnUsername() + { + var result = await _service.GetNickname(MockUser.User.Username); + result.Should().Be(MockUser.User.Username); + } + + [Fact] + public async Task GetNickname_ShouldReturnData() + { + const string nickname = "aaaaaa"; + { + var context = _testDatabase.DatabaseContext; + var userId = (await context.Users.Where(u => u.Name == MockUser.User.Username).Select(u => new { u.Id }).SingleAsync()).Id; + context.UserDetails.Add(new UserDetail + { + Nickname = nickname, + UserId = userId + }); + await context.SaveChangesAsync(); + } + var result = await _service.GetNickname(MockUser.User.Username); + result.Should().Be(nickname); + } + + [Theory] + [InlineData(null, typeof(ArgumentNullException))] + [InlineData("", typeof(UsernameBadFormatException))] + [InlineData("a!a", typeof(UsernameBadFormatException))] + [InlineData("usernotexist", typeof(UserNotExistException))] + public async Task SetNickname_ShouldThrow(string username, Type exceptionType) + { + await _service.Awaiting(s => s.SetNickname(username, null)).Should().ThrowAsync(exceptionType); + } + + [Fact] + public async Task SetNickname_ShouldThrow_ArgumentException() + { + await _service.Awaiting(s => s.SetNickname("uuu", new string('a', 50))).Should().ThrowAsync(); + } + + [Fact] + public async Task SetNickname_ShouldWork() + { + var username = MockUser.User.Username; + var user = await _testDatabase.DatabaseContext.Users.Where(u => u.Name == username).Include(u => u.Detail).SingleAsync(); + + var nickname1 = "nickname1"; + var nickname2 = "nickname2"; + + await _service.SetNickname(username, null); + user.Detail.Should().BeNull(); + + await _service.SetNickname(username, nickname1); + user.Detail.Should().NotBeNull(); + user.Detail.Nickname.Should().Be(nickname1); + + await _service.SetNickname(username, nickname2); + user.Detail.Should().NotBeNull(); + user.Detail.Nickname.Should().Be(nickname2); + + await _service.SetNickname(username, null); + user.Detail.Should().NotBeNull(); + user.Detail.Nickname.Should().BeNull(); + } + } +} diff --git a/Timeline.Tests/UserAvatarServiceTest.cs b/Timeline.Tests/UserAvatarServiceTest.cs deleted file mode 100644 index 1f71f6f6..00000000 --- a/Timeline.Tests/UserAvatarServiceTest.cs +++ /dev/null @@ -1,284 +0,0 @@ -using FluentAssertions; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Moq; -using SixLabors.ImageSharp.Formats.Png; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Timeline.Entities; -using Timeline.Services; -using Timeline.Tests.Helpers; -using Timeline.Tests.Mock.Data; -using Timeline.Tests.Mock.Services; -using Xunit; -using Xunit.Abstractions; - -namespace Timeline.Tests -{ - public class UserAvatarValidatorTest : IClassFixture - { - private readonly UserAvatarValidator _validator; - - public UserAvatarValidatorTest(UserAvatarValidator validator) - { - _validator = validator; - } - - [Fact] - public void CantDecode() - { - Avatar avatar = new Avatar - { - Data = Encoding.ASCII.GetBytes("This is not a image."), - Type = "image/png" - }; - _validator.Awaiting(v => v.Validate(avatar)) - .Should().Throw() - .Where(e => e.Avatar == avatar && e.Error == AvatarFormatException.ErrorReason.CantDecode); - } - - [Fact] - public void UnmatchedFormat() - { - Avatar avatar = new Avatar - { - Data = ImageHelper.CreatePngWithSize(100, 100), - Type = "image/jpeg" - }; - _validator.Awaiting(v => v.Validate(avatar)) - .Should().Throw() - .Where(e => e.Avatar == avatar && e.Error == AvatarFormatException.ErrorReason.UnmatchedFormat); - } - - [Fact] - public void BadSize() - { - Avatar avatar = new Avatar - { - Data = ImageHelper.CreatePngWithSize(100, 200), - Type = PngFormat.Instance.DefaultMimeType - }; - _validator.Awaiting(v => v.Validate(avatar)) - .Should().Throw() - .Where(e => e.Avatar == avatar && e.Error == AvatarFormatException.ErrorReason.BadSize); - } - - [Fact] - public void Success() - { - Avatar avatar = new Avatar - { - Data = ImageHelper.CreatePngWithSize(100, 100), - Type = PngFormat.Instance.DefaultMimeType - }; - _validator.Awaiting(v => v.Validate(avatar)) - .Should().NotThrow(); - } - } - - public class UserAvatarServiceTest : IDisposable - { - private UserAvatar CreateMockAvatarEntity(string key) => new UserAvatar - { - Type = $"image/test{key}", - Data = Encoding.ASCII.GetBytes($"mock{key}"), - ETag = $"etag{key}", - LastModified = DateTime.Now - }; - - private AvatarInfo CreateMockAvatarInfo(string key) => new AvatarInfo - { - Avatar = new Avatar - { - Type = $"image/test{key}", - Data = Encoding.ASCII.GetBytes($"mock{key}") - }, - LastModified = DateTime.Now - }; - - private Avatar CreateMockAvatar(string key) => new Avatar - { - Type = $"image/test{key}", - Data = Encoding.ASCII.GetBytes($"mock{key}") - }; - - private static Avatar ToAvatar(UserAvatar entity) - { - return new Avatar - { - Data = entity.Data, - Type = entity.Type - }; - } - - private static AvatarInfo ToAvatarInfo(UserAvatar entity) - { - return new AvatarInfo - { - Avatar = ToAvatar(entity), - LastModified = entity.LastModified - }; - } - - private readonly Mock _mockDefaultAvatarProvider; - private readonly Mock _mockValidator; - private readonly Mock _mockETagGenerator; - private readonly Mock _mockClock; - - private readonly TestDatabase _database; - - private readonly UserAvatarService _service; - - public UserAvatarServiceTest() - { - _mockDefaultAvatarProvider = new Mock(); - _mockValidator = new Mock(); - _mockETagGenerator = new Mock(); - _mockClock = new Mock(); - - _database = new TestDatabase(); - - _service = new UserAvatarService(NullLogger.Instance, _database.DatabaseContext, _mockDefaultAvatarProvider.Object, _mockValidator.Object, _mockETagGenerator.Object, _mockClock.Object); - } - - public void Dispose() - { - _database.Dispose(); - } - - [Theory] - [InlineData(null, typeof(ArgumentNullException))] - [InlineData("", typeof(UsernameBadFormatException))] - [InlineData("a!a", typeof(UsernameBadFormatException))] - [InlineData("usernotexist", typeof(UserNotExistException))] - public async Task GetAvatarETag_ShouldThrow(string username, Type exceptionType) - { - await _service.Awaiting(s => s.GetAvatarETag(username)).Should().ThrowAsync(exceptionType); - } - - [Fact] - public async Task GetAvatarETag_ShouldReturn_Default() - { - const string etag = "aaaaaa"; - _mockDefaultAvatarProvider.Setup(p => p.GetDefaultAvatarETag()).ReturnsAsync(etag); - (await _service.GetAvatarETag(MockUser.User.Username)).Should().Be(etag); - } - - [Fact] - public async Task GetAvatarETag_ShouldReturn_Data() - { - string username = MockUser.User.Username; - var mockAvatarEntity = CreateMockAvatarEntity("aaa"); - { - var context = _database.DatabaseContext; - var user = await context.Users.Where(u => u.Name == username).Include(u => u.Avatar).SingleAsync(); - user.Avatar = mockAvatarEntity; - await context.SaveChangesAsync(); - } - (await _service.GetAvatarETag(username)).Should().BeEquivalentTo(mockAvatarEntity.ETag); - } - - [Theory] - [InlineData(null, typeof(ArgumentNullException))] - [InlineData("", typeof(UsernameBadFormatException))] - [InlineData("a!a", typeof(UsernameBadFormatException))] - [InlineData("usernotexist", typeof(UserNotExistException))] - public async Task GetAvatar_ShouldThrow(string username, Type exceptionType) - { - await _service.Awaiting(s => s.GetAvatar(username)).Should().ThrowAsync(exceptionType); - - } - - [Fact] - public async Task GetAvatar_ShouldReturn_Default() - { - var mockAvatar = CreateMockAvatarInfo("aaa"); - _mockDefaultAvatarProvider.Setup(p => p.GetDefaultAvatar()).ReturnsAsync(mockAvatar); - string username = MockUser.User.Username; - (await _service.GetAvatar(username)).Should().BeEquivalentTo(mockAvatar); - } - - [Fact] - public async Task GetAvatar_ShouldReturn_Data() - { - string username = MockUser.User.Username; - var mockAvatarEntity = CreateMockAvatarEntity("aaa"); - { - var context = _database.DatabaseContext; - var user = await context.Users.Where(u => u.Name == username).Include(u => u.Avatar).SingleAsync(); - user.Avatar = mockAvatarEntity; - await context.SaveChangesAsync(); - } - - (await _service.GetAvatar(username)).Should().BeEquivalentTo(ToAvatarInfo(mockAvatarEntity)); - } - - public static IEnumerable SetAvatar_ShouldThrow_Data() - { - yield return new object[] { null, null, typeof(ArgumentNullException) }; - yield return new object[] { "", null, typeof(UsernameBadFormatException) }; - yield return new object[] { "u!u", null, typeof(UsernameBadFormatException) }; - yield return new object[] { null, new Avatar { Type = null, Data = new[] { (byte)0x00 } }, typeof(ArgumentException) }; - yield return new object[] { null, new Avatar { Type = "", Data = new[] { (byte)0x00 } }, typeof(ArgumentException) }; - yield return new object[] { null, new Avatar { Type = "aaa", Data = null }, typeof(ArgumentException) }; - yield return new object[] { "usernotexist", null, typeof(UserNotExistException) }; - } - - [Theory] - [MemberData(nameof(SetAvatar_ShouldThrow_Data))] - public async Task SetAvatar_ShouldThrow(string username, Avatar avatar, Type exceptionType) - { - await _service.Awaiting(s => s.SetAvatar(username, avatar)).Should().ThrowAsync(exceptionType); - } - - [Fact] - public async Task SetAvatar_Should_Work() - { - string username = MockUser.User.Username; - - var user = await _database.DatabaseContext.Users.Where(u => u.Name == username).Include(u => u.Avatar).SingleAsync(); - - var avatar1 = CreateMockAvatar("aaa"); - var avatar2 = CreateMockAvatar("bbb"); - - string etag1 = "etagaaa"; - string etag2 = "etagbbb"; - - DateTime dateTime1 = DateTime.Now.AddSeconds(2); - DateTime dateTime2 = DateTime.Now.AddSeconds(10); - DateTime dateTime3 = DateTime.Now.AddSeconds(20); - - // create - _mockETagGenerator.Setup(g => g.Generate(avatar1.Data)).ReturnsAsync(etag1); - _mockClock.Setup(c => c.GetCurrentTime()).Returns(dateTime1); - await _service.SetAvatar(username, avatar1); - user.Avatar.Should().NotBeNull(); - user.Avatar.Type.Should().Be(avatar1.Type); - user.Avatar.Data.Should().Equal(avatar1.Data); - user.Avatar.ETag.Should().Be(etag1); - user.Avatar.LastModified.Should().Be(dateTime1); - - // modify - _mockETagGenerator.Setup(g => g.Generate(avatar2.Data)).ReturnsAsync(etag2); - _mockClock.Setup(c => c.GetCurrentTime()).Returns(dateTime2); - await _service.SetAvatar(username, avatar2); - user.Avatar.Should().NotBeNull(); - user.Avatar.Type.Should().Be(avatar2.Type); - user.Avatar.Data.Should().Equal(avatar2.Data); - user.Avatar.ETag.Should().Be(etag2); - user.Avatar.LastModified.Should().Be(dateTime2); - - // delete - _mockClock.Setup(c => c.GetCurrentTime()).Returns(dateTime3); - await _service.SetAvatar(username, null); - user.Avatar.Type.Should().BeNull(); - user.Avatar.Data.Should().BeNull(); - user.Avatar.ETag.Should().BeNull(); - user.Avatar.LastModified.Should().Be(dateTime3); - } - } -} diff --git a/Timeline/Entities/DatabaseContext.cs b/Timeline/Entities/DatabaseContext.cs index e1b98e7d..6c005b30 100644 --- a/Timeline/Entities/DatabaseContext.cs +++ b/Timeline/Entities/DatabaseContext.cs @@ -19,5 +19,6 @@ namespace Timeline.Entities public DbSet Users { get; set; } = default!; public DbSet UserAvatars { get; set; } = default!; + public DbSet UserDetails { get; set; } = default!; } } diff --git a/Timeline/Entities/User.cs b/Timeline/Entities/User.cs index 6e8e4967..02352b03 100644 --- a/Timeline/Entities/User.cs +++ b/Timeline/Entities/User.cs @@ -28,5 +28,7 @@ namespace Timeline.Entities public long Version { get; set; } public UserAvatar? Avatar { get; set; } + + public UserDetail? Detail { get; set; } } } diff --git a/Timeline/Entities/UserDetail.cs b/Timeline/Entities/UserDetail.cs new file mode 100644 index 00000000..45f87e2b --- /dev/null +++ b/Timeline/Entities/UserDetail.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Linq; +using System.Threading.Tasks; + +namespace Timeline.Entities +{ + [Table("user_details")] + public class UserDetail + { + [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + [Column("nickname"), MaxLength(26)] + public string? Nickname { get; set; } + + public long UserId { get; set; } + } +} diff --git a/Timeline/Resources/Services/UserDetailService.Designer.cs b/Timeline/Resources/Services/UserDetailService.Designer.cs new file mode 100644 index 00000000..2f586b36 --- /dev/null +++ b/Timeline/Resources/Services/UserDetailService.Designer.cs @@ -0,0 +1,99 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Timeline.Resources.Services { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class UserDetailService { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal UserDetailService() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Timeline.Resources.Services.UserDetailService", typeof(UserDetailService).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Length of nickname can't be bigger than 10.. + /// + internal static string ExceptionNicknameTooLong { + get { + return ResourceManager.GetString("ExceptionNicknameTooLong", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A user_details entity has been created. User id is {0}. Nickname is {1}.. + /// + internal static string LogEntityNicknameCreate { + get { + return ResourceManager.GetString("LogEntityNicknameCreate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Nickname of a user_details entity has been updated to a new value. User id is {0}. New value is {1}.. + /// + internal static string LogEntityNicknameSetNotNull { + get { + return ResourceManager.GetString("LogEntityNicknameSetNotNull", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Nickname of a user_details entity has been updated to null. User id is {0}.. + /// + internal static string LogEntityNicknameSetToNull { + get { + return ResourceManager.GetString("LogEntityNicknameSetToNull", resourceCulture); + } + } + } +} diff --git a/Timeline/Resources/Services/UserDetailService.resx b/Timeline/Resources/Services/UserDetailService.resx new file mode 100644 index 00000000..ea32aeda --- /dev/null +++ b/Timeline/Resources/Services/UserDetailService.resx @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Length of nickname can't be bigger than 10. + + + A user_details entity has been created. User id is {0}. Nickname is {1}. + + + Nickname of a user_details entity has been updated to a new value. User id is {0}. New value is {1}. + + + Nickname of a user_details entity has been updated to null. User id is {0}. + + \ No newline at end of file diff --git a/Timeline/Services/DatabaseExtensions.cs b/Timeline/Services/DatabaseExtensions.cs index 8cbc8fef..140c3146 100644 --- a/Timeline/Services/DatabaseExtensions.cs +++ b/Timeline/Services/DatabaseExtensions.cs @@ -9,6 +9,8 @@ namespace Timeline.Services { internal static class DatabaseExtensions { + private static readonly UsernameValidator usernameValidator = new UsernameValidator(); + /// /// Check the existence and get the id of the user. /// @@ -17,11 +19,11 @@ namespace Timeline.Services /// Thrown if is null. /// Thrown if is of bad format. /// Thrown if user does not exist. - internal static async Task CheckAndGetUser(DbSet userDbSet, UsernameValidator validator, string username) + internal static async Task CheckAndGetUser(DbSet userDbSet, string? username) { if (username == null) throw new ArgumentNullException(nameof(username)); - var (result, message) = validator.Validate(username); + var (result, message) = usernameValidator.Validate(username); if (!result) throw new UsernameBadFormatException(username, message); diff --git a/Timeline/Services/UserAvatarService.cs b/Timeline/Services/UserAvatarService.cs index 2afe9093..01201864 100644 --- a/Timeline/Services/UserAvatarService.cs +++ b/Timeline/Services/UserAvatarService.cs @@ -177,8 +177,6 @@ namespace Timeline.Services private readonly IETagGenerator _eTagGenerator; - private readonly UsernameValidator _usernameValidator; - private readonly IClock _clock; public UserAvatarService( @@ -194,13 +192,12 @@ namespace Timeline.Services _defaultUserAvatarProvider = defaultUserAvatarProvider; _avatarValidator = avatarValidator; _eTagGenerator = eTagGenerator; - _usernameValidator = new UsernameValidator(); _clock = clock; } public async Task GetAvatarETag(string username) { - var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, _usernameValidator, username); + var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username); var eTag = (await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.ETag }).SingleOrDefaultAsync())?.ETag; if (eTag == null) @@ -211,7 +208,7 @@ namespace Timeline.Services public async Task GetAvatar(string username) { - var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, _usernameValidator, username); + var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username); var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.Type, a.Data, a.LastModified }).SingleOrDefaultAsync(); @@ -253,7 +250,7 @@ namespace Timeline.Services throw new ArgumentException(Resources.Services.UserAvatarService.ExceptionAvatarTypeNullOrEmpty, nameof(avatar)); } - var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, _usernameValidator, username); + var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username); var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleOrDefaultAsync(); if (avatar == null) diff --git a/Timeline/Services/UserDetailService.cs b/Timeline/Services/UserDetailService.cs new file mode 100644 index 00000000..0b24e4e2 --- /dev/null +++ b/Timeline/Services/UserDetailService.cs @@ -0,0 +1,102 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Entities; +using static Timeline.Resources.Services.UserDetailService; + +namespace Timeline.Services +{ + public interface IUserDetailService + { + /// + /// Get the nickname of the user with given username. + /// If the user does not set a nickname, the username is returned as the nickname. + /// + /// The username of the user to get nickname of. + /// The nickname of the user. + /// Thrown when is null. + /// Thrown when is of bad format. + /// Thrown when the user does not exist. + Task GetNickname(string username); + + /// + /// Set the nickname of the user with given username. + /// + /// The username of the user to set nickname of. + /// The nickname. Pass null to unset. + /// Thrown when is null. + /// Thrown when is not null but its length is bigger than 10. + /// Thrown when is of bad format. + /// Thrown when the user does not exist. + Task SetNickname(string username, string? nickname); + } + + public class UserDetailService : IUserDetailService + { + private readonly DatabaseContext _database; + + private readonly ILogger _logger; + + public UserDetailService(DatabaseContext database, ILogger logger) + { + _database = database; + _logger = logger; + } + + public async Task GetNickname(string username) + { + var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username); + var nickname = _database.UserDetails.Where(d => d.UserId == userId).Select(d => new { d.Nickname }).SingleOrDefault()?.Nickname; + return nickname ?? username; + } + + public async Task SetNickname(string username, string? nickname) + { + if (nickname != null && nickname.Length > 10) + { + throw new ArgumentException(ExceptionNicknameTooLong, nameof(nickname)); + } + var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username); + var userDetail = _database.UserDetails.Where(d => d.UserId == userId).SingleOrDefault(); + if (nickname == null) + { + if (userDetail == null || userDetail.Nickname == null) + { + return; + } + else + { + userDetail.Nickname = null; + await _database.SaveChangesAsync(); + _logger.LogInformation(LogEntityNicknameSetToNull, userId); + } + } + else + { + var create = userDetail == null; + if (create) + { + userDetail = new UserDetail + { + UserId = userId + }; + } + userDetail!.Nickname = nickname; + if (create) + { + _database.UserDetails.Add(userDetail); + } + await _database.SaveChangesAsync(); + if (create) + { + _logger.LogInformation(LogEntityNicknameCreate, userId, nickname); + } + else + { + _logger.LogInformation(LogEntityNicknameSetNotNull, userId, nickname); + } + } + } + } +} diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index b989cd3b..0260d725 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -89,6 +89,11 @@ True UserAvatarService.resx + + True + True + UserDetailService.resx + True True @@ -146,6 +151,10 @@ ResXFileCodeGenerator UserAvatarService.Designer.cs + + ResXFileCodeGenerator + UserDetailService.Designer.cs + ResXFileCodeGenerator UserService.Designer.cs -- cgit v1.2.3