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 From c11a1b7be5d41bb1825a7190c708fdb04923a4fd Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Sun, 27 Oct 2019 22:52:21 +0800 Subject: Add error code tests. --- .gitignore | 4 + Timeline.Tests/ErrorCodeTest.cs | 52 +++++++++ Timeline.Tests/IntegratedTests/UserAvatarTest.cs | 9 +- Timeline/ErrorCodes.cs | 19 ++-- Timeline/Filters/ContentHeaderAttributes.cs | 52 --------- Timeline/Filters/Header.cs | 99 +++++++++++++++++ Timeline/Models/Http/Common.cs | 17 +-- Timeline/Resources/Filters.Designer.cs | 90 +++++++++++++++ Timeline/Resources/Filters.resx | 129 ++++++++++++++++++++++ Timeline/Resources/Filters.zh.resx | 129 ++++++++++++++++++++++ Timeline/Resources/Models/Http/Common.Designer.cs | 31 +----- Timeline/Resources/Models/Http/Common.resx | 11 +- Timeline/Resources/Models/Http/Common.zh.resx | 13 +-- Timeline/Timeline.csproj | 17 ++- 14 files changed, 535 insertions(+), 137 deletions(-) create mode 100644 Timeline.Tests/ErrorCodeTest.cs delete mode 100644 Timeline/Filters/ContentHeaderAttributes.cs create mode 100644 Timeline/Filters/Header.cs create mode 100644 Timeline/Resources/Filters.Designer.cs create mode 100644 Timeline/Resources/Filters.resx create mode 100644 Timeline/Resources/Filters.zh.resx diff --git a/.gitignore b/.gitignore index 41ffa34d..10d8a462 100644 --- a/.gitignore +++ b/.gitignore @@ -229,3 +229,7 @@ _Pvt_Extensions # FAKE - F# Make .fake/ + + +# My draft files. +*.draft diff --git a/Timeline.Tests/ErrorCodeTest.cs b/Timeline.Tests/ErrorCodeTest.cs new file mode 100644 index 00000000..78a58131 --- /dev/null +++ b/Timeline.Tests/ErrorCodeTest.cs @@ -0,0 +1,52 @@ +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Xunit; +using Xunit.Abstractions; + +namespace Timeline.Tests +{ + public class ErrorCodeTest + { + private readonly ITestOutputHelper _output; + + public ErrorCodeTest(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void ShouldWork() + { + var errorCodes = new Dictionary(); + + void RecursiveCheckErrorCode(Type type) + { + foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy) + .Where(fi => fi.IsLiteral && !fi.IsInitOnly && fi.FieldType == typeof(int))) + { + var name = type.FullName + "." + field.Name; + var value = (int)field.GetRawConstantValue(); + _output.WriteLine($"Find error code {name} , value is {value}."); + + value.Should().BeInRange(1000_0000, 9999_9999, "Error code should have exactly 8 digits."); + + errorCodes.Should().NotContainKey(value, + "identical error codes are found and conflict paths are {0} and {1}", + name, errorCodes.GetValueOrDefault(value)); + + errorCodes.Add(value, name); + } + + foreach (var nestedType in type.GetNestedTypes()) + { + RecursiveCheckErrorCode(nestedType); + } + } + + RecursiveCheckErrorCode(typeof(ErrorCodes)); + } + } +} diff --git a/Timeline.Tests/IntegratedTests/UserAvatarTest.cs b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs index ce389046..ad2e11df 100644 --- a/Timeline.Tests/IntegratedTests/UserAvatarTest.cs +++ b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs @@ -13,7 +13,6 @@ using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; -using Timeline.Controllers; using Timeline.Services; using Timeline.Tests.Helpers; using Timeline.Tests.Helpers.Authentication; @@ -95,7 +94,7 @@ namespace Timeline.Tests.IntegratedTests request.Headers.TryAddWithoutValidation("If-None-Match", "\"dsdfd"); var res = await client.SendAsync(request); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.Should().HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Http.Common.Header.BadFormat_IfNonMatch); + .And.Should().HaveCommonBody().Which.Code.Should().Be(Header.IfNonMatch.BadFormat); } { @@ -125,7 +124,7 @@ namespace Timeline.Tests.IntegratedTests content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); var res = await client.PutAsync("users/user/avatar", content); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.Should().HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Http.Common.Header.Missing_ContentLength); + .And.Should().HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Http.Filter.Header.ContentLength.Missing); ; } { @@ -133,7 +132,7 @@ namespace Timeline.Tests.IntegratedTests content.Headers.ContentLength = 1; var res = await client.PutAsync("users/user/avatar", content); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.Should().HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Http.Common.Header.Missing_ContentType); + .And.Should().HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Http.Filter.Header.ContentType.Missing); } { @@ -142,7 +141,7 @@ namespace Timeline.Tests.IntegratedTests content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); var res = await client.PutAsync("users/user/avatar", content); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.Should().HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Http.Common.Header.Zero_ContentLength); + .And.Should().HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Http.Filter.Header.ContentLength.Zero); } { diff --git a/Timeline/ErrorCodes.cs b/Timeline/ErrorCodes.cs index 5e7f003a..c246953b 100644 --- a/Timeline/ErrorCodes.cs +++ b/Timeline/ErrorCodes.cs @@ -15,22 +15,21 @@ { public const int InvalidModel = 10000000; - public static class Header // cc = 01 + public static class Header // cc = 0x { - public const int Missing_ContentType = 10000101; // dd = 01 - public const int Missing_ContentLength = 10000102; // dd = 02 - public const int Zero_ContentLength = 10000103; // dd = 03 - public const int BadFormat_IfNonMatch = 10000104; // dd = 04 + public static class IfNonMatch // cc = 01 + { + public const int BadFormat = 10000101; + } } - public static class Content // cc = 02 + public static class Content // cc = 11 { - public const int TooBig = 1000201; - public const int UnmatchedLength_Smaller = 10030202; - public const int UnmatchedLength_Bigger = 10030203; + public const int TooBig = 10001101; + public const int UnmatchedLength_Smaller = 10001102; + public const int UnmatchedLength_Bigger = 10001103; } } } - } } diff --git a/Timeline/Filters/ContentHeaderAttributes.cs b/Timeline/Filters/ContentHeaderAttributes.cs deleted file mode 100644 index 99bd1540..00000000 --- a/Timeline/Filters/ContentHeaderAttributes.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Localization; -using Timeline.Models.Http; - -namespace Timeline.Filters -{ - public class RequireContentTypeAttribute : ActionFilterAttribute - { - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")] - public override void OnActionExecuting(ActionExecutingContext context) - { - if (context.HttpContext.Request.ContentType == null) - { - context.Result = new BadRequestObjectResult(HeaderErrorResponse.MissingContentType()); - } - } - } - - public class RequireContentLengthAttribute : ActionFilterAttribute - { - public RequireContentLengthAttribute() - : this(true) - { - - } - - public RequireContentLengthAttribute(bool requireNonZero) - { - RequireNonZero = requireNonZero; - } - - public bool RequireNonZero { get; set; } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")] - public override void OnActionExecuting(ActionExecutingContext context) - { - if (context.HttpContext.Request.ContentLength == null) - { - context.Result = new BadRequestObjectResult(HeaderErrorResponse.MissingContentLength()); - return; - } - - if (RequireNonZero && context.HttpContext.Request.ContentLength.Value == 0) - { - context.Result = new BadRequestObjectResult(HeaderErrorResponse.ZeroContentLength()); - return; - } - } - } -} diff --git a/Timeline/Filters/Header.cs b/Timeline/Filters/Header.cs new file mode 100644 index 00000000..f5fb16aa --- /dev/null +++ b/Timeline/Filters/Header.cs @@ -0,0 +1,99 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Timeline.Models.Http; +using static Timeline.Resources.Filters; + +namespace Timeline +{ + public static partial class ErrorCodes + { + public static partial class Http + { + public static partial class Filter // bxx = 1xx + { + public static partial class Header // bbb = 100 + { + public static class ContentType // cc = 01 + { + public const int Missing = 11000101; // dd = 01 + } + + public static class ContentLength // cc = 02 + { + public const int Missing = 11000201; // dd = 01 + public const int Zero = 11000202; // dd = 02 + } + } + } + + } + } +} + +namespace Timeline.Filters +{ + public class RequireContentTypeAttribute : ActionFilterAttribute + { + internal static CommonResponse CreateResponse() + { + return new CommonResponse( + ErrorCodes.Http.Filter.Header.ContentType.Missing, + MessageHeaderContentTypeMissing); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")] + public override void OnActionExecuting(ActionExecutingContext context) + { + if (context.HttpContext.Request.ContentType == null) + { + context.Result = new BadRequestObjectResult(CreateResponse()); + } + } + } + + public class RequireContentLengthAttribute : ActionFilterAttribute + { + internal static CommonResponse CreateMissingResponse() + { + return new CommonResponse( + ErrorCodes.Http.Filter.Header.ContentLength.Missing, + MessageHeaderContentLengthMissing); + } + + internal static CommonResponse CreateZeroResponse() + { + return new CommonResponse( + ErrorCodes.Http.Filter.Header.ContentLength.Zero, + MessageHeaderContentLengthZero); + } + + public RequireContentLengthAttribute() + : this(true) + { + + } + + public RequireContentLengthAttribute(bool requireNonZero) + { + RequireNonZero = requireNonZero; + } + + public bool RequireNonZero { get; set; } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")] + public override void OnActionExecuting(ActionExecutingContext context) + { + if (context.HttpContext.Request.ContentLength == null) + { + context.Result = new BadRequestObjectResult(CreateMissingResponse()); + return; + } + + if (RequireNonZero && context.HttpContext.Request.ContentLength.Value == 0) + { + context.Result = new BadRequestObjectResult(CreateZeroResponse()); + return; + } + } + } +} diff --git a/Timeline/Models/Http/Common.cs b/Timeline/Models/Http/Common.cs index 2c9ee9e7..2a88b3a3 100644 --- a/Timeline/Models/Http/Common.cs +++ b/Timeline/Models/Http/Common.cs @@ -27,24 +27,9 @@ namespace Timeline.Models.Http internal static class HeaderErrorResponse { - internal static CommonResponse MissingContentType() - { - return new CommonResponse(ErrorCodes.Http.Common.Header.Missing_ContentType, MessageHeaderMissingContentType); - } - - internal static CommonResponse MissingContentLength() - { - return new CommonResponse(ErrorCodes.Http.Common.Header.Missing_ContentLength, MessageHeaderMissingContentLength); - } - - internal static CommonResponse ZeroContentLength() - { - return new CommonResponse(ErrorCodes.Http.Common.Header.Zero_ContentLength, MessageHeaderZeroContentLength); - } - internal static CommonResponse BadIfNonMatch() { - return new CommonResponse(ErrorCodes.Http.Common.Header.BadFormat_IfNonMatch, MessageHeaderBadIfNonMatch); + return new CommonResponse(ErrorCodes.Http.Common.Header.IfNonMatch.BadFormat, MessageHeaderIfNonMatchBad); } } diff --git a/Timeline/Resources/Filters.Designer.cs b/Timeline/Resources/Filters.Designer.cs new file mode 100644 index 00000000..ae3565f7 --- /dev/null +++ b/Timeline/Resources/Filters.Designer.cs @@ -0,0 +1,90 @@ +//------------------------------------------------------------------------------ +// +// 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 { + 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 Filters { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Filters() { + } + + /// + /// 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.Filters", typeof(Filters).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 Header Content-Length is missing or of bad format.. + /// + internal static string MessageHeaderContentLengthMissing { + get { + return ResourceManager.GetString("MessageHeaderContentLengthMissing", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Header Content-Length must not be 0.. + /// + internal static string MessageHeaderContentLengthZero { + get { + return ResourceManager.GetString("MessageHeaderContentLengthZero", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Header Content-Type is required.. + /// + internal static string MessageHeaderContentTypeMissing { + get { + return ResourceManager.GetString("MessageHeaderContentTypeMissing", resourceCulture); + } + } + } +} diff --git a/Timeline/Resources/Filters.resx b/Timeline/Resources/Filters.resx new file mode 100644 index 00000000..d2b7e68a --- /dev/null +++ b/Timeline/Resources/Filters.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Header Content-Length is missing or of bad format. + + + Header Content-Length must not be 0. + + + Header Content-Type is required. + + \ No newline at end of file diff --git a/Timeline/Resources/Filters.zh.resx b/Timeline/Resources/Filters.zh.resx new file mode 100644 index 00000000..90e97e49 --- /dev/null +++ b/Timeline/Resources/Filters.zh.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + 请求头Content-Length缺失或者格式不对。 + + + 请求头Content-Length不能为0。 + + + 缺少必需的请求头Content-Type。 + + \ No newline at end of file diff --git a/Timeline/Resources/Models/Http/Common.Designer.cs b/Timeline/Resources/Models/Http/Common.Designer.cs index 2df1e447..4eebd2bc 100644 --- a/Timeline/Resources/Models/Http/Common.Designer.cs +++ b/Timeline/Resources/Models/Http/Common.Designer.cs @@ -108,36 +108,9 @@ namespace Timeline.Resources.Models.Http { /// /// Looks up a localized string similar to Header If-Non-Match is of bad format.. /// - internal static string MessageHeaderBadIfNonMatch { + internal static string MessageHeaderIfNonMatchBad { get { - return ResourceManager.GetString("MessageHeaderBadIfNonMatch", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Header Content-Length is missing or of bad format.. - /// - internal static string MessageHeaderMissingContentLength { - get { - return ResourceManager.GetString("MessageHeaderMissingContentLength", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Header Content-Type is required.. - /// - internal static string MessageHeaderMissingContentType { - get { - return ResourceManager.GetString("MessageHeaderMissingContentType", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Header Content-Length must not be 0.. - /// - internal static string MessageHeaderZeroContentLength { - get { - return ResourceManager.GetString("MessageHeaderZeroContentLength", resourceCulture); + return ResourceManager.GetString("MessageHeaderIfNonMatchBad", resourceCulture); } } diff --git a/Timeline/Resources/Models/Http/Common.resx b/Timeline/Resources/Models/Http/Common.resx index 433c341c..540c6c58 100644 --- a/Timeline/Resources/Models/Http/Common.resx +++ b/Timeline/Resources/Models/Http/Common.resx @@ -132,18 +132,9 @@ The item does not exist, so nothing is changed. - + Header If-Non-Match is of bad format. - - Header Content-Length is missing or of bad format. - - - Header Content-Type is required. - - - Header Content-Length must not be 0. - A new item is created. diff --git a/Timeline/Resources/Models/Http/Common.zh.resx b/Timeline/Resources/Models/Http/Common.zh.resx index cbdd6fb9..467916a2 100644 --- a/Timeline/Resources/Models/Http/Common.zh.resx +++ b/Timeline/Resources/Models/Http/Common.zh.resx @@ -132,17 +132,8 @@ 要删除的项目不存在,什么都没有修改。 - - 头If-Non-Match格式不对。 - - - 头Content-Length缺失或者格式不对。 - - - 缺少必需的头Content-Type。 - - - 头Content-Length不能为0。 + + 请求头If-Non-Match格式不对。 创建了一个新项目。 diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index 0260d725..bd195475 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -64,6 +64,11 @@ True UserController.resx + + True + True + Filters.resx + True True @@ -119,10 +124,6 @@ ResXFileCodeGenerator TokenController.Designer.cs - - Designer - - ResXFileCodeGenerator UserAvatarController.Designer.cs @@ -131,6 +132,10 @@ ResXFileCodeGenerator UserController.Designer.cs + + ResXFileCodeGenerator + Filters.Designer.cs + ResXFileCodeGenerator Common.Designer.cs @@ -160,4 +165,8 @@ UserService.Designer.cs + + + + -- cgit v1.2.3 From 006d799d2fe5f081c188f95a8590c4b75a93caae Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Mon, 28 Oct 2019 23:35:00 +0800 Subject: Add UserDetailController unit tests. --- .../Controllers/UserDetailControllerTest.cs | 93 ++++++++++++++ .../Helpers/AssertionResponseExtensions.cs | 141 --------------------- Timeline.Tests/Helpers/ParameterInfoAssertions.cs | 63 +++++++++ Timeline.Tests/Helpers/ReflectionHelper.cs | 13 ++ Timeline.Tests/Helpers/ResponseAssertions.cs | 141 +++++++++++++++++++++ Timeline/Controllers/UserDetailController.cs | 44 +++++++ Timeline/Filters/User.cs | 42 ++++++ Timeline/Resources/Filters.Designer.cs | 9 ++ Timeline/Resources/Filters.resx | 3 + Timeline/Resources/Filters.zh.resx | 3 + 10 files changed, 411 insertions(+), 141 deletions(-) create mode 100644 Timeline.Tests/Controllers/UserDetailControllerTest.cs delete mode 100644 Timeline.Tests/Helpers/AssertionResponseExtensions.cs create mode 100644 Timeline.Tests/Helpers/ParameterInfoAssertions.cs create mode 100644 Timeline.Tests/Helpers/ReflectionHelper.cs create mode 100644 Timeline.Tests/Helpers/ResponseAssertions.cs create mode 100644 Timeline/Controllers/UserDetailController.cs create mode 100644 Timeline/Filters/User.cs diff --git a/Timeline.Tests/Controllers/UserDetailControllerTest.cs b/Timeline.Tests/Controllers/UserDetailControllerTest.cs new file mode 100644 index 00000000..99341c40 --- /dev/null +++ b/Timeline.Tests/Controllers/UserDetailControllerTest.cs @@ -0,0 +1,93 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Moq; +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Timeline.Controllers; +using Timeline.Filters; +using Timeline.Models.Validation; +using Timeline.Services; +using Timeline.Tests.Helpers; +using Xunit; + +namespace Timeline.Tests.Controllers +{ + public class UserDetailControllerTest : IDisposable + { + private readonly Mock _mockUserDetailService; + private readonly UserDetailController _controller; + + public UserDetailControllerTest() + { + _mockUserDetailService = new Mock(); + _controller = new UserDetailController(_mockUserDetailService.Object); + } + + public void Dispose() + { + _controller.Dispose(); + } + + [Fact] + public void AttributeTest() + { + typeof(UserDetailController).Should().BeDecoratedWith(); + + var getNickname = typeof(UserDetailController).GetMethod(nameof(UserDetailController.GetNickname)); + getNickname.Should().BeDecoratedWith() + .And.BeDecoratedWith(); + getNickname.GetParameter("username").Should().BeDecoratedWith() + .And.BeDecoratedWith(); + + var putNickname = typeof(UserDetailController).GetMethod(nameof(UserDetailController.PutNickname)); + putNickname.Should().BeDecoratedWith() + .And.BeDecoratedWith(); + putNickname.GetParameter("username").Should().BeDecoratedWith() + .And.BeDecoratedWith(); + var stringLengthAttributeOnPutBody = putNickname.GetParameter("body").Should().BeDecoratedWith() + .And.BeDecoratedWith() + .Which; + stringLengthAttributeOnPutBody.MinimumLength.Should().Be(1); + stringLengthAttributeOnPutBody.MaximumLength.Should().Be(10); + + var deleteNickname = typeof(UserDetailController).GetMethod(nameof(UserDetailController.DeleteNickname)); + deleteNickname.Should().BeDecoratedWith() + .And.BeDecoratedWith(); + deleteNickname.GetParameter("username").Should().BeDecoratedWith() + .And.BeDecoratedWith(); + } + + [Fact] + public async Task GetNickname_ShouldWork() + { + const string username = "uuu"; + const string nickname = "nnn"; + _mockUserDetailService.Setup(s => s.GetNickname(username)).ReturnsAsync(nickname); + var actionResult = await _controller.GetNickname(username); + actionResult.Result.Should().BeAssignableTo(nickname); + _mockUserDetailService.VerifyAll(); + } + + [Fact] + public async Task PutNickname_ShouldWork() + { + const string username = "uuu"; + const string nickname = "nnn"; + _mockUserDetailService.Setup(s => s.SetNickname(username, nickname)).Returns(Task.CompletedTask); + var actionResult = await _controller.PutNickname(username, nickname); + actionResult.Should().BeAssignableTo(); + _mockUserDetailService.VerifyAll(); + } + + [Fact] + public async Task DeleteNickname_ShouldWork() + { + const string username = "uuu"; + _mockUserDetailService.Setup(s => s.SetNickname(username, null)).Returns(Task.CompletedTask); + var actionResult = await _controller.DeleteNickname(username); + actionResult.Should().BeAssignableTo(); + _mockUserDetailService.VerifyAll(); + } + } +} diff --git a/Timeline.Tests/Helpers/AssertionResponseExtensions.cs b/Timeline.Tests/Helpers/AssertionResponseExtensions.cs deleted file mode 100644 index 08f10b2b..00000000 --- a/Timeline.Tests/Helpers/AssertionResponseExtensions.cs +++ /dev/null @@ -1,141 +0,0 @@ -using FluentAssertions; -using FluentAssertions.Execution; -using FluentAssertions.Formatting; -using FluentAssertions.Primitives; -using Newtonsoft.Json; -using System; -using System.Net; -using System.Net.Http; -using System.Text; -using Timeline.Models.Http; - -namespace Timeline.Tests.Helpers -{ - public class HttpResponseMessageValueFormatter : IValueFormatter - { - public bool CanHandle(object value) - { - return value is HttpResponseMessage; - } - - public string Format(object value, FormattingContext context, FormatChild formatChild) - { - string newline = context.UseLineBreaks ? Environment.NewLine : ""; - string padding = new string('\t', context.Depth); - - var res = (HttpResponseMessage)value; - - var builder = new StringBuilder(); - builder.Append($"{newline}{padding} Status Code: {res.StatusCode} ; Body: "); - - try - { - var body = res.Content.ReadAsStringAsync().Result; - if (body.Length > 40) - { - body = body[0..40] + " ..."; - } - builder.Append(body); - } - catch (AggregateException) - { - builder.Append("NOT A STRING."); - } - - return builder.ToString(); - } - } - - public class HttpResponseMessageAssertions - : ReferenceTypeAssertions - { - static HttpResponseMessageAssertions() - { - Formatter.AddFormatter(new HttpResponseMessageValueFormatter()); - } - - public HttpResponseMessageAssertions(HttpResponseMessage instance) - { - Subject = instance; - } - - protected override string Identifier => "HttpResponseMessage"; - - public AndConstraint HaveStatusCode(int expected, string because = "", params object[] becauseArgs) - { - return HaveStatusCode((HttpStatusCode)expected, because, becauseArgs); - } - - public AndConstraint HaveStatusCode(HttpStatusCode expected, string because = "", params object[] becauseArgs) - { - Execute.Assertion.BecauseOf(because, becauseArgs) - .ForCondition(Subject.StatusCode == expected) - .FailWith("Expected status code of {context:HttpResponseMessage} to be {0}{reason}, but found {1}.", expected, Subject.StatusCode); - return new AndConstraint(Subject); - } - - public AndWhichConstraint HaveJsonBody(string because = "", params object[] becauseArgs) - { - var a = Execute.Assertion.BecauseOf(because, becauseArgs); - string body; - try - { - body = Subject.Content.ReadAsStringAsync().Result; - } - catch (AggregateException e) - { - a.FailWith("Expected response body of {context:HttpResponseMessage} to be json string{reason}, but failed to read it or it was not a string. Exception is {0}.", e.InnerExceptions); - return new AndWhichConstraint(Subject, null); - } - - var result = JsonConvert.DeserializeObject(body); - return new AndWhichConstraint(Subject, result); - } - } - - public static class AssertionResponseExtensions - { - public static HttpResponseMessageAssertions Should(this HttpResponseMessage instance) - { - return new HttpResponseMessageAssertions(instance); - } - - public static AndWhichConstraint HaveCommonBody(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) - { - return assertions.HaveJsonBody(because, becauseArgs); - } - - public static AndWhichConstraint> HaveCommonDataBody(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) - { - return assertions.HaveJsonBody>(because, becauseArgs); - } - - public static void BePut(this HttpResponseMessageAssertions assertions, bool create, string because = "", params object[] becauseArgs) - { - var body = assertions.HaveStatusCode(create ? 201 : 200, because, becauseArgs) - .And.Should().HaveJsonBody(because, becauseArgs) - .Which; - body.Code.Should().Be(0); - body.Data.Create.Should().Be(create); - } - - public static void BeDelete(this HttpResponseMessageAssertions assertions, bool delete, string because = "", params object[] becauseArgs) - { - var body = assertions.HaveStatusCode(200, because, becauseArgs) - .And.Should().HaveJsonBody(because, becauseArgs) - .Which; - body.Code.Should().Be(0); - body.Data.Delete.Should().Be(delete); - } - - public static void BeInvalidModel(this HttpResponseMessageAssertions assertions, string message = null) - { - message = string.IsNullOrEmpty(message) ? "" : ", " + message; - assertions.HaveStatusCode(400, "Invalid Model Error must have 400 status code{0}", message) - .And.Should().HaveCommonBody("Invalid Model Error must have CommonResponse body{0}", message) - .Which.Code.Should().Be(ErrorCodes.Http.Common.InvalidModel, - "Invalid Model Error must have code {0} in body{1}", - ErrorCodes.Http.Common.InvalidModel, message); - } - } -} diff --git a/Timeline.Tests/Helpers/ParameterInfoAssertions.cs b/Timeline.Tests/Helpers/ParameterInfoAssertions.cs new file mode 100644 index 00000000..e3becee1 --- /dev/null +++ b/Timeline.Tests/Helpers/ParameterInfoAssertions.cs @@ -0,0 +1,63 @@ +using FluentAssertions; +using FluentAssertions.Execution; +using FluentAssertions.Formatting; +using FluentAssertions.Primitives; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace Timeline.Tests.Helpers +{ + public class ParameterInfoValueFormatter : IValueFormatter + { + public bool CanHandle(object value) + { + return value is ParameterInfo; + } + + public string Format(object value, FormattingContext context, FormatChild formatChild) + { + var param = (ParameterInfo)value; + return $"{param.Member.DeclaringType.FullName}.{param.Member.Name}#{param.Name}"; + } + } + + public class ParameterInfoAssertions : ReferenceTypeAssertions + { + static ParameterInfoAssertions() + { + Formatter.AddFormatter(new ParameterInfoValueFormatter()); + } + + public ParameterInfoAssertions(ParameterInfo parameterInfo) + { + Subject = parameterInfo; + } + + protected override string Identifier => "parameter"; + + public AndWhichConstraint BeDecoratedWith(string because = "", params object[] becauseArgs) + where TAttribute : Attribute + { + var attribute = Subject.GetCustomAttribute(false); + + Execute.Assertion + .BecauseOf(because, becauseArgs) + .ForCondition(attribute != null) + .FailWith("Expected {0} {1} to be decorated with {2}{reason}, but that attribute was not found.", + Identifier, Subject, typeof(TAttribute).FullName); + + return new AndWhichConstraint(this, attribute); + } + } + + public static class ParameterInfoAssertionExtensions + { + public static ParameterInfoAssertions Should(this ParameterInfo parameterInfo) + { + return new ParameterInfoAssertions(parameterInfo); + } + } +} diff --git a/Timeline.Tests/Helpers/ReflectionHelper.cs b/Timeline.Tests/Helpers/ReflectionHelper.cs new file mode 100644 index 00000000..3f6036e3 --- /dev/null +++ b/Timeline.Tests/Helpers/ReflectionHelper.cs @@ -0,0 +1,13 @@ +using System.Linq; +using System.Reflection; + +namespace Timeline.Tests.Helpers +{ + public static class ReflectionHelper + { + public static ParameterInfo GetParameter(this MethodInfo methodInfo, string name) + { + return methodInfo.GetParameters().Where(p => p.Name == name).Single(); + } + } +} diff --git a/Timeline.Tests/Helpers/ResponseAssertions.cs b/Timeline.Tests/Helpers/ResponseAssertions.cs new file mode 100644 index 00000000..08f10b2b --- /dev/null +++ b/Timeline.Tests/Helpers/ResponseAssertions.cs @@ -0,0 +1,141 @@ +using FluentAssertions; +using FluentAssertions.Execution; +using FluentAssertions.Formatting; +using FluentAssertions.Primitives; +using Newtonsoft.Json; +using System; +using System.Net; +using System.Net.Http; +using System.Text; +using Timeline.Models.Http; + +namespace Timeline.Tests.Helpers +{ + public class HttpResponseMessageValueFormatter : IValueFormatter + { + public bool CanHandle(object value) + { + return value is HttpResponseMessage; + } + + public string Format(object value, FormattingContext context, FormatChild formatChild) + { + string newline = context.UseLineBreaks ? Environment.NewLine : ""; + string padding = new string('\t', context.Depth); + + var res = (HttpResponseMessage)value; + + var builder = new StringBuilder(); + builder.Append($"{newline}{padding} Status Code: {res.StatusCode} ; Body: "); + + try + { + var body = res.Content.ReadAsStringAsync().Result; + if (body.Length > 40) + { + body = body[0..40] + " ..."; + } + builder.Append(body); + } + catch (AggregateException) + { + builder.Append("NOT A STRING."); + } + + return builder.ToString(); + } + } + + public class HttpResponseMessageAssertions + : ReferenceTypeAssertions + { + static HttpResponseMessageAssertions() + { + Formatter.AddFormatter(new HttpResponseMessageValueFormatter()); + } + + public HttpResponseMessageAssertions(HttpResponseMessage instance) + { + Subject = instance; + } + + protected override string Identifier => "HttpResponseMessage"; + + public AndConstraint HaveStatusCode(int expected, string because = "", params object[] becauseArgs) + { + return HaveStatusCode((HttpStatusCode)expected, because, becauseArgs); + } + + public AndConstraint HaveStatusCode(HttpStatusCode expected, string because = "", params object[] becauseArgs) + { + Execute.Assertion.BecauseOf(because, becauseArgs) + .ForCondition(Subject.StatusCode == expected) + .FailWith("Expected status code of {context:HttpResponseMessage} to be {0}{reason}, but found {1}.", expected, Subject.StatusCode); + return new AndConstraint(Subject); + } + + public AndWhichConstraint HaveJsonBody(string because = "", params object[] becauseArgs) + { + var a = Execute.Assertion.BecauseOf(because, becauseArgs); + string body; + try + { + body = Subject.Content.ReadAsStringAsync().Result; + } + catch (AggregateException e) + { + a.FailWith("Expected response body of {context:HttpResponseMessage} to be json string{reason}, but failed to read it or it was not a string. Exception is {0}.", e.InnerExceptions); + return new AndWhichConstraint(Subject, null); + } + + var result = JsonConvert.DeserializeObject(body); + return new AndWhichConstraint(Subject, result); + } + } + + public static class AssertionResponseExtensions + { + public static HttpResponseMessageAssertions Should(this HttpResponseMessage instance) + { + return new HttpResponseMessageAssertions(instance); + } + + public static AndWhichConstraint HaveCommonBody(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) + { + return assertions.HaveJsonBody(because, becauseArgs); + } + + public static AndWhichConstraint> HaveCommonDataBody(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) + { + return assertions.HaveJsonBody>(because, becauseArgs); + } + + public static void BePut(this HttpResponseMessageAssertions assertions, bool create, string because = "", params object[] becauseArgs) + { + var body = assertions.HaveStatusCode(create ? 201 : 200, because, becauseArgs) + .And.Should().HaveJsonBody(because, becauseArgs) + .Which; + body.Code.Should().Be(0); + body.Data.Create.Should().Be(create); + } + + public static void BeDelete(this HttpResponseMessageAssertions assertions, bool delete, string because = "", params object[] becauseArgs) + { + var body = assertions.HaveStatusCode(200, because, becauseArgs) + .And.Should().HaveJsonBody(because, becauseArgs) + .Which; + body.Code.Should().Be(0); + body.Data.Delete.Should().Be(delete); + } + + public static void BeInvalidModel(this HttpResponseMessageAssertions assertions, string message = null) + { + message = string.IsNullOrEmpty(message) ? "" : ", " + message; + assertions.HaveStatusCode(400, "Invalid Model Error must have 400 status code{0}", message) + .And.Should().HaveCommonBody("Invalid Model Error must have CommonResponse body{0}", message) + .Which.Code.Should().Be(ErrorCodes.Http.Common.InvalidModel, + "Invalid Model Error must have code {0} in body{1}", + ErrorCodes.Http.Common.InvalidModel, message); + } + } +} diff --git a/Timeline/Controllers/UserDetailController.cs b/Timeline/Controllers/UserDetailController.cs new file mode 100644 index 00000000..ef13b462 --- /dev/null +++ b/Timeline/Controllers/UserDetailController.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Mvc; +using System.Threading.Tasks; +using Timeline.Filters; +using Timeline.Models.Validation; +using Timeline.Services; +using System.ComponentModel.DataAnnotations; + +namespace Timeline.Controllers +{ + [ApiController] + public class UserDetailController : Controller + { + private readonly IUserDetailService _service; + + public UserDetailController(IUserDetailService service) + { + _service = service; + } + + [HttpGet("users/{username}/nickname")] + [CatchUserNotExistException] + public async Task> GetNickname([FromRoute][Username] string username) + { + return Ok(await _service.GetNickname(username)); + } + + [HttpPut("users/{username}/nickname")] + [CatchUserNotExistException] + public async Task PutNickname([FromRoute][Username] string username, + [FromBody][StringLength(10, MinimumLength = 1)] string body) + { + await _service.SetNickname(username, body); + return Ok(); + } + + [HttpDelete("users/{username}/nickname")] + [CatchUserNotExistException] + public async Task DeleteNickname([FromRoute][Username] string username) + { + await _service.SetNickname(username, null); + return Ok(); + } + } +} diff --git a/Timeline/Filters/User.cs b/Timeline/Filters/User.cs new file mode 100644 index 00000000..22fae938 --- /dev/null +++ b/Timeline/Filters/User.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using System; +using Timeline.Models.Http; + +namespace Timeline +{ + public static partial class ErrorCodes + { + public static partial class Http + { + public static partial class Filter // bxx = 1xx + { + public static class User // bbb = 101 + { + public const int NotExist = 11010001; + } + + } + } + } +} + +namespace Timeline.Filters +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] + public class CatchUserNotExistExceptionAttribute : ExceptionFilterAttribute + { + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "ASP.Net already checked.")] + public override void OnException(ExceptionContext context) + { + var body = new CommonResponse( + ErrorCodes.Http.Filter.User.NotExist, + Resources.Filters.MessageUserNotExist); + + if (context.HttpContext.Request.Method == "GET") + context.Result = new NotFoundObjectResult(body); + else + context.Result = new BadRequestObjectResult(body); + } + } +} diff --git a/Timeline/Resources/Filters.Designer.cs b/Timeline/Resources/Filters.Designer.cs index ae3565f7..e3c8be41 100644 --- a/Timeline/Resources/Filters.Designer.cs +++ b/Timeline/Resources/Filters.Designer.cs @@ -86,5 +86,14 @@ namespace Timeline.Resources { return ResourceManager.GetString("MessageHeaderContentTypeMissing", resourceCulture); } } + + /// + /// Looks up a localized string similar to The user does not exist.. + /// + internal static string MessageUserNotExist { + get { + return ResourceManager.GetString("MessageUserNotExist", resourceCulture); + } + } } } diff --git a/Timeline/Resources/Filters.resx b/Timeline/Resources/Filters.resx index d2b7e68a..ba1fcee8 100644 --- a/Timeline/Resources/Filters.resx +++ b/Timeline/Resources/Filters.resx @@ -126,4 +126,7 @@ Header Content-Type is required. + + The user does not exist. + \ No newline at end of file diff --git a/Timeline/Resources/Filters.zh.resx b/Timeline/Resources/Filters.zh.resx index 90e97e49..690a3e39 100644 --- a/Timeline/Resources/Filters.zh.resx +++ b/Timeline/Resources/Filters.zh.resx @@ -126,4 +126,7 @@ 缺少必需的请求头Content-Type。 + + 用户不存在。 + \ No newline at end of file -- cgit v1.2.3 From d3a1bf5f2939049f11e77f91ad9ddea30d8acd64 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Thu, 31 Oct 2019 00:56:46 +0800 Subject: Continue to construct feature and tests. --- Timeline.Tests/Controllers/UserControllerTest.cs | 1 - .../Controllers/UserDetailControllerTest.cs | 5 ++ Timeline.Tests/Helpers/HttpClientExtensions.cs | 12 +++ Timeline.Tests/Helpers/ResponseAssertions.cs | 5 ++ Timeline.Tests/IntegratedTests/UserAvatarTest.cs | 4 +- Timeline.Tests/IntegratedTests/UserDetailTest.cs | 83 ++++++++++++++++++ Timeline.Tests/Properties/launchSettings.json | 52 ++++++------ Timeline.Tests/Timeline.Tests.csproj | 4 + Timeline/Auth/Attribute.cs | 21 +++++ Timeline/Auth/MyAuthenticationHandler.cs | 99 ++++++++++++++++++++++ Timeline/Auth/PrincipalExtensions.cs | 13 +++ Timeline/Authentication/Attribute.cs | 21 ----- Timeline/Authentication/AuthHandler.cs | 99 ---------------------- Timeline/Authentication/PrincipalExtensions.cs | 13 --- .../Controllers/Testing/TestingAuthController.cs | 2 +- Timeline/Controllers/UserAvatarController.cs | 2 +- Timeline/Controllers/UserController.cs | 2 +- Timeline/Controllers/UserDetailController.cs | 5 ++ Timeline/Filters/User.cs | 66 ++++++++++++--- Timeline/Formatters/StringInputFormatter.cs | 27 ++++++ Timeline/Resources/Filters.Designer.cs | 36 ++++++++ Timeline/Resources/Filters.resx | 12 +++ Timeline/Resources/Filters.zh.resx | 3 + Timeline/Startup.cs | 18 ++-- 24 files changed, 424 insertions(+), 181 deletions(-) create mode 100644 Timeline.Tests/IntegratedTests/UserDetailTest.cs create mode 100644 Timeline/Auth/Attribute.cs create mode 100644 Timeline/Auth/MyAuthenticationHandler.cs create mode 100644 Timeline/Auth/PrincipalExtensions.cs delete mode 100644 Timeline/Authentication/Attribute.cs delete mode 100644 Timeline/Authentication/AuthHandler.cs delete mode 100644 Timeline/Authentication/PrincipalExtensions.cs create mode 100644 Timeline/Formatters/StringInputFormatter.cs diff --git a/Timeline.Tests/Controllers/UserControllerTest.cs b/Timeline.Tests/Controllers/UserControllerTest.cs index a9cce970..83b8cdcf 100644 --- a/Timeline.Tests/Controllers/UserControllerTest.cs +++ b/Timeline.Tests/Controllers/UserControllerTest.cs @@ -13,7 +13,6 @@ using Timeline.Models.Http; using Timeline.Services; using Timeline.Tests.Helpers; using Timeline.Tests.Mock.Data; -using Timeline.Tests.Mock.Services; using Xunit; using static Timeline.ErrorCodes.Http.User; diff --git a/Timeline.Tests/Controllers/UserDetailControllerTest.cs b/Timeline.Tests/Controllers/UserDetailControllerTest.cs index 99341c40..ffd88790 100644 --- a/Timeline.Tests/Controllers/UserDetailControllerTest.cs +++ b/Timeline.Tests/Controllers/UserDetailControllerTest.cs @@ -1,4 +1,5 @@ using FluentAssertions; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Moq; using System; @@ -42,6 +43,8 @@ namespace Timeline.Tests.Controllers var putNickname = typeof(UserDetailController).GetMethod(nameof(UserDetailController.PutNickname)); putNickname.Should().BeDecoratedWith() + .And.BeDecoratedWith() + .And.BeDecoratedWith() .And.BeDecoratedWith(); putNickname.GetParameter("username").Should().BeDecoratedWith() .And.BeDecoratedWith(); @@ -53,6 +56,8 @@ namespace Timeline.Tests.Controllers var deleteNickname = typeof(UserDetailController).GetMethod(nameof(UserDetailController.DeleteNickname)); deleteNickname.Should().BeDecoratedWith() + .And.BeDecoratedWith() + .And.BeDecoratedWith() .And.BeDecoratedWith(); deleteNickname.GetParameter("username").Should().BeDecoratedWith() .And.BeDecoratedWith(); diff --git a/Timeline.Tests/Helpers/HttpClientExtensions.cs b/Timeline.Tests/Helpers/HttpClientExtensions.cs index 38641f90..6513bbe7 100644 --- a/Timeline.Tests/Helpers/HttpClientExtensions.cs +++ b/Timeline.Tests/Helpers/HttpClientExtensions.cs @@ -35,5 +35,17 @@ namespace Timeline.Tests.Helpers content.Headers.ContentType = new MediaTypeHeaderValue(mimeType); return client.PutAsync(url, content); } + + public static Task PutStringAsync(this HttpClient client, string url, string body, string mimeType = null) + { + return client.PutStringAsync(new Uri(url, UriKind.RelativeOrAbsolute), body, mimeType); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")] + public static Task PutStringAsync(this HttpClient client, Uri url, string body, string mimeType = null) + { + var content = new StringContent(body, Encoding.UTF8, mimeType ?? MediaTypeNames.Text.Plain); + return client.PutAsync(url, content); + } } } diff --git a/Timeline.Tests/Helpers/ResponseAssertions.cs b/Timeline.Tests/Helpers/ResponseAssertions.cs index 08f10b2b..db86ff59 100644 --- a/Timeline.Tests/Helpers/ResponseAssertions.cs +++ b/Timeline.Tests/Helpers/ResponseAssertions.cs @@ -91,6 +91,11 @@ namespace Timeline.Tests.Helpers var result = JsonConvert.DeserializeObject(body); return new AndWhichConstraint(Subject, result); } + + internal void HaveStatusCode(object statusCode) + { + throw new NotImplementedException(); + } } public static class AssertionResponseExtensions diff --git a/Timeline.Tests/IntegratedTests/UserAvatarTest.cs b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs index ad2e11df..b338665e 100644 --- a/Timeline.Tests/IntegratedTests/UserAvatarTest.cs +++ b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs @@ -22,12 +22,12 @@ using static Timeline.ErrorCodes.Http.UserAvatar; namespace Timeline.Tests.IntegratedTests { - public class UserAvatarUnitTest : IClassFixture>, IDisposable + public class UserAvatarTest : IClassFixture>, IDisposable { private readonly TestApplication _testApp; private readonly WebApplicationFactory _factory; - public UserAvatarUnitTest(WebApplicationFactory factory) + public UserAvatarTest(WebApplicationFactory factory) { _testApp = new TestApplication(factory); _factory = _testApp.Factory; diff --git a/Timeline.Tests/IntegratedTests/UserDetailTest.cs b/Timeline.Tests/IntegratedTests/UserDetailTest.cs new file mode 100644 index 00000000..ff2c03a5 --- /dev/null +++ b/Timeline.Tests/IntegratedTests/UserDetailTest.cs @@ -0,0 +1,83 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.Testing; +using System; +using System.Net; +using System.Threading.Tasks; +using Timeline.Tests.Helpers; +using Timeline.Tests.Helpers.Authentication; +using Timeline.Tests.Mock.Data; +using Xunit; + +namespace Timeline.Tests.IntegratedTests +{ + public class UserDetailTest : IClassFixture>, IDisposable + { + private readonly TestApplication _testApp; + private readonly WebApplicationFactory _factory; + + public UserDetailTest(WebApplicationFactory factory) + { + _testApp = new TestApplication(factory); + _factory = _testApp.Factory; + } + + public void Dispose() + { + _testApp.Dispose(); + } + + [Fact] + public async Task PermissionTest() + { + { // unauthorize + using var client = _factory.CreateDefaultClient(); + { // GET + var res = await client.GetAsync($"users/{MockUser.User.Username}/nickname"); + res.Should().HaveStatusCode(HttpStatusCode.OK); + } + { // PUT + var res = await client.PutStringAsync($"users/{MockUser.User.Username}/nickname", "aaa"); + res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); + } + { // DELETE + var res = await client.DeleteAsync($"users/{MockUser.User.Username}/nickname"); + res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); + } + } + { // user + using var client = await _factory.CreateClientAsUser(); + { // GET + var res = await client.GetAsync($"users/{MockUser.User.Username}/nickname"); + res.Should().HaveStatusCode(HttpStatusCode.OK); + } + { // PUT self + var res = await client.PutStringAsync($"users/{MockUser.User.Username}/nickname", "aaa"); + res.Should().HaveStatusCode(HttpStatusCode.OK); + } + { // PUT other + var res = await client.PutStringAsync($"users/{MockUser.Admin.Username}/nickname", "aaa"); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden); + } + { // DELETE self + var res = await client.DeleteAsync($"users/{MockUser.User.Username}/nickname"); + res.Should().HaveStatusCode(HttpStatusCode.OK); + } + { // DELETE other + var res = await client.DeleteAsync($"users/{MockUser.Admin.Username}/nickname"); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden); + } + } + { // user + using var client = await _factory.CreateClientAsAdmin(); + { // PUT other + var res = await client.PutStringAsync($"users/{MockUser.User.Username}/nickname", "aaa"); + res.Should().HaveStatusCode(HttpStatusCode.OK); + } + { // DELETE other + var res = await client.DeleteAsync($"users/{MockUser.User.Username}/nickname"); + res.Should().HaveStatusCode(HttpStatusCode.OK); + } + } + } + } +} diff --git a/Timeline.Tests/Properties/launchSettings.json b/Timeline.Tests/Properties/launchSettings.json index 0c1cae5d..7a94d57a 100644 --- a/Timeline.Tests/Properties/launchSettings.json +++ b/Timeline.Tests/Properties/launchSettings.json @@ -1,27 +1,27 @@ -{ - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:11197/", - "sslPort": 0 - } - }, - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "Timeline.Tests": { - "commandName": "Project", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "https://localhost:11199/;http://localhost:11198/" - } - } +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:52040/", + "sslPort": 0 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Timeline.Tests": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:52041/" + } + } } \ No newline at end of file diff --git a/Timeline.Tests/Timeline.Tests.csproj b/Timeline.Tests/Timeline.Tests.csproj index 497a00b7..21e887eb 100644 --- a/Timeline.Tests/Timeline.Tests.csproj +++ b/Timeline.Tests/Timeline.Tests.csproj @@ -31,4 +31,8 @@ + + + + diff --git a/Timeline/Auth/Attribute.cs b/Timeline/Auth/Attribute.cs new file mode 100644 index 00000000..86d0109b --- /dev/null +++ b/Timeline/Auth/Attribute.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Authorization; +using Timeline.Entities; + +namespace Timeline.Auth +{ + public class AdminAuthorizeAttribute : AuthorizeAttribute + { + public AdminAuthorizeAttribute() + { + Roles = UserRoles.Admin; + } + } + + public class UserAuthorizeAttribute : AuthorizeAttribute + { + public UserAuthorizeAttribute() + { + Roles = UserRoles.User; + } + } +} diff --git a/Timeline/Auth/MyAuthenticationHandler.cs b/Timeline/Auth/MyAuthenticationHandler.cs new file mode 100644 index 00000000..f5dcd697 --- /dev/null +++ b/Timeline/Auth/MyAuthenticationHandler.cs @@ -0,0 +1,99 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; +using System; +using System.Linq; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Timeline.Models; +using Timeline.Services; +using static Timeline.Resources.Authentication.AuthHandler; + +namespace Timeline.Auth +{ + public static class AuthenticationConstants + { + public const string Scheme = "Bearer"; + public const string DisplayName = "My Jwt Auth Scheme"; + } + + public class MyAuthenticationOptions : AuthenticationSchemeOptions + { + /// + /// The query param key to search for token. If null then query params are not searched for token. Default to "token". + /// + public string TokenQueryParamKey { get; set; } = "token"; + } + + public class MyAuthenticationHandler : AuthenticationHandler + { + private readonly ILogger _logger; + private readonly IUserService _userService; + + public MyAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IUserService userService) + : base(options, logger, encoder, clock) + { + _logger = logger.CreateLogger(); + _userService = userService; + } + + // return null if no token is found + private string? ExtractToken() + { + // check the authorization header + string header = Request.Headers[HeaderNames.Authorization]; + if (!string.IsNullOrEmpty(header) && header.StartsWith("Bearer ", StringComparison.InvariantCultureIgnoreCase)) + { + var token = header.Substring("Bearer ".Length).Trim(); + _logger.LogInformation(LogTokenFoundInHeader, token); + return token; + } + + // check the query params + var paramQueryKey = Options.TokenQueryParamKey; + if (!string.IsNullOrEmpty(paramQueryKey)) + { + string token = Request.Query[paramQueryKey]; + if (!string.IsNullOrEmpty(token)) + { + _logger.LogInformation(LogTokenFoundInQuery, paramQueryKey, token); + return token; + } + } + + // not found anywhere then return null + return null; + } + + protected override async Task HandleAuthenticateAsync() + { + var token = ExtractToken(); + if (string.IsNullOrEmpty(token)) + { + _logger.LogInformation(LogTokenNotFound); + return AuthenticateResult.NoResult(); + } + + try + { + var userInfo = await _userService.VerifyToken(token); + + var identity = new ClaimsIdentity(AuthenticationConstants.Scheme); + identity.AddClaim(new Claim(identity.NameClaimType, userInfo.Username, ClaimValueTypes.String)); + identity.AddClaims(UserRoleConvert.ToArray(userInfo.Administrator).Select(role => new Claim(identity.RoleClaimType, role, ClaimValueTypes.String))); + + var principal = new ClaimsPrincipal(); + principal.AddIdentity(identity); + + return AuthenticateResult.Success(new AuthenticationTicket(principal, AuthenticationConstants.Scheme)); + } + catch (Exception e) when (!(e is ArgumentException)) + { + _logger.LogInformation(e, LogTokenValidationFail); + return AuthenticateResult.Fail(e); + } + } + } +} diff --git a/Timeline/Auth/PrincipalExtensions.cs b/Timeline/Auth/PrincipalExtensions.cs new file mode 100644 index 00000000..ad7a887f --- /dev/null +++ b/Timeline/Auth/PrincipalExtensions.cs @@ -0,0 +1,13 @@ +using System.Security.Principal; +using Timeline.Entities; + +namespace Timeline.Auth +{ + internal static class PrincipalExtensions + { + internal static bool IsAdministrator(this IPrincipal principal) + { + return principal.IsInRole(UserRoles.Admin); + } + } +} diff --git a/Timeline/Authentication/Attribute.cs b/Timeline/Authentication/Attribute.cs deleted file mode 100644 index 370b37e1..00000000 --- a/Timeline/Authentication/Attribute.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Microsoft.AspNetCore.Authorization; -using Timeline.Entities; - -namespace Timeline.Authentication -{ - public class AdminAuthorizeAttribute : AuthorizeAttribute - { - public AdminAuthorizeAttribute() - { - Roles = UserRoles.Admin; - } - } - - public class UserAuthorizeAttribute : AuthorizeAttribute - { - public UserAuthorizeAttribute() - { - Roles = UserRoles.User; - } - } -} diff --git a/Timeline/Authentication/AuthHandler.cs b/Timeline/Authentication/AuthHandler.cs deleted file mode 100644 index 2b457eb1..00000000 --- a/Timeline/Authentication/AuthHandler.cs +++ /dev/null @@ -1,99 +0,0 @@ -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Microsoft.Net.Http.Headers; -using System; -using System.Linq; -using System.Security.Claims; -using System.Text.Encodings.Web; -using System.Threading.Tasks; -using Timeline.Models; -using Timeline.Services; -using static Timeline.Resources.Authentication.AuthHandler; - -namespace Timeline.Authentication -{ - static class AuthConstants - { - public const string Scheme = "Bearer"; - public const string DisplayName = "My Jwt Auth Scheme"; - } - - public class AuthOptions : AuthenticationSchemeOptions - { - /// - /// The query param key to search for token. If null then query params are not searched for token. Default to "token". - /// - public string TokenQueryParamKey { get; set; } = "token"; - } - - public class AuthHandler : AuthenticationHandler - { - private readonly ILogger _logger; - private readonly IUserService _userService; - - public AuthHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, IUserService userService) - : base(options, logger, encoder, clock) - { - _logger = logger.CreateLogger(); - _userService = userService; - } - - // return null if no token is found - private string? ExtractToken() - { - // check the authorization header - string header = Request.Headers[HeaderNames.Authorization]; - if (!string.IsNullOrEmpty(header) && header.StartsWith("Bearer ", StringComparison.InvariantCultureIgnoreCase)) - { - var token = header.Substring("Bearer ".Length).Trim(); - _logger.LogInformation(LogTokenFoundInHeader, token); - return token; - } - - // check the query params - var paramQueryKey = Options.TokenQueryParamKey; - if (!string.IsNullOrEmpty(paramQueryKey)) - { - string token = Request.Query[paramQueryKey]; - if (!string.IsNullOrEmpty(token)) - { - _logger.LogInformation(LogTokenFoundInQuery, paramQueryKey, token); - return token; - } - } - - // not found anywhere then return null - return null; - } - - protected override async Task HandleAuthenticateAsync() - { - var token = ExtractToken(); - if (string.IsNullOrEmpty(token)) - { - _logger.LogInformation(LogTokenNotFound); - return AuthenticateResult.NoResult(); - } - - try - { - var userInfo = await _userService.VerifyToken(token); - - var identity = new ClaimsIdentity(AuthConstants.Scheme); - identity.AddClaim(new Claim(identity.NameClaimType, userInfo.Username, ClaimValueTypes.String)); - identity.AddClaims(UserRoleConvert.ToArray(userInfo.Administrator).Select(role => new Claim(identity.RoleClaimType, role, ClaimValueTypes.String))); - - var principal = new ClaimsPrincipal(); - principal.AddIdentity(identity); - - return AuthenticateResult.Success(new AuthenticationTicket(principal, AuthConstants.Scheme)); - } - catch (Exception e) when (!(e is ArgumentException)) - { - _logger.LogInformation(e, LogTokenValidationFail); - return AuthenticateResult.Fail(e); - } - } - } -} diff --git a/Timeline/Authentication/PrincipalExtensions.cs b/Timeline/Authentication/PrincipalExtensions.cs deleted file mode 100644 index 8d77ab62..00000000 --- a/Timeline/Authentication/PrincipalExtensions.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Security.Principal; -using Timeline.Entities; - -namespace Timeline.Authentication -{ - internal static class PrincipalExtensions - { - internal static bool IsAdministrator(this IPrincipal principal) - { - return principal.IsInRole(UserRoles.Admin); - } - } -} diff --git a/Timeline/Controllers/Testing/TestingAuthController.cs b/Timeline/Controllers/Testing/TestingAuthController.cs index 67b5b2ef..4d3b3ec7 100644 --- a/Timeline/Controllers/Testing/TestingAuthController.cs +++ b/Timeline/Controllers/Testing/TestingAuthController.cs @@ -1,6 +1,6 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using Timeline.Authentication; +using Timeline.Auth; namespace Timeline.Controllers.Testing { diff --git a/Timeline/Controllers/UserAvatarController.cs b/Timeline/Controllers/UserAvatarController.cs index 7c77897d..7625f962 100644 --- a/Timeline/Controllers/UserAvatarController.cs +++ b/Timeline/Controllers/UserAvatarController.cs @@ -6,7 +6,7 @@ using Microsoft.Net.Http.Headers; using System; using System.Linq; using System.Threading.Tasks; -using Timeline.Authentication; +using Timeline.Auth; using Timeline.Filters; using Timeline.Helpers; using Timeline.Models.Http; diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs index 7b441c3a..0d950cd7 100644 --- a/Timeline/Controllers/UserController.cs +++ b/Timeline/Controllers/UserController.cs @@ -3,7 +3,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System.Globalization; using System.Threading.Tasks; -using Timeline.Authentication; +using Timeline.Auth; using Timeline.Helpers; using Timeline.Models; using Timeline.Models.Http; diff --git a/Timeline/Controllers/UserDetailController.cs b/Timeline/Controllers/UserDetailController.cs index ef13b462..9de9899e 100644 --- a/Timeline/Controllers/UserDetailController.cs +++ b/Timeline/Controllers/UserDetailController.cs @@ -4,6 +4,7 @@ using Timeline.Filters; using Timeline.Models.Validation; using Timeline.Services; using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Authorization; namespace Timeline.Controllers { @@ -25,6 +26,8 @@ namespace Timeline.Controllers } [HttpPut("users/{username}/nickname")] + [Authorize] + [SelfOrAdmin] [CatchUserNotExistException] public async Task PutNickname([FromRoute][Username] string username, [FromBody][StringLength(10, MinimumLength = 1)] string body) @@ -34,6 +37,8 @@ namespace Timeline.Controllers } [HttpDelete("users/{username}/nickname")] + [Authorize] + [SelfOrAdmin] [CatchUserNotExistException] public async Task DeleteNickname([FromRoute][Username] string username) { diff --git a/Timeline/Filters/User.cs b/Timeline/Filters/User.cs index 22fae938..16c76750 100644 --- a/Timeline/Filters/User.cs +++ b/Timeline/Filters/User.cs @@ -1,7 +1,13 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using System; +using Timeline.Auth; using Timeline.Models.Http; +using Timeline.Services; +using static Timeline.Resources.Filters; namespace Timeline { @@ -13,9 +19,10 @@ namespace Timeline { public static class User // bbb = 101 { - public const int NotExist = 11010001; - } + public const int NotExist = 11010101; + public const int NotSelfOrAdminForbid = 11010201; + } } } } @@ -23,20 +30,59 @@ namespace Timeline namespace Timeline.Filters { + public class SelfOrAdminAttribute : ActionFilterAttribute + { + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")] + public override void OnActionExecuting(ActionExecutingContext context) + { + var logger = context.HttpContext.RequestServices.GetRequiredService>(); + + var user = context.HttpContext.User; + + if (user == null) + { + logger.LogError(LogSelfOrAdminNoUser); + return; + } + + if (context.ModelState.TryGetValue("username", out var model)) + { + if (model.RawValue is string username) + { + if (!user.IsAdministrator() && user.Identity.Name != username) + { + context.Result = new ObjectResult( + new CommonResponse(ErrorCodes.Http.Filter.User.NotSelfOrAdminForbid, MessageSelfOrAdminForbid)) + { StatusCode = StatusCodes.Status403Forbidden }; + } + } + else + { + logger.LogError(LogSelfOrAdminUsernameNotString); + } + } + else + { + logger.LogError(LogSelfOrAdminNoUsername); + } + } + } + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] public class CatchUserNotExistExceptionAttribute : ExceptionFilterAttribute { [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "ASP.Net already checked.")] public override void OnException(ExceptionContext context) { - var body = new CommonResponse( - ErrorCodes.Http.Filter.User.NotExist, - Resources.Filters.MessageUserNotExist); + if (context.Exception is UserNotExistException) + { + var body = new CommonResponse(ErrorCodes.Http.Filter.User.NotExist, MessageUserNotExist); - if (context.HttpContext.Request.Method == "GET") - context.Result = new NotFoundObjectResult(body); - else - context.Result = new BadRequestObjectResult(body); + if (context.HttpContext.Request.Method == "GET") + context.Result = new NotFoundObjectResult(body); + else + context.Result = new BadRequestObjectResult(body); + } } } } diff --git a/Timeline/Formatters/StringInputFormatter.cs b/Timeline/Formatters/StringInputFormatter.cs new file mode 100644 index 00000000..90847e36 --- /dev/null +++ b/Timeline/Formatters/StringInputFormatter.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.Net.Http.Headers; +using System.IO; +using System.Net.Mime; +using System.Text; +using System.Threading.Tasks; + +namespace Timeline.Formatters +{ + public class StringInputFormatter : TextInputFormatter + { + public StringInputFormatter() + { + SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse(MediaTypeNames.Text.Plain)); + SupportedEncodings.Add(Encoding.UTF8); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods")] + public override async Task ReadRequestBodyAsync(InputFormatterContext context, Encoding effectiveEncoding) + { + var request = context.HttpContext.Request; + using var reader = new StreamReader(request.Body, effectiveEncoding); + var stringContent = await reader.ReadToEndAsync(); + return await InputFormatterResult.SuccessAsync(stringContent); + } + } +} diff --git a/Timeline/Resources/Filters.Designer.cs b/Timeline/Resources/Filters.Designer.cs index e3c8be41..3481e4ae 100644 --- a/Timeline/Resources/Filters.Designer.cs +++ b/Timeline/Resources/Filters.Designer.cs @@ -60,6 +60,33 @@ namespace Timeline.Resources { } } + /// + /// Looks up a localized string similar to You apply a SelfOrAdminAttribute on an action, but there is no user. Try add AuthorizeAttribute.. + /// + internal static string LogSelfOrAdminNoUser { + get { + return ResourceManager.GetString("LogSelfOrAdminNoUser", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You apply a SelfOrAdminAttribute on an action, but it does not have a model named username.. + /// + internal static string LogSelfOrAdminNoUsername { + get { + return ResourceManager.GetString("LogSelfOrAdminNoUsername", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You apply a SelfOrAdminAttribute on an action, found a model named username, but it is not string.. + /// + internal static string LogSelfOrAdminUsernameNotString { + get { + return ResourceManager.GetString("LogSelfOrAdminUsernameNotString", resourceCulture); + } + } + /// /// Looks up a localized string similar to Header Content-Length is missing or of bad format.. /// @@ -87,6 +114,15 @@ namespace Timeline.Resources { } } + /// + /// Looks up a localized string similar to You can't access the resource unless you are the owner or administrator.. + /// + internal static string MessageSelfOrAdminForbid { + get { + return ResourceManager.GetString("MessageSelfOrAdminForbid", resourceCulture); + } + } + /// /// Looks up a localized string similar to The user does not exist.. /// diff --git a/Timeline/Resources/Filters.resx b/Timeline/Resources/Filters.resx index ba1fcee8..b91d4612 100644 --- a/Timeline/Resources/Filters.resx +++ b/Timeline/Resources/Filters.resx @@ -117,6 +117,15 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + You apply a SelfOrAdminAttribute on an action, but there is no user. Try add AuthorizeAttribute. + + + You apply a SelfOrAdminAttribute on an action, but it does not have a model named username. + + + You apply a SelfOrAdminAttribute on an action, found a model named username, but it is not string. + Header Content-Length is missing or of bad format. @@ -126,6 +135,9 @@ Header Content-Type is required. + + You can't access the resource unless you are the owner or administrator. + The user does not exist. diff --git a/Timeline/Resources/Filters.zh.resx b/Timeline/Resources/Filters.zh.resx index 690a3e39..159ac04a 100644 --- a/Timeline/Resources/Filters.zh.resx +++ b/Timeline/Resources/Filters.zh.resx @@ -126,6 +126,9 @@ 缺少必需的请求头Content-Type。 + + 你无权访问该资源除非你是资源的拥有者或者管理员。 + 用户不存在。 diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index b44add6f..f6abf36d 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -8,9 +8,10 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using System.Collections.Generic; using System.Globalization; -using Timeline.Authentication; +using Timeline.Auth; using Timeline.Configs; using Timeline.Entities; +using Timeline.Formatters; using Timeline.Helpers; using Timeline.Services; @@ -31,17 +32,22 @@ namespace Timeline // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { - services.AddControllers() + services.AddControllers(setup => + { + setup.InputFormatters.Add(new StringInputFormatter()); + }) .ConfigureApiBehaviorOptions(options => { options.InvalidModelStateResponseFactory = InvalidModelResponseFactory.Factory; }) - .AddNewtonsoftJson(); + .AddNewtonsoftJson(); // TODO: Remove this. services.Configure(Configuration.GetSection(nameof(JwtConfig))); var jwtConfig = Configuration.GetSection(nameof(JwtConfig)).Get(); - services.AddAuthentication(AuthConstants.Scheme) - .AddScheme(AuthConstants.Scheme, AuthConstants.DisplayName, o => { }); + services.AddAuthentication(AuthenticationConstants.Scheme) + .AddScheme(AuthenticationConstants.Scheme, AuthenticationConstants.DisplayName, o => { }); + services.AddAuthorization(); + var corsConfig = Configuration.GetSection("Cors").Get(); services.AddCors(setup => @@ -62,8 +68,8 @@ namespace Timeline services.AddScoped(); services.AddTransient(); services.AddTransient(); - services.AddUserAvatarService(); + services.AddScoped(); var databaseConfig = Configuration.GetSection(nameof(DatabaseConfig)).Get(); -- cgit v1.2.3 From 17822cbf9b7e2dcc4ae71fd8c2caa6bec40adc5c Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Thu, 31 Oct 2019 14:15:37 +0800 Subject: Complete integrated tests. --- Timeline.Tests/Helpers/ResponseAssertions.cs | 27 ++++---- Timeline.Tests/IntegratedTests/TokenTest.cs | 12 ++-- Timeline.Tests/IntegratedTests/UserAvatarTest.cs | 30 ++++----- Timeline.Tests/IntegratedTests/UserDetailTest.cs | 81 ++++++++++++++++++++++++ Timeline.Tests/IntegratedTests/UserTest.cs | 16 ++--- 5 files changed, 121 insertions(+), 45 deletions(-) diff --git a/Timeline.Tests/Helpers/ResponseAssertions.cs b/Timeline.Tests/Helpers/ResponseAssertions.cs index db86ff59..0e6f215b 100644 --- a/Timeline.Tests/Helpers/ResponseAssertions.cs +++ b/Timeline.Tests/Helpers/ResponseAssertions.cs @@ -61,20 +61,20 @@ namespace Timeline.Tests.Helpers protected override string Identifier => "HttpResponseMessage"; - public AndConstraint HaveStatusCode(int expected, string because = "", params object[] becauseArgs) + public AndConstraint HaveStatusCode(int expected, string because = "", params object[] becauseArgs) { return HaveStatusCode((HttpStatusCode)expected, because, becauseArgs); } - public AndConstraint HaveStatusCode(HttpStatusCode expected, string because = "", params object[] becauseArgs) + public AndConstraint HaveStatusCode(HttpStatusCode expected, string because = "", params object[] becauseArgs) { Execute.Assertion.BecauseOf(because, becauseArgs) .ForCondition(Subject.StatusCode == expected) .FailWith("Expected status code of {context:HttpResponseMessage} to be {0}{reason}, but found {1}.", expected, Subject.StatusCode); - return new AndConstraint(Subject); + return new AndConstraint(this); } - public AndWhichConstraint HaveJsonBody(string because = "", params object[] becauseArgs) + public AndWhichConstraint HaveJsonBody(string because = "", params object[] becauseArgs) { var a = Execute.Assertion.BecauseOf(because, becauseArgs); string body; @@ -85,16 +85,11 @@ namespace Timeline.Tests.Helpers catch (AggregateException e) { a.FailWith("Expected response body of {context:HttpResponseMessage} to be json string{reason}, but failed to read it or it was not a string. Exception is {0}.", e.InnerExceptions); - return new AndWhichConstraint(Subject, null); + return new AndWhichConstraint(this, null); } var result = JsonConvert.DeserializeObject(body); - return new AndWhichConstraint(Subject, result); - } - - internal void HaveStatusCode(object statusCode) - { - throw new NotImplementedException(); + return new AndWhichConstraint(this, result); } } @@ -105,12 +100,12 @@ namespace Timeline.Tests.Helpers return new HttpResponseMessageAssertions(instance); } - public static AndWhichConstraint HaveCommonBody(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) + public static AndWhichConstraint HaveCommonBody(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) { return assertions.HaveJsonBody(because, becauseArgs); } - public static AndWhichConstraint> HaveCommonDataBody(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) + public static AndWhichConstraint> HaveCommonDataBody(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) { return assertions.HaveJsonBody>(because, becauseArgs); } @@ -118,7 +113,7 @@ namespace Timeline.Tests.Helpers public static void BePut(this HttpResponseMessageAssertions assertions, bool create, string because = "", params object[] becauseArgs) { var body = assertions.HaveStatusCode(create ? 201 : 200, because, becauseArgs) - .And.Should().HaveJsonBody(because, becauseArgs) + .And.HaveJsonBody(because, becauseArgs) .Which; body.Code.Should().Be(0); body.Data.Create.Should().Be(create); @@ -127,7 +122,7 @@ namespace Timeline.Tests.Helpers public static void BeDelete(this HttpResponseMessageAssertions assertions, bool delete, string because = "", params object[] becauseArgs) { var body = assertions.HaveStatusCode(200, because, becauseArgs) - .And.Should().HaveJsonBody(because, becauseArgs) + .And.HaveJsonBody(because, becauseArgs) .Which; body.Code.Should().Be(0); body.Data.Delete.Should().Be(delete); @@ -137,7 +132,7 @@ namespace Timeline.Tests.Helpers { message = string.IsNullOrEmpty(message) ? "" : ", " + message; assertions.HaveStatusCode(400, "Invalid Model Error must have 400 status code{0}", message) - .And.Should().HaveCommonBody("Invalid Model Error must have CommonResponse body{0}", message) + .And.HaveCommonBody("Invalid Model Error must have CommonResponse body{0}", message) .Which.Code.Should().Be(ErrorCodes.Http.Common.InvalidModel, "Invalid Model Error must have code {0} in body{1}", ErrorCodes.Http.Common.InvalidModel, message); diff --git a/Timeline.Tests/IntegratedTests/TokenTest.cs b/Timeline.Tests/IntegratedTests/TokenTest.cs index e9b6e1e9..111e8d8e 100644 --- a/Timeline.Tests/IntegratedTests/TokenTest.cs +++ b/Timeline.Tests/IntegratedTests/TokenTest.cs @@ -69,7 +69,7 @@ namespace Timeline.Tests.IntegratedTests var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = username, Password = password }); response.Should().HaveStatusCode(400) - .And.Should().HaveCommonBody() + .And.HaveCommonBody() .Which.Code.Should().Be(Create.BadCredential); } @@ -80,7 +80,7 @@ namespace Timeline.Tests.IntegratedTests var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = MockUser.User.Username, Password = MockUser.User.Password }); var body = response.Should().HaveStatusCode(200) - .And.Should().HaveJsonBody().Which; + .And.HaveJsonBody().Which; body.Token.Should().NotBeNullOrWhiteSpace(); body.User.Should().BeEquivalentTo(MockUser.User.Info); } @@ -100,7 +100,7 @@ namespace Timeline.Tests.IntegratedTests var response = await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = "bad token hahaha" }); response.Should().HaveStatusCode(400) - .And.Should().HaveCommonBody() + .And.HaveCommonBody() .Which.Code.Should().Be(Verify.BadFormat); } @@ -120,7 +120,7 @@ namespace Timeline.Tests.IntegratedTests (await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = token })) .Should().HaveStatusCode(400) - .And.Should().HaveCommonBody() + .And.HaveCommonBody() .Which.Code.Should().Be(Verify.OldVersion); } @@ -139,7 +139,7 @@ namespace Timeline.Tests.IntegratedTests (await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = token })) .Should().HaveStatusCode(400) - .And.Should().HaveCommonBody() + .And.HaveCommonBody() .Which.Code.Should().Be(Verify.UserNotExist); } @@ -169,7 +169,7 @@ namespace Timeline.Tests.IntegratedTests var response = await client.PostAsJsonAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = createTokenResult.Token }); response.Should().HaveStatusCode(200) - .And.Should().HaveJsonBody() + .And.HaveJsonBody() .Which.User.Should().BeEquivalentTo(MockUser.User.Info); } } diff --git a/Timeline.Tests/IntegratedTests/UserAvatarTest.cs b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs index b338665e..2310fc66 100644 --- a/Timeline.Tests/IntegratedTests/UserAvatarTest.cs +++ b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs @@ -53,7 +53,7 @@ namespace Timeline.Tests.IntegratedTests { var res = await client.GetAsync("users/usernotexist/avatar"); res.Should().HaveStatusCode(404) - .And.Should().HaveCommonBody() + .And.HaveCommonBody() .Which.Code.Should().Be(Get.UserNotExist); } @@ -94,7 +94,7 @@ namespace Timeline.Tests.IntegratedTests request.Headers.TryAddWithoutValidation("If-None-Match", "\"dsdfd"); var res = await client.SendAsync(request); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.Should().HaveCommonBody().Which.Code.Should().Be(Header.IfNonMatch.BadFormat); + .And.HaveCommonBody().Which.Code.Should().Be(Header.IfNonMatch.BadFormat); } { @@ -124,7 +124,7 @@ namespace Timeline.Tests.IntegratedTests content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); var res = await client.PutAsync("users/user/avatar", content); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.Should().HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Http.Filter.Header.ContentLength.Missing); ; + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Http.Filter.Header.ContentLength.Missing); ; } { @@ -132,7 +132,7 @@ namespace Timeline.Tests.IntegratedTests content.Headers.ContentLength = 1; var res = await client.PutAsync("users/user/avatar", content); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.Should().HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Http.Filter.Header.ContentType.Missing); + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Http.Filter.Header.ContentType.Missing); } { @@ -141,7 +141,7 @@ namespace Timeline.Tests.IntegratedTests content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); var res = await client.PutAsync("users/user/avatar", content); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.Should().HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Http.Filter.Header.ContentLength.Zero); + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Http.Filter.Header.ContentLength.Zero); } { @@ -155,7 +155,7 @@ namespace Timeline.Tests.IntegratedTests content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); var res = await client.PutAsync("users/user/avatar", content); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.Should().HaveCommonBody().Which.Code.Should().Be(Content.TooBig); + .And.HaveCommonBody().Which.Code.Should().Be(Content.TooBig); } { @@ -164,7 +164,7 @@ namespace Timeline.Tests.IntegratedTests content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); var res = await client.PutAsync("users/user/avatar", content); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.Should().HaveCommonBody().Which.Code.Should().Be(Content.UnmatchedLength_Smaller); + .And.HaveCommonBody().Which.Code.Should().Be(Content.UnmatchedLength_Smaller); } { @@ -173,25 +173,25 @@ namespace Timeline.Tests.IntegratedTests content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); var res = await client.PutAsync("users/user/avatar", content); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.Should().HaveCommonBody().Which.Code.Should().Be(Content.UnmatchedLength_Bigger); + .And.HaveCommonBody().Which.Code.Should().Be(Content.UnmatchedLength_Bigger); } { var res = await client.PutByteArrayAsync("users/user/avatar", new[] { (byte)0x00 }, "image/png"); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.Should().HaveCommonBody().Which.Code.Should().Be(Put.BadFormat_CantDecode); + .And.HaveCommonBody().Which.Code.Should().Be(Put.BadFormat_CantDecode); } { var res = await client.PutByteArrayAsync("users/user/avatar", mockAvatar.Data, "image/jpeg"); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.Should().HaveCommonBody().Which.Code.Should().Be(Put.BadFormat_UnmatchedFormat); + .And.HaveCommonBody().Which.Code.Should().Be(Put.BadFormat_UnmatchedFormat); } { var res = await client.PutByteArrayAsync("users/user/avatar", ImageHelper.CreatePngWithSize(100, 200), "image/png"); res.Should().HaveStatusCode(HttpStatusCode.BadRequest) - .And.Should().HaveCommonBody().Which.Code.Should().Be(Put.BadFormat_BadSize); + .And.HaveCommonBody().Which.Code.Should().Be(Put.BadFormat_BadSize); } { @@ -221,13 +221,13 @@ namespace Timeline.Tests.IntegratedTests { var res = await client.PutByteArrayAsync("users/admin/avatar", new[] { (byte)0x00 }, "image/png"); res.Should().HaveStatusCode(HttpStatusCode.Forbidden) - .And.Should().HaveCommonBody().Which.Code.Should().Be(Put.Forbid); + .And.HaveCommonBody().Which.Code.Should().Be(Put.Forbid); } { var res = await client.DeleteAsync("users/admin/avatar"); res.Should().HaveStatusCode(HttpStatusCode.Forbidden) - .And.Should().HaveCommonBody().Which.Code.Should().Be(Delete.Forbid); + .And.HaveCommonBody().Which.Code.Should().Be(Delete.Forbid); } for (int i = 0; i < 2; i++) // double delete should work. @@ -254,14 +254,14 @@ namespace Timeline.Tests.IntegratedTests { var res = await client.PutByteArrayAsync("users/usernotexist/avatar", new[] { (byte)0x00 }, "image/png"); res.Should().HaveStatusCode(400) - .And.Should().HaveCommonBody() + .And.HaveCommonBody() .Which.Code.Should().Be(Put.UserNotExist); } { var res = await client.DeleteAsync("users/usernotexist/avatar"); res.Should().HaveStatusCode(400) - .And.Should().HaveCommonBody().Which.Code.Should().Be(Delete.UserNotExist); + .And.HaveCommonBody().Which.Code.Should().Be(Delete.UserNotExist); } } diff --git a/Timeline.Tests/IntegratedTests/UserDetailTest.cs b/Timeline.Tests/IntegratedTests/UserDetailTest.cs index ff2c03a5..8f2b6925 100644 --- a/Timeline.Tests/IntegratedTests/UserDetailTest.cs +++ b/Timeline.Tests/IntegratedTests/UserDetailTest.cs @@ -2,6 +2,8 @@ using Microsoft.AspNetCore.Mvc.Testing; using System; using System.Net; +using System.Net.Http.Headers; +using System.Net.Mime; using System.Threading.Tasks; using Timeline.Tests.Helpers; using Timeline.Tests.Helpers.Authentication; @@ -79,5 +81,84 @@ namespace Timeline.Tests.IntegratedTests } } } + + [Fact] + public async Task FunctionTest() + { + var url = $"users/{MockUser.User.Username}/nickname"; + var userNotExistUrl = "users/usernotexist/nickname"; + { + using var client = await _factory.CreateClientAsUser(); + { + var res = await client.GetAsync(userNotExistUrl); + res.Should().HaveStatusCode(HttpStatusCode.NotFound) + .And.HaveCommonBody() + .Which.Code.Should().Be(ErrorCodes.Http.Filter.User.NotExist); + + } + { + var res = await client.GetAsync(url); + res.Should().HaveStatusCode(HttpStatusCode.OK); + res.Content.Headers.ContentType.Should().Be(new MediaTypeHeaderValue(MediaTypeNames.Text.Plain) { CharSet = "utf-8" }); + var body = await res.Content.ReadAsStringAsync(); + body.Should().Be(MockUser.User.Username); + } + { + var res = await client.PutStringAsync(url, ""); + res.Should().BeInvalidModel(); + } + { + var res = await client.PutStringAsync(url, new string('a', 11)); + res.Should().BeInvalidModel(); + } + var nickname1 = "nnn"; + var nickname2 = "nn2"; + { + var res = await client.PutStringAsync(url, nickname1); + res.Should().HaveStatusCode(HttpStatusCode.OK); + (await client.GetStringAsync(url)).Should().Be(nickname1); + } + { + var res = await client.PutStringAsync(url, nickname2); + res.Should().HaveStatusCode(HttpStatusCode.OK); + (await client.GetStringAsync(url)).Should().Be(nickname2); + } + { + var res = await client.DeleteAsync(url); + res.Should().HaveStatusCode(HttpStatusCode.OK); + (await client.GetStringAsync(url)).Should().Be(MockUser.User.Username); + } + { + var res = await client.DeleteAsync(url); + res.Should().HaveStatusCode(HttpStatusCode.OK); + } + } + { + using var client = await _factory.CreateClientAsAdmin(); + { + var res = await client.PutStringAsync(userNotExistUrl, "aaa"); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.HaveCommonBody() + .Which.Code.Should().Be(ErrorCodes.Http.Filter.User.NotExist); + } + { + var res = await client.DeleteAsync(userNotExistUrl); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.HaveCommonBody() + .Which.Code.Should().Be(ErrorCodes.Http.Filter.User.NotExist); + } + var nickname = "nnn"; + { + var res = await client.PutStringAsync(url, nickname); + res.Should().HaveStatusCode(HttpStatusCode.OK); + (await client.GetStringAsync(url)).Should().Be(nickname); + } + { + var res = await client.DeleteAsync(url); + res.Should().HaveStatusCode(HttpStatusCode.OK); + (await client.GetStringAsync(url)).Should().Be(MockUser.User.Username); + } + } + } } } diff --git a/Timeline.Tests/IntegratedTests/UserTest.cs b/Timeline.Tests/IntegratedTests/UserTest.cs index ec70b7e8..7e99ddba 100644 --- a/Timeline.Tests/IntegratedTests/UserTest.cs +++ b/Timeline.Tests/IntegratedTests/UserTest.cs @@ -36,7 +36,7 @@ namespace Timeline.Tests.IntegratedTests using var client = await _factory.CreateClientAsAdmin(); var res = await client.GetAsync("users"); res.Should().HaveStatusCode(200) - .And.Should().HaveJsonBody() + .And.HaveJsonBody() .Which.Should().BeEquivalentTo(MockUser.UserInfoList); } @@ -46,7 +46,7 @@ namespace Timeline.Tests.IntegratedTests using var client = await _factory.CreateClientAsAdmin(); var res = await client.GetAsync("users/" + MockUser.User.Username); res.Should().HaveStatusCode(200) - .And.Should().HaveJsonBody() + .And.HaveJsonBody() .Which.Should().BeEquivalentTo(MockUser.User.Info); } @@ -64,7 +64,7 @@ namespace Timeline.Tests.IntegratedTests using var client = await _factory.CreateClientAsAdmin(); var res = await client.GetAsync("users/usernotexist"); res.Should().HaveStatusCode(404) - .And.Should().HaveCommonBody() + .And.HaveCommonBody() .Which.Code.Should().Be(Get.NotExist); } @@ -89,7 +89,7 @@ namespace Timeline.Tests.IntegratedTests { var res = await client.GetAsync("users/" + username); res.Should().HaveStatusCode(200) - .And.Should().HaveJsonBody() + .And.HaveJsonBody() .Which.Administrator.Should().Be(administrator); } @@ -128,7 +128,7 @@ namespace Timeline.Tests.IntegratedTests using var client = await _factory.CreateClientAsAdmin(); var res = await client.PatchAsJsonAsync("users/usernotexist", new UserPatchRequest { }); res.Should().HaveStatusCode(404) - .And.Should().HaveCommonBody() + .And.HaveCommonBody() .Which.Code.Should().Be(Patch.NotExist); } @@ -208,7 +208,7 @@ namespace Timeline.Tests.IntegratedTests var res = await client.PostAsJsonAsync(changeUsernameUrl, new ChangeUsernameRequest { OldUsername = "usernotexist", NewUsername = "newUsername" }); res.Should().HaveStatusCode(400) - .And.Should().HaveCommonBody() + .And.HaveCommonBody() .Which.Code.Should().Be(Op.ChangeUsername.NotExist); } @@ -219,7 +219,7 @@ namespace Timeline.Tests.IntegratedTests var res = await client.PostAsJsonAsync(changeUsernameUrl, new ChangeUsernameRequest { OldUsername = MockUser.User.Username, NewUsername = MockUser.Admin.Username }); res.Should().HaveStatusCode(400) - .And.Should().HaveCommonBody() + .And.HaveCommonBody() .Which.Code.Should().Be(Op.ChangeUsername.AlreadyExist); } @@ -258,7 +258,7 @@ namespace Timeline.Tests.IntegratedTests using var client = await _factory.CreateClientAsUser(); var res = await client.PostAsJsonAsync(changePasswordUrl, new ChangePasswordRequest { OldPassword = "???", NewPassword = "???" }); res.Should().HaveStatusCode(400) - .And.Should().HaveCommonBody() + .And.HaveCommonBody() .Which.Code.Should().Be(Op.ChangePassword.BadOldPassword); } -- cgit v1.2.3 From 2c3744ab5db476b64a32c19b50153e3e6166b0e6 Mon Sep 17 00:00:00 2001 From: 杨宇千 Date: Thu, 31 Oct 2019 14:58:36 +0800 Subject: Recreate database and migrations. --- Timeline.Tests/PasswordGenerator.cs | 24 ++++ .../20190412102517_InitCreate.Designer.cs | 43 ------- Timeline/Migrations/20190412102517_InitCreate.cs | 32 ----- .../20190412144150_AddAdminUser.Designer.cs | 43 ------- Timeline/Migrations/20190412144150_AddAdminUser.cs | 19 --- ...0412153003_MakeColumnsInUserNotNull.Designer.cs | 46 ------- .../20190412153003_MakeColumnsInUserNotNull.cs | 52 -------- .../20190719115321_Add-User-Version.Designer.cs | 49 -------- .../Migrations/20190719115321_Add-User-Version.cs | 23 ---- .../Migrations/20190817094408_Enhance1.Designer.cs | 52 -------- Timeline/Migrations/20190817094408_Enhance1.cs | 41 ------ ...190817094602_RenameTableUserToUsers.Designer.cs | 52 -------- .../20190817094602_RenameTableUserToUsers.cs | 39 ------ .../20190818174505_AddUserAvatar.Designer.cs | 83 ------------- .../Migrations/20190818174505_AddUserAvatar.cs | 62 ---------- ...0190819074906_AddAvatarLastModified.Designer.cs | 86 ------------- .../20190819074906_AddAvatarLastModified.cs | 114 ----------------- .../20190819080823_AddIndexForUserName.Designer.cs | 88 ------------- .../20190819080823_AddIndexForUserName.cs | 22 ---- .../20190820155221_AddAvatarETag.Designer.cs | 92 -------------- .../Migrations/20190820155221_AddAvatarETag.cs | 23 ---- ...90820155354_MakeUserNameIndexUnique.Designer.cs | 93 -------------- .../20190820155354_MakeUserNameIndexUnique.cs | 32 ----- .../20191031064541_Initialize.Designer.cs | 137 +++++++++++++++++++++ Timeline/Migrations/20191031064541_Initialize.cs | 104 ++++++++++++++++ .../Migrations/DatabaseContextModelSnapshot.cs | 69 ++++++----- 26 files changed, 300 insertions(+), 1220 deletions(-) create mode 100644 Timeline.Tests/PasswordGenerator.cs delete mode 100644 Timeline/Migrations/20190412102517_InitCreate.Designer.cs delete mode 100644 Timeline/Migrations/20190412102517_InitCreate.cs delete mode 100644 Timeline/Migrations/20190412144150_AddAdminUser.Designer.cs delete mode 100644 Timeline/Migrations/20190412144150_AddAdminUser.cs delete mode 100644 Timeline/Migrations/20190412153003_MakeColumnsInUserNotNull.Designer.cs delete mode 100644 Timeline/Migrations/20190412153003_MakeColumnsInUserNotNull.cs delete mode 100644 Timeline/Migrations/20190719115321_Add-User-Version.Designer.cs delete mode 100644 Timeline/Migrations/20190719115321_Add-User-Version.cs delete mode 100644 Timeline/Migrations/20190817094408_Enhance1.Designer.cs delete mode 100644 Timeline/Migrations/20190817094408_Enhance1.cs delete mode 100644 Timeline/Migrations/20190817094602_RenameTableUserToUsers.Designer.cs delete mode 100644 Timeline/Migrations/20190817094602_RenameTableUserToUsers.cs delete mode 100644 Timeline/Migrations/20190818174505_AddUserAvatar.Designer.cs delete mode 100644 Timeline/Migrations/20190818174505_AddUserAvatar.cs delete mode 100644 Timeline/Migrations/20190819074906_AddAvatarLastModified.Designer.cs delete mode 100644 Timeline/Migrations/20190819074906_AddAvatarLastModified.cs delete mode 100644 Timeline/Migrations/20190819080823_AddIndexForUserName.Designer.cs delete mode 100644 Timeline/Migrations/20190819080823_AddIndexForUserName.cs delete mode 100644 Timeline/Migrations/20190820155221_AddAvatarETag.Designer.cs delete mode 100644 Timeline/Migrations/20190820155221_AddAvatarETag.cs delete mode 100644 Timeline/Migrations/20190820155354_MakeUserNameIndexUnique.Designer.cs delete mode 100644 Timeline/Migrations/20190820155354_MakeUserNameIndexUnique.cs create mode 100644 Timeline/Migrations/20191031064541_Initialize.Designer.cs create mode 100644 Timeline/Migrations/20191031064541_Initialize.cs diff --git a/Timeline.Tests/PasswordGenerator.cs b/Timeline.Tests/PasswordGenerator.cs new file mode 100644 index 00000000..6c07836b --- /dev/null +++ b/Timeline.Tests/PasswordGenerator.cs @@ -0,0 +1,24 @@ +using System; +using Timeline.Services; +using Xunit; +using Xunit.Abstractions; + +namespace Timeline.Tests +{ + public class PasswordGenerator + { + private readonly ITestOutputHelper _output; + + public PasswordGenerator(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void Generate() + { + var service = new PasswordService(); + _output.WriteLine(service.HashPassword("crupest")); + } + } +} diff --git a/Timeline/Migrations/20190412102517_InitCreate.Designer.cs b/Timeline/Migrations/20190412102517_InitCreate.Designer.cs deleted file mode 100644 index 86a46720..00000000 --- a/Timeline/Migrations/20190412102517_InitCreate.Designer.cs +++ /dev/null @@ -1,43 +0,0 @@ -// -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Timeline.Entities; - -namespace Timeline.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20190412102517_InitCreate")] - partial class InitCreate - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.3-servicing-35854") - .HasAnnotation("Relational:MaxIdentifierLength", 64); - - modelBuilder.Entity("Timeline.Models.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id"); - - b.Property("EncryptedPassword") - .HasColumnName("password"); - - b.Property("Name") - .HasColumnName("name"); - - b.Property("RoleString") - .HasColumnName("roles"); - - b.HasKey("Id"); - - b.ToTable("user"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Timeline/Migrations/20190412102517_InitCreate.cs b/Timeline/Migrations/20190412102517_InitCreate.cs deleted file mode 100644 index 55930f6d..00000000 --- a/Timeline/Migrations/20190412102517_InitCreate.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Timeline.Migrations -{ - public partial class InitCreate : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "user", - columns: table => new - { - id = table.Column(nullable: false) - .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), - name = table.Column(nullable: true), - password = table.Column(nullable: true), - roles = table.Column(nullable: true) - }, - constraints: table => - { - table.PrimaryKey("PK_user", x => x.id); - }); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "user"); - } - } -} diff --git a/Timeline/Migrations/20190412144150_AddAdminUser.Designer.cs b/Timeline/Migrations/20190412144150_AddAdminUser.Designer.cs deleted file mode 100644 index 2e6b5f74..00000000 --- a/Timeline/Migrations/20190412144150_AddAdminUser.Designer.cs +++ /dev/null @@ -1,43 +0,0 @@ -// -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Timeline.Entities; - -namespace Timeline.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20190412144150_AddAdminUser")] - partial class AddAdminUser - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.3-servicing-35854") - .HasAnnotation("Relational:MaxIdentifierLength", 64); - - modelBuilder.Entity("Timeline.Models.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id"); - - b.Property("EncryptedPassword") - .HasColumnName("password"); - - b.Property("Name") - .HasColumnName("name"); - - b.Property("RoleString") - .HasColumnName("roles"); - - b.HasKey("Id"); - - b.ToTable("user"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Timeline/Migrations/20190412144150_AddAdminUser.cs b/Timeline/Migrations/20190412144150_AddAdminUser.cs deleted file mode 100644 index b6760f3e..00000000 --- a/Timeline/Migrations/20190412144150_AddAdminUser.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; -using Timeline.Services; - -namespace Timeline.Migrations -{ - public partial class AddAdminUser : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.InsertData("user", new string[] { "name", "password", "roles" }, - new string[] { "crupest", new PasswordService().HashPassword("yang0101"), "user,admin" }); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DeleteData("user", "name", "crupest"); - } - } -} diff --git a/Timeline/Migrations/20190412153003_MakeColumnsInUserNotNull.Designer.cs b/Timeline/Migrations/20190412153003_MakeColumnsInUserNotNull.Designer.cs deleted file mode 100644 index 263efb8a..00000000 --- a/Timeline/Migrations/20190412153003_MakeColumnsInUserNotNull.Designer.cs +++ /dev/null @@ -1,46 +0,0 @@ -// -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Timeline.Entities; - -namespace Timeline.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20190412153003_MakeColumnsInUserNotNull")] - partial class MakeColumnsInUserNotNull - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.3-servicing-35854") - .HasAnnotation("Relational:MaxIdentifierLength", 64); - - modelBuilder.Entity("Timeline.Models.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id"); - - b.Property("EncryptedPassword") - .IsRequired() - .HasColumnName("password"); - - b.Property("Name") - .IsRequired() - .HasColumnName("name"); - - b.Property("RoleString") - .IsRequired() - .HasColumnName("roles"); - - b.HasKey("Id"); - - b.ToTable("user"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Timeline/Migrations/20190412153003_MakeColumnsInUserNotNull.cs b/Timeline/Migrations/20190412153003_MakeColumnsInUserNotNull.cs deleted file mode 100644 index 12053906..00000000 --- a/Timeline/Migrations/20190412153003_MakeColumnsInUserNotNull.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Timeline.Migrations -{ - public partial class MakeColumnsInUserNotNull : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "roles", - table: "user", - nullable: false, - oldClrType: typeof(string), - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "name", - table: "user", - nullable: false, - oldClrType: typeof(string), - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "password", - table: "user", - nullable: false, - oldClrType: typeof(string), - oldNullable: true); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "roles", - table: "user", - nullable: true, - oldClrType: typeof(string)); - - migrationBuilder.AlterColumn( - name: "name", - table: "user", - nullable: true, - oldClrType: typeof(string)); - - migrationBuilder.AlterColumn( - name: "password", - table: "user", - nullable: true, - oldClrType: typeof(string)); - } - } -} diff --git a/Timeline/Migrations/20190719115321_Add-User-Version.Designer.cs b/Timeline/Migrations/20190719115321_Add-User-Version.Designer.cs deleted file mode 100644 index d61259b5..00000000 --- a/Timeline/Migrations/20190719115321_Add-User-Version.Designer.cs +++ /dev/null @@ -1,49 +0,0 @@ -// -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Timeline.Entities; - -namespace Timeline.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20190719115321_Add-User-Version")] - partial class AddUserVersion - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") - .HasAnnotation("Relational:MaxIdentifierLength", 64); - - modelBuilder.Entity("Timeline.Models.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id"); - - b.Property("EncryptedPassword") - .IsRequired() - .HasColumnName("password"); - - b.Property("Name") - .IsRequired() - .HasColumnName("name"); - - b.Property("RoleString") - .IsRequired() - .HasColumnName("roles"); - - b.Property("Version") - .HasColumnName("version"); - - b.HasKey("Id"); - - b.ToTable("user"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Timeline/Migrations/20190719115321_Add-User-Version.cs b/Timeline/Migrations/20190719115321_Add-User-Version.cs deleted file mode 100644 index 4d6bd60a..00000000 --- a/Timeline/Migrations/20190719115321_Add-User-Version.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Timeline.Migrations -{ - public partial class AddUserVersion : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "version", - table: "user", - nullable: false, - defaultValue: 0L); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "version", - table: "user"); - } - } -} diff --git a/Timeline/Migrations/20190817094408_Enhance1.Designer.cs b/Timeline/Migrations/20190817094408_Enhance1.Designer.cs deleted file mode 100644 index 89d159dd..00000000 --- a/Timeline/Migrations/20190817094408_Enhance1.Designer.cs +++ /dev/null @@ -1,52 +0,0 @@ -// -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Timeline.Entities; - -namespace Timeline.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20190817094408_Enhance1")] - partial class Enhance1 - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") - .HasAnnotation("Relational:MaxIdentifierLength", 64); - - modelBuilder.Entity("Timeline.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id"); - - b.Property("EncryptedPassword") - .IsRequired() - .HasColumnName("password"); - - b.Property("Name") - .IsRequired() - .HasColumnName("name") - .HasMaxLength(26); - - b.Property("RoleString") - .IsRequired() - .HasColumnName("roles"); - - b.Property("Version") - .ValueGeneratedOnAdd() - .HasColumnName("version") - .HasDefaultValue(0L); - - b.HasKey("Id"); - - b.ToTable("user"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Timeline/Migrations/20190817094408_Enhance1.cs b/Timeline/Migrations/20190817094408_Enhance1.cs deleted file mode 100644 index 0eae3ef9..00000000 --- a/Timeline/Migrations/20190817094408_Enhance1.cs +++ /dev/null @@ -1,41 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Timeline.Migrations -{ - public partial class Enhance1 : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "version", - table: "user", - nullable: false, - defaultValue: 0L, - oldClrType: typeof(long)); - - migrationBuilder.AlterColumn( - name: "name", - table: "user", - maxLength: 26, - nullable: false, - oldClrType: typeof(string)); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.AlterColumn( - name: "version", - table: "user", - nullable: false, - oldClrType: typeof(long), - oldDefaultValue: 0L); - - migrationBuilder.AlterColumn( - name: "name", - table: "user", - nullable: false, - oldClrType: typeof(string), - oldMaxLength: 26); - } - } -} diff --git a/Timeline/Migrations/20190817094602_RenameTableUserToUsers.Designer.cs b/Timeline/Migrations/20190817094602_RenameTableUserToUsers.Designer.cs deleted file mode 100644 index 6ad4d475..00000000 --- a/Timeline/Migrations/20190817094602_RenameTableUserToUsers.Designer.cs +++ /dev/null @@ -1,52 +0,0 @@ -// -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Timeline.Entities; - -namespace Timeline.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20190817094602_RenameTableUserToUsers")] - partial class RenameTableUserToUsers - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") - .HasAnnotation("Relational:MaxIdentifierLength", 64); - - modelBuilder.Entity("Timeline.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id"); - - b.Property("EncryptedPassword") - .IsRequired() - .HasColumnName("password"); - - b.Property("Name") - .IsRequired() - .HasColumnName("name") - .HasMaxLength(26); - - b.Property("RoleString") - .IsRequired() - .HasColumnName("roles"); - - b.Property("Version") - .ValueGeneratedOnAdd() - .HasColumnName("version") - .HasDefaultValue(0L); - - b.HasKey("Id"); - - b.ToTable("users"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Timeline/Migrations/20190817094602_RenameTableUserToUsers.cs b/Timeline/Migrations/20190817094602_RenameTableUserToUsers.cs deleted file mode 100644 index 042096eb..00000000 --- a/Timeline/Migrations/20190817094602_RenameTableUserToUsers.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Timeline.Migrations -{ - public partial class RenameTableUserToUsers : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropPrimaryKey( - name: "PK_user", - table: "user"); - - migrationBuilder.RenameTable( - name: "user", - newName: "users"); - - migrationBuilder.AddPrimaryKey( - name: "PK_users", - table: "users", - column: "id"); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropPrimaryKey( - name: "PK_users", - table: "users"); - - migrationBuilder.RenameTable( - name: "users", - newName: "user"); - - migrationBuilder.AddPrimaryKey( - name: "PK_user", - table: "user", - column: "id"); - } - } -} diff --git a/Timeline/Migrations/20190818174505_AddUserAvatar.Designer.cs b/Timeline/Migrations/20190818174505_AddUserAvatar.Designer.cs deleted file mode 100644 index b0a105d0..00000000 --- a/Timeline/Migrations/20190818174505_AddUserAvatar.Designer.cs +++ /dev/null @@ -1,83 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Timeline.Entities; - -namespace Timeline.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20190818174505_AddUserAvatar")] - partial class AddUserAvatar - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") - .HasAnnotation("Relational:MaxIdentifierLength", 64); - - modelBuilder.Entity("Timeline.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id"); - - b.Property("AvatarId"); - - b.Property("EncryptedPassword") - .IsRequired() - .HasColumnName("password"); - - b.Property("Name") - .IsRequired() - .HasColumnName("name") - .HasMaxLength(26); - - b.Property("RoleString") - .IsRequired() - .HasColumnName("roles"); - - b.Property("Version") - .ValueGeneratedOnAdd() - .HasColumnName("version") - .HasDefaultValue(0L); - - b.HasKey("Id"); - - b.HasIndex("AvatarId"); - - b.ToTable("users"); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatar", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id"); - - b.Property("Data") - .IsRequired() - .HasColumnName("data"); - - b.Property("Type") - .IsRequired() - .HasColumnName("type"); - - b.HasKey("Id"); - - b.ToTable("user_avatars"); - }); - - modelBuilder.Entity("Timeline.Entities.User", b => - { - b.HasOne("Timeline.Entities.UserAvatar", "Avatar") - .WithMany() - .HasForeignKey("AvatarId"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Timeline/Migrations/20190818174505_AddUserAvatar.cs b/Timeline/Migrations/20190818174505_AddUserAvatar.cs deleted file mode 100644 index 7e0843c4..00000000 --- a/Timeline/Migrations/20190818174505_AddUserAvatar.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Timeline.Migrations -{ - public partial class AddUserAvatar : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "AvatarId", - table: "users", - nullable: true); - - migrationBuilder.CreateTable( - name: "user_avatars", - columns: table => new - { - id = table.Column(nullable: false) - .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), - data = table.Column(nullable: false), - type = table.Column(nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_user_avatars", x => x.id); - }); - - migrationBuilder.CreateIndex( - name: "IX_users_AvatarId", - table: "users", - column: "AvatarId"); - - migrationBuilder.AddForeignKey( - name: "FK_users_user_avatars_AvatarId", - table: "users", - column: "AvatarId", - principalTable: "user_avatars", - principalColumn: "id", - onDelete: ReferentialAction.Restrict); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_users_user_avatars_AvatarId", - table: "users"); - - migrationBuilder.DropTable( - name: "user_avatars"); - - migrationBuilder.DropIndex( - name: "IX_users_AvatarId", - table: "users"); - - migrationBuilder.DropColumn( - name: "AvatarId", - table: "users"); - } - } -} diff --git a/Timeline/Migrations/20190819074906_AddAvatarLastModified.Designer.cs b/Timeline/Migrations/20190819074906_AddAvatarLastModified.Designer.cs deleted file mode 100644 index a6fe7941..00000000 --- a/Timeline/Migrations/20190819074906_AddAvatarLastModified.Designer.cs +++ /dev/null @@ -1,86 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Timeline.Entities; - -namespace Timeline.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20190819074906_AddAvatarLastModified")] - partial class AddAvatarLastModified - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") - .HasAnnotation("Relational:MaxIdentifierLength", 64); - - modelBuilder.Entity("Timeline.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id"); - - b.Property("EncryptedPassword") - .IsRequired() - .HasColumnName("password"); - - b.Property("Name") - .IsRequired() - .HasColumnName("name") - .HasMaxLength(26); - - b.Property("RoleString") - .IsRequired() - .HasColumnName("roles"); - - b.Property("Version") - .ValueGeneratedOnAdd() - .HasColumnName("version") - .HasDefaultValue(0L); - - b.HasKey("Id"); - - b.ToTable("users"); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatar", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id"); - - b.Property("Data") - .HasColumnName("data"); - - b.Property("LastModified") - .HasColumnName("last_modified"); - - b.Property("Type") - .HasColumnName("type"); - - b.Property("UserId"); - - b.HasKey("Id"); - - b.HasIndex("UserId") - .IsUnique(); - - b.ToTable("user_avatars"); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatar", b => - { - b.HasOne("Timeline.Entities.User") - .WithOne("Avatar") - .HasForeignKey("Timeline.Entities.UserAvatar", "UserId") - .OnDelete(DeleteBehavior.Cascade); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Timeline/Migrations/20190819074906_AddAvatarLastModified.cs b/Timeline/Migrations/20190819074906_AddAvatarLastModified.cs deleted file mode 100644 index d9b5e8cf..00000000 --- a/Timeline/Migrations/20190819074906_AddAvatarLastModified.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Timeline.Migrations -{ - public partial class AddAvatarLastModified : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_users_user_avatars_AvatarId", - table: "users"); - - migrationBuilder.DropIndex( - name: "IX_users_AvatarId", - table: "users"); - - migrationBuilder.DropColumn( - name: "AvatarId", - table: "users"); - - migrationBuilder.AlterColumn( - name: "type", - table: "user_avatars", - nullable: true, - oldClrType: typeof(string)); - - migrationBuilder.AlterColumn( - name: "data", - table: "user_avatars", - nullable: true, - oldClrType: typeof(byte[])); - - migrationBuilder.AddColumn( - name: "last_modified", - table: "user_avatars", - nullable: false, - defaultValue: DateTime.Now); - - migrationBuilder.AddColumn( - name: "UserId", - table: "user_avatars", - nullable: false, - defaultValue: 0L); - - migrationBuilder.CreateIndex( - name: "IX_user_avatars_UserId", - table: "user_avatars", - column: "UserId", - unique: true); - - migrationBuilder.AddForeignKey( - name: "FK_user_avatars_users_UserId", - table: "user_avatars", - column: "UserId", - principalTable: "users", - principalColumn: "id", - onDelete: ReferentialAction.Cascade); - - // Note! Remember to manually create avatar entities for all users. - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropForeignKey( - name: "FK_user_avatars_users_UserId", - table: "user_avatars"); - - migrationBuilder.DropIndex( - name: "IX_user_avatars_UserId", - table: "user_avatars"); - - migrationBuilder.DropColumn( - name: "last_modified", - table: "user_avatars"); - - migrationBuilder.DropColumn( - name: "UserId", - table: "user_avatars"); - - migrationBuilder.AddColumn( - name: "AvatarId", - table: "users", - nullable: true); - - migrationBuilder.AlterColumn( - name: "type", - table: "user_avatars", - nullable: false, - oldClrType: typeof(string), - oldNullable: true); - - migrationBuilder.AlterColumn( - name: "data", - table: "user_avatars", - nullable: false, - oldClrType: typeof(byte[]), - oldNullable: true); - - migrationBuilder.CreateIndex( - name: "IX_users_AvatarId", - table: "users", - column: "AvatarId"); - - migrationBuilder.AddForeignKey( - name: "FK_users_user_avatars_AvatarId", - table: "users", - column: "AvatarId", - principalTable: "user_avatars", - principalColumn: "id", - onDelete: ReferentialAction.Restrict); - } - } -} diff --git a/Timeline/Migrations/20190819080823_AddIndexForUserName.Designer.cs b/Timeline/Migrations/20190819080823_AddIndexForUserName.Designer.cs deleted file mode 100644 index d45a057d..00000000 --- a/Timeline/Migrations/20190819080823_AddIndexForUserName.Designer.cs +++ /dev/null @@ -1,88 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Timeline.Entities; - -namespace Timeline.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20190819080823_AddIndexForUserName")] - partial class AddIndexForUserName - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") - .HasAnnotation("Relational:MaxIdentifierLength", 64); - - modelBuilder.Entity("Timeline.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id"); - - b.Property("EncryptedPassword") - .IsRequired() - .HasColumnName("password"); - - b.Property("Name") - .IsRequired() - .HasColumnName("name") - .HasMaxLength(26); - - b.Property("RoleString") - .IsRequired() - .HasColumnName("roles"); - - b.Property("Version") - .ValueGeneratedOnAdd() - .HasColumnName("version") - .HasDefaultValue(0L); - - b.HasKey("Id"); - - b.HasIndex("Name"); - - b.ToTable("users"); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatar", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id"); - - b.Property("Data") - .HasColumnName("data"); - - b.Property("LastModified") - .HasColumnName("last_modified"); - - b.Property("Type") - .HasColumnName("type"); - - b.Property("UserId"); - - b.HasKey("Id"); - - b.HasIndex("UserId") - .IsUnique(); - - b.ToTable("user_avatars"); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatar", b => - { - b.HasOne("Timeline.Entities.User") - .WithOne("Avatar") - .HasForeignKey("Timeline.Entities.UserAvatar", "UserId") - .OnDelete(DeleteBehavior.Cascade); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Timeline/Migrations/20190819080823_AddIndexForUserName.cs b/Timeline/Migrations/20190819080823_AddIndexForUserName.cs deleted file mode 100644 index b910a174..00000000 --- a/Timeline/Migrations/20190819080823_AddIndexForUserName.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Timeline.Migrations -{ - public partial class AddIndexForUserName : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateIndex( - name: "IX_users_name", - table: "users", - column: "name"); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropIndex( - name: "IX_users_name", - table: "users"); - } - } -} diff --git a/Timeline/Migrations/20190820155221_AddAvatarETag.Designer.cs b/Timeline/Migrations/20190820155221_AddAvatarETag.Designer.cs deleted file mode 100644 index e7c7cb2f..00000000 --- a/Timeline/Migrations/20190820155221_AddAvatarETag.Designer.cs +++ /dev/null @@ -1,92 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Timeline.Entities; - -namespace Timeline.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20190820155221_AddAvatarETag")] - partial class AddAvatarETag - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") - .HasAnnotation("Relational:MaxIdentifierLength", 64); - - modelBuilder.Entity("Timeline.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id"); - - b.Property("EncryptedPassword") - .IsRequired() - .HasColumnName("password"); - - b.Property("Name") - .IsRequired() - .HasColumnName("name") - .HasMaxLength(26); - - b.Property("RoleString") - .IsRequired() - .HasColumnName("roles"); - - b.Property("Version") - .ValueGeneratedOnAdd() - .HasColumnName("version") - .HasDefaultValue(0L); - - b.HasKey("Id"); - - b.HasIndex("Name"); - - b.ToTable("users"); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatar", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id"); - - b.Property("Data") - .HasColumnName("data"); - - b.Property("ETag") - .HasColumnName("etag") - .HasMaxLength(30); - - b.Property("LastModified") - .HasColumnName("last_modified"); - - b.Property("Type") - .HasColumnName("type"); - - b.Property("UserId"); - - b.HasKey("Id"); - - b.HasIndex("UserId") - .IsUnique(); - - b.ToTable("user_avatars"); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatar", b => - { - b.HasOne("Timeline.Entities.User") - .WithOne("Avatar") - .HasForeignKey("Timeline.Entities.UserAvatar", "UserId") - .OnDelete(DeleteBehavior.Cascade); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Timeline/Migrations/20190820155221_AddAvatarETag.cs b/Timeline/Migrations/20190820155221_AddAvatarETag.cs deleted file mode 100644 index db352b5d..00000000 --- a/Timeline/Migrations/20190820155221_AddAvatarETag.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Timeline.Migrations -{ - public partial class AddAvatarETag : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.AddColumn( - name: "etag", - table: "user_avatars", - maxLength: 30, - nullable: true); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropColumn( - name: "etag", - table: "user_avatars"); - } - } -} diff --git a/Timeline/Migrations/20190820155354_MakeUserNameIndexUnique.Designer.cs b/Timeline/Migrations/20190820155354_MakeUserNameIndexUnique.Designer.cs deleted file mode 100644 index 420cd41c..00000000 --- a/Timeline/Migrations/20190820155354_MakeUserNameIndexUnique.Designer.cs +++ /dev/null @@ -1,93 +0,0 @@ -// -using System; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Timeline.Entities; - -namespace Timeline.Migrations -{ - [DbContext(typeof(DatabaseContext))] - [Migration("20190820155354_MakeUserNameIndexUnique")] - partial class MakeUserNameIndexUnique - { - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") - .HasAnnotation("Relational:MaxIdentifierLength", 64); - - modelBuilder.Entity("Timeline.Entities.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id"); - - b.Property("EncryptedPassword") - .IsRequired() - .HasColumnName("password"); - - b.Property("Name") - .IsRequired() - .HasColumnName("name") - .HasMaxLength(26); - - b.Property("RoleString") - .IsRequired() - .HasColumnName("roles"); - - b.Property("Version") - .ValueGeneratedOnAdd() - .HasColumnName("version") - .HasDefaultValue(0L); - - b.HasKey("Id"); - - b.HasIndex("Name") - .IsUnique(); - - b.ToTable("users"); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatar", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnName("id"); - - b.Property("Data") - .HasColumnName("data"); - - b.Property("ETag") - .HasColumnName("etag") - .HasMaxLength(30); - - b.Property("LastModified") - .HasColumnName("last_modified"); - - b.Property("Type") - .HasColumnName("type"); - - b.Property("UserId"); - - b.HasKey("Id"); - - b.HasIndex("UserId") - .IsUnique(); - - b.ToTable("user_avatars"); - }); - - modelBuilder.Entity("Timeline.Entities.UserAvatar", b => - { - b.HasOne("Timeline.Entities.User") - .WithOne("Avatar") - .HasForeignKey("Timeline.Entities.UserAvatar", "UserId") - .OnDelete(DeleteBehavior.Cascade); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Timeline/Migrations/20190820155354_MakeUserNameIndexUnique.cs b/Timeline/Migrations/20190820155354_MakeUserNameIndexUnique.cs deleted file mode 100644 index 01d72450..00000000 --- a/Timeline/Migrations/20190820155354_MakeUserNameIndexUnique.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Timeline.Migrations -{ - public partial class MakeUserNameIndexUnique : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropIndex( - name: "IX_users_name", - table: "users"); - - migrationBuilder.CreateIndex( - name: "IX_users_name", - table: "users", - column: "name", - unique: true); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropIndex( - name: "IX_users_name", - table: "users"); - - migrationBuilder.CreateIndex( - name: "IX_users_name", - table: "users", - column: "name"); - } - } -} diff --git a/Timeline/Migrations/20191031064541_Initialize.Designer.cs b/Timeline/Migrations/20191031064541_Initialize.Designer.cs new file mode 100644 index 00000000..60cb4095 --- /dev/null +++ b/Timeline/Migrations/20191031064541_Initialize.Designer.cs @@ -0,0 +1,137 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Timeline.Entities; + +namespace Timeline.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20191031064541_Initialize")] + partial class Initialize + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Timeline.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("bigint"); + + b.Property("EncryptedPassword") + .IsRequired() + .HasColumnName("password") + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasColumnName("name") + .HasColumnType("varchar(26)") + .HasMaxLength(26); + + b.Property("RoleString") + .IsRequired() + .HasColumnName("roles") + .HasColumnType("longtext"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnName("version") + .HasColumnType("bigint") + .HasDefaultValue(0L); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatar", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("bigint"); + + b.Property("Data") + .HasColumnName("data") + .HasColumnType("longblob"); + + b.Property("ETag") + .HasColumnName("etag") + .HasColumnType("varchar(30)") + .HasMaxLength(30); + + b.Property("LastModified") + .HasColumnName("last_modified") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnName("type") + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_avatars"); + }); + + modelBuilder.Entity("Timeline.Entities.UserDetail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("bigint"); + + b.Property("Nickname") + .HasColumnName("nickname") + .HasColumnType("varchar(26)") + .HasMaxLength(26); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_details"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatar", b => + { + b.HasOne("Timeline.Entities.User", null) + .WithOne("Avatar") + .HasForeignKey("Timeline.Entities.UserAvatar", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Timeline.Entities.UserDetail", b => + { + b.HasOne("Timeline.Entities.User", null) + .WithOne("Detail") + .HasForeignKey("Timeline.Entities.UserDetail", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Timeline/Migrations/20191031064541_Initialize.cs b/Timeline/Migrations/20191031064541_Initialize.cs new file mode 100644 index 00000000..416f7c06 --- /dev/null +++ b/Timeline/Migrations/20191031064541_Initialize.cs @@ -0,0 +1,104 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Timeline.Migrations +{ + public partial class Initialize : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "users", + columns: table => new + { + id = table.Column(nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + name = table.Column(maxLength: 26, nullable: false), + password = table.Column(nullable: false), + roles = table.Column(nullable: false), + version = table.Column(nullable: false, defaultValue: 0L) + }, + constraints: table => + { + table.PrimaryKey("PK_users", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "user_avatars", + columns: table => new + { + id = table.Column(nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + data = table.Column(nullable: true), + type = table.Column(nullable: true), + etag = table.Column(maxLength: 30, nullable: true), + last_modified = table.Column(nullable: false), + UserId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_user_avatars", x => x.id); + table.ForeignKey( + name: "FK_user_avatars_users_UserId", + column: x => x.UserId, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "user_details", + columns: table => new + { + id = table.Column(nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + nickname = table.Column(maxLength: 26, nullable: true), + UserId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_user_details", x => x.id); + table.ForeignKey( + name: "FK_user_details_users_UserId", + column: x => x.UserId, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_user_avatars_UserId", + table: "user_avatars", + column: "UserId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_user_details_UserId", + table: "user_details", + column: "UserId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_users_name", + table: "users", + column: "name", + unique: true); + + migrationBuilder.InsertData("users", new string[] { "name", "password", "roles" }, + new object[] { "administrator", "AQAAAAEAACcQAAAAENsspZrk8Wo+UuMyg6QuWJsNvRg6gVu4K/TumVod3h9GVLX9zDVuQQds3o7V8QWJ2w==", "user,admin" }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "user_avatars"); + + migrationBuilder.DropTable( + name: "user_details"); + + migrationBuilder.DropTable( + name: "users"); + } + } +} diff --git a/Timeline/Migrations/DatabaseContextModelSnapshot.cs b/Timeline/Migrations/DatabaseContextModelSnapshot.cs index 1328b855..697fbbec 100644 --- a/Timeline/Migrations/DatabaseContextModelSnapshot.cs +++ b/Timeline/Migrations/DatabaseContextModelSnapshot.cs @@ -14,31 +14,36 @@ namespace Timeline.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "2.2.6-servicing-10079") + .HasAnnotation("ProductVersion", "3.0.0") .HasAnnotation("Relational:MaxIdentifierLength", 64); modelBuilder.Entity("Timeline.Entities.User", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnName("id"); + .HasColumnName("id") + .HasColumnType("bigint"); b.Property("EncryptedPassword") .IsRequired() - .HasColumnName("password"); + .HasColumnName("password") + .HasColumnType("longtext"); b.Property("Name") .IsRequired() .HasColumnName("name") + .HasColumnType("varchar(26)") .HasMaxLength(26); b.Property("RoleString") .IsRequired() - .HasColumnName("roles"); + .HasColumnName("roles") + .HasColumnType("longtext"); b.Property("Version") .ValueGeneratedOnAdd() .HasColumnName("version") + .HasColumnType("bigint") .HasDefaultValue(0L); b.HasKey("Id"); @@ -53,22 +58,28 @@ namespace Timeline.Migrations { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnName("id"); + .HasColumnName("id") + .HasColumnType("bigint"); b.Property("Data") - .HasColumnName("data"); + .HasColumnName("data") + .HasColumnType("longblob"); b.Property("ETag") .HasColumnName("etag") + .HasColumnType("varchar(30)") .HasMaxLength(30); b.Property("LastModified") - .HasColumnName("last_modified"); + .HasColumnName("last_modified") + .HasColumnType("datetime(6)"); b.Property("Type") - .HasColumnName("type"); + .HasColumnName("type") + .HasColumnType("longtext"); - b.Property("UserId"); + b.Property("UserId") + .HasColumnType("bigint"); b.HasKey("Id"); @@ -78,32 +89,20 @@ namespace Timeline.Migrations b.ToTable("user_avatars"); }); - modelBuilder.Entity("Timeline.Entities.UserDetailEntity", b => + modelBuilder.Entity("Timeline.Entities.UserDetail", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnName("id"); - - b.Property("Description") - .HasColumnName("description"); - - b.Property("EMail") - .HasColumnName("email") - .HasMaxLength(50); + .HasColumnName("id") + .HasColumnType("bigint"); b.Property("Nickname") .HasColumnName("nickname") - .HasMaxLength(15); - - b.Property("PhoneNumber") - .HasColumnName("phone_number") - .HasMaxLength(15); - - b.Property("QQ") - .HasColumnName("qq") - .HasMaxLength(15); + .HasColumnType("varchar(26)") + .HasMaxLength(26); - b.Property("UserId"); + b.Property("UserId") + .HasColumnType("bigint"); b.HasKey("Id"); @@ -115,18 +114,20 @@ namespace Timeline.Migrations modelBuilder.Entity("Timeline.Entities.UserAvatar", b => { - b.HasOne("Timeline.Entities.User") + b.HasOne("Timeline.Entities.User", null) .WithOne("Avatar") .HasForeignKey("Timeline.Entities.UserAvatar", "UserId") - .OnDelete(DeleteBehavior.Cascade); + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); }); - modelBuilder.Entity("Timeline.Entities.UserDetailEntity", b => + modelBuilder.Entity("Timeline.Entities.UserDetail", b => { - b.HasOne("Timeline.Entities.User") + b.HasOne("Timeline.Entities.User", null) .WithOne("Detail") - .HasForeignKey("Timeline.Entities.UserDetailEntity", "UserId") - .OnDelete(DeleteBehavior.Cascade); + .HasForeignKey("Timeline.Entities.UserDetail", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); }); #pragma warning restore 612, 618 } -- cgit v1.2.3