aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author杨宇千 <crupest@outlook.com>2019-08-21 18:33:07 +0800
committer杨宇千 <crupest@outlook.com>2019-08-21 18:33:07 +0800
commitb3296b38176e26e410306bb19ef43da1523811b8 (patch)
tree72f5a8f65048f1495dddff7f12cedbd5eb4e39fc
parenta585c6e35829e9f2b4b0b8ce8c6b395e5ea84f2c (diff)
downloadtimeline-b3296b38176e26e410306bb19ef43da1523811b8.tar.gz
timeline-b3296b38176e26e410306bb19ef43da1523811b8.tar.bz2
timeline-b3296b38176e26e410306bb19ef43da1523811b8.zip
Add database entity and service.
-rw-r--r--Timeline.Tests/UserDetailServiceTest.cs180
-rw-r--r--Timeline/Entities/DatabaseContext.cs4
-rw-r--r--Timeline/Entities/UserDetail.cs26
-rw-r--r--Timeline/Models/UserDetail.cs31
-rw-r--r--Timeline/Models/Validation/UserDetailValidator.cs11
-rw-r--r--Timeline/Services/DatabaseExtensions.cs30
-rw-r--r--Timeline/Services/UserAvatarService.cs21
-rw-r--r--Timeline/Services/UserDetailService.cs90
8 files changed, 374 insertions, 19 deletions
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<UserDetailService>(), _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<ArgumentException>()
+ .Where(e => e.ParamName == "username" && e.Message.Contains("null", StringComparison.OrdinalIgnoreCase));
+ _service.Invoking(s => s.GetUserDetail("")).Should().Throw<ArgumentException>()
+ .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<UserNotExistException>()
+ .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<ArgumentException>()
+ .Where(e => e.ParamName == "username" && e.Message.Contains("null", StringComparison.OrdinalIgnoreCase));
+ _service.Invoking(s => s.UpdateUserDetail("", new UserDetail())).Should().Throw<ArgumentException>()
+ .Where(e => e.ParamName == "username" && e.Message.Contains("empty", StringComparison.OrdinalIgnoreCase));
+ _service.Invoking(s => s.UpdateUserDetail("aaa", null)).Should().Throw<ArgumentException>()
+ .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<UserNotExistException>()
+ .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<User> Users { get; set; }
public DbSet<UserAvatar> UserAvatars { get; set; }
+ public DbSet<UserDetailEntity> 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
+ {
+ /// <summary>
+ /// Check the existence and get the id of the user.
+ /// </summary>
+ /// <param name="username">The username of the user.</param>
+ /// <returns>The user id.</returns>
+ /// <exception cref="ArgumentException">Thrown if <paramref name="username"/> is null or empty.</exception>
+ /// <exception cref="UserNotExistException">Thrown if user does not exist.</exception>
+ public static async Task<long> CheckAndGetUser(DbSet<User> 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<string> 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<AvatarInfo> 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
+ {
+ /// <summary>
+ /// Get the detail of user.
+ /// </summary>
+ /// <param name="username">The username to get user detail of.</param>
+ /// <returns>The user detail.</returns>
+ /// <exception cref="ArgumentException">Thrown if <paramref name="username"/> is null or empty.</exception>
+ /// <exception cref="UserNotExistException">Thrown if user doesn't exist.</exception>
+ Task<UserDetail> GetUserDetail(string username);
+
+ /// <summary>
+ /// Update the detail of user. This function does not do data check.
+ /// </summary>
+ /// <param name="username">The username to get user detail of.</param>
+ /// <param name="detail">The detail to update. Can't be null. Any null member means not set.</param>
+ /// <exception cref="ArgumentException">Thrown if <paramref name="username"/> is null or empty or <paramref name="detail"/> is null.</exception>
+ /// <exception cref="UserNotExistException">Thrown if user doesn't exist.</exception>
+ Task UpdateUserDetail(string username, UserDetail detail);
+ }
+
+ public class UserDetailService : IUserDetailService
+ {
+ private readonly ILogger<UserDetailService> _logger;
+
+ private readonly DatabaseContext _databaseContext;
+
+ public UserDetailService(ILogger<UserDetailService> logger, DatabaseContext databaseContext)
+ {
+ _logger = logger;
+ _databaseContext = databaseContext;
+ }
+
+ // Check the existence of user detail entry
+ private async Task<UserDetailEntity> 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<UserDetail> 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();
+ }
+ }
+}