From eba5936d0ed97d50d4c2d263624c02457539c8c1 Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 16 Jan 2021 18:38:17 +0800 Subject: feat: Add search service. --- BackEnd/Timeline/Services/SearchService.cs | 98 ++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 BackEnd/Timeline/Services/SearchService.cs (limited to 'BackEnd/Timeline/Services/SearchService.cs') diff --git a/BackEnd/Timeline/Services/SearchService.cs b/BackEnd/Timeline/Services/SearchService.cs new file mode 100644 index 00000000..eac81fd0 --- /dev/null +++ b/BackEnd/Timeline/Services/SearchService.cs @@ -0,0 +1,98 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Entities; + +namespace Timeline.Services +{ + public class SearchResultItem + { + public SearchResultItem(TItem item, int score) + { + Item = item; + Score = score; + } + + public TItem Item { get; set; } = default!; + + /// + /// Bigger is better. + /// + public int Score { get; set; } + } + + public class SearchResult + { +#pragma warning disable CA2227 // Collection properties should be read only + public List> Items { get; set; } = new(); +#pragma warning restore CA2227 // Collection properties should be read only + } + + public interface ISearchService + { + /// + /// Search timelines whose name or title contains query string. + /// + /// String to contain. + /// Search results. + /// Thrown when is null. + /// Thrown when is empty. + Task> SearchTimeline(string query); + + /// + /// Search users whose username or nickname contains query string. + /// + /// String to contain. + /// Search results. + /// Thrown when is null. + /// Thrown when is empty. + Task> SearchUser(string query); + } + + public class SearchService : ISearchService + { + private readonly DatabaseContext _database; + + public SearchService(DatabaseContext database) + { + _database = database; + } + + public async Task> SearchTimeline(string query) + { + if (query is null) + throw new ArgumentNullException(nameof(query)); + if (query.Length == 0) + throw new ArgumentException("Query string can't be empty.", nameof(query)); + + var nameLikeTimelines = await _database.Timelines.Include(t => t.Owner).Where(t => t.Name == null ? t.Owner.Username.Contains(query) : t.Name.Contains(query)).ToListAsync(); + var titleLikeTimelines = await _database.Timelines.Where(t => t.Title != null && t.Title.Contains(query)).ToListAsync(); + + var searchResult = new SearchResult(); + searchResult.Items.AddRange(nameLikeTimelines.Select(t => new SearchResultItem(t, 2))); + searchResult.Items.AddRange(titleLikeTimelines.Select(t => new SearchResultItem(t, 1))); + + return searchResult; + } + + public async Task> SearchUser(string query) + { + if (query is null) + throw new ArgumentNullException(nameof(query)); + if (query.Length == 0) + throw new ArgumentException("Query string can't be empty.", nameof(query)); + + var usernameLikeUsers = await _database.Users.Where(u => u.Username.Contains(query)).ToListAsync(); + var nicknameLikeUsers = await _database.Users.Where(u => u.Nickname != null && u.Nickname.Contains(query)).ToListAsync(); + + var searchResult = new SearchResult(); + searchResult.Items.AddRange(usernameLikeUsers.Select(u => new SearchResultItem(u, 2))); + searchResult.Items.AddRange(nicknameLikeUsers.Select(u => new SearchResultItem(u, 1))); + + return searchResult; + + } + } +} -- cgit v1.2.3 From 280698f244bdaf3fbe2896d7104826d132c0b95a Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 16 Jan 2021 19:48:09 +0800 Subject: test: Add unit tests for search service. --- .../Timeline.Tests/Services/DatabaseBasedTest.cs | 55 ----------------- .../Timeline.Tests/Services/SearchServiceTest.cs | 49 +++++++++++++++ BackEnd/Timeline.Tests/Services/ServiceTestBase.cs | 71 ++++++++++++++++++++++ .../Services/UserDeleteServiceTest.cs | 4 +- .../Services/UserPermissionServiceTest.cs | 4 +- BackEnd/Timeline/Services/SearchService.cs | 6 ++ 6 files changed, 130 insertions(+), 59 deletions(-) delete mode 100644 BackEnd/Timeline.Tests/Services/DatabaseBasedTest.cs create mode 100644 BackEnd/Timeline.Tests/Services/SearchServiceTest.cs create mode 100644 BackEnd/Timeline.Tests/Services/ServiceTestBase.cs (limited to 'BackEnd/Timeline/Services/SearchService.cs') diff --git a/BackEnd/Timeline.Tests/Services/DatabaseBasedTest.cs b/BackEnd/Timeline.Tests/Services/DatabaseBasedTest.cs deleted file mode 100644 index 90fb6463..00000000 --- a/BackEnd/Timeline.Tests/Services/DatabaseBasedTest.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Threading.Tasks; -using Timeline.Entities; -using Timeline.Tests.Helpers; -using Xunit; -using Xunit.Abstractions; - -namespace Timeline.Tests.Services -{ - public abstract class DatabaseBasedTest : IAsyncLifetime - { - protected TestDatabase TestDatabase { get; } - protected DatabaseContext Database { get; private set; } = default!; - - private readonly ITestOutputHelper? _testOutputHelper; - - protected DatabaseBasedTest(bool databaseCreateUsers = true, ITestOutputHelper? testOutputHelper = null) - { - _testOutputHelper = testOutputHelper; - TestDatabase = new TestDatabase(databaseCreateUsers); - } - - protected DatabaseBasedTest(ITestOutputHelper? testOutputHelper) : this(true, testOutputHelper) { } - - public async Task InitializeAsync() - { - await TestDatabase.InitializeAsync(); - Database = TestDatabase.CreateContext(_testOutputHelper); - await OnDatabaseCreatedAsync(); - OnDatabaseCreated(); - } - - public async Task DisposeAsync() - { - BeforeDatabaseDestroy(); - await BeforeDatabaseDestroyAsync(); - await Database.DisposeAsync(); - await TestDatabase.DisposeAsync(); - } - - - protected virtual void OnDatabaseCreated() { } - protected virtual void BeforeDatabaseDestroy() { } - - - protected virtual Task OnDatabaseCreatedAsync() - { - return Task.CompletedTask; - } - - protected virtual Task BeforeDatabaseDestroyAsync() - { - return Task.CompletedTask; - } - } -} diff --git a/BackEnd/Timeline.Tests/Services/SearchServiceTest.cs b/BackEnd/Timeline.Tests/Services/SearchServiceTest.cs new file mode 100644 index 00000000..968352c0 --- /dev/null +++ b/BackEnd/Timeline.Tests/Services/SearchServiceTest.cs @@ -0,0 +1,49 @@ +using FluentAssertions; +using System.Threading.Tasks; +using Timeline.Services; +using Xunit; + +namespace Timeline.Tests.Services +{ + public class SearchServiceTest : ServiceTestBase + { + private SearchService _service = default!; + + protected override void OnInitialize() + { + _service = new SearchService(Database); + } + + [Fact] + public async Task TimelineSearch_Should_Work() + { + await TimelineService.CreateTimeline("hahaha", UserId); + var t2 = await TimelineService.CreateTimeline("bababa", UserId); + await TimelineService.ChangeProperty(t2.Id, new TimelineChangePropertyParams { Title = "hahaha" }); + await TimelineService.CreateTimeline("bbbbbb", UserId); + + var searchResult = await _service.SearchTimeline("hah"); + searchResult.Items.Should().HaveCount(2); + searchResult.Items[0].Item.Name.Should().Be("hahaha"); + searchResult.Items[0].Score.Should().Be(2); + searchResult.Items[1].Item.Name.Should().Be("bababa"); + searchResult.Items[1].Score.Should().Be(1); + } + + [Fact] + public async Task UserSearch_Should_Work() + { + await UserService.CreateUser("hahaha", "p"); + var u2 = await UserService.CreateUser("bababa", "p"); + await UserService.ModifyUser(u2.Id, new ModifyUserParams { Nickname = "hahaha" }); + await UserService.CreateUser("bbbbbb", "p"); + + var searchResult = await _service.SearchUser("hah"); + searchResult.Items.Should().HaveCount(2); + searchResult.Items[0].Item.Username.Should().Be("hahaha"); + searchResult.Items[0].Score.Should().Be(2); + searchResult.Items[1].Item.Username.Should().Be("bababa"); + searchResult.Items[1].Score.Should().Be(1); + } + } +} diff --git a/BackEnd/Timeline.Tests/Services/ServiceTestBase.cs b/BackEnd/Timeline.Tests/Services/ServiceTestBase.cs new file mode 100644 index 00000000..5a3e1e19 --- /dev/null +++ b/BackEnd/Timeline.Tests/Services/ServiceTestBase.cs @@ -0,0 +1,71 @@ +using Microsoft.Extensions.Logging.Abstractions; +using System.Threading.Tasks; +using Timeline.Entities; +using Timeline.Services; +using Timeline.Tests.Helpers; +using Xunit; +using Xunit.Abstractions; + +namespace Timeline.Tests.Services +{ + public abstract class ServiceTestBase : IAsyncLifetime + { + protected TestDatabase TestDatabase { get; } + protected DatabaseContext Database { get; private set; } = default!; + + private readonly ITestOutputHelper? _testOutputHelper; + + protected TestClock Clock { get; } = new TestClock(); + protected UserService UserService { get; private set; } = default!; + protected TimelineService TimelineService { get; private set; } = default!; + + protected long UserId { get; private set; } + protected long AdminId { get; private set; } + + protected ServiceTestBase(bool databaseCreateUsers = true, ITestOutputHelper? testOutputHelper = null) + { + _testOutputHelper = testOutputHelper; + TestDatabase = new TestDatabase(databaseCreateUsers); + } + + protected ServiceTestBase(ITestOutputHelper? testOutputHelper) : this(true, testOutputHelper) { } + + public async Task InitializeAsync() + { + await TestDatabase.InitializeAsync(); + Database = TestDatabase.CreateContext(_testOutputHelper); + + UserService = new UserService(NullLogger.Instance, Database, new PasswordService(), Clock); + TimelineService = new TimelineService(Database, UserService, Clock); + + UserId = await UserService.GetUserIdByUsername("user"); + AdminId = await UserService.GetUserIdByUsername("admin"); + + await OnInitializeAsync(); + OnInitialize(); + } + + public async Task DisposeAsync() + { + OnDispose(); + await OnDisposeAsync(); + await Database.DisposeAsync(); + await TestDatabase.DisposeAsync(); + } + + + protected virtual void OnInitialize() { } + protected virtual void OnDispose() { } + + + protected virtual Task OnInitializeAsync() + { + return Task.CompletedTask; + } + + protected virtual Task OnDisposeAsync() + { + return Task.CompletedTask; + } + } +} diff --git a/BackEnd/Timeline.Tests/Services/UserDeleteServiceTest.cs b/BackEnd/Timeline.Tests/Services/UserDeleteServiceTest.cs index 59c0a9af..10014d2b 100644 --- a/BackEnd/Timeline.Tests/Services/UserDeleteServiceTest.cs +++ b/BackEnd/Timeline.Tests/Services/UserDeleteServiceTest.cs @@ -8,12 +8,12 @@ using Xunit; namespace Timeline.Tests.Services { - public class UserDeleteServiceTest : DatabaseBasedTest + public class UserDeleteServiceTest : ServiceTestBase { private readonly Mock _mockTimelinePostService = new Mock(); private UserDeleteService _service = default!; - protected override void OnDatabaseCreated() + protected override void OnInitialize() { _service = new UserDeleteService(NullLogger.Instance, Database, _mockTimelinePostService.Object); } diff --git a/BackEnd/Timeline.Tests/Services/UserPermissionServiceTest.cs b/BackEnd/Timeline.Tests/Services/UserPermissionServiceTest.cs index f20a7d62..0c43c025 100644 --- a/BackEnd/Timeline.Tests/Services/UserPermissionServiceTest.cs +++ b/BackEnd/Timeline.Tests/Services/UserPermissionServiceTest.cs @@ -7,7 +7,7 @@ using Xunit; namespace Timeline.Tests.Services { - public class UserPermissionServiceTest : DatabaseBasedTest + public class UserPermissionServiceTest : ServiceTestBase { private UserPermissionService _service = default!; @@ -16,7 +16,7 @@ namespace Timeline.Tests.Services } - protected override void OnDatabaseCreated() + protected override void OnInitialize() { _service = new UserPermissionService(Database); } diff --git a/BackEnd/Timeline/Services/SearchService.cs b/BackEnd/Timeline/Services/SearchService.cs index eac81fd0..680ef9e3 100644 --- a/BackEnd/Timeline/Services/SearchService.cs +++ b/BackEnd/Timeline/Services/SearchService.cs @@ -39,6 +39,9 @@ namespace Timeline.Services /// Search results. /// Thrown when is null. /// Thrown when is empty. + /// + /// Implementation should promise high score is at first. + /// Task> SearchTimeline(string query); /// @@ -48,6 +51,9 @@ namespace Timeline.Services /// Search results. /// Thrown when is null. /// Thrown when is empty. + /// + /// Implementation should promise high score is at first. + /// Task> SearchUser(string query); } -- cgit v1.2.3