From f3c7912caec2e9eee8a685d8751894f357528a71 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 19 Nov 2019 23:18:45 +0800 Subject: Complete integrated tests??? Fix bugs. --- Timeline.Tests/DatabaseTest.cs | 2 +- .../Authentication/AuthenticationExtensions.cs | 28 ++- Timeline.Tests/Helpers/TestApplication.cs | 22 +-- .../IntegratedTests/IntegratedTestBase.cs | 8 + .../IntegratedTests/PersonalTimelineTest.cs | 219 +++++++++++++++++++++ Timeline.Tests/Mock/Data/TestDatabase.cs | 70 +++++-- Timeline.Tests/Mock/Data/TestUsers.cs | 25 --- Timeline.Tests/Services/UserAvatarServiceTest.cs | 8 +- Timeline.Tests/Services/UserDetailServiceTest.cs | 6 +- Timeline/Services/TimelineService.cs | 4 +- 10 files changed, 320 insertions(+), 72 deletions(-) diff --git a/Timeline.Tests/DatabaseTest.cs b/Timeline.Tests/DatabaseTest.cs index fc153c24..20f57c40 100644 --- a/Timeline.Tests/DatabaseTest.cs +++ b/Timeline.Tests/DatabaseTest.cs @@ -15,7 +15,7 @@ namespace Timeline.Tests public DatabaseTest() { _database = new TestDatabase(); - _context = _database.DatabaseContext; + _context = _database.Context; } public void Dispose() diff --git a/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs b/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs index 6a78be7a..4048bb73 100644 --- a/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs +++ b/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs @@ -1,5 +1,4 @@ using Microsoft.AspNetCore.Mvc.Testing; -using Newtonsoft.Json; using System; using System.Net.Http; using System.Threading.Tasks; @@ -22,9 +21,8 @@ namespace Timeline.Tests.Helpers.Authentication public static async Task CreateUserTokenAsync(this HttpClient client, string username, string password, int? expireOffset = null) { var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = username, Password = password, Expire = expireOffset }); - response.Should().HaveStatusCode(200); - var result = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); - return result; + return response.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; } public static async Task CreateClientWithCredential(this WebApplicationFactory factory, string username, string password) where T : class @@ -35,14 +33,19 @@ namespace Timeline.Tests.Helpers.Authentication return client; } + public static Task CreateClientAs(this WebApplicationFactory factory, MockUser user) where T : class + { + return CreateClientWithCredential(factory, user.Username, user.Password); + } + public static Task CreateClientAsUser(this WebApplicationFactory factory) where T : class { - return factory.CreateClientWithCredential(MockUser.User.Username, MockUser.User.Password); + return factory.CreateClientAs(MockUser.User); } public static Task CreateClientAsAdmin(this WebApplicationFactory factory) where T : class { - return factory.CreateClientWithCredential(MockUser.Admin.Username, MockUser.Admin.Password); + return factory.CreateClientAs(MockUser.Admin); } public static Task CreateClientAs(this WebApplicationFactory factory, AuthType authType) where T : class @@ -55,5 +58,18 @@ namespace Timeline.Tests.Helpers.Authentication _ => throw new InvalidOperationException("Unknown auth type.") }; } + + public static MockUser GetMockUser(this AuthType authType) + { + return authType switch + { + AuthType.None => null, + AuthType.User => MockUser.User, + AuthType.Admin => MockUser.Admin, + _ => throw new InvalidOperationException("Unknown auth type.") + }; + } + + public static string GetUsername(this AuthType authType) => authType.GetMockUser().Username; } } diff --git a/Timeline.Tests/Helpers/TestApplication.cs b/Timeline.Tests/Helpers/TestApplication.cs index b0187a30..5862f452 100644 --- a/Timeline.Tests/Helpers/TestApplication.cs +++ b/Timeline.Tests/Helpers/TestApplication.cs @@ -10,26 +10,11 @@ namespace Timeline.Tests.Helpers { public class TestApplication : IDisposable { - public SqliteConnection DatabaseConnection { get; } = new SqliteConnection("Data Source=:memory:;"); + public TestDatabase Database { get; } = new TestDatabase(); public WebApplicationFactory Factory { get; } public TestApplication(WebApplicationFactory factory) { - // We should keep the connection, so the database is persisted but not recreate every time. - // See https://docs.microsoft.com/en-us/ef/core/miscellaneous/testing/sqlite#writing-tests . - DatabaseConnection.Open(); - - { - var options = new DbContextOptionsBuilder() - .UseSqlite(DatabaseConnection) - .Options; - - using (var context = new DatabaseContext(options)) - { - TestDatabase.InitDatabase(context); - }; - } - Factory = factory.WithWebHostBuilder(builder => { builder.ConfigureServices(services => @@ -37,7 +22,7 @@ namespace Timeline.Tests.Helpers services.AddEntityFrameworkSqlite(); services.AddDbContext(options => { - options.UseSqlite(DatabaseConnection); + options.UseSqlite(Database.Connection); }); }); }); @@ -45,8 +30,7 @@ namespace Timeline.Tests.Helpers public void Dispose() { - DatabaseConnection.Close(); - DatabaseConnection.Dispose(); + Database.Dispose(); } } } diff --git a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs b/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs index c4d72faf..2dfaf82e 100644 --- a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs +++ b/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Timeline.Tests.Helpers; +using Timeline.Tests.Mock.Data; using Xunit; namespace Timeline.Tests.IntegratedTests @@ -23,5 +24,12 @@ namespace Timeline.Tests.IntegratedTests { TestApp.Dispose(); } + + protected void CreateExtraMockUsers(int count) + { + TestApp.Database.CreateExtraMockUsers(count); + } + + protected IReadOnlyList ExtraMockUsers => TestApp.Database.ExtraMockUsers; } } diff --git a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs index 2e5b86fa..705675f9 100644 --- a/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs +++ b/Timeline.Tests/IntegratedTests/PersonalTimelineTest.cs @@ -11,6 +11,7 @@ using Timeline.Models; using Timeline.Models.Http; using Timeline.Tests.Helpers; using Timeline.Tests.Helpers.Authentication; +using Timeline.Tests.Mock.Data; using Xunit; namespace Timeline.Tests.IntegratedTests @@ -242,5 +243,223 @@ namespace Timeline.Tests.IntegratedTests } } } + + + [Fact] + public async Task Permission_Post_Create() + { + CreateExtraMockUsers(1); + + using (var client = await Factory.CreateClientAsUser()) + { + var res = await client.PostAsJsonAsync("users/user/timeline/op/member", + new TimelineMemberChangeRequest { Add = new List { "user0" } }); + res.Should().HaveStatusCode(200); + } + + using (var client = Factory.CreateDefaultClient()) + { + { // no auth should get 401 + var res = await client.PostAsJsonAsync("users/user/timeline/postop/create", + new TimelinePostCreateRequest { Content = "aaa" }); + res.Should().HaveStatusCode(401); + } + } + + using (var client = await Factory.CreateClientAsUser()) + { + { // post self's + var res = await client.PostAsJsonAsync("users/user/timeline/postop/create", + new TimelinePostCreateRequest { Content = "aaa" }); + res.Should().HaveStatusCode(200); + } + { // post other not as a member should get 403 + var res = await client.PostAsJsonAsync("users/admin/timeline/postop/create", + new TimelinePostCreateRequest { Content = "aaa" }); + res.Should().HaveStatusCode(403); + } + } + + using (var client = await Factory.CreateClientAsAdmin()) + { + { // post as admin + var res = await client.PostAsJsonAsync("users/user/timeline/postop/create", + new TimelinePostCreateRequest { Content = "aaa" }); + res.Should().HaveStatusCode(200); + } + } + + using (var client = await Factory.CreateClientAs(ExtraMockUsers[0])) + { + { // post as member + var res = await client.PostAsJsonAsync("users/user/timeline/postop/create", + new TimelinePostCreateRequest { Content = "aaa" }); + res.Should().HaveStatusCode(200); + } + } + } + + [Fact] + public async Task Permission_Post_Delete() + { + CreateExtraMockUsers(2); + + async Task CreatePost(MockUser auth, string timeline) + { + using var client = await Factory.CreateClientAs(auth); + var res = await client.PostAsJsonAsync($"users/{timeline}/timeline/postop/create", + new TimelinePostCreateRequest { Content = "aaa" }); + return res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Id; + } + + using (var client = await Factory.CreateClientAsUser()) + { + var res = await client.PostAsJsonAsync("users/user/timeline/op/member", + new TimelineMemberChangeRequest { Add = new List { "user0", "user1" } }); + res.Should().HaveStatusCode(200); + } + + { // no auth should get 401 + using var client = Factory.CreateDefaultClient(); + var res = await client.PostAsJsonAsync("users/user/timeline/postop/delete", + new TimelinePostDeleteRequest { Id = 12 }); + res.Should().HaveStatusCode(401); + } + + { // self can delete self + var postId = await CreatePost(MockUser.User, "user"); + using var client = await Factory.CreateClientAsUser(); + var res = await client.PostAsJsonAsync("users/user/timeline/postop/delete", + new TimelinePostDeleteRequest { Id = postId }); + res.Should().HaveStatusCode(200); + } + + { // admin can delete any + var postId = await CreatePost(MockUser.User, "user"); + using var client = await Factory.CreateClientAsAdmin(); + var res = await client.PostAsJsonAsync("users/user/timeline/postop/delete", + new TimelinePostDeleteRequest { Id = postId }); + res.Should().HaveStatusCode(200); + } + + { // owner can delete other + var postId = await CreatePost(ExtraMockUsers[0], "user"); + using var client = await Factory.CreateClientAsUser(); + var res = await client.PostAsJsonAsync("users/user/timeline/postop/delete", + new TimelinePostDeleteRequest { Id = postId }); + res.Should().HaveStatusCode(200); + } + + { // author can delete self + var postId = await CreatePost(ExtraMockUsers[0], "user"); + using var client = await Factory.CreateClientAs(ExtraMockUsers[0]); + var res = await client.PostAsJsonAsync("users/user/timeline/postop/delete", + new TimelinePostDeleteRequest { Id = postId }); + res.Should().HaveStatusCode(200); + } + + { // otherwise is forbidden + var postId = await CreatePost(ExtraMockUsers[0], "user"); + using var client = await Factory.CreateClientAs(ExtraMockUsers[1]); + var res = await client.PostAsJsonAsync("users/user/timeline/postop/delete", + new TimelinePostDeleteRequest { Id = postId }); + res.Should().HaveStatusCode(403); + } + } + + [Fact] + public async Task Post_Op_Should_Work() + { + { + using var client = await Factory.CreateClientAsUser(); + { + var res = await client.GetAsync("users/user/timeline/posts"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().NotBeNull().And.BeEmpty(); + } + { + var res = await client.PostAsJsonAsync("users/user/timeline/postop/create", + new TimelinePostCreateRequest { Content = null }); + res.Should().BeInvalidModel(); + } + const string mockContent = "aaa"; + TimelinePostCreateResponse createRes; + { + var res = await client.PostAsJsonAsync("users/user/timeline/postop/create", + new TimelinePostCreateRequest { Content = mockContent }); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which; + body.Should().NotBeNull(); + createRes = body; + } + { + var res = await client.GetAsync("users/user/timeline/posts"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().NotBeNull().And.BeEquivalentTo( + new TimelinePostInfo + { + Id = createRes.Id, + Author = "user", + Content = mockContent, + Time = createRes.Time + }); + } + const string mockContent2 = "bbb"; + var mockTime2 = DateTime.Now.AddDays(-1); + TimelinePostCreateResponse createRes2; + { + var res = await client.PostAsJsonAsync("users/user/timeline/postop/create", + new TimelinePostCreateRequest { Content = mockContent2, Time = mockTime2 }); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which; + body.Should().NotBeNull(); + createRes2 = body; + } + { + var res = await client.GetAsync("users/user/timeline/posts"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().NotBeNull().And.BeEquivalentTo( + new TimelinePostInfo + { + Id = createRes.Id, + Author = "user", + Content = mockContent, + Time = createRes.Time + }, + new TimelinePostInfo + { + Id = createRes2.Id, + Author = "user", + Content = mockContent2, + Time = createRes2.Time + }); + } + { + var res = await client.PostAsJsonAsync("users/user/timeline/postop/delete", + new TimelinePostDeleteRequest { Id = createRes.Id }); + res.Should().HaveStatusCode(200); + } + { + var res = await client.GetAsync("users/user/timeline/posts"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().NotBeNull().And.BeEquivalentTo( + new TimelinePostInfo + { + Id = createRes2.Id, + Author = "user", + Content = mockContent2, + Time = createRes2.Time + }); + } + } + } } } diff --git a/Timeline.Tests/Mock/Data/TestDatabase.cs b/Timeline.Tests/Mock/Data/TestDatabase.cs index 1e662546..1f396177 100644 --- a/Timeline.Tests/Mock/Data/TestDatabase.cs +++ b/Timeline.Tests/Mock/Data/TestDatabase.cs @@ -1,42 +1,88 @@ using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using System; +using System.Collections.Generic; +using System.Linq; using Timeline.Entities; +using Timeline.Models; +using Timeline.Services; namespace Timeline.Tests.Mock.Data { public class TestDatabase : IDisposable { - public static void InitDatabase(DatabaseContext context) + // currently password service is thread safe, so we share a static one. + private static PasswordService PasswordService { get; } = new PasswordService(); + + private static User CreateEntityFromMock(MockUser user) + { + return new User + { + Name = user.Username, + EncryptedPassword = PasswordService.HashPassword(user.Password), + RoleString = UserRoleConvert.ToString(user.Administrator), + Avatar = null + }; + } + + private static IEnumerable CreateDefaultMockEntities() + { + // emmmmmmm. Never reuse the user instances because EF Core uses them, which will cause strange things. + yield return CreateEntityFromMock(MockUser.User); + yield return CreateEntityFromMock(MockUser.Admin); + } + + private static void InitDatabase(DatabaseContext context) { context.Database.EnsureCreated(); - context.Users.AddRange(MockUser.CreateMockEntities()); + context.Users.AddRange(CreateDefaultMockEntities()); context.SaveChanges(); } public TestDatabase() { - DatabaseConnection = new SqliteConnection("Data Source=:memory:;"); - DatabaseConnection.Open(); + Connection = new SqliteConnection("Data Source=:memory:;"); + Connection.Open(); var options = new DbContextOptionsBuilder() - .UseSqlite(DatabaseConnection) + .UseSqlite(Connection) .Options; - DatabaseContext = new DatabaseContext(options); + Context = new DatabaseContext(options); + + InitDatabase(Context); + } + + private List _extraMockUsers; + + public IReadOnlyList ExtraMockUsers => _extraMockUsers; + + public void CreateExtraMockUsers(int count) + { + if (count <= 0) + throw new ArgumentOutOfRangeException(nameof(count), count, "Additional user count must be bigger than 0."); + if (_extraMockUsers != null) + throw new InvalidOperationException("Already create mock users."); + + _extraMockUsers = new List(); + for (int i = 0; i < count; i++) + { + _extraMockUsers.Add(new MockUser($"user{i}", $"password", false)); + } - InitDatabase(DatabaseContext); + Context.AddRange(_extraMockUsers.Select(u => CreateEntityFromMock(u))); + Context.SaveChanges(); } public void Dispose() { - DatabaseContext.Dispose(); + Context.Dispose(); - DatabaseConnection.Close(); - DatabaseConnection.Dispose(); + Connection.Close(); + Connection.Dispose(); } - public SqliteConnection DatabaseConnection { get; } - public DatabaseContext DatabaseContext { get; } + public SqliteConnection Connection { get; } + public DatabaseContext Context { get; } } } diff --git a/Timeline.Tests/Mock/Data/TestUsers.cs b/Timeline.Tests/Mock/Data/TestUsers.cs index fa75236a..443d3cf9 100644 --- a/Timeline.Tests/Mock/Data/TestUsers.cs +++ b/Timeline.Tests/Mock/Data/TestUsers.cs @@ -1,8 +1,5 @@ -using System; using System.Collections.Generic; -using Timeline.Entities; using Timeline.Models; -using Timeline.Services; namespace Timeline.Tests.Mock.Data { @@ -24,27 +21,5 @@ namespace Timeline.Tests.Mock.Data public static MockUser Admin { get; } = new MockUser("admin", "adminpassword", true); public static IReadOnlyList UserInfoList { get; } = new List { User.Info, Admin.Info }; - - // emmmmmmm. Never reuse the user instances because EF Core uses them, which will cause strange things. - public static IEnumerable CreateMockEntities() - { - var passwordService = new PasswordService(); - User Create(MockUser user) - { - return new User - { - Name = user.Username, - EncryptedPassword = passwordService.HashPassword(user.Password), - RoleString = UserRoleConvert.ToString(user.Administrator), - Avatar = null - }; - } - - return new List - { - Create(User), - Create(Admin) - }; - } } } diff --git a/Timeline.Tests/Services/UserAvatarServiceTest.cs b/Timeline.Tests/Services/UserAvatarServiceTest.cs index cf3d2a0a..033a5e90 100644 --- a/Timeline.Tests/Services/UserAvatarServiceTest.cs +++ b/Timeline.Tests/Services/UserAvatarServiceTest.cs @@ -139,7 +139,7 @@ namespace Timeline.Tests.Services _database = new TestDatabase(); - _service = new UserAvatarService(NullLogger.Instance, _database.DatabaseContext, _mockDefaultAvatarProvider.Object, _mockValidator.Object, _mockETagGenerator.Object, _mockClock.Object); + _service = new UserAvatarService(NullLogger.Instance, _database.Context, _mockDefaultAvatarProvider.Object, _mockValidator.Object, _mockETagGenerator.Object, _mockClock.Object); } public void Dispose() @@ -171,7 +171,7 @@ namespace Timeline.Tests.Services string username = MockUser.User.Username; var mockAvatarEntity = CreateMockAvatarEntity("aaa"); { - var context = _database.DatabaseContext; + var context = _database.Context; var user = await context.Users.Where(u => u.Name == username).Include(u => u.Avatar).SingleAsync(); user.Avatar = mockAvatarEntity; await context.SaveChangesAsync(); @@ -205,7 +205,7 @@ namespace Timeline.Tests.Services string username = MockUser.User.Username; var mockAvatarEntity = CreateMockAvatarEntity("aaa"); { - var context = _database.DatabaseContext; + var context = _database.Context; var user = await context.Users.Where(u => u.Name == username).Include(u => u.Avatar).SingleAsync(); user.Avatar = mockAvatarEntity; await context.SaveChangesAsync(); @@ -237,7 +237,7 @@ namespace Timeline.Tests.Services { string username = MockUser.User.Username; - var user = await _database.DatabaseContext.Users.Where(u => u.Name == username).Include(u => u.Avatar).SingleAsync(); + var user = await _database.Context.Users.Where(u => u.Name == username).Include(u => u.Avatar).SingleAsync(); var avatar1 = CreateMockAvatar("aaa"); var avatar2 = CreateMockAvatar("bbb"); diff --git a/Timeline.Tests/Services/UserDetailServiceTest.cs b/Timeline.Tests/Services/UserDetailServiceTest.cs index c7037c6e..bddb1494 100644 --- a/Timeline.Tests/Services/UserDetailServiceTest.cs +++ b/Timeline.Tests/Services/UserDetailServiceTest.cs @@ -21,7 +21,7 @@ namespace Timeline.Tests.Services public UserDetailServiceTest() { _testDatabase = new TestDatabase(); - _service = new UserDetailService(_testDatabase.DatabaseContext, NullLogger.Instance); + _service = new UserDetailService(_testDatabase.Context, NullLogger.Instance); } public void Dispose() @@ -51,7 +51,7 @@ namespace Timeline.Tests.Services { const string nickname = "aaaaaa"; { - var context = _testDatabase.DatabaseContext; + var context = _testDatabase.Context; var userId = (await context.Users.Where(u => u.Name == MockUser.User.Username).Select(u => new { u.Id }).SingleAsync()).Id; context.UserDetails.Add(new UserDetail { @@ -84,7 +84,7 @@ namespace Timeline.Tests.Services 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 user = await _testDatabase.Context.Users.Where(u => u.Name == username).Include(u => u.Detail).SingleAsync(); var nickname1 = "nickname1"; var nickname2 = "nickname2"; diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index 1e64353c..a2ff4098 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -605,12 +605,12 @@ namespace Timeline.Services var timelineEntity = await Database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync(); - var postEntity = await Database.Timelines.Where(p => p.Id == id).Select(p => new { p.OwnerId }).SingleOrDefaultAsync(); + var postEntity = await Database.TimelinePosts.Where(p => p.Id == id).Select(p => new { p.AuthorId }).SingleOrDefaultAsync(); if (postEntity == null) throw new TimelinePostNotExistException(id); - return timelineEntity.OwnerId == userId || postEntity.OwnerId == userId; + return timelineEntity.OwnerId == userId || postEntity.AuthorId == userId; } public async Task IsMemberOf(string name, string username) -- cgit v1.2.3