From b3296b38176e26e410306bb19ef43da1523811b8 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Wed, 21 Aug 2019 18:33:07 +0800 Subject: Add database entity and service. --- Timeline.Tests/UserDetailServiceTest.cs | 180 ++++++++++++++++++++++ Timeline/Entities/DatabaseContext.cs | 4 +- Timeline/Entities/UserDetail.cs | 26 ++++ Timeline/Models/UserDetail.cs | 31 ++++ Timeline/Models/Validation/UserDetailValidator.cs | 11 ++ Timeline/Services/DatabaseExtensions.cs | 30 ++++ Timeline/Services/UserAvatarService.cs | 21 +-- Timeline/Services/UserDetailService.cs | 90 +++++++++++ 8 files changed, 374 insertions(+), 19 deletions(-) create mode 100644 Timeline.Tests/UserDetailServiceTest.cs create mode 100644 Timeline/Entities/UserDetail.cs create mode 100644 Timeline/Models/UserDetail.cs create mode 100644 Timeline/Models/Validation/UserDetailValidator.cs create mode 100644 Timeline/Services/DatabaseExtensions.cs create mode 100644 Timeline/Services/UserDetailService.cs diff --git a/Timeline.Tests/UserDetailServiceTest.cs b/Timeline.Tests/UserDetailServiceTest.cs new file mode 100644 index 00000000..fa8df3ae --- /dev/null +++ b/Timeline.Tests/UserDetailServiceTest.cs @@ -0,0 +1,180 @@ +using FluentAssertions; +using Microsoft.Extensions.Logging; +using System; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Entities; +using Timeline.Models; +using Timeline.Services; +using Timeline.Tests.Helpers; +using Timeline.Tests.Mock.Data; +using Xunit; +using Xunit.Abstractions; + +namespace Timeline.Tests +{ + public class UserDetailServiceTest : IDisposable + { + private readonly LoggerFactory _loggerFactory; + private readonly TestDatabase _database; + + private readonly UserDetailService _service; + + public UserDetailServiceTest(ITestOutputHelper outputHelper) + { + _loggerFactory = MyTestLoggerFactory.Create(outputHelper); + _database = new TestDatabase(); + + _service = new UserDetailService(_loggerFactory.CreateLogger(), _database.DatabaseContext); + } + + public void Dispose() + { + _loggerFactory.Dispose(); + _database.Dispose(); + } + + [Fact] + public void GetDetail_ShouldThrow_ArgumentException() + { + // no need to await because arguments are checked syncronizedly. + _service.Invoking(s => s.GetUserDetail(null)).Should().Throw() + .Where(e => e.ParamName == "username" && e.Message.Contains("null", StringComparison.OrdinalIgnoreCase)); + _service.Invoking(s => s.GetUserDetail("")).Should().Throw() + .Where(e => e.ParamName == "username" && e.Message.Contains("empty", StringComparison.OrdinalIgnoreCase)); + } + + [Fact] + public void GetDetail_ShouldThrow_UserNotExistException() + { + const string username = "usernotexist"; + _service.Awaiting(s => s.GetUserDetail(username)).Should().Throw() + .Where(e => e.Username == username); + } + + [Fact] + public async Task GetDetail_Should_Create_And_ReturnDefault() + { + { + var detail = await _service.GetUserDetail(MockUsers.UserUsername); + detail.Should().BeEquivalentTo(new UserDetail()); + } + + { + var context = _database.DatabaseContext; + var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUsers.UserUsername); + var detail = context.UserDetails.Where(e => e.UserId == userId).Single(); + detail.QQ.Should().BeNullOrEmpty(); + detail.EMail.Should().BeNullOrEmpty(); + detail.PhoneNumber.Should().BeNullOrEmpty(); + detail.Description.Should().BeNullOrEmpty(); + } + } + + [Fact] + public async Task GetDetail_Should_ReturnData() + { + const string email = "ha@aaa.net"; + const string description = "hahaha"; + + var context = _database.DatabaseContext; + UserDetailEntity entity; + + { + var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUsers.UserUsername); + entity = new UserDetailEntity + { + EMail = email, + Description = description, + UserId = userId + }; + context.Add(entity); + await context.SaveChangesAsync(); + } + + { + var detail = await _service.GetUserDetail(MockUsers.UserUsername); + detail.Should().BeEquivalentTo(new UserDetail + { + EMail = email, + Description = description + }); + } + } + + [Fact] + public void UpdateDetail_ShouldThrow_ArgumentException() + { + // no need to await because arguments are checked syncronizedly. + _service.Invoking(s => s.UpdateUserDetail(null, new UserDetail())).Should().Throw() + .Where(e => e.ParamName == "username" && e.Message.Contains("null", StringComparison.OrdinalIgnoreCase)); + _service.Invoking(s => s.UpdateUserDetail("", new UserDetail())).Should().Throw() + .Where(e => e.ParamName == "username" && e.Message.Contains("empty", StringComparison.OrdinalIgnoreCase)); + _service.Invoking(s => s.UpdateUserDetail("aaa", null)).Should().Throw() + .Where(e => e.ParamName == "detail"); + } + + [Fact] + public void UpdateDetail_ShouldThrow_UserNotExistException() + { + const string username = "usernotexist"; + _service.Awaiting(s => s.UpdateUserDetail(username, new UserDetail())).Should().Throw() + .Where(e => e.Username == username); + } + + [Fact] + public async Task UpdateDetail_Should_Work() + { + UserDetailEntity entity; + + await _service.UpdateUserDetail(MockUsers.UserUsername, new UserDetail()); + + { + var context = _database.DatabaseContext; + var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUsers.UserUsername); + entity = context.UserDetails.Where(e => e.UserId == userId).Single(); + entity.QQ.Should().BeNullOrEmpty(); + entity.EMail.Should().BeNullOrEmpty(); + entity.PhoneNumber.Should().BeNullOrEmpty(); + entity.Description.Should().BeNullOrEmpty(); + } + + const string email = "ha@aaa.net"; + const string phoneNumber = "12345678910"; + const string description = "hahaha"; + + await _service.UpdateUserDetail(MockUsers.UserUsername, new UserDetail + { + EMail = email, + PhoneNumber = phoneNumber, + Description = description + }); + + { + var context = _database.DatabaseContext; + var userId = await DatabaseExtensions.CheckAndGetUser(context.Users, MockUsers.UserUsername); + entity = context.UserDetails.Where(e => e.UserId == userId).Single(); + entity.QQ.Should().BeNullOrEmpty(); + entity.EMail.Should().Be(email); + entity.PhoneNumber.Should().Be(phoneNumber); + entity.Description.Should().Be(description); + } + + const string newDescription = "new description"; + + await _service.UpdateUserDetail(MockUsers.UserUsername, new UserDetail + { + EMail = null, + PhoneNumber = "", + Description = newDescription + }); + + { + entity.QQ.Should().BeNullOrEmpty(); + entity.EMail.Should().Be(email); + entity.PhoneNumber.Should().BeNullOrEmpty(); + entity.Description.Should().Be(newDescription); + } + } + } +} diff --git a/Timeline/Entities/DatabaseContext.cs b/Timeline/Entities/DatabaseContext.cs index 6e1fc638..d9815660 100644 --- a/Timeline/Entities/DatabaseContext.cs +++ b/Timeline/Entities/DatabaseContext.cs @@ -28,8 +28,9 @@ namespace Timeline.Entities [Column("version"), Required] public long Version { get; set; } - [Required] public UserAvatar Avatar { get; set; } + + public UserDetailEntity Detail { get; set; } } public class DatabaseContext : DbContext @@ -48,5 +49,6 @@ namespace Timeline.Entities public DbSet Users { get; set; } public DbSet UserAvatars { get; set; } + public DbSet UserDetails { get; set; } } } diff --git a/Timeline/Entities/UserDetail.cs b/Timeline/Entities/UserDetail.cs new file mode 100644 index 00000000..ee829717 --- /dev/null +++ b/Timeline/Entities/UserDetail.cs @@ -0,0 +1,26 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Timeline.Entities +{ + [Table("user_details")] + public class UserDetailEntity + { + [Column("id"), Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + [Column("qq"), MaxLength(15)] + public string QQ { get; set; } + + [Column("email"), MaxLength(30)] + public string EMail { get; set; } + + [Column("phone_number"), MaxLength(15)] + public string PhoneNumber { get; set; } + + [Column("description")] + public string Description { get; set; } + + public long UserId { get; set; } + } +} diff --git a/Timeline/Models/UserDetail.cs b/Timeline/Models/UserDetail.cs new file mode 100644 index 00000000..91439c6a --- /dev/null +++ b/Timeline/Models/UserDetail.cs @@ -0,0 +1,31 @@ +using Timeline.Entities; + +namespace Timeline.Models +{ + public class UserDetail + { + public string QQ { get; set; } + public string EMail { get; set; } + public string PhoneNumber { get; set; } + public string Description { get; set; } + + private static string CoerceEmptyToNull(string value) + { + if (string.IsNullOrEmpty(value)) + return null; + else + return value; + } + + public static UserDetail From(UserDetailEntity entity) + { + return new UserDetail + { + QQ = CoerceEmptyToNull(entity.QQ), + EMail = CoerceEmptyToNull(entity.EMail), + PhoneNumber = CoerceEmptyToNull(entity.PhoneNumber), + Description = CoerceEmptyToNull(entity.Description) + }; + } + } +} diff --git a/Timeline/Models/Validation/UserDetailValidator.cs b/Timeline/Models/Validation/UserDetailValidator.cs new file mode 100644 index 00000000..5fdaec00 --- /dev/null +++ b/Timeline/Models/Validation/UserDetailValidator.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Timeline.Models.Validation +{ + public class UserDetailValidator + { + } +} diff --git a/Timeline/Services/DatabaseExtensions.cs b/Timeline/Services/DatabaseExtensions.cs new file mode 100644 index 00000000..a37cf05b --- /dev/null +++ b/Timeline/Services/DatabaseExtensions.cs @@ -0,0 +1,30 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Entities; + +namespace Timeline.Services +{ + public static class DatabaseExtensions + { + /// + /// Check the existence and get the id of the user. + /// + /// The username of the user. + /// The user id. + /// Thrown if is null or empty. + /// Thrown if user does not exist. + public static async Task CheckAndGetUser(DbSet userDbSet, string username) + { + if (string.IsNullOrEmpty(username)) + throw new ArgumentException("Username is null or empty.", nameof(username)); + + var userId = await userDbSet.Where(u => u.Name == username).Select(u => u.Id).SingleOrDefaultAsync(); + if (userId == 0) + throw new UserNotExistException(username); + return userId; + } + } +} diff --git a/Timeline/Services/UserAvatarService.cs b/Timeline/Services/UserAvatarService.cs index 7b1f405c..5c380dd8 100644 --- a/Timeline/Services/UserAvatarService.cs +++ b/Timeline/Services/UserAvatarService.cs @@ -219,12 +219,7 @@ namespace Timeline.Services public async Task GetAvatarETag(string username) { - if (string.IsNullOrEmpty(username)) - throw new ArgumentException("Username is null or empty.", nameof(username)); - - var userId = await _database.Users.Where(u => u.Name == username).Select(u => u.Id).SingleOrDefaultAsync(); - if (userId == 0) - throw new UserNotExistException(username); + var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username); var eTag = (await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.ETag }).SingleAsync()).ETag; if (eTag == null) @@ -235,12 +230,7 @@ namespace Timeline.Services public async Task GetAvatar(string username) { - if (string.IsNullOrEmpty(username)) - throw new ArgumentException("Username is null or empty.", nameof(username)); - - var userId = await _database.Users.Where(u => u.Name == username).Select(u => u.Id).SingleOrDefaultAsync(); - if (userId == 0) - throw new UserNotExistException(username); + var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username); var avatar = await _database.UserAvatars.Where(a => a.UserId == userId).Select(a => new { a.Type, a.Data, a.LastModified }).SingleAsync(); @@ -272,9 +262,6 @@ namespace Timeline.Services public async Task SetAvatar(string username, Avatar avatar) { - if (string.IsNullOrEmpty(username)) - throw new ArgumentException("Username is null or empty.", nameof(username)); - if (avatar != null) { if (string.IsNullOrEmpty(avatar.Type)) @@ -283,9 +270,7 @@ namespace Timeline.Services throw new ArgumentException("Data of avatar is null.", nameof(avatar)); } - var userId = await _database.Users.Where(u => u.Name == username).Select(u => u.Id).SingleOrDefaultAsync(); - if (userId == 0) - throw new UserNotExistException(username); + var userId = await DatabaseExtensions.CheckAndGetUser(_database.Users, username); var avatarEntity = await _database.UserAvatars.Where(a => a.UserId == userId).SingleAsync(); diff --git a/Timeline/Services/UserDetailService.cs b/Timeline/Services/UserDetailService.cs new file mode 100644 index 00000000..c3a2a1af --- /dev/null +++ b/Timeline/Services/UserDetailService.cs @@ -0,0 +1,90 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using System; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Entities; +using Timeline.Models; + +namespace Timeline.Services +{ + public interface IUserDetailService + { + /// + /// Get the detail of user. + /// + /// The username to get user detail of. + /// The user detail. + /// Thrown if is null or empty. + /// Thrown if user doesn't exist. + Task GetUserDetail(string username); + + /// + /// Update the detail of user. This function does not do data check. + /// + /// The username to get user detail of. + /// The detail to update. Can't be null. Any null member means not set. + /// Thrown if is null or empty or is null. + /// Thrown if user doesn't exist. + Task UpdateUserDetail(string username, UserDetail detail); + } + + public class UserDetailService : IUserDetailService + { + private readonly ILogger _logger; + + private readonly DatabaseContext _databaseContext; + + public UserDetailService(ILogger logger, DatabaseContext databaseContext) + { + _logger = logger; + _databaseContext = databaseContext; + } + + // Check the existence of user detail entry + private async Task CheckAndInit(long userId) + { + var detail = await _databaseContext.UserDetails.Where(e => e.UserId == userId).SingleOrDefaultAsync(); + if (detail == null) + { + detail = new UserDetailEntity() + { + UserId = userId + }; + _databaseContext.UserDetails.Add(detail); + await _databaseContext.SaveChangesAsync(); + } + return detail; + } + + public async Task GetUserDetail(string username) + { + var userId = await DatabaseExtensions.CheckAndGetUser(_databaseContext.Users, username); + var detailEntity = await CheckAndInit(userId); + return UserDetail.From(detailEntity); + } + + public async Task UpdateUserDetail(string username, UserDetail detail) + { + if (detail == null) + throw new ArgumentNullException(nameof(detail)); + + var userId = await DatabaseExtensions.CheckAndGetUser(_databaseContext.Users, username); + var detailEntity = await CheckAndInit(userId); + + if (detail.QQ != null) + detailEntity.QQ = detail.QQ; + + if (detail.EMail != null) + detailEntity.EMail = detail.EMail; + + if (detail.PhoneNumber != null) + detailEntity.PhoneNumber = detail.PhoneNumber; + + if (detail.Description != null) + detailEntity.Description = detail.Description; + + await _databaseContext.SaveChangesAsync(); + } + } +} -- cgit v1.2.3