From c69a18f66721404dac3a04a090d04bf248964a9f Mon Sep 17 00:00:00 2001 From: crupest Date: Wed, 25 Nov 2020 23:54:49 +0800 Subject: build: Upgrade dependency. --- BackEnd/Timeline.Tests/packages.lock.json | 50 +++++++++++++++---------------- BackEnd/Timeline/Timeline.csproj | 4 +-- BackEnd/Timeline/packages.lock.json | 50 +++++++++++++++---------------- 3 files changed, 52 insertions(+), 52 deletions(-) diff --git a/BackEnd/Timeline.Tests/packages.lock.json b/BackEnd/Timeline.Tests/packages.lock.json index 7d84213e..50b90c3c 100644 --- a/BackEnd/Timeline.Tests/packages.lock.json +++ b/BackEnd/Timeline.Tests/packages.lock.json @@ -763,8 +763,8 @@ }, "NJsonSchema": { "type": "Transitive", - "resolved": "10.2.1", - "contentHash": "/BtWbYTusyoSgQkCB4eYijMfZotB/rfASDsl1k9evlkm5vlOP4s4Y09TOzBChU77d/qUABVYL1Xf+TB8E0Wfpw==", + "resolved": "10.3.1", + "contentHash": "k5ptrRSxMy1lZXxU7dXW2Gy9Q7uPufSGtb609tfuFdo+w45UMHdBolvbWeEq482BPXhYfoBZ2uNzjJgcny2o3g==", "dependencies": { "Namotion.Reflection": "1.0.14", "Newtonsoft.Json": "9.0.1" @@ -772,56 +772,56 @@ }, "NSwag.Annotations": { "type": "Transitive", - "resolved": "13.8.2", - "contentHash": "/GO+35CjPYQTPS5/Q8udM5JAMEWVo8JsrkV2Uw3OW4/AJU9iOS7t6WJid6ZlkpLMjnW7oex9mvJ2EZNE4eOG/Q==" + "resolved": "13.9.4", + "contentHash": "qsOYnNMUJJ5VpgYmQsyNkDKbJnMaRo4lGBgkaBlZsHsWGG+HizNkx+HuHkRtI0ks28jqZXpVDxDmnuyq/SwFnw==" }, "NSwag.AspNetCore": { "type": "Transitive", - "resolved": "13.8.2", - "contentHash": "SNGlVSZoMyywBWueZBxl3B/nfaIM0fAcuNhTD/cfMKUn3Cn/Oi8d45HZY5vAPqczvppTbk4cZXyVwWDOfgiPbA==", + "resolved": "13.9.4", + "contentHash": "2+QqWsUMfwOy1pFsacA4hjVW0tmKrcGG4O1JCVhM93q7QfqbG/ndt002a/GGGyaMsuK7HRKmYJ8nmo5tzOE1tg==", "dependencies": { "Microsoft.AspNetCore.Mvc.Core": "1.0.4", "Microsoft.AspNetCore.Mvc.Formatters.Json": "1.0.4", "Microsoft.AspNetCore.StaticFiles": "1.0.4", "Microsoft.Extensions.ApiDescription.Server": "3.0.0", "Microsoft.Extensions.FileProviders.Embedded": "1.0.1", - "NSwag.Annotations": "13.8.2", - "NSwag.Core": "13.8.2", - "NSwag.Generation": "13.8.2", - "NSwag.Generation.AspNetCore": "13.8.2", + "NSwag.Annotations": "13.9.4", + "NSwag.Core": "13.9.4", + "NSwag.Generation": "13.9.4", + "NSwag.Generation.AspNetCore": "13.9.4", "System.IO.FileSystem": "4.3.0", "System.Xml.XPath.XDocument": "4.0.1" } }, "NSwag.Core": { "type": "Transitive", - "resolved": "13.8.2", - "contentHash": "Hm6pU9qFJuXLo3b27+JTXztfeuI/15Ob1sDsfUu4rchN0+bMogtn8Lia8KVbcalw/M+hXc0rWTFp5ueP23e+iA==", + "resolved": "13.9.4", + "contentHash": "iNhgBGWT5yEYL3uV6Xhla+VspVaN3NfDi+rjDugWLErU+A7uxV71D1i9OQkW37rdOFKiaixJAXPaLA6JA8c8hw==", "dependencies": { - "NJsonSchema": "10.2.1", + "NJsonSchema": "10.3.1", "Newtonsoft.Json": "9.0.1" } }, "NSwag.Generation": { "type": "Transitive", - "resolved": "13.8.2", - "contentHash": "LBIrpHFRZeMMbqL1hdyGb7r8v+T52aOCARxwfAmzE+MlOHVpjsIxyNSXht9EzBFMbSH0tj7CK2Ugo7bm+zUssg==", + "resolved": "13.9.4", + "contentHash": "Y6qqOYUEoYZRL5nYshzYn0b7Nz9Rzr6qCdVkah6mQq39Pom/XQgygaV6JR3t0dacDYg/XmVsMn++bdLPQs9rAw==", "dependencies": { - "NJsonSchema": "10.2.1", - "NSwag.Core": "13.8.2", + "NJsonSchema": "10.3.1", + "NSwag.Core": "13.9.4", "Newtonsoft.Json": "9.0.1" } }, "NSwag.Generation.AspNetCore": { "type": "Transitive", - "resolved": "13.8.2", - "contentHash": "0ydVv6OidspZ/MS6qmU8hswGtXwq5YZPg+2a2PHGD6jNp2Fef4j1wC3xa3hplDAq7cK+BgpyDKtvj9+X01+P5g==", + "resolved": "13.9.4", + "contentHash": "N0HGoPJsK67GAtNnPln0MLPnmv9wVp9Ev5sfEuWQIa/VHMkXQL6IyvItXiigDLb8VHXnqUbggU7WBg9Ay6h8oQ==", "dependencies": { "Microsoft.AspNetCore.Mvc.ApiExplorer": "1.0.4", "Microsoft.AspNetCore.Mvc.Core": "1.0.4", "Microsoft.AspNetCore.Mvc.Formatters.Json": "1.0.4", - "NJsonSchema": "10.2.1", - "NSwag.Generation": "13.8.2" + "NJsonSchema": "10.3.1", + "NSwag.Generation": "13.9.4" } }, "NuGet.Frameworks": { @@ -938,8 +938,8 @@ }, "SixLabors.ImageSharp": { "type": "Transitive", - "resolved": "1.0.1", - "contentHash": "DjLoFNdUfsDP7RhPpr5hcUhl1XiejqBML9uDWuOUwCkc0Y+sG9IJLLbqSOi9XeoWqPviwdcDm1F8nKdF0qTYIQ==" + "resolved": "1.0.2", + "contentHash": "iZJ37Iss3pUkFl961x1aka85QuvgY9oNZabHijzVnHs4QTz6EMNx3zjJDyvK/0+Ryj6JPv/PC7GVIJXLHtu2nQ==" }, "SQLitePCLRaw.bundle_e_sqlite3": { "type": "Transitive", @@ -1985,8 +1985,8 @@ "Microsoft.EntityFrameworkCore": "5.0.0", "Microsoft.EntityFrameworkCore.Analyzers": "5.0.0", "Microsoft.EntityFrameworkCore.Sqlite": "5.0.0", - "NSwag.AspNetCore": "13.8.2", - "SixLabors.ImageSharp": "1.0.1", + "NSwag.AspNetCore": "13.9.4", + "SixLabors.ImageSharp": "1.0.2", "System.IdentityModel.Tokens.Jwt": "6.8.0", "Timeline.ErrorCodes": "1.0.0" } diff --git a/BackEnd/Timeline/Timeline.csproj b/BackEnd/Timeline/Timeline.csproj index 0cb1b5ba..70536e00 100644 --- a/BackEnd/Timeline/Timeline.csproj +++ b/BackEnd/Timeline/Timeline.csproj @@ -45,8 +45,8 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/BackEnd/Timeline/packages.lock.json b/BackEnd/Timeline/packages.lock.json index 36fdbcf2..36442da7 100644 --- a/BackEnd/Timeline/packages.lock.json +++ b/BackEnd/Timeline/packages.lock.json @@ -87,28 +87,28 @@ }, "NSwag.AspNetCore": { "type": "Direct", - "requested": "[13.8.2, )", - "resolved": "13.8.2", - "contentHash": "SNGlVSZoMyywBWueZBxl3B/nfaIM0fAcuNhTD/cfMKUn3Cn/Oi8d45HZY5vAPqczvppTbk4cZXyVwWDOfgiPbA==", + "requested": "[13.9.4, )", + "resolved": "13.9.4", + "contentHash": "2+QqWsUMfwOy1pFsacA4hjVW0tmKrcGG4O1JCVhM93q7QfqbG/ndt002a/GGGyaMsuK7HRKmYJ8nmo5tzOE1tg==", "dependencies": { "Microsoft.AspNetCore.Mvc.Core": "1.0.4", "Microsoft.AspNetCore.Mvc.Formatters.Json": "1.0.4", "Microsoft.AspNetCore.StaticFiles": "1.0.4", "Microsoft.Extensions.ApiDescription.Server": "3.0.0", "Microsoft.Extensions.FileProviders.Embedded": "1.0.1", - "NSwag.Annotations": "13.8.2", - "NSwag.Core": "13.8.2", - "NSwag.Generation": "13.8.2", - "NSwag.Generation.AspNetCore": "13.8.2", + "NSwag.Annotations": "13.9.4", + "NSwag.Core": "13.9.4", + "NSwag.Generation": "13.9.4", + "NSwag.Generation.AspNetCore": "13.9.4", "System.IO.FileSystem": "4.3.0", "System.Xml.XPath.XDocument": "4.0.1" } }, "SixLabors.ImageSharp": { "type": "Direct", - "requested": "[1.0.1, )", - "resolved": "1.0.1", - "contentHash": "DjLoFNdUfsDP7RhPpr5hcUhl1XiejqBML9uDWuOUwCkc0Y+sG9IJLLbqSOi9XeoWqPviwdcDm1F8nKdF0qTYIQ==" + "requested": "[1.0.2, )", + "resolved": "1.0.2", + "contentHash": "iZJ37Iss3pUkFl961x1aka85QuvgY9oNZabHijzVnHs4QTz6EMNx3zjJDyvK/0+Ryj6JPv/PC7GVIJXLHtu2nQ==" }, "System.IdentityModel.Tokens.Jwt": { "type": "Direct", @@ -648,8 +648,8 @@ }, "NJsonSchema": { "type": "Transitive", - "resolved": "10.2.1", - "contentHash": "/BtWbYTusyoSgQkCB4eYijMfZotB/rfASDsl1k9evlkm5vlOP4s4Y09TOzBChU77d/qUABVYL1Xf+TB8E0Wfpw==", + "resolved": "10.3.1", + "contentHash": "k5ptrRSxMy1lZXxU7dXW2Gy9Q7uPufSGtb609tfuFdo+w45UMHdBolvbWeEq482BPXhYfoBZ2uNzjJgcny2o3g==", "dependencies": { "Namotion.Reflection": "1.0.14", "Newtonsoft.Json": "9.0.1" @@ -657,38 +657,38 @@ }, "NSwag.Annotations": { "type": "Transitive", - "resolved": "13.8.2", - "contentHash": "/GO+35CjPYQTPS5/Q8udM5JAMEWVo8JsrkV2Uw3OW4/AJU9iOS7t6WJid6ZlkpLMjnW7oex9mvJ2EZNE4eOG/Q==" + "resolved": "13.9.4", + "contentHash": "qsOYnNMUJJ5VpgYmQsyNkDKbJnMaRo4lGBgkaBlZsHsWGG+HizNkx+HuHkRtI0ks28jqZXpVDxDmnuyq/SwFnw==" }, "NSwag.Core": { "type": "Transitive", - "resolved": "13.8.2", - "contentHash": "Hm6pU9qFJuXLo3b27+JTXztfeuI/15Ob1sDsfUu4rchN0+bMogtn8Lia8KVbcalw/M+hXc0rWTFp5ueP23e+iA==", + "resolved": "13.9.4", + "contentHash": "iNhgBGWT5yEYL3uV6Xhla+VspVaN3NfDi+rjDugWLErU+A7uxV71D1i9OQkW37rdOFKiaixJAXPaLA6JA8c8hw==", "dependencies": { - "NJsonSchema": "10.2.1", + "NJsonSchema": "10.3.1", "Newtonsoft.Json": "9.0.1" } }, "NSwag.Generation": { "type": "Transitive", - "resolved": "13.8.2", - "contentHash": "LBIrpHFRZeMMbqL1hdyGb7r8v+T52aOCARxwfAmzE+MlOHVpjsIxyNSXht9EzBFMbSH0tj7CK2Ugo7bm+zUssg==", + "resolved": "13.9.4", + "contentHash": "Y6qqOYUEoYZRL5nYshzYn0b7Nz9Rzr6qCdVkah6mQq39Pom/XQgygaV6JR3t0dacDYg/XmVsMn++bdLPQs9rAw==", "dependencies": { - "NJsonSchema": "10.2.1", - "NSwag.Core": "13.8.2", + "NJsonSchema": "10.3.1", + "NSwag.Core": "13.9.4", "Newtonsoft.Json": "9.0.1" } }, "NSwag.Generation.AspNetCore": { "type": "Transitive", - "resolved": "13.8.2", - "contentHash": "0ydVv6OidspZ/MS6qmU8hswGtXwq5YZPg+2a2PHGD6jNp2Fef4j1wC3xa3hplDAq7cK+BgpyDKtvj9+X01+P5g==", + "resolved": "13.9.4", + "contentHash": "N0HGoPJsK67GAtNnPln0MLPnmv9wVp9Ev5sfEuWQIa/VHMkXQL6IyvItXiigDLb8VHXnqUbggU7WBg9Ay6h8oQ==", "dependencies": { "Microsoft.AspNetCore.Mvc.ApiExplorer": "1.0.4", "Microsoft.AspNetCore.Mvc.Core": "1.0.4", "Microsoft.AspNetCore.Mvc.Formatters.Json": "1.0.4", - "NJsonSchema": "10.2.1", - "NSwag.Generation": "13.8.2" + "NJsonSchema": "10.3.1", + "NSwag.Generation": "13.9.4" } }, "runtime.native.System": { -- cgit v1.2.3 From 080330966333fe61b6a9d5413c6b05b9ea77f4dc Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 26 Nov 2020 20:02:03 +0800 Subject: feat: Add highlight timeline entity and service. --- .../Services/HighlightTimelineServiceTest.cs | 22 ++++ BackEnd/Timeline/Entities/DatabaseContext.cs | 2 + .../Timeline/Entities/HighlightTimelineEntity.cs | 24 +++++ .../Exceptions/TimelineNotExistException.cs | 7 +- .../Timeline/Services/HighlightTimelineService.cs | 112 +++++++++++++++++++++ BackEnd/Timeline/Services/TimelineService.cs | 43 ++++++++ BackEnd/Timeline/Services/UserService.cs | 12 +++ 7 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs create mode 100644 BackEnd/Timeline/Entities/HighlightTimelineEntity.cs create mode 100644 BackEnd/Timeline/Services/HighlightTimelineService.cs diff --git a/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs b/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs new file mode 100644 index 00000000..950fa974 --- /dev/null +++ b/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Services; + +namespace Timeline.Tests.Services +{ + public class HighlightTimelineServiceTest : DatabaseBasedTest + { + private UserService _userService; + private TimelineService _timelineService; + + private HighlightTimelineService _service; + + protected override void OnDatabaseCreated() + { + + } + + } +} diff --git a/BackEnd/Timeline/Entities/DatabaseContext.cs b/BackEnd/Timeline/Entities/DatabaseContext.cs index e4203392..4205c2cf 100644 --- a/BackEnd/Timeline/Entities/DatabaseContext.cs +++ b/BackEnd/Timeline/Entities/DatabaseContext.cs @@ -29,6 +29,8 @@ namespace Timeline.Entities public DbSet Timelines { get; set; } = default!; public DbSet TimelinePosts { get; set; } = default!; public DbSet TimelineMembers { get; set; } = default!; + public DbSet HighlightTimelines { get; set; } = default!; + public DbSet JwtToken { get; set; } = default!; public DbSet Data { get; set; } = default!; } diff --git a/BackEnd/Timeline/Entities/HighlightTimelineEntity.cs b/BackEnd/Timeline/Entities/HighlightTimelineEntity.cs new file mode 100644 index 00000000..0a38c8a6 --- /dev/null +++ b/BackEnd/Timeline/Entities/HighlightTimelineEntity.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Timeline.Entities +{ + [Table("highlight_timelines")] + public record HighlightTimelineEntity + { + [Key, Column("id"), DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public long Id { get; set; } + + [Column("timeline_id")] + public long TimelineId { get; set; } + + [ForeignKey(nameof(TimelineId))] + public TimelineEntity Timeline { get; set; } = default!; + + [Column("operator_id")] + public long? OperatorId { get; set; } + + [ForeignKey(nameof(OperatorId))] + public UserEntity? Operator { get; set; } + } +} diff --git a/BackEnd/Timeline/Services/Exceptions/TimelineNotExistException.cs b/BackEnd/Timeline/Services/Exceptions/TimelineNotExistException.cs index 70970b24..ef882ffe 100644 --- a/BackEnd/Timeline/Services/Exceptions/TimelineNotExistException.cs +++ b/BackEnd/Timeline/Services/Exceptions/TimelineNotExistException.cs @@ -6,7 +6,11 @@ namespace Timeline.Services.Exceptions [Serializable] public class TimelineNotExistException : EntityNotExistException { - public TimelineNotExistException() : this(null, null) { } + public TimelineNotExistException() : this((long?)null) { } + public TimelineNotExistException(long? id) : this(id, null) { } + public TimelineNotExistException(long? id, Exception? inner) : this(id, null, inner) { } + public TimelineNotExistException(long? id, string? message, Exception? inner) : base(EntityNames.Timeline, null, message, inner) { TimelineId = id; } + public TimelineNotExistException(string? timelineName) : this(timelineName, null) { } public TimelineNotExistException(string? timelineName, Exception? inner) : this(timelineName, null, inner) { } public TimelineNotExistException(string? timelineName, string? message, Exception? inner = null) @@ -17,5 +21,6 @@ namespace Timeline.Services.Exceptions System.Runtime.Serialization.StreamingContext context) : base(info, context) { } public string? TimelineName { get; set; } + public long? TimelineId { get; set; } } } diff --git a/BackEnd/Timeline/Services/HighlightTimelineService.cs b/BackEnd/Timeline/Services/HighlightTimelineService.cs new file mode 100644 index 00000000..7528d9b0 --- /dev/null +++ b/BackEnd/Timeline/Services/HighlightTimelineService.cs @@ -0,0 +1,112 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Entities; +using Timeline.Services.Exceptions; + +namespace Timeline.Services +{ + public interface IHighlightTimelineService + { + /// + /// Get all highlight timelines. + /// + /// A list of all highlight timelines. + Task> GetHighlightTimelines(); + + /// + /// Add a timeline to highlight list. + /// + /// The timeline name. + /// The user id of operator. + /// Thrown when is null. + /// Thrown when is not a valid timeline name. + /// Thrown when timeline with given name does not exist. + /// Thrown when user with given operator id does not exist. + Task AddHighlightTimeline(string timelineName, long? operatorId); + + /// + /// Remove a timeline from highlight list. + /// + /// The timeline name. + /// The user id of operator. + /// True if deletion is actually performed. Otherwise false (timeline was not in the list). + /// Thrown when is null. + /// Thrown when is not a valid timeline name. + /// Thrown when timeline with given name does not exist. + /// Thrown when user with given operator id does not exist. + Task RemoveHighlightTimeline(string timelineName, long? operatorId); + } + + public class HighlightTimelineService : IHighlightTimelineService + { + private readonly DatabaseContext _database; + private readonly IUserService _userService; + private readonly ITimelineService _timelineService; + + public HighlightTimelineService(DatabaseContext database, IUserService userService, ITimelineService timelineService) + { + _database = database; + _userService = userService; + _timelineService = timelineService; + } + + public async Task AddHighlightTimeline(string timelineName, long? operatorId) + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + var timelineId = await _timelineService.GetTimelineIdByName(timelineName); + + if (operatorId.HasValue && !await _userService.CheckUserExistence(operatorId.Value)) + { + throw new UserNotExistException(null, operatorId.Value, "User with given operator id does not exist.", null); + } + + var alreadyIs = await _database.HighlightTimelines.AnyAsync(t => t.TimelineId == timelineId); + + if (alreadyIs) return; + + _database.HighlightTimelines.Add(new HighlightTimelineEntity { TimelineId = timelineId, OperatorId = operatorId }); + await _database.SaveChangesAsync(); + } + + public async Task> GetHighlightTimelines() + { + var entities = await _database.HighlightTimelines.Select(t => new { t.Id }).ToListAsync(); + + var result = new List(); + + foreach (var entity in entities) + { + result.Add(await _timelineService.GetTimelineById(entity.Id)); + } + + return result; + } + + public async Task RemoveHighlightTimeline(string timelineName, long? operatorId) + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + var timelineId = await _timelineService.GetTimelineIdByName(timelineName); + + if (operatorId.HasValue && !await _userService.CheckUserExistence(operatorId.Value)) + { + throw new UserNotExistException(null, operatorId.Value, "User with given operator id does not exist.", null); + } + + var entity = await _database.HighlightTimelines.SingleOrDefaultAsync(t => t.TimelineId == timelineId); + + if (entity == null) return false; + + _database.HighlightTimelines.Remove(entity); + await _database.SaveChangesAsync(); + + return true; + } + } +} diff --git a/BackEnd/Timeline/Services/TimelineService.cs b/BackEnd/Timeline/Services/TimelineService.cs index 769e8bed..f8c729bf 100644 --- a/BackEnd/Timeline/Services/TimelineService.cs +++ b/BackEnd/Timeline/Services/TimelineService.cs @@ -79,6 +79,19 @@ namespace Timeline.Services /// Task GetTimelineLastModifiedTime(string timelineName); + /// + /// Get the timeline id by name. + /// + /// Timeline name. + /// Id of the timeline. + /// Thrown when is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + Task GetTimelineIdByName(string timelineName); + /// /// Get the timeline unique id. /// @@ -105,6 +118,14 @@ namespace Timeline.Services /// Task GetTimeline(string timelineName); + /// + /// Get timeline by id. + /// + /// Id of timeline. + /// The timeline. + /// Thrown when timeline with given id does not exist. + Task GetTimelineById(long id); + /// /// Set the properties of a timeline. /// @@ -572,6 +593,18 @@ namespace Timeline.Services return timelineEntity.UniqueId; } + public async Task GetTimelineIdByName(string timelineName) + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + var timelineId = await FindTimelineId(timelineName); + + var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.Id }).SingleAsync(); + + return timelineEntity.Id; + } + public async Task GetTimeline(string timelineName) { if (timelineName == null) @@ -584,6 +617,16 @@ namespace Timeline.Services return await MapTimelineFromEntity(timelineEntity); } + public async Task GetTimelineById(long id) + { + var timelineEntity = await _database.Timelines.Where(t => t.Id == id).Include(t => t.Members).SingleOrDefaultAsync(); + + if (timelineEntity is null) + throw new TimelineNotExistException(id); + + return await MapTimelineFromEntity(timelineEntity); + } + public async Task> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false) { modifiedSince = modifiedSince?.MyToUtc(); diff --git a/BackEnd/Timeline/Services/UserService.cs b/BackEnd/Timeline/Services/UserService.cs index 2c5644cd..76c24666 100644 --- a/BackEnd/Timeline/Services/UserService.cs +++ b/BackEnd/Timeline/Services/UserService.cs @@ -38,6 +38,13 @@ namespace Timeline.Services /// Thrown when password is wrong. Task VerifyCredential(string username, string password); + /// + /// Check if a user exists. + /// + /// The id of the user. + /// True if exists. Otherwise false. + Task CheckUserExistence(long id); + /// /// Try to get a user by id. /// @@ -188,6 +195,11 @@ namespace Timeline.Services return await CreateUserFromEntity(entity); } + public async Task CheckUserExistence(long id) + { + return await _databaseContext.Users.AnyAsync(u => u.Id == id); + } + public async Task GetUser(long id) { var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); -- cgit v1.2.3 From 43ac8b704e47e05d259f35d0a9cdb4de6c787ee5 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 26 Nov 2020 21:04:42 +0800 Subject: refactor: ... --- BackEnd/Timeline.Tests/GlobalSuppressions.cs | 25 +- .../Services/TimelinePostServiceTest.cs | 200 +++++++ .../Timeline.Tests/Services/TimelineServiceTest.cs | 170 +----- .../Services/UserDeleteServiceTest.cs | 4 +- BackEnd/Timeline/Controllers/TimelineController.cs | 18 +- BackEnd/Timeline/Services/BasicTimelineService.cs | 122 +++++ BackEnd/Timeline/Services/BasicUserService.cs | 66 +++ .../Timeline/Services/HighlightTimelineService.cs | 4 +- BackEnd/Timeline/Services/TimelinePostService.cs | 493 +++++++++++++++++ BackEnd/Timeline/Services/TimelineService.cs | 588 +-------------------- BackEnd/Timeline/Services/UserDeleteService.cs | 8 +- BackEnd/Timeline/Services/UserService.cs | 41 +- BackEnd/Timeline/Startup.cs | 18 +- 13 files changed, 939 insertions(+), 818 deletions(-) create mode 100644 BackEnd/Timeline.Tests/Services/TimelinePostServiceTest.cs create mode 100644 BackEnd/Timeline/Services/BasicTimelineService.cs create mode 100644 BackEnd/Timeline/Services/BasicUserService.cs create mode 100644 BackEnd/Timeline/Services/TimelinePostService.cs diff --git a/BackEnd/Timeline.Tests/GlobalSuppressions.cs b/BackEnd/Timeline.Tests/GlobalSuppressions.cs index 0f873033..da9481f1 100644 --- a/BackEnd/Timeline.Tests/GlobalSuppressions.cs +++ b/BackEnd/Timeline.Tests/GlobalSuppressions.cs @@ -3,14 +3,17 @@ // Project-level suppressions either have no target or are given // a specific target and scoped to a namespace, type, member, etc. -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "This is not a UI application.")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Tests name have underscores.")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Test may catch all exceptions.")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "Test classes can be nested.")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "This is redundant.")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1063:Implement IDisposable Correctly", Justification = "Test classes do not need to implement it that way.")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = "Test classes do not need to implement it that way.")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2234:Pass system uri objects instead of strings", Justification = "I really don't understand this rule.")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Tests do not need make strings resources.")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1054:Uri parameters should not be strings", Justification = "That's unnecessary.")] -[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1056:Uri properties should not be strings", Justification = "That's unnecessary.")] +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "This is not a UI application.")] +[assembly: SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Tests name have underscores.")] +[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Test may catch all exceptions.")] +[assembly: SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "Test classes can be nested.")] +[assembly: SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "This is redundant.")] +[assembly: SuppressMessage("Design", "CA1063:Implement IDisposable Correctly", Justification = "Test classes do not need to implement it that way.")] +[assembly: SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = "Test classes do not need to implement it that way.")] +[assembly: SuppressMessage("Usage", "CA2234:Pass system uri objects instead of strings", Justification = "I really don't understand this rule.")] +[assembly: SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Tests do not need make strings resources.")] +[assembly: SuppressMessage("Design", "CA1054:Uri parameters should not be strings", Justification = "That's unnecessary.")] +[assembly: SuppressMessage("Design", "CA1056:Uri properties should not be strings", Justification = "That's unnecessary.")] +[assembly: SuppressMessage("Design", "CA1001:Types that own disposable fields should be disposable", Justification = "We use another contract like IAsyncLifetime.")] diff --git a/BackEnd/Timeline.Tests/Services/TimelinePostServiceTest.cs b/BackEnd/Timeline.Tests/Services/TimelinePostServiceTest.cs new file mode 100644 index 00000000..97512be5 --- /dev/null +++ b/BackEnd/Timeline.Tests/Services/TimelinePostServiceTest.cs @@ -0,0 +1,200 @@ +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using System; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Models; +using Timeline.Services; +using Timeline.Tests.Helpers; +using Xunit; + +namespace Timeline.Tests.Services +{ + public class TimelinePostServiceTest : DatabaseBasedTest + { + private readonly PasswordService _passwordService = new PasswordService(); + + private readonly ETagGenerator _eTagGenerator = new ETagGenerator(); + + private readonly ImageValidator _imageValidator = new ImageValidator(); + + private readonly TestClock _clock = new TestClock(); + + private DataManager _dataManager = default!; + + private UserPermissionService _userPermissionService = default!; + + private UserService _userService = default!; + + private TimelineService _timelineService = default!; + + private TimelinePostService _timelinePostService = default!; + + private UserDeleteService _userDeleteService = default!; + + protected override void OnDatabaseCreated() + { + _dataManager = new DataManager(Database, _eTagGenerator); + _userPermissionService = new UserPermissionService(Database); + _userService = new UserService(NullLogger.Instance, Database, _passwordService, _clock, _userPermissionService); + _timelineService = new TimelineService(Database, _userService, _clock); + _timelinePostService = new TimelinePostService(NullLogger.Instance, Database, _timelineService, _userService, _dataManager, _imageValidator, _clock); + _userDeleteService = new UserDeleteService(NullLogger.Instance, Database, _timelinePostService); + } + + protected override void BeforeDatabaseDestroy() + { + _eTagGenerator.Dispose(); + } + + [Theory] + [InlineData("@user")] + [InlineData("tl")] + public async Task GetPosts_ModifiedSince(string timelineName) + { + _clock.ForwardCurrentTime(); + + var userId = await _userService.GetUserIdByUsername("user"); + + var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); + if (!isPersonal) + await _timelineService.CreateTimeline(timelineName, userId); + + var postContentList = new string[] { "a", "b", "c", "d" }; + + DateTime testPoint = new DateTime(); + + foreach (var (content, index) in postContentList.Select((v, i) => (v, i))) + { + var t = _clock.ForwardCurrentTime(); + if (index == 1) + testPoint = t; + await _timelinePostService.CreateTextPost(timelineName, userId, content, null); + } + + var posts = await _timelinePostService.GetPosts(timelineName, testPoint); + posts.Should().HaveCount(3) + .And.Subject.Select(p => ((TextTimelinePostContent)p.Content!).Text).Should().Equal(postContentList.Skip(1)); + } + + [Theory] + [InlineData("@user")] + [InlineData("tl")] + public async Task GetPosts_IncludeDeleted(string timelineName) + { + var userId = await _userService.GetUserIdByUsername("user"); + + var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); + if (!isPersonal) + await _timelineService.CreateTimeline(timelineName, userId); + + var postContentList = new string[] { "a", "b", "c", "d" }; + + foreach (var content in postContentList) + { + await _timelinePostService.CreateTextPost(timelineName, userId, content, null); + } + + var posts = await _timelinePostService.GetPosts(timelineName); + posts.Should().HaveCount(4); + posts.Select(p => p.Deleted).Should().Equal(Enumerable.Repeat(false, posts.Count)); + posts.Select(p => ((TextTimelinePostContent)p.Content!).Text).Should().Equal(postContentList); + + foreach (var id in new long[] { posts[0].Id, posts[2].Id }) + { + await _timelinePostService.DeletePost(timelineName, id); + } + + posts = await _timelinePostService.GetPosts(timelineName); + posts.Should().HaveCount(2); + posts.Select(p => p.Deleted).Should().Equal(Enumerable.Repeat(false, posts.Count)); + posts.Select(p => ((TextTimelinePostContent)p.Content!).Text).Should().Equal(new string[] { "b", "d" }); + + posts = await _timelinePostService.GetPosts(timelineName, includeDeleted: true); + posts.Should().HaveCount(4); + posts.Select(p => p.Deleted).Should().Equal(new bool[] { true, false, true, false }); + posts.Where(p => !p.Deleted).Select(p => ((TextTimelinePostContent)p.Content!).Text).Should().Equal(new string[] { "b", "d" }); + } + + [Theory] + [InlineData("@admin")] + [InlineData("tl")] + public async Task GetPosts_ModifiedSince_UsernameChange(string timelineName) + { + var time1 = _clock.ForwardCurrentTime(); + + var userId = await _userService.GetUserIdByUsername("user"); + + var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); + if (!isPersonal) + await _timelineService.CreateTimeline(timelineName, userId); + + var postContentList = new string[] { "a", "b", "c", "d" }; + + foreach (var (content, index) in postContentList.Select((v, i) => (v, i))) + { + await _timelinePostService.CreateTextPost(timelineName, userId, content, null); + } + + var time2 = _clock.ForwardCurrentTime(); + + { + var posts = await _timelinePostService.GetPosts(timelineName, time2); + posts.Should().HaveCount(0); + } + + { + await _userService.ModifyUser(userId, new ModifyUserParams { Nickname = "haha" }); + var posts = await _timelinePostService.GetPosts(timelineName, time2); + posts.Should().HaveCount(0); + } + + { + await _userService.ModifyUser(userId, new ModifyUserParams { Username = "haha" }); + var posts = await _timelinePostService.GetPosts(timelineName, time2); + posts.Should().HaveCount(4); + } + } + + [Theory] + [InlineData("@admin")] + [InlineData("tl")] + public async Task GetPosts_ModifiedSince_UserDelete(string timelineName) + { + var time1 = _clock.ForwardCurrentTime(); + + var userId = await _userService.GetUserIdByUsername("user"); + var adminId = await _userService.GetUserIdByUsername("admin"); + + var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); + if (!isPersonal) + await _timelineService.CreateTimeline(timelineName, adminId); + + var postContentList = new string[] { "a", "b", "c", "d" }; + + foreach (var (content, index) in postContentList.Select((v, i) => (v, i))) + { + await _timelinePostService.CreateTextPost(timelineName, userId, content, null); + } + + var time2 = _clock.ForwardCurrentTime(); + + { + var posts = await _timelinePostService.GetPosts(timelineName, time2); + posts.Should().HaveCount(0); + } + + await _userDeleteService.DeleteUser("user"); + + { + var posts = await _timelinePostService.GetPosts(timelineName, time2); + posts.Should().HaveCount(0); + } + + { + var posts = await _timelinePostService.GetPosts(timelineName, time2, true); + posts.Should().HaveCount(4); + } + } + } +} diff --git a/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs b/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs index 5f2c20e8..fac0b6f3 100644 --- a/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs +++ b/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.Logging.Abstractions; using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Timeline.Models; using Timeline.Services; @@ -12,38 +11,23 @@ using Xunit; namespace Timeline.Tests.Services { - public class TimelineServiceTest : DatabaseBasedTest, IDisposable + public class TimelineServiceTest : DatabaseBasedTest { private readonly PasswordService _passwordService = new PasswordService(); - private readonly ETagGenerator _eTagGenerator = new ETagGenerator(); - - private readonly ImageValidator _imageValidator = new ImageValidator(); - private readonly TestClock _clock = new TestClock(); - private DataManager _dataManager = default!; - private UserPermissionService _userPermissionService = default!; private UserService _userService = default!; private TimelineService _timelineService = default!; - private UserDeleteService _userDeleteService = default!; - protected override void OnDatabaseCreated() { - _dataManager = new DataManager(Database, _eTagGenerator); _userPermissionService = new UserPermissionService(Database); _userService = new UserService(NullLogger.Instance, Database, _passwordService, _clock, _userPermissionService); - _timelineService = new TimelineService(NullLogger.Instance, Database, _dataManager, _userService, _imageValidator, _clock); - _userDeleteService = new UserDeleteService(NullLogger.Instance, Database, _timelineService); - } - - public void Dispose() - { - _eTagGenerator.Dispose(); + _timelineService = new TimelineService(Database, _userService, _clock); } [Theory] @@ -109,156 +93,6 @@ namespace Timeline.Tests.Services await GetAndCheck(); } - [Theory] - [InlineData("@user")] - [InlineData("tl")] - public async Task GetPosts_ModifiedSince(string timelineName) - { - _clock.ForwardCurrentTime(); - - var userId = await _userService.GetUserIdByUsername("user"); - - var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); - if (!isPersonal) - await _timelineService.CreateTimeline(timelineName, userId); - - var postContentList = new string[] { "a", "b", "c", "d" }; - - DateTime testPoint = new DateTime(); - - foreach (var (content, index) in postContentList.Select((v, i) => (v, i))) - { - var t = _clock.ForwardCurrentTime(); - if (index == 1) - testPoint = t; - await _timelineService.CreateTextPost(timelineName, userId, content, null); - } - - var posts = await _timelineService.GetPosts(timelineName, testPoint); - posts.Should().HaveCount(3) - .And.Subject.Select(p => ((TextTimelinePostContent)p.Content!).Text).Should().Equal(postContentList.Skip(1)); - } - - [Theory] - [InlineData("@user")] - [InlineData("tl")] - public async Task GetPosts_IncludeDeleted(string timelineName) - { - var userId = await _userService.GetUserIdByUsername("user"); - - var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); - if (!isPersonal) - await _timelineService.CreateTimeline(timelineName, userId); - - var postContentList = new string[] { "a", "b", "c", "d" }; - - foreach (var content in postContentList) - { - await _timelineService.CreateTextPost(timelineName, userId, content, null); - } - - var posts = await _timelineService.GetPosts(timelineName); - posts.Should().HaveCount(4); - posts.Select(p => p.Deleted).Should().Equal(Enumerable.Repeat(false, posts.Count)); - posts.Select(p => ((TextTimelinePostContent)p.Content!).Text).Should().Equal(postContentList); - - foreach (var id in new long[] { posts[0].Id, posts[2].Id }) - { - await _timelineService.DeletePost(timelineName, id); - } - - posts = await _timelineService.GetPosts(timelineName); - posts.Should().HaveCount(2); - posts.Select(p => p.Deleted).Should().Equal(Enumerable.Repeat(false, posts.Count)); - posts.Select(p => ((TextTimelinePostContent)p.Content!).Text).Should().Equal(new string[] { "b", "d" }); - - posts = await _timelineService.GetPosts(timelineName, includeDeleted: true); - posts.Should().HaveCount(4); - posts.Select(p => p.Deleted).Should().Equal(new bool[] { true, false, true, false }); - posts.Where(p => !p.Deleted).Select(p => ((TextTimelinePostContent)p.Content!).Text).Should().Equal(new string[] { "b", "d" }); - } - - [Theory] - [InlineData("@admin")] - [InlineData("tl")] - public async Task GetPosts_ModifiedSince_UsernameChange(string timelineName) - { - var time1 = _clock.ForwardCurrentTime(); - - var userId = await _userService.GetUserIdByUsername("user"); - - var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); - if (!isPersonal) - await _timelineService.CreateTimeline(timelineName, userId); - - var postContentList = new string[] { "a", "b", "c", "d" }; - - foreach (var (content, index) in postContentList.Select((v, i) => (v, i))) - { - await _timelineService.CreateTextPost(timelineName, userId, content, null); - } - - var time2 = _clock.ForwardCurrentTime(); - - { - var posts = await _timelineService.GetPosts(timelineName, time2); - posts.Should().HaveCount(0); - } - - { - await _userService.ModifyUser(userId, new ModifyUserParams { Nickname = "haha" }); - var posts = await _timelineService.GetPosts(timelineName, time2); - posts.Should().HaveCount(0); - } - - { - await _userService.ModifyUser(userId, new ModifyUserParams { Username = "haha" }); - var posts = await _timelineService.GetPosts(timelineName, time2); - posts.Should().HaveCount(4); - } - } - - [Theory] - [InlineData("@admin")] - [InlineData("tl")] - public async Task GetPosts_ModifiedSince_UserDelete(string timelineName) - { - var time1 = _clock.ForwardCurrentTime(); - - var userId = await _userService.GetUserIdByUsername("user"); - var adminId = await _userService.GetUserIdByUsername("admin"); - - var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); - if (!isPersonal) - await _timelineService.CreateTimeline(timelineName, adminId); - - var postContentList = new string[] { "a", "b", "c", "d" }; - - foreach (var (content, index) in postContentList.Select((v, i) => (v, i))) - { - await _timelineService.CreateTextPost(timelineName, userId, content, null); - } - - var time2 = _clock.ForwardCurrentTime(); - - { - var posts = await _timelineService.GetPosts(timelineName, time2); - posts.Should().HaveCount(0); - } - - await _userDeleteService.DeleteUser("user"); - - { - var posts = await _timelineService.GetPosts(timelineName, time2); - posts.Should().HaveCount(0); - } - - { - var posts = await _timelineService.GetPosts(timelineName, time2, true); - posts.Should().HaveCount(4); - } - } - [Theory] [InlineData("@admin")] [InlineData("tl")] diff --git a/BackEnd/Timeline.Tests/Services/UserDeleteServiceTest.cs b/BackEnd/Timeline.Tests/Services/UserDeleteServiceTest.cs index be11564e..59c0a9af 100644 --- a/BackEnd/Timeline.Tests/Services/UserDeleteServiceTest.cs +++ b/BackEnd/Timeline.Tests/Services/UserDeleteServiceTest.cs @@ -10,12 +10,12 @@ namespace Timeline.Tests.Services { public class UserDeleteServiceTest : DatabaseBasedTest { - private readonly Mock _mockTimelineService = new Mock(); + private readonly Mock _mockTimelinePostService = new Mock(); private UserDeleteService _service = default!; protected override void OnDatabaseCreated() { - _service = new UserDeleteService(NullLogger.Instance, Database, _mockTimelineService.Object); + _service = new UserDeleteService(NullLogger.Instance, Database, _mockTimelinePostService.Object); } [Fact] diff --git a/BackEnd/Timeline/Controllers/TimelineController.cs b/BackEnd/Timeline/Controllers/TimelineController.cs index 45060b5d..0ffadc50 100644 --- a/BackEnd/Timeline/Controllers/TimelineController.cs +++ b/BackEnd/Timeline/Controllers/TimelineController.cs @@ -29,17 +29,19 @@ namespace Timeline.Controllers private readonly IUserService _userService; private readonly ITimelineService _service; + private readonly ITimelinePostService _postService; private readonly IMapper _mapper; /// /// /// - public TimelineController(ILogger logger, IUserService userService, ITimelineService service, IMapper mapper) + public TimelineController(ILogger logger, IUserService userService, ITimelineService service, ITimelinePostService timelinePostService, IMapper mapper) { _logger = logger; _userService = userService; _service = service; + _postService = timelinePostService; _mapper = mapper; } @@ -187,7 +189,7 @@ namespace Timeline.Controllers return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } - List posts = await _service.GetPosts(name, modifiedSince, includeDeleted ?? false); + List posts = await _postService.GetPosts(name, modifiedSince, includeDeleted ?? false); var result = _mapper.Map>(posts); return result; @@ -217,9 +219,9 @@ namespace Timeline.Controllers try { - return await DataCacheHelper.GenerateActionResult(this, () => _service.GetPostDataETag(name, id), async () => + return await DataCacheHelper.GenerateActionResult(this, () => _postService.GetPostDataETag(name, id), async () => { - var data = await _service.GetPostData(name, id); + var data = await _postService.GetPostData(name, id); return data; }); } @@ -264,7 +266,7 @@ namespace Timeline.Controllers { return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_TextContentTextRequired)); } - post = await _service.CreateTextPost(name, id, text, body.Time); + post = await _postService.CreateTextPost(name, id, text, body.Time); } else if (content.Type == TimelinePostContentTypes.Image) { @@ -285,7 +287,7 @@ namespace Timeline.Controllers try { - post = await _service.CreateImagePost(name, id, data, body.Time); + post = await _postService.CreateImagePost(name, id, data, body.Time); } catch (ImageException) { @@ -315,13 +317,13 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task> PostDelete([FromRoute][GeneralTimelineName] string name, [FromRoute] long id) { - if (!UserHasAllTimelineManagementPermission && !await _service.HasPostModifyPermission(name, id, this.GetUserId())) + if (!UserHasAllTimelineManagementPermission && !await _postService.HasPostModifyPermission(name, id, this.GetUserId())) { return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } try { - await _service.DeletePost(name, id); + await _postService.DeletePost(name, id); return CommonDeleteResponse.Delete(); } catch (TimelinePostNotExistException) diff --git a/BackEnd/Timeline/Services/BasicTimelineService.cs b/BackEnd/Timeline/Services/BasicTimelineService.cs new file mode 100644 index 00000000..0d9f64a9 --- /dev/null +++ b/BackEnd/Timeline/Services/BasicTimelineService.cs @@ -0,0 +1,122 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Entities; +using Timeline.Models; +using Timeline.Models.Validation; +using Timeline.Services.Exceptions; + +namespace Timeline.Services +{ + /// + /// This service provide some basic timeline functions, which should be used internally for other services. + /// + public interface IBasicTimelineService + { + /// + /// Get the timeline id by name. + /// + /// Timeline name. + /// Id of the timeline. + /// Thrown when is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + /// + /// If name is of personal timeline and the timeline does not exist, it will be created if user exists. + /// If the user does not exist, will be thrown with as inner exception. + /// + Task GetTimelineIdByName(string timelineName); + } + + + public class BasicTimelineService : IBasicTimelineService + { + private readonly DatabaseContext _database; + + private readonly IBasicUserService _basicUserService; + private readonly IClock _clock; + + private readonly GeneralTimelineNameValidator _generalTimelineNameValidator = new GeneralTimelineNameValidator(); + + public BasicTimelineService(DatabaseContext database, IBasicUserService basicUserService, IClock clock) + { + _database = database; + _basicUserService = basicUserService; + _clock = clock; + } + + protected TimelineEntity CreateNewTimelineEntity(string? name, long ownerId) + { + var currentTime = _clock.GetCurrentTime(); + + return new TimelineEntity + { + Name = name, + NameLastModified = currentTime, + OwnerId = ownerId, + Visibility = TimelineVisibility.Register, + CreateTime = currentTime, + LastModified = currentTime, + CurrentPostLocalId = 0, + Members = new List() + }; + } + + public async Task GetTimelineIdByName(string timelineName) + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + if (!_generalTimelineNameValidator.Validate(timelineName, out var message)) + throw new ArgumentException(message); + + timelineName = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); + + if (isPersonal) + { + long userId; + try + { + userId = await _basicUserService.GetUserIdByUsername(timelineName); + } + catch (UserNotExistException e) + { + throw new TimelineNotExistException(timelineName, e); + } + + var timelineEntity = await _database.Timelines.Where(t => t.OwnerId == userId && t.Name == null).Select(t => new { t.Id }).SingleOrDefaultAsync(); + + if (timelineEntity != null) + { + return timelineEntity.Id; + } + else + { + var newTimelineEntity = CreateNewTimelineEntity(null, userId); + _database.Timelines.Add(newTimelineEntity); + await _database.SaveChangesAsync(); + + return newTimelineEntity.Id; + } + } + else + { + var timelineEntity = await _database.Timelines.Where(t => t.Name == timelineName).Select(t => new { t.Id }).SingleOrDefaultAsync(); + + if (timelineEntity == null) + { + throw new TimelineNotExistException(timelineName); + } + else + { + return timelineEntity.Id; + } + } + } + } +} diff --git a/BackEnd/Timeline/Services/BasicUserService.cs b/BackEnd/Timeline/Services/BasicUserService.cs new file mode 100644 index 00000000..fbbb6677 --- /dev/null +++ b/BackEnd/Timeline/Services/BasicUserService.cs @@ -0,0 +1,66 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Entities; +using Timeline.Models.Validation; +using Timeline.Services.Exceptions; + +namespace Timeline.Services +{ + /// + /// This service provide some basic user features, which should be used internally for other services. + /// + public interface IBasicUserService + { + /// + /// Check if a user exists. + /// + /// The id of the user. + /// True if exists. Otherwise false. + Task CheckUserExistence(long id); + + /// + /// Get the user id of given username. + /// + /// Username of the user. + /// The id of the user. + /// Thrown when is null. + /// Thrown when is of bad format. + /// Thrown when the user with given username does not exist. + Task GetUserIdByUsername(string username); + } + + public class BasicUserService : IBasicUserService + { + private readonly DatabaseContext _database; + + private readonly UsernameValidator _usernameValidator = new UsernameValidator(); + + public BasicUserService(DatabaseContext database) + { + _database = database; + } + + public async Task CheckUserExistence(long id) + { + return await _database.Users.AnyAsync(u => u.Id == id); + } + + public async Task GetUserIdByUsername(string username) + { + if (username == null) + throw new ArgumentNullException(nameof(username)); + + if (!_usernameValidator.Validate(username, out var message)) + throw new ArgumentException(message); + + var entity = await _database.Users.Where(user => user.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); + + if (entity == null) + throw new UserNotExistException(username); + + return entity.Id; + } + } +} diff --git a/BackEnd/Timeline/Services/HighlightTimelineService.cs b/BackEnd/Timeline/Services/HighlightTimelineService.cs index 7528d9b0..88ad4a4b 100644 --- a/BackEnd/Timeline/Services/HighlightTimelineService.cs +++ b/BackEnd/Timeline/Services/HighlightTimelineService.cs @@ -43,10 +43,10 @@ namespace Timeline.Services public class HighlightTimelineService : IHighlightTimelineService { private readonly DatabaseContext _database; - private readonly IUserService _userService; + private readonly IBasicUserService _userService; private readonly ITimelineService _timelineService; - public HighlightTimelineService(DatabaseContext database, IUserService userService, ITimelineService timelineService) + public HighlightTimelineService(DatabaseContext database, IBasicUserService userService, ITimelineService timelineService) { _database = database; _userService = userService; diff --git a/BackEnd/Timeline/Services/TimelinePostService.cs b/BackEnd/Timeline/Services/TimelinePostService.cs new file mode 100644 index 00000000..36fcdbca --- /dev/null +++ b/BackEnd/Timeline/Services/TimelinePostService.cs @@ -0,0 +1,493 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Entities; +using Timeline.Helpers; +using Timeline.Models; +using Timeline.Services.Exceptions; +using SixLabors.ImageSharp; +using static Timeline.Resources.Services.TimelineService; +using Microsoft.Extensions.Logging; + +namespace Timeline.Services +{ + public class PostData : ICacheableData + { +#pragma warning disable CA1819 // Properties should not return arrays + public byte[] Data { get; set; } = default!; +#pragma warning restore CA1819 // Properties should not return arrays + public string Type { get; set; } = default!; + public string ETag { get; set; } = default!; + public DateTime? LastModified { get; set; } // TODO: Why nullable? + } + + public interface ITimelinePostService + { + /// + /// Get all the posts in the timeline. + /// + /// The name of the timeline. + /// The time that posts have been modified since. + /// Whether include deleted posts. + /// A list of all posts. + /// Thrown when is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + Task> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false); + + /// + /// Get the etag of data of a post. + /// + /// The name of the timeline of the post. + /// The id of the post. + /// The etag of the data. + /// Thrown when is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + /// Thrown when post of does not exist or has been deleted. + /// Thrown when post has no data. + /// + Task GetPostDataETag(string timelineName, long postId); + + /// + /// Get the data of a post. + /// + /// The name of the timeline of the post. + /// The id of the post. + /// The etag of the data. + /// Thrown when is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + /// Thrown when post of does not exist or has been deleted. + /// Thrown when post has no data. + /// + Task GetPostData(string timelineName, long postId); + + /// + /// Create a new text post in timeline. + /// + /// The name of the timeline to create post against. + /// The author's user id. + /// The content text. + /// The time of the post. If null, then current time is used. + /// The info of the created post. + /// Thrown when or is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + /// Thrown if user of does not exist. + Task CreateTextPost(string timelineName, long authorId, string text, DateTime? time); + + /// + /// Create a new image post in timeline. + /// + /// The name of the timeline to create post against. + /// The author's user id. + /// The image data. + /// The time of the post. If null, then use current time. + /// The info of the created post. + /// Thrown when or is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + /// Thrown if user of does not exist. + /// Thrown if data is not a image. Validated by . + Task CreateImagePost(string timelineName, long authorId, byte[] imageData, DateTime? time); + + /// + /// Delete a post. + /// + /// The name of the timeline to delete post against. + /// The id of the post to delete. + /// Thrown when is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + /// Thrown when the post with given id does not exist or is deleted already. + /// + /// First use to check the permission. + /// + Task DeletePost(string timelineName, long postId); + + /// + /// Delete all posts of the given user. Used when delete a user. + /// + /// The id of the user. + Task DeleteAllPostsOfUser(long userId); + + /// + /// Verify whether a user has the permission to modify a post. + /// + /// The name of the timeline. + /// The id of the post. + /// The id of the user to check on. + /// True if you want it to throw . Default false. + /// True if can modify, false if can't modify. + /// Thrown when is null. + /// Throw when is of bad format. + /// + /// Thrown when timeline with name does not exist. + /// If it is a personal timeline, then inner exception is . + /// + /// Thrown when the post with given id does not exist or is deleted already and is true. + /// + /// Unless is true, this method should return true if the post does not exist. + /// If the post is deleted, its author info still exists, so it is checked as the post is not deleted unless is true. + /// This method does not check whether the user is administrator. + /// It only checks whether he is the author of the post or the owner of the timeline. + /// Return false when user with modifier id does not exist. + /// + Task HasPostModifyPermission(string timelineName, long postId, long modifierId, bool throwOnPostNotExist = false); + } + + public class TimelinePostService : ITimelinePostService + { + private readonly ILogger _logger; + private readonly DatabaseContext _database; + private readonly IBasicTimelineService _basicTimelineService; + private readonly IUserService _userService; + private readonly IDataManager _dataManager; + private readonly IImageValidator _imageValidator; + private readonly IClock _clock; + + public TimelinePostService(ILogger logger, DatabaseContext database, IBasicTimelineService basicTimelineService, IUserService userService, IDataManager dataManager, IImageValidator imageValidator, IClock clock) + { + _logger = logger; + _database = database; + _basicTimelineService = basicTimelineService; + _userService = userService; + _dataManager = dataManager; + _imageValidator = imageValidator; + _clock = clock; + } + + private async Task MapTimelinePostFromEntity(TimelinePostEntity entity, string timelineName) + { + User? author = entity.AuthorId.HasValue ? await _userService.GetUser(entity.AuthorId.Value) : null; + + ITimelinePostContent? content = null; + + if (entity.Content != null) + { + var type = entity.ContentType; + + content = type switch + { + TimelinePostContentTypes.Text => new TextTimelinePostContent(entity.Content), + TimelinePostContentTypes.Image => new ImageTimelinePostContent(entity.Content), + _ => throw new DatabaseCorruptedException(string.Format(CultureInfo.InvariantCulture, ExceptionDatabaseUnknownContentType, type)) + }; + } + + return new TimelinePost( + id: entity.LocalId, + author: author, + content: content, + time: entity.Time, + lastUpdated: entity.LastUpdated, + timelineName: timelineName + ); + } + + public async Task> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false) + { + modifiedSince = modifiedSince?.MyToUtc(); + + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName); + IQueryable query = _database.TimelinePosts.Where(p => p.TimelineId == timelineId); + + if (!includeDeleted) + { + query = query.Where(p => p.Content != null); + } + + if (modifiedSince.HasValue) + { + query = query.Include(p => p.Author).Where(p => p.LastUpdated >= modifiedSince || (p.Author != null && p.Author.UsernameChangeTime >= modifiedSince)); + } + + query = query.OrderBy(p => p.Time); + + var postEntities = await query.ToListAsync(); + + var posts = new List(); + foreach (var entity in postEntities) + { + posts.Add(await MapTimelinePostFromEntity(entity, timelineName)); + } + return posts; + } + + public async Task GetPostDataETag(string timelineName, long postId) + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName); + + var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync(); + + if (postEntity == null) + throw new TimelinePostNotExistException(timelineName, postId, false); + + if (postEntity.Content == null) + throw new TimelinePostNotExistException(timelineName, postId, true); + + if (postEntity.ContentType != TimelinePostContentTypes.Image) + throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost); + + var tag = postEntity.Content; + + return tag; + } + + public async Task GetPostData(string timelineName, long postId) + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName); + var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync(); + + if (postEntity == null) + throw new TimelinePostNotExistException(timelineName, postId, false); + + if (postEntity.Content == null) + throw new TimelinePostNotExistException(timelineName, postId, true); + + if (postEntity.ContentType != TimelinePostContentTypes.Image) + throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost); + + var tag = postEntity.Content; + + byte[] data; + + try + { + data = await _dataManager.GetEntry(tag); + } + catch (InvalidOperationException e) + { + throw new DatabaseCorruptedException(ExceptionGetDataDataEntryNotExist, e); + } + + if (postEntity.ExtraContent == null) + { + _logger.LogWarning(LogGetDataNoFormat); + var format = Image.DetectFormat(data); + postEntity.ExtraContent = format.DefaultMimeType; + await _database.SaveChangesAsync(); + } + + return new PostData + { + Data = data, + Type = postEntity.ExtraContent, + ETag = tag, + LastModified = postEntity.LastUpdated + }; + } + + public async Task CreateTextPost(string timelineName, long authorId, string text, DateTime? time) + { + time = time?.MyToUtc(); + + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + if (text == null) + throw new ArgumentNullException(nameof(text)); + + var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName); + var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); + + var author = await _userService.GetUser(authorId); + + var currentTime = _clock.GetCurrentTime(); + var finalTime = time ?? currentTime; + + timelineEntity.CurrentPostLocalId += 1; + + var postEntity = new TimelinePostEntity + { + LocalId = timelineEntity.CurrentPostLocalId, + ContentType = TimelinePostContentTypes.Text, + Content = text, + AuthorId = authorId, + TimelineId = timelineId, + Time = finalTime, + LastUpdated = currentTime + }; + _database.TimelinePosts.Add(postEntity); + await _database.SaveChangesAsync(); + + + return new TimelinePost( + id: postEntity.LocalId, + content: new TextTimelinePostContent(text), + time: finalTime, + author: author, + lastUpdated: currentTime, + timelineName: timelineName + ); + } + + public async Task CreateImagePost(string timelineName, long authorId, byte[] data, DateTime? time) + { + time = time?.MyToUtc(); + + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + if (data == null) + throw new ArgumentNullException(nameof(data)); + + var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName); + var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); + + var author = await _userService.GetUser(authorId); + + var imageFormat = await _imageValidator.Validate(data); + + var imageFormatText = imageFormat.DefaultMimeType; + + var tag = await _dataManager.RetainEntry(data); + + var currentTime = _clock.GetCurrentTime(); + var finalTime = time ?? currentTime; + + timelineEntity.CurrentPostLocalId += 1; + + var postEntity = new TimelinePostEntity + { + LocalId = timelineEntity.CurrentPostLocalId, + ContentType = TimelinePostContentTypes.Image, + Content = tag, + ExtraContent = imageFormatText, + AuthorId = authorId, + TimelineId = timelineId, + Time = finalTime, + LastUpdated = currentTime + }; + _database.TimelinePosts.Add(postEntity); + await _database.SaveChangesAsync(); + + return new TimelinePost( + id: postEntity.LocalId, + content: new ImageTimelinePostContent(tag), + time: finalTime, + author: author, + lastUpdated: currentTime, + timelineName: timelineName + ); + } + + public async Task DeletePost(string timelineName, long id) + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName); + + var post = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == id).SingleOrDefaultAsync(); + + if (post == null) + throw new TimelinePostNotExistException(timelineName, id, false); + + if (post.Content == null) + throw new TimelinePostNotExistException(timelineName, id, true); + + string? dataTag = null; + + if (post.ContentType == TimelinePostContentTypes.Image) + { + dataTag = post.Content; + } + + post.Content = null; + post.LastUpdated = _clock.GetCurrentTime(); + + await _database.SaveChangesAsync(); + + if (dataTag != null) + { + await _dataManager.FreeEntry(dataTag); + } + } + + public async Task DeleteAllPostsOfUser(long userId) + { + var posts = await _database.TimelinePosts.Where(p => p.AuthorId == userId).ToListAsync(); + + var now = _clock.GetCurrentTime(); + + var dataTags = new List(); + + foreach (var post in posts) + { + if (post.Content != null) + { + if (post.ContentType == TimelinePostContentTypes.Image) + { + dataTags.Add(post.Content); + } + post.Content = null; + } + post.LastUpdated = now; + } + + await _database.SaveChangesAsync(); + + foreach (var dataTag in dataTags) + { + await _dataManager.FreeEntry(dataTag); + } + } + + public async Task HasPostModifyPermission(string timelineName, long postId, long modifierId, bool throwOnPostNotExist = false) + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + var timelineId = await _basicTimelineService.GetTimelineIdByName(timelineName); + + var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync(); + + var postEntity = await _database.TimelinePosts.Where(p => p.Id == postId).Select(p => new { p.Content, p.AuthorId }).SingleOrDefaultAsync(); + + if (postEntity == null) + { + if (throwOnPostNotExist) + throw new TimelinePostNotExistException(timelineName, postId, false); + else + return true; + } + + if (postEntity.Content == null && throwOnPostNotExist) + { + throw new TimelinePostNotExistException(timelineName, postId, true); + } + + return timelineEntity.OwnerId == modifierId || postEntity.AuthorId == modifierId; + } + } +} diff --git a/BackEnd/Timeline/Services/TimelineService.cs b/BackEnd/Timeline/Services/TimelineService.cs index f8c729bf..f943f8b4 100644 --- a/BackEnd/Timeline/Services/TimelineService.cs +++ b/BackEnd/Timeline/Services/TimelineService.cs @@ -51,20 +51,10 @@ namespace Timeline.Services public long UserId { get; set; } } - public class PostData : ICacheableData - { -#pragma warning disable CA1819 // Properties should not return arrays - public byte[] Data { get; set; } = default!; -#pragma warning restore CA1819 // Properties should not return arrays - public string Type { get; set; } = default!; - public string ETag { get; set; } = default!; - public DateTime? LastModified { get; set; } // TODO: Why nullable? - } - /// /// This define the interface of both personal timeline and ordinary timeline. /// - public interface ITimelineService + public interface ITimelineService : IBasicTimelineService { /// /// Get the timeline last modified time (not include name change). @@ -79,19 +69,6 @@ namespace Timeline.Services /// Task GetTimelineLastModifiedTime(string timelineName); - /// - /// Get the timeline id by name. - /// - /// Timeline name. - /// Id of the timeline. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - Task GetTimelineIdByName(string timelineName); - /// /// Get the timeline unique id. /// @@ -139,112 +116,7 @@ namespace Timeline.Services /// Task ChangeProperty(string timelineName, TimelineChangePropertyRequest newProperties); - /// - /// Get all the posts in the timeline. - /// - /// The name of the timeline. - /// The time that posts have been modified since. - /// Whether include deleted posts. - /// A list of all posts. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - Task> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false); - /// - /// Get the etag of data of a post. - /// - /// The name of the timeline of the post. - /// The id of the post. - /// The etag of the data. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - /// Thrown when post of does not exist or has been deleted. - /// Thrown when post has no data. - /// - Task GetPostDataETag(string timelineName, long postId); - - /// - /// Get the data of a post. - /// - /// The name of the timeline of the post. - /// The id of the post. - /// The etag of the data. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - /// Thrown when post of does not exist or has been deleted. - /// Thrown when post has no data. - /// - Task GetPostData(string timelineName, long postId); - - /// - /// Create a new text post in timeline. - /// - /// The name of the timeline to create post against. - /// The author's user id. - /// The content text. - /// The time of the post. If null, then current time is used. - /// The info of the created post. - /// Thrown when or is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - /// Thrown if user of does not exist. - Task CreateTextPost(string timelineName, long authorId, string text, DateTime? time); - - /// - /// Create a new image post in timeline. - /// - /// The name of the timeline to create post against. - /// The author's user id. - /// The image data. - /// The time of the post. If null, then use current time. - /// The info of the created post. - /// Thrown when or is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - /// Thrown if user of does not exist. - /// Thrown if data is not a image. Validated by . - Task CreateImagePost(string timelineName, long authorId, byte[] imageData, DateTime? time); - - /// - /// Delete a post. - /// - /// The name of the timeline to delete post against. - /// The id of the post to delete. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - /// Thrown when the post with given id does not exist or is deleted already. - /// - /// First use to check the permission. - /// - Task DeletePost(string timelineName, long postId); - - /// - /// Delete all posts of the given user. Used when delete a user. - /// - /// The id of the user. - Task DeleteAllPostsOfUser(long userId); /// /// Change member of timeline. @@ -305,29 +177,6 @@ namespace Timeline.Services /// Task HasReadPermission(string timelineName, long? visitorId); - /// - /// Verify whether a user has the permission to modify a post. - /// - /// The name of the timeline. - /// The id of the post. - /// The id of the user to check on. - /// True if you want it to throw . Default false. - /// True if can modify, false if can't modify. - /// Thrown when is null. - /// Throw when is of bad format. - /// - /// Thrown when timeline with name does not exist. - /// If it is a personal timeline, then inner exception is . - /// - /// Thrown when the post with given id does not exist or is deleted already and is true. - /// - /// Unless is true, this method should return true if the post does not exist. - /// If the post is deleted, its author info still exists, so it is checked as the post is not deleted unless is true. - /// This method does not check whether the user is administrator. - /// It only checks whether he is the author of the post or the owner of the timeline. - /// Return false when user with modifier id does not exist. - /// - Task HasPostModifyPermission(string timelineName, long postId, long modifierId, bool throwOnPostNotExist = false); /// /// Verify whether a user is member of a timeline. @@ -395,28 +244,20 @@ namespace Timeline.Services Task ChangeTimelineName(string oldTimelineName, string newTimelineName); } - public class TimelineService : ITimelineService + public class TimelineService : BasicTimelineService, ITimelineService { - public TimelineService(ILogger logger, DatabaseContext database, IDataManager dataManager, IUserService userService, IImageValidator imageValidator, IClock clock) + public TimelineService(DatabaseContext database, IUserService userService, IClock clock) + : base(database, userService, clock) { - _logger = logger; _database = database; - _dataManager = dataManager; _userService = userService; - _imageValidator = imageValidator; _clock = clock; } - private readonly ILogger _logger; - private readonly DatabaseContext _database; - private readonly IDataManager _dataManager; - private readonly IUserService _userService; - private readonly IImageValidator _imageValidator; - private readonly IClock _clock; private readonly UsernameValidator _usernameValidator = new UsernameValidator(); @@ -459,122 +300,12 @@ namespace Timeline.Services }; } - private async Task MapTimelinePostFromEntity(TimelinePostEntity entity, string timelineName) - { - User? author = entity.AuthorId.HasValue ? await _userService.GetUser(entity.AuthorId.Value) : null; - - ITimelinePostContent? content = null; - - if (entity.Content != null) - { - var type = entity.ContentType; - - content = type switch - { - TimelinePostContentTypes.Text => new TextTimelinePostContent(entity.Content), - TimelinePostContentTypes.Image => new ImageTimelinePostContent(entity.Content), - _ => throw new DatabaseCorruptedException(string.Format(CultureInfo.InvariantCulture, ExceptionDatabaseUnknownContentType, type)) - }; - } - - return new TimelinePost( - id: entity.LocalId, - author: author, - content: content, - time: entity.Time, - lastUpdated: entity.LastUpdated, - timelineName: timelineName - ); - } - - private TimelineEntity CreateNewTimelineEntity(string? name, long ownerId) - { - var currentTime = _clock.GetCurrentTime(); - - return new TimelineEntity - { - Name = name, - NameLastModified = currentTime, - OwnerId = ownerId, - Visibility = TimelineVisibility.Register, - CreateTime = currentTime, - LastModified = currentTime, - CurrentPostLocalId = 0, - Members = new List() - }; - } - - - - // Get timeline id by name. If it is a personal timeline and it does not exist, it will be created. - // - // This method will check the name format and if it is invalid, ArgumentException is thrown. - // - // For personal timeline, if the user does not exist, TimelineNotExistException will be thrown with UserNotExistException as inner exception. - // For ordinary timeline, if the timeline does not exist, TimelineNotExistException will be thrown. - // - // It follows all timeline-related function common interface contracts. - private async Task FindTimelineId(string timelineName) - { - timelineName = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); - - if (isPersonal) - { - long userId; - try - { - userId = await _userService.GetUserIdByUsername(timelineName); - } - catch (ArgumentException e) - { - throw new ArgumentException(ExceptionFindTimelineUsernameBadFormat, nameof(timelineName), e); - } - catch (UserNotExistException e) - { - throw new TimelineNotExistException(timelineName, e); - } - - var timelineEntity = await _database.Timelines.Where(t => t.OwnerId == userId && t.Name == null).Select(t => new { t.Id }).SingleOrDefaultAsync(); - - if (timelineEntity != null) - { - return timelineEntity.Id; - } - else - { - var newTimelineEntity = CreateNewTimelineEntity(null, userId); - _database.Timelines.Add(newTimelineEntity); - await _database.SaveChangesAsync(); - - return newTimelineEntity.Id; - } - } - else - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - ValidateTimelineName(timelineName, nameof(timelineName)); - - var timelineEntity = await _database.Timelines.Where(t => t.Name == timelineName).Select(t => new { t.Id }).SingleOrDefaultAsync(); - - if (timelineEntity == null) - { - throw new TimelineNotExistException(timelineName); - } - else - { - return timelineEntity.Id; - } - } - } - public async Task GetTimelineLastModifiedTime(string timelineName) { if (timelineName == null) throw new ArgumentNullException(nameof(timelineName)); - var timelineId = await FindTimelineId(timelineName); + var timelineId = await GetTimelineIdByName(timelineName); var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.LastModified }).SingleAsync(); @@ -586,31 +317,19 @@ namespace Timeline.Services if (timelineName == null) throw new ArgumentNullException(nameof(timelineName)); - var timelineId = await FindTimelineId(timelineName); + var timelineId = await GetTimelineIdByName(timelineName); var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.UniqueId }).SingleAsync(); return timelineEntity.UniqueId; } - public async Task GetTimelineIdByName(string timelineName) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - - var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.Id }).SingleAsync(); - - return timelineEntity.Id; - } - public async Task GetTimeline(string timelineName) { if (timelineName == null) throw new ArgumentNullException(nameof(timelineName)); - var timelineId = await FindTimelineId(timelineName); + var timelineId = await GetTimelineIdByName(timelineName); var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Include(t => t.Members).SingleAsync(); @@ -627,262 +346,6 @@ namespace Timeline.Services return await MapTimelineFromEntity(timelineEntity); } - public async Task> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false) - { - modifiedSince = modifiedSince?.MyToUtc(); - - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - IQueryable query = _database.TimelinePosts.Where(p => p.TimelineId == timelineId); - - if (!includeDeleted) - { - query = query.Where(p => p.Content != null); - } - - if (modifiedSince.HasValue) - { - query = query.Include(p => p.Author).Where(p => p.LastUpdated >= modifiedSince || (p.Author != null && p.Author.UsernameChangeTime >= modifiedSince)); - } - - query = query.OrderBy(p => p.Time); - - var postEntities = await query.ToListAsync(); - - var posts = new List(); - foreach (var entity in postEntities) - { - posts.Add(await MapTimelinePostFromEntity(entity, timelineName)); - } - return posts; - } - - public async Task GetPostDataETag(string timelineName, long postId) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - - var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync(); - - if (postEntity == null) - throw new TimelinePostNotExistException(timelineName, postId, false); - - if (postEntity.Content == null) - throw new TimelinePostNotExistException(timelineName, postId, true); - - if (postEntity.ContentType != TimelinePostContentTypes.Image) - throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost); - - var tag = postEntity.Content; - - return tag; - } - - public async Task GetPostData(string timelineName, long postId) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - var postEntity = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == postId).SingleOrDefaultAsync(); - - if (postEntity == null) - throw new TimelinePostNotExistException(timelineName, postId, false); - - if (postEntity.Content == null) - throw new TimelinePostNotExistException(timelineName, postId, true); - - if (postEntity.ContentType != TimelinePostContentTypes.Image) - throw new TimelinePostNoDataException(ExceptionGetDataNonImagePost); - - var tag = postEntity.Content; - - byte[] data; - - try - { - data = await _dataManager.GetEntry(tag); - } - catch (InvalidOperationException e) - { - throw new DatabaseCorruptedException(ExceptionGetDataDataEntryNotExist, e); - } - - if (postEntity.ExtraContent == null) - { - _logger.LogWarning(LogGetDataNoFormat); - var format = Image.DetectFormat(data); - postEntity.ExtraContent = format.DefaultMimeType; - await _database.SaveChangesAsync(); - } - - return new PostData - { - Data = data, - Type = postEntity.ExtraContent, - ETag = tag, - LastModified = postEntity.LastUpdated - }; - } - - public async Task CreateTextPost(string timelineName, long authorId, string text, DateTime? time) - { - time = time?.MyToUtc(); - - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - if (text == null) - throw new ArgumentNullException(nameof(text)); - - var timelineId = await FindTimelineId(timelineName); - var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); - - var author = await _userService.GetUser(authorId); - - var currentTime = _clock.GetCurrentTime(); - var finalTime = time ?? currentTime; - - timelineEntity.CurrentPostLocalId += 1; - - var postEntity = new TimelinePostEntity - { - LocalId = timelineEntity.CurrentPostLocalId, - ContentType = TimelinePostContentTypes.Text, - Content = text, - AuthorId = authorId, - TimelineId = timelineId, - Time = finalTime, - LastUpdated = currentTime - }; - _database.TimelinePosts.Add(postEntity); - await _database.SaveChangesAsync(); - - - return new TimelinePost( - id: postEntity.LocalId, - content: new TextTimelinePostContent(text), - time: finalTime, - author: author, - lastUpdated: currentTime, - timelineName: timelineName - ); - } - - public async Task CreateImagePost(string timelineName, long authorId, byte[] data, DateTime? time) - { - time = time?.MyToUtc(); - - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - if (data == null) - throw new ArgumentNullException(nameof(data)); - - var timelineId = await FindTimelineId(timelineName); - var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); - - var author = await _userService.GetUser(authorId); - - var imageFormat = await _imageValidator.Validate(data); - - var imageFormatText = imageFormat.DefaultMimeType; - - var tag = await _dataManager.RetainEntry(data); - - var currentTime = _clock.GetCurrentTime(); - var finalTime = time ?? currentTime; - - timelineEntity.CurrentPostLocalId += 1; - - var postEntity = new TimelinePostEntity - { - LocalId = timelineEntity.CurrentPostLocalId, - ContentType = TimelinePostContentTypes.Image, - Content = tag, - ExtraContent = imageFormatText, - AuthorId = authorId, - TimelineId = timelineId, - Time = finalTime, - LastUpdated = currentTime - }; - _database.TimelinePosts.Add(postEntity); - await _database.SaveChangesAsync(); - - return new TimelinePost( - id: postEntity.LocalId, - content: new ImageTimelinePostContent(tag), - time: finalTime, - author: author, - lastUpdated: currentTime, - timelineName: timelineName - ); - } - - public async Task DeletePost(string timelineName, long id) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - - var post = await _database.TimelinePosts.Where(p => p.TimelineId == timelineId && p.LocalId == id).SingleOrDefaultAsync(); - - if (post == null) - throw new TimelinePostNotExistException(timelineName, id, false); - - if (post.Content == null) - throw new TimelinePostNotExistException(timelineName, id, true); - - string? dataTag = null; - - if (post.ContentType == TimelinePostContentTypes.Image) - { - dataTag = post.Content; - } - - post.Content = null; - post.LastUpdated = _clock.GetCurrentTime(); - - await _database.SaveChangesAsync(); - - if (dataTag != null) - { - await _dataManager.FreeEntry(dataTag); - } - } - - public async Task DeleteAllPostsOfUser(long userId) - { - var posts = await _database.TimelinePosts.Where(p => p.AuthorId == userId).ToListAsync(); - - var now = _clock.GetCurrentTime(); - - var dataTags = new List(); - - foreach (var post in posts) - { - if (post.Content != null) - { - if (post.ContentType == TimelinePostContentTypes.Image) - { - dataTags.Add(post.Content); - } - post.Content = null; - } - post.LastUpdated = now; - } - - await _database.SaveChangesAsync(); - - foreach (var dataTag in dataTags) - { - await _dataManager.FreeEntry(dataTag); - } - } - public async Task ChangeProperty(string timelineName, TimelineChangePropertyRequest newProperties) { if (timelineName == null) @@ -890,7 +353,7 @@ namespace Timeline.Services if (newProperties == null) throw new ArgumentNullException(nameof(newProperties)); - var timelineId = await FindTimelineId(timelineName); + var timelineId = await GetTimelineIdByName(timelineName); var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); @@ -976,7 +439,7 @@ namespace Timeline.Services if (simplifiedAdd == null && simplifiedRemove == null) return; - var timelineId = await FindTimelineId(timelineName); + var timelineId = await GetTimelineIdByName(timelineName); async Task?> CheckExistenceAndGetId(List? list) { @@ -1016,7 +479,7 @@ namespace Timeline.Services if (timelineName == null) throw new ArgumentNullException(nameof(timelineName)); - var timelineId = await FindTimelineId(timelineName); + var timelineId = await GetTimelineIdByName(timelineName); var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync(); return userId == timelineEntity.OwnerId; @@ -1027,7 +490,7 @@ namespace Timeline.Services if (timelineName == null) throw new ArgumentNullException(nameof(timelineName)); - var timelineId = await FindTimelineId(timelineName); + var timelineId = await GetTimelineIdByName(timelineName); var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.Visibility }).SingleAsync(); if (timelineEntity.Visibility == TimelineVisibility.Public) @@ -1047,39 +510,12 @@ namespace Timeline.Services } } - public async Task HasPostModifyPermission(string timelineName, long postId, long modifierId, bool throwOnPostNotExist = false) - { - if (timelineName == null) - throw new ArgumentNullException(nameof(timelineName)); - - var timelineId = await FindTimelineId(timelineName); - - var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync(); - - var postEntity = await _database.TimelinePosts.Where(p => p.Id == postId).Select(p => new { p.Content, p.AuthorId }).SingleOrDefaultAsync(); - - if (postEntity == null) - { - if (throwOnPostNotExist) - throw new TimelinePostNotExistException(timelineName, postId, false); - else - return true; - } - - if (postEntity.Content == null && throwOnPostNotExist) - { - throw new TimelinePostNotExistException(timelineName, postId, true); - } - - return timelineEntity.OwnerId == modifierId || postEntity.AuthorId == modifierId; - } - public async Task IsMemberOf(string timelineName, long userId) { if (timelineName == null) throw new ArgumentNullException(nameof(timelineName)); - var timelineId = await FindTimelineId(timelineName); + var timelineId = await GetTimelineIdByName(timelineName); var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).Select(t => new { t.OwnerId }).SingleAsync(); diff --git a/BackEnd/Timeline/Services/UserDeleteService.cs b/BackEnd/Timeline/Services/UserDeleteService.cs index 5365313b..a4e77abc 100644 --- a/BackEnd/Timeline/Services/UserDeleteService.cs +++ b/BackEnd/Timeline/Services/UserDeleteService.cs @@ -31,15 +31,15 @@ namespace Timeline.Services private readonly DatabaseContext _databaseContext; - private readonly ITimelineService _timelineService; + private readonly ITimelinePostService _timelinePostService; private readonly UsernameValidator _usernameValidator = new UsernameValidator(); - public UserDeleteService(ILogger logger, DatabaseContext databaseContext, ITimelineService timelineService) + public UserDeleteService(ILogger logger, DatabaseContext databaseContext, ITimelinePostService timelinePostService) { _logger = logger; _databaseContext = databaseContext; - _timelineService = timelineService; + _timelinePostService = timelinePostService; } public async Task DeleteUser(string username) @@ -59,7 +59,7 @@ namespace Timeline.Services if (user.Id == 1) throw new InvalidOperationOnRootUserException("Can't delete root user."); - await _timelineService.DeleteAllPostsOfUser(user.Id); + await _timelinePostService.DeleteAllPostsOfUser(user.Id); _databaseContext.Users.Remove(user); diff --git a/BackEnd/Timeline/Services/UserService.cs b/BackEnd/Timeline/Services/UserService.cs index 76c24666..cded3ff1 100644 --- a/BackEnd/Timeline/Services/UserService.cs +++ b/BackEnd/Timeline/Services/UserService.cs @@ -24,7 +24,7 @@ namespace Timeline.Services public string? Nickname { get; set; } } - public interface IUserService + public interface IUserService : IBasicUserService { /// /// Try to verify the given username and password. @@ -38,12 +38,6 @@ namespace Timeline.Services /// Thrown when password is wrong. Task VerifyCredential(string username, string password); - /// - /// Check if a user exists. - /// - /// The id of the user. - /// True if exists. Otherwise false. - Task CheckUserExistence(long id); /// /// Try to get a user by id. @@ -53,15 +47,6 @@ namespace Timeline.Services /// Thrown when the user with given id does not exist. Task GetUser(long id); - /// - /// Get the user id of given username. - /// - /// Username of the user. - /// The id of the user. - /// Thrown when is null. - /// Thrown when is of bad format. - /// Thrown when the user with given username does not exist. - Task GetUserIdByUsername(string username); /// /// List all users. @@ -106,7 +91,7 @@ namespace Timeline.Services Task ChangePassword(long id, string oldPassword, string newPassword); } - public class UserService : IUserService + public class UserService : BasicUserService, IUserService { private readonly ILogger _logger; private readonly IClock _clock; @@ -119,7 +104,7 @@ namespace Timeline.Services private readonly UsernameValidator _usernameValidator = new UsernameValidator(); private readonly NicknameValidator _nicknameValidator = new NicknameValidator(); - public UserService(ILogger logger, DatabaseContext databaseContext, IPasswordService passwordService, IClock clock, IUserPermissionService userPermissionService) + public UserService(ILogger logger, DatabaseContext databaseContext, IPasswordService passwordService, IClock clock, IUserPermissionService userPermissionService) : base(databaseContext) { _logger = logger; _clock = clock; @@ -195,11 +180,6 @@ namespace Timeline.Services return await CreateUserFromEntity(entity); } - public async Task CheckUserExistence(long id) - { - return await _databaseContext.Users.AnyAsync(u => u.Id == id); - } - public async Task GetUser(long id) { var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); @@ -210,21 +190,6 @@ namespace Timeline.Services return await CreateUserFromEntity(user); } - public async Task GetUserIdByUsername(string username) - { - if (username == null) - throw new ArgumentNullException(nameof(username)); - - CheckUsernameFormat(username, nameof(username)); - - var entity = await _databaseContext.Users.Where(user => user.Username == username).Select(u => new { u.Id }).SingleOrDefaultAsync(); - - if (entity == null) - throw new UserNotExistException(username); - - return entity.Id; - } - public async Task> GetUsers() { List result = new(); diff --git a/BackEnd/Timeline/Startup.cs b/BackEnd/Timeline/Startup.cs index 532c63d0..63e2a0dd 100644 --- a/BackEnd/Timeline/Startup.cs +++ b/BackEnd/Timeline/Startup.cs @@ -72,34 +72,34 @@ namespace Timeline services.AddAuthentication(AuthenticationConstants.Scheme) .AddScheme(AuthenticationConstants.Scheme, AuthenticationConstants.DisplayName, o => { }); services.AddAuthorization(); - services.AddSingleton(); - services.AddSingleton(); + services.TryAddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddAutoMapper(GetType().Assembly); services.AddTransient(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddTransient(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); - - services.AddScoped(); - services.AddScoped(); - - services.AddScoped(); - services.AddUserAvatarService(); + services.AddScoped(); services.AddScoped(); + services.AddScoped(); - services.TryAddSingleton(); services.AddDbContext((services, options) => { -- cgit v1.2.3 From c2ca954fc8bc0f12ad2ece715cb6c4a633a23119 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 26 Nov 2020 23:43:11 +0800 Subject: refactor: ... --- .../Services/HighlightTimelineServiceTest.cs | 6 +- BackEnd/Timeline/Controllers/TokenController.cs | 4 +- BackEnd/Timeline/Controllers/UserController.cs | 6 +- BackEnd/Timeline/Services/TimelinePostService.cs | 4 +- BackEnd/Timeline/Services/TimelineService.cs | 3 - BackEnd/Timeline/Services/UserCredentialService.cs | 104 +++++++++++++++++++++ BackEnd/Timeline/Services/UserService.cs | 68 -------------- BackEnd/Timeline/Services/UserTokenManager.cs | 7 +- BackEnd/Timeline/Startup.cs | 2 +- 9 files changed, 120 insertions(+), 84 deletions(-) create mode 100644 BackEnd/Timeline/Services/UserCredentialService.cs diff --git a/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs b/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs index 950fa974..a4cd983d 100644 --- a/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs +++ b/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Timeline.Services; +using Timeline.Services; namespace Timeline.Tests.Services { diff --git a/BackEnd/Timeline/Controllers/TokenController.cs b/BackEnd/Timeline/Controllers/TokenController.cs index 8f2ca600..41ec21e6 100644 --- a/BackEnd/Timeline/Controllers/TokenController.cs +++ b/BackEnd/Timeline/Controllers/TokenController.cs @@ -22,6 +22,7 @@ namespace Timeline.Controllers [ProducesErrorResponseType(typeof(CommonResponse))] public class TokenController : Controller { + private readonly IUserCredentialService _userCredentialService; private readonly IUserTokenManager _userTokenManager; private readonly ILogger _logger; private readonly IClock _clock; @@ -29,8 +30,9 @@ namespace Timeline.Controllers private readonly IMapper _mapper; /// - public TokenController(IUserTokenManager userTokenManager, ILogger logger, IClock clock, IMapper mapper) + public TokenController(IUserCredentialService userCredentialService, IUserTokenManager userTokenManager, ILogger logger, IClock clock, IMapper mapper) { + _userCredentialService = userCredentialService; _userTokenManager = userTokenManager; _logger = logger; _clock = clock; diff --git a/BackEnd/Timeline/Controllers/UserController.cs b/BackEnd/Timeline/Controllers/UserController.cs index 8edae139..626a116f 100644 --- a/BackEnd/Timeline/Controllers/UserController.cs +++ b/BackEnd/Timeline/Controllers/UserController.cs @@ -26,15 +26,17 @@ namespace Timeline.Controllers { private readonly ILogger _logger; private readonly IUserService _userService; + private readonly IUserCredentialService _userCredentialService; private readonly IUserPermissionService _userPermissionService; private readonly IUserDeleteService _userDeleteService; private readonly IMapper _mapper; /// - public UserController(ILogger logger, IUserService userService, IUserPermissionService userPermissionService, IUserDeleteService userDeleteService, IMapper mapper) + public UserController(ILogger logger, IUserService userService, IUserCredentialService userCredentialService, IUserPermissionService userPermissionService, IUserDeleteService userDeleteService, IMapper mapper) { _logger = logger; _userService = userService; + _userCredentialService = userCredentialService; _userPermissionService = userPermissionService; _userDeleteService = userDeleteService; _mapper = mapper; @@ -190,7 +192,7 @@ namespace Timeline.Controllers { try { - await _userService.ChangePassword(this.GetUserId(), request.OldPassword, request.NewPassword); + await _userCredentialService.ChangePassword(this.GetUserId(), request.OldPassword, request.NewPassword); return Ok(); } catch (BadPasswordException e) diff --git a/BackEnd/Timeline/Services/TimelinePostService.cs b/BackEnd/Timeline/Services/TimelinePostService.cs index 36fcdbca..a1176a68 100644 --- a/BackEnd/Timeline/Services/TimelinePostService.cs +++ b/BackEnd/Timeline/Services/TimelinePostService.cs @@ -1,4 +1,6 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using SixLabors.ImageSharp; using System; using System.Collections.Generic; using System.Globalization; @@ -8,9 +10,7 @@ using Timeline.Entities; using Timeline.Helpers; using Timeline.Models; using Timeline.Services.Exceptions; -using SixLabors.ImageSharp; using static Timeline.Resources.Services.TimelineService; -using Microsoft.Extensions.Logging; namespace Timeline.Services { diff --git a/BackEnd/Timeline/Services/TimelineService.cs b/BackEnd/Timeline/Services/TimelineService.cs index f943f8b4..b8ec354a 100644 --- a/BackEnd/Timeline/Services/TimelineService.cs +++ b/BackEnd/Timeline/Services/TimelineService.cs @@ -1,13 +1,10 @@ using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using SixLabors.ImageSharp; using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading.Tasks; using Timeline.Entities; -using Timeline.Helpers; using Timeline.Models; using Timeline.Models.Validation; using Timeline.Services.Exceptions; diff --git a/BackEnd/Timeline/Services/UserCredentialService.cs b/BackEnd/Timeline/Services/UserCredentialService.cs new file mode 100644 index 00000000..e5c3581b --- /dev/null +++ b/BackEnd/Timeline/Services/UserCredentialService.cs @@ -0,0 +1,104 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Entities; +using Timeline.Helpers; +using Timeline.Models; +using Timeline.Models.Validation; +using Timeline.Services.Exceptions; + +namespace Timeline.Services +{ + public interface IUserCredentialService + { + /// + /// Try to verify the given username and password. + /// + /// The username of the user to verify. + /// The password of the user to verify. + /// User id. + /// Thrown when or is null. + /// Thrown when is of bad format or is empty. + /// Thrown when the user with given username does not exist. + /// Thrown when password is wrong. + Task VerifyCredential(string username, string password); + + /// + /// Try to change a user's password with old password. + /// + /// The id of user to change password of. + /// Old password. + /// New password. + /// Thrown if or is null. + /// Thrown if or is empty. + /// Thrown if the user with given username does not exist. + /// Thrown if the old password is wrong. + Task ChangePassword(long id, string oldPassword, string newPassword); + } + + public class UserCredentialService : IUserCredentialService + { + private readonly ILogger _logger; + private readonly DatabaseContext _database; + private readonly IPasswordService _passwordService; + + private readonly UsernameValidator _usernameValidator = new UsernameValidator(); + + public UserCredentialService(ILogger logger, DatabaseContext database, IPasswordService passwordService) + { + _logger = logger; + _database = database; + _passwordService = passwordService; + } + + public async Task VerifyCredential(string username, string password) + { + if (username == null) + throw new ArgumentNullException(nameof(username)); + if (password == null) + throw new ArgumentNullException(nameof(password)); + if (!_usernameValidator.Validate(username, out var message)) + throw new ArgumentException(message); + if (password.Length == 0) + throw new ArgumentException("Password can't be empty."); + + var entity = await _database.Users.Where(u => u.Username == username).Select(u => new { u.Id, u.Password }).SingleOrDefaultAsync(); + + if (entity == null) + throw new UserNotExistException(username); + + if (!_passwordService.VerifyPassword(entity.Password, password)) + throw new BadPasswordException(password); + + return entity.Id; + } + + public async Task ChangePassword(long id, string oldPassword, string newPassword) + { + if (oldPassword == null) + throw new ArgumentNullException(nameof(oldPassword)); + if (newPassword == null) + throw new ArgumentNullException(nameof(newPassword)); + if (oldPassword.Length == 0) + throw new ArgumentException("Old password can't be empty."); + if (newPassword.Length == 0) + throw new ArgumentException("New password can't be empty."); + + var entity = await _database.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); + + if (entity == null) + throw new UserNotExistException(id); + + if (!_passwordService.VerifyPassword(entity.Password, oldPassword)) + throw new BadPasswordException(oldPassword); + + entity.Password = _passwordService.HashPassword(newPassword); + entity.Version += 1; + await _database.SaveChangesAsync(); + _logger.LogInformation(Log.Format(Resources.Services.UserService.LogDatabaseUpdate, ("Id", id), ("Operation", "Change password"))); + } + } +} diff --git a/BackEnd/Timeline/Services/UserService.cs b/BackEnd/Timeline/Services/UserService.cs index cded3ff1..9395cc52 100644 --- a/BackEnd/Timeline/Services/UserService.cs +++ b/BackEnd/Timeline/Services/UserService.cs @@ -26,19 +26,6 @@ namespace Timeline.Services public interface IUserService : IBasicUserService { - /// - /// Try to verify the given username and password. - /// - /// The username of the user to verify. - /// The password of the user to verify. - /// The user info and auth info. - /// Thrown when or is null. - /// Thrown when is of bad format or is empty. - /// Thrown when the user with given username does not exist. - /// Thrown when password is wrong. - Task VerifyCredential(string username, string password); - - /// /// Try to get a user by id. /// @@ -47,7 +34,6 @@ namespace Timeline.Services /// Thrown when the user with given id does not exist. Task GetUser(long id); - /// /// List all users. /// @@ -77,18 +63,6 @@ namespace Timeline.Services /// Version will increase if password is changed. /// Task ModifyUser(long id, ModifyUserParams? param); - - /// - /// Try to change a user's password with old password. - /// - /// The id of user to change password of. - /// Old password. - /// New password. - /// Thrown if or is null. - /// Thrown if or is empty. - /// Thrown if the user with given username does not exist. - /// Thrown if the old password is wrong. - Task ChangePassword(long id, string oldPassword, string newPassword); } public class UserService : BasicUserService, IUserService @@ -159,26 +133,7 @@ namespace Timeline.Services }; } - public async Task VerifyCredential(string username, string password) - { - if (username == null) - throw new ArgumentNullException(nameof(username)); - if (password == null) - throw new ArgumentNullException(nameof(password)); - - CheckUsernameFormat(username, nameof(username)); - CheckPasswordFormat(password, nameof(password)); - - var entity = await _databaseContext.Users.Where(u => u.Username == username).SingleOrDefaultAsync(); - - if (entity == null) - throw new UserNotExistException(username); - if (!_passwordService.VerifyPassword(entity.Password, password)) - throw new BadPasswordException(password); - - return await CreateUserFromEntity(entity); - } public async Task GetUser(long id) { @@ -288,28 +243,5 @@ namespace Timeline.Services return await CreateUserFromEntity(entity); } - - public async Task ChangePassword(long id, string oldPassword, string newPassword) - { - if (oldPassword == null) - throw new ArgumentNullException(nameof(oldPassword)); - if (newPassword == null) - throw new ArgumentNullException(nameof(newPassword)); - CheckPasswordFormat(oldPassword, nameof(oldPassword)); - CheckPasswordFormat(newPassword, nameof(newPassword)); - - var entity = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); - - if (entity == null) - throw new UserNotExistException(id); - - if (!_passwordService.VerifyPassword(entity.Password, oldPassword)) - throw new BadPasswordException(oldPassword); - - entity.Password = _passwordService.HashPassword(newPassword); - entity.Version += 1; - await _databaseContext.SaveChangesAsync(); - _logger.LogInformation(Log.Format(LogDatabaseUpdate, ("Id", id), ("Operation", "Change password"))); - } } } diff --git a/BackEnd/Timeline/Services/UserTokenManager.cs b/BackEnd/Timeline/Services/UserTokenManager.cs index 09ecd19c..831329e6 100644 --- a/BackEnd/Timeline/Services/UserTokenManager.cs +++ b/BackEnd/Timeline/Services/UserTokenManager.cs @@ -45,13 +45,15 @@ namespace Timeline.Services { private readonly ILogger _logger; private readonly IUserService _userService; + private readonly IUserCredentialService _userCredentialService; private readonly IUserTokenService _userTokenService; private readonly IClock _clock; - public UserTokenManager(ILogger logger, IUserService userService, IUserTokenService userTokenService, IClock clock) + public UserTokenManager(ILogger logger, IUserService userService, IUserCredentialService userCredentialService, IUserTokenService userTokenService, IClock clock) { _logger = logger; _userService = userService; + _userCredentialService = userCredentialService; _userTokenService = userTokenService; _clock = clock; } @@ -65,7 +67,8 @@ namespace Timeline.Services if (password == null) throw new ArgumentNullException(nameof(password)); - var user = await _userService.VerifyCredential(username, password); + var userId = await _userCredentialService.VerifyCredential(username, password); + var user = await _userService.GetUser(userId); var token = _userTokenService.GenerateToken(new UserTokenInfo { Id = user.Id, Version = user.Version, ExpireAt = expireAt }); return new UserTokenCreateResult { Token = token, User = user }; diff --git a/BackEnd/Timeline/Startup.cs b/BackEnd/Timeline/Startup.cs index 63e2a0dd..bf34f9e2 100644 --- a/BackEnd/Timeline/Startup.cs +++ b/BackEnd/Timeline/Startup.cs @@ -90,6 +90,7 @@ namespace Timeline services.AddTransient(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -100,7 +101,6 @@ namespace Timeline services.AddScoped(); services.AddScoped(); - services.AddDbContext((services, options) => { var pathProvider = services.GetRequiredService(); -- cgit v1.2.3 From 3f4e88757f961532b84df85e86d21995655a29d4 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 27 Nov 2020 00:07:09 +0800 Subject: refactor: ... --- .../HttpClientTimelineExtensions.cs | 8 +- .../IntegratedTests/HttpClientUserExtensions.cs | 4 +- .../IntegratedTests/IntegratedTestBase.cs | 4 +- .../Timeline.Tests/IntegratedTests/TimelineTest.cs | 150 ++++++++++----------- .../Timeline.Tests/IntegratedTests/TokenTest.cs | 24 ++-- BackEnd/Timeline.Tests/IntegratedTests/UserTest.cs | 66 ++++----- .../Timeline.Tests/Services/TimelineServiceTest.cs | 2 +- BackEnd/Timeline/Controllers/TimelineController.cs | 32 ++--- BackEnd/Timeline/Controllers/TokenController.cs | 12 +- BackEnd/Timeline/Controllers/UserController.cs | 12 +- BackEnd/Timeline/Models/Http/Timeline.cs | 45 +++---- BackEnd/Timeline/Models/Http/TimelineController.cs | 21 ++- BackEnd/Timeline/Models/Http/TokenController.cs | 20 +-- BackEnd/Timeline/Models/Http/User.cs | 105 +++++++++++++++ BackEnd/Timeline/Models/Http/UserController.cs | 18 +-- BackEnd/Timeline/Models/Http/UserInfo.cs | 105 --------------- BackEnd/Timeline/Models/Timeline.cs | 98 -------------- BackEnd/Timeline/Models/TimelineInfo.cs | 130 ++++++++++++++++++ BackEnd/Timeline/Models/User.cs | 21 --- BackEnd/Timeline/Models/UserInfo.cs | 48 +++++++ .../Timeline/Services/HighlightTimelineService.cs | 7 +- BackEnd/Timeline/Services/TimelinePostService.cs | 24 ++-- BackEnd/Timeline/Services/TimelineService.cs | 54 ++++---- BackEnd/Timeline/Services/UserCredentialService.cs | 2 - BackEnd/Timeline/Services/UserService.cs | 45 +++---- BackEnd/Timeline/Services/UserTokenManager.cs | 6 +- 26 files changed, 561 insertions(+), 502 deletions(-) create mode 100644 BackEnd/Timeline/Models/Http/User.cs delete mode 100644 BackEnd/Timeline/Models/Http/UserInfo.cs delete mode 100644 BackEnd/Timeline/Models/Timeline.cs create mode 100644 BackEnd/Timeline/Models/TimelineInfo.cs delete mode 100644 BackEnd/Timeline/Models/User.cs create mode 100644 BackEnd/Timeline/Models/UserInfo.cs diff --git a/BackEnd/Timeline.Tests/IntegratedTests/HttpClientTimelineExtensions.cs b/BackEnd/Timeline.Tests/IntegratedTests/HttpClientTimelineExtensions.cs index 8e48ccbf..ac60ce7c 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/HttpClientTimelineExtensions.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/HttpClientTimelineExtensions.cs @@ -6,11 +6,11 @@ namespace Timeline.Tests.IntegratedTests { public static class HttpClientTimelineExtensions { - public static Task GetTimelineAsync(this HttpClient client, string timelineName) - => client.TestGetAsync($"timelines/{timelineName}"); + public static Task GetTimelineAsync(this HttpClient client, string timelineName) + => client.TestGetAsync($"timelines/{timelineName}"); - public static Task PatchTimelineAsync(this HttpClient client, string timelineName, TimelinePatchRequest body) - => client.TestPatchAsync($"timelines/{timelineName}", body); + public static Task PatchTimelineAsync(this HttpClient client, string timelineName, HttpTimelinePatchRequest body) + => client.TestPatchAsync($"timelines/{timelineName}", body); public static Task PutTimelineMemberAsync(this HttpClient client, string timelineName, string memberUsername) => client.TestPutAsync($"timelines/{timelineName}/members/{memberUsername}"); diff --git a/BackEnd/Timeline.Tests/IntegratedTests/HttpClientUserExtensions.cs b/BackEnd/Timeline.Tests/IntegratedTests/HttpClientUserExtensions.cs index 81787eef..7ca62e38 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/HttpClientUserExtensions.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/HttpClientUserExtensions.cs @@ -6,7 +6,7 @@ namespace Timeline.Tests.IntegratedTests { public static class HttpClientUserExtensions { - public static Task GetUserAsync(this HttpClient client, string username) - => client.TestGetAsync($"users/{username}"); + public static Task GetUserAsync(this HttpClient client, string username) + => client.TestGetAsync($"users/{username}"); } } diff --git a/BackEnd/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs b/BackEnd/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs index e426ac98..82aed24e 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs @@ -103,8 +103,8 @@ namespace Timeline.Tests.IntegratedTests public async Task CreateClientWithCredential(string username, string password, bool setApiBase = true) { var client = await CreateDefaultClient(setApiBase); - var res = await client.TestPostAsync("token/create", - new CreateTokenRequest { Username = username, Password = password }); + var res = await client.TestPostAsync("token/create", + new HttpCreateTokenRequest { Username = username, Password = password }); var token = res.Token; client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); return client; diff --git a/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs index 9845e1b1..12dd2b8d 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs @@ -19,20 +19,20 @@ namespace Timeline.Tests.IntegratedTests { public static class TimelineHelper { - public static TimelinePostContentInfo TextPostContent(string text) + public static HttpTimelinePostContent TextPostContent(string text) { - return new TimelinePostContentInfo + return new HttpTimelinePostContent { Type = "text", Text = text }; } - public static TimelinePostCreateRequest TextPostCreateRequest(string text, DateTime? time = null) + public static HttpTimelinePostCreateRequest TextPostCreateRequest(string text, DateTime? time = null) { - return new TimelinePostCreateRequest + return new HttpTimelinePostCreateRequest { - Content = new TimelinePostCreateRequestContent + Content = new HttpTimelinePostCreateRequestContent { Type = "text", Text = text @@ -72,7 +72,7 @@ namespace Timeline.Tests.IntegratedTests { - var body = await client.TestGetAsync("timelines/@user1"); + var body = await client.TestGetAsync("timelines/@user1"); body.Owner.Should().BeEquivalentTo(await client.GetUserAsync("user1")); body.Visibility.Should().Be(TimelineVisibility.Register); body.Description.Should().Be(""); @@ -84,7 +84,7 @@ namespace Timeline.Tests.IntegratedTests } { - var body = await client.TestGetAsync("timelines/t1"); + var body = await client.TestGetAsync("timelines/t1"); body.Owner.Should().BeEquivalentTo(await client.GetUserAsync("user1")); body.Visibility.Should().Be(TimelineVisibility.Register); body.Description.Should().Be(""); @@ -101,7 +101,7 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateDefaultClient(); - var result = new List + var result = new List { await client.GetTimelineAsync("@user1") }; @@ -112,7 +112,7 @@ namespace Timeline.Tests.IntegratedTests } - var body = await client.TestGetAsync>("timelines"); + var body = await client.TestGetAsync>("timelines"); body.Should().BeEquivalentTo(result); } @@ -127,14 +127,14 @@ namespace Timeline.Tests.IntegratedTests await client.TestGetAssertInvalidModelAsync("timelines?visibility=aaa"); } - var testResultRelate = new List(); - var testResultOwn = new List(); - var testResultJoin = new List(); - var testResultOwnPrivate = new List(); - var testResultRelatePublic = new List(); - var testResultRelateRegister = new List(); - var testResultJoinPrivate = new List(); - var testResultPublic = new List(); + var testResultRelate = new List(); + var testResultOwn = new List(); + var testResultJoin = new List(); + var testResultOwnPrivate = new List(); + var testResultRelatePublic = new List(); + var testResultRelateRegister = new List(); + var testResultJoinPrivate = new List(); + var testResultPublic = new List(); { using var client = await CreateClientAsUser(); @@ -185,8 +185,8 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateClientAs(3); - await client.PatchTimelineAsync("@user3", new TimelinePatchRequest { Visibility = TimelineVisibility.Private }); - await client.PatchTimelineAsync("t3", new TimelinePatchRequest { Visibility = TimelineVisibility.Register }); + await client.PatchTimelineAsync("@user3", new HttpTimelinePatchRequest { Visibility = TimelineVisibility.Private }); + await client.PatchTimelineAsync("t3", new HttpTimelinePatchRequest { Visibility = TimelineVisibility.Register }); { var timeline = await client.GetTimelineAsync("@user3"); @@ -206,9 +206,9 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateDefaultClient(); - async Task TestAgainst(string url, List against) + async Task TestAgainst(string url, List against) { - var body = await client.TestGetAsync>(url); + var body = await client.TestGetAsync>(url); body.Should().BeEquivalentTo(against); } @@ -236,7 +236,7 @@ namespace Timeline.Tests.IntegratedTests await client.TestPostAssertInvalidModelAsync("timelines", new TimelineCreateRequest { Name = "!!!" }); { - var body = await client.TestPostAsync("timelines", new TimelineCreateRequest { Name = "aaa" }); + var body = await client.TestPostAsync("timelines", new TimelineCreateRequest { Name = "aaa" }); body.Should().BeEquivalentTo(await client.GetTimelineAsync("aaa")); } @@ -342,7 +342,7 @@ namespace Timeline.Tests.IntegratedTests var timelineName = generator(1); - async Task AssertMembers(List members) + async Task AssertMembers(List members) { var body = await client.GetTimelineAsync(timelineName); body.Members.Should().NotBeNull().And.BeEquivalentTo(members); @@ -358,7 +358,7 @@ namespace Timeline.Tests.IntegratedTests await client.TestPutAssertErrorAsync($"timelines/{timelineName}/members/usernotexist", errorCode: ErrorCodes.TimelineController.MemberPut_NotExist); await AssertEmptyMembers(); await client.PutTimelineMemberAsync(timelineName, "user2"); - await AssertMembers(new List { await client.GetUserAsync("user2") }); + await AssertMembers(new List { await client.GetUserAsync("user2") }); await client.DeleteTimelineMemberAsync(timelineName, "user2", true); await AssertEmptyMembers(); await client.DeleteTimelineMemberAsync(timelineName, "aaa", false); @@ -462,7 +462,7 @@ namespace Timeline.Tests.IntegratedTests async Task CreatePost(int userNumber) { using var client = await CreateClientAs(userNumber); - var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest("aaa")); + var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest("aaa")); return body.Id; } @@ -515,28 +515,28 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsUser(); { - var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); + var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); body.Should().BeEmpty(); } const string mockContent = "aaa"; - TimelinePostInfo createRes; + HttpTimelinePost createRes; { - var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest(mockContent)); + var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest(mockContent)); body.Content.Should().BeEquivalentTo(TimelineHelper.TextPostContent(mockContent)); body.Author.Should().BeEquivalentTo(await client.GetUserAsync("user1")); body.Deleted.Should().BeFalse(); createRes = body; } { - var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); + var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); body.Should().BeEquivalentTo(createRes); } const string mockContent2 = "bbb"; var mockTime2 = DateTime.UtcNow.AddDays(-1); - TimelinePostInfo createRes2; + HttpTimelinePost createRes2; { - var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest(mockContent2, mockTime2)); + var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest(mockContent2, mockTime2)); body.Should().NotBeNull(); body.Content.Should().BeEquivalentTo(TimelineHelper.TextPostContent(mockContent2)); body.Author.Should().BeEquivalentTo(await client.GetUserAsync("user1")); @@ -545,7 +545,7 @@ namespace Timeline.Tests.IntegratedTests createRes2 = body; } { - var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); + var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); body.Should().BeEquivalentTo(createRes, createRes2); } { @@ -554,7 +554,7 @@ namespace Timeline.Tests.IntegratedTests await client.TestDeleteAsync($"timelines/{generator(1)}/posts/30000", false); } { - var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); + var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); body.Should().BeEquivalentTo(createRes2); } } @@ -567,7 +567,7 @@ namespace Timeline.Tests.IntegratedTests async Task CreatePost(DateTime time) { - var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest("aaa", time)); + var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest("aaa", time)); return body.Id; } @@ -577,7 +577,7 @@ namespace Timeline.Tests.IntegratedTests var id2 = await CreatePost(now); { - var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); + var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); body.Select(p => p.Id).Should().Equal(id1, id2, id0); } } @@ -588,15 +588,15 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateClientAsUser(); var postUrl = $"timelines/{generator(1)}/posts"; - await client.TestPostAssertInvalidModelAsync(postUrl, new TimelinePostCreateRequest { Content = null! }); - await client.TestPostAssertInvalidModelAsync(postUrl, new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = null! } }); - await client.TestPostAssertInvalidModelAsync(postUrl, new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "hahaha" } }); - await client.TestPostAssertInvalidModelAsync(postUrl, new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "text", Text = null } }); - await client.TestPostAssertInvalidModelAsync(postUrl, new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "image", Data = null } }); + await client.TestPostAssertInvalidModelAsync(postUrl, new HttpTimelinePostCreateRequest { Content = null! }); + await client.TestPostAssertInvalidModelAsync(postUrl, new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Type = null! } }); + await client.TestPostAssertInvalidModelAsync(postUrl, new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Type = "hahaha" } }); + await client.TestPostAssertInvalidModelAsync(postUrl, new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Type = "text", Text = null } }); + await client.TestPostAssertInvalidModelAsync(postUrl, new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Type = "image", Data = null } }); // image not base64 - await client.TestPostAssertInvalidModelAsync(postUrl, new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "image", Data = "!!!" } }); + await client.TestPostAssertInvalidModelAsync(postUrl, new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Type = "image", Data = "!!!" } }); // image base64 not image - await client.TestPostAssertInvalidModelAsync(postUrl, new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "image", Data = Convert.ToBase64String(new byte[] { 0x01, 0x02, 0x03 }) } }); + await client.TestPostAssertInvalidModelAsync(postUrl, new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Type = "image", Data = Convert.ToBase64String(new byte[] { 0x01, 0x02, 0x03 }) } }); } [Theory] @@ -608,7 +608,7 @@ namespace Timeline.Tests.IntegratedTests long postId; string postImageUrl; - void AssertPostContent(TimelinePostContentInfo content) + void AssertPostContent(HttpTimelinePostContent content) { content.Type.Should().Be(TimelinePostContentTypes.Image); content.Url.Should().EndWith($"timelines/{generator(1)}/posts/{postId}/data"); @@ -618,10 +618,10 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsUser(); { - var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", - new TimelinePostCreateRequest + var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", + new HttpTimelinePostCreateRequest { - Content = new TimelinePostCreateRequestContent + Content = new HttpTimelinePostCreateRequestContent { Type = TimelinePostContentTypes.Image, Data = Convert.ToBase64String(imageData) @@ -633,7 +633,7 @@ namespace Timeline.Tests.IntegratedTests } { - var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); + var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); body.Should().HaveCount(1); var post = body[0]; post.Id.Should().Be(postId); @@ -655,7 +655,7 @@ namespace Timeline.Tests.IntegratedTests await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{postId}", false); { - var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); + var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts"); body.Should().BeEmpty(); } @@ -677,7 +677,7 @@ namespace Timeline.Tests.IntegratedTests long postId; { - var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest("aaa")); + var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", TimelineHelper.TextPostCreateRequest("aaa")); postId = body.Id; } @@ -726,12 +726,12 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsUser(); var postContentList = new List { "a", "b", "c", "d" }; - var posts = new List(); + var posts = new List(); foreach (var content in postContentList) { - var post = await client.TestPostAsync($"timelines/{generator(1)}/posts", - new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } }); + var post = await client.TestPostAsync($"timelines/{generator(1)}/posts", + new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } }); posts.Add(post); await Task.Delay(1000); } @@ -739,7 +739,7 @@ namespace Timeline.Tests.IntegratedTests await client.TestDeleteAsync($"timelines/{generator(1)}/posts/{posts[2].Id}", true); { - var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts?modifiedSince={posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture) }"); + var body = await client.TestGetAsync>($"timelines/{generator(1)}/posts?modifiedSince={posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture) }"); body.Should().HaveCount(2) .And.Subject.Select(p => p.Content!.Text).Should().Equal("b", "d"); } @@ -752,12 +752,12 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsUser(); var postContentList = new List { "a", "b", "c", "d" }; - var posts = new List(); + var posts = new List(); foreach (var content in postContentList) { - var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", - new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } }); + var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", + new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } }); posts.Add(body); } @@ -767,7 +767,7 @@ namespace Timeline.Tests.IntegratedTests } { - posts = await client.TestGetAsync>($"timelines/{generator(1)}/posts?includeDeleted=true"); + posts = await client.TestGetAsync>($"timelines/{generator(1)}/posts?includeDeleted=true"); posts.Should().HaveCount(4); posts.Select(p => p.Deleted).Should().Equal(true, false, true, false); posts.Select(p => p.Content == null).Should().Equal(true, false, true, false); @@ -781,12 +781,12 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsUser(); var postContentList = new List { "a", "b", "c", "d" }; - var posts = new List(); + var posts = new List(); foreach (var (content, index) in postContentList.Select((v, i) => (v, i))) { - var post = await client.TestPostAsync($"timelines/{generator(1)}/posts", - new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } }); + var post = await client.TestPostAsync($"timelines/{generator(1)}/posts", + new HttpTimelinePostCreateRequest { Content = new HttpTimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } }); posts.Add(post); await Task.Delay(1000); } @@ -795,7 +795,7 @@ namespace Timeline.Tests.IntegratedTests { - posts = await client.TestGetAsync>($"timelines/{generator(1)}/posts?modifiedSince={posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture)}&includeDeleted=true"); + posts = await client.TestGetAsync>($"timelines/{generator(1)}/posts?modifiedSince={posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture)}&includeDeleted=true"); posts.Should().HaveCount(3); posts.Select(p => p.Deleted).Should().Equal(false, true, false); posts.Select(p => p.Content == null).Should().Equal(false, true, false); @@ -809,7 +809,7 @@ namespace Timeline.Tests.IntegratedTests using var client = await CreateClientAsUser(); DateTime lastModifiedTime; - TimelineInfo timeline; + HttpTimeline timeline; string uniqueId; { @@ -830,7 +830,7 @@ namespace Timeline.Tests.IntegratedTests { - var body = await client.TestGetAsync($"timelines/{generator(1)}", + var body = await client.TestGetAsync($"timelines/{generator(1)}", headerSetup: (headers, _) => { headers.IfModifiedSince = lastModifiedTime.AddSeconds(-1); @@ -843,7 +843,7 @@ namespace Timeline.Tests.IntegratedTests } { - var body = await client.TestGetAsync($"timelines/{generator(1)}?ifModifiedSince={lastModifiedTime.AddSeconds(-1).ToString("s", CultureInfo.InvariantCulture) }"); + var body = await client.TestGetAsync($"timelines/{generator(1)}?ifModifiedSince={lastModifiedTime.AddSeconds(-1).ToString("s", CultureInfo.InvariantCulture) }"); body.Should().BeEquivalentTo(timeline); } @@ -853,7 +853,7 @@ namespace Timeline.Tests.IntegratedTests { var testUniqueId = (uniqueId[0] == 'a' ? "b" : "a") + uniqueId[1..]; - var body = await client.TestGetAsync($"timelines/{generator(1)}?ifModifiedSince={lastModifiedTime.AddSeconds(1).ToString("s", CultureInfo.InvariantCulture) }&checkUniqueId={testUniqueId}"); + var body = await client.TestGetAsync($"timelines/{generator(1)}?ifModifiedSince={lastModifiedTime.AddSeconds(1).ToString("s", CultureInfo.InvariantCulture) }&checkUniqueId={testUniqueId}"); body.Should().BeEquivalentTo(timeline); } } @@ -870,7 +870,7 @@ namespace Timeline.Tests.IntegratedTests } { - var body = await client.PatchTimelineAsync(generator(1), new TimelinePatchRequest { Title = "atitle" }); + var body = await client.PatchTimelineAsync(generator(1), new HttpTimelinePatchRequest { Title = "atitle" }); body.Title.Should().Be("atitle"); } @@ -885,26 +885,26 @@ namespace Timeline.Tests.IntegratedTests { { using var client = await CreateDefaultClient(); - await client.TestPostAssertUnauthorizedAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "t1", NewName = "tttttttt" }); + await client.TestPostAssertUnauthorizedAsync("timelineop/changename", new HttpTimelineChangeNameRequest { OldName = "t1", NewName = "tttttttt" }); } { using var client = await CreateClientAs(2); - await client.TestPostAssertForbiddenAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "t1", NewName = "tttttttt" }); + await client.TestPostAssertForbiddenAsync("timelineop/changename", new HttpTimelineChangeNameRequest { OldName = "t1", NewName = "tttttttt" }); } using (var client = await CreateClientAsUser()) { - await client.TestPostAssertInvalidModelAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "!!!", NewName = "tttttttt" }); - await client.TestPostAssertInvalidModelAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "ttt", NewName = "!!!!" }); - await client.TestPostAssertErrorAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "ttttt", NewName = "tttttttt" }, errorCode: ErrorCodes.TimelineController.NotExist); + await client.TestPostAssertInvalidModelAsync("timelineop/changename", new HttpTimelineChangeNameRequest { OldName = "!!!", NewName = "tttttttt" }); + await client.TestPostAssertInvalidModelAsync("timelineop/changename", new HttpTimelineChangeNameRequest { OldName = "ttt", NewName = "!!!!" }); + await client.TestPostAssertErrorAsync("timelineop/changename", new HttpTimelineChangeNameRequest { OldName = "ttttt", NewName = "tttttttt" }, errorCode: ErrorCodes.TimelineController.NotExist); - await client.TestPostAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "t1", NewName = "newt" }); + await client.TestPostAsync("timelineop/changename", new HttpTimelineChangeNameRequest { OldName = "t1", NewName = "newt" }); await client.TestGetAsync("timelines/t1", expectedStatusCode: HttpStatusCode.NotFound); { - var body = await client.TestGetAsync("timelines/newt"); + var body = await client.TestGetAsync("timelines/newt"); body.Name.Should().Be("newt"); } } @@ -920,9 +920,9 @@ namespace Timeline.Tests.IntegratedTests string etag; { - var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", new TimelinePostCreateRequest + var body = await client.TestPostAsync($"timelines/{generator(1)}/posts", new HttpTimelinePostCreateRequest { - Content = new TimelinePostCreateRequestContent + Content = new HttpTimelinePostCreateRequestContent { Type = TimelinePostContentTypes.Image, Data = Convert.ToBase64String(ImageHelper.CreatePngWithSize(100, 50)) diff --git a/BackEnd/Timeline.Tests/IntegratedTests/TokenTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/TokenTest.cs index a5208618..fdf1af99 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/TokenTest.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/TokenTest.cs @@ -14,9 +14,9 @@ namespace Timeline.Tests.IntegratedTests private const string CreateTokenUrl = "token/create"; private const string VerifyTokenUrl = "token/verify"; - private static async Task CreateUserTokenAsync(HttpClient client, string username, string password, int? expireOffset = null) + private static async Task CreateUserTokenAsync(HttpClient client, string username, string password, int? expireOffset = null) { - return await client.TestPostAsync(CreateTokenUrl, new CreateTokenRequest { Username = username, Password = password, Expire = expireOffset }); + return await client.TestPostAsync(CreateTokenUrl, new HttpCreateTokenRequest { Username = username, Password = password, Expire = expireOffset }); } public static IEnumerable CreateToken_InvalidModel_Data() @@ -32,7 +32,7 @@ namespace Timeline.Tests.IntegratedTests public async Task CreateToken_InvalidModel(string username, string password, int expire) { using var client = await CreateDefaultClient(); - await client.TestPostAssertInvalidModelAsync(CreateTokenUrl, new CreateTokenRequest + await client.TestPostAssertInvalidModelAsync(CreateTokenUrl, new HttpCreateTokenRequest { Username = username, Password = password, @@ -52,7 +52,7 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateDefaultClient(); await client.TestPostAssertErrorAsync(CreateTokenUrl, - new CreateTokenRequest { Username = username, Password = password }, + new HttpCreateTokenRequest { Username = username, Password = password }, errorCode: ErrorCodes.TokenController.Create_BadCredential); } @@ -60,8 +60,8 @@ namespace Timeline.Tests.IntegratedTests public async Task CreateToken_Success() { using var client = await CreateDefaultClient(); - var body = await client.TestPostAsync(CreateTokenUrl, - new CreateTokenRequest { Username = "user1", Password = "user1pw" }); + var body = await client.TestPostAsync(CreateTokenUrl, + new HttpCreateTokenRequest { Username = "user1", Password = "user1pw" }); body.Token.Should().NotBeNullOrWhiteSpace(); body.User.Should().BeEquivalentTo(await client.GetUserAsync("user1")); } @@ -70,7 +70,7 @@ namespace Timeline.Tests.IntegratedTests public async Task VerifyToken_InvalidModel() { using var client = await CreateDefaultClient(); - await client.TestPostAssertInvalidModelAsync(VerifyTokenUrl, new VerifyTokenRequest { Token = null! }); + await client.TestPostAssertInvalidModelAsync(VerifyTokenUrl, new HttpVerifyTokenRequest { Token = null! }); } [Fact] @@ -78,7 +78,7 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateDefaultClient(); await client.TestPostAssertErrorAsync(VerifyTokenUrl, - new VerifyTokenRequest { Token = "bad token hahaha" }, + new HttpVerifyTokenRequest { Token = "bad token hahaha" }, errorCode: ErrorCodes.TokenController.Verify_BadFormat); } @@ -97,7 +97,7 @@ namespace Timeline.Tests.IntegratedTests } await client.TestPostAssertErrorAsync(VerifyTokenUrl, - new VerifyTokenRequest { Token = token }, + new HttpVerifyTokenRequest { Token = token }, errorCode: ErrorCodes.TokenController.Verify_OldVersion); } @@ -114,7 +114,7 @@ namespace Timeline.Tests.IntegratedTests } await client.TestPostAssertErrorAsync(VerifyTokenUrl, - new VerifyTokenRequest { Token = token }, + new HttpVerifyTokenRequest { Token = token }, errorCode: ErrorCodes.TokenController.Verify_UserNotExist); } @@ -141,8 +141,8 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateDefaultClient(); var createTokenResult = await CreateUserTokenAsync(client, "user1", "user1pw"); - var body = await client.TestPostAsync(VerifyTokenUrl, - new VerifyTokenRequest { Token = createTokenResult.Token }); + var body = await client.TestPostAsync(VerifyTokenUrl, + new HttpVerifyTokenRequest { Token = createTokenResult.Token }); body.User.Should().BeEquivalentTo(await client.GetUserAsync("user1")); } } diff --git a/BackEnd/Timeline.Tests/IntegratedTests/UserTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/UserTest.cs index e0ebf635..56dbf92a 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/UserTest.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/UserTest.cs @@ -12,7 +12,7 @@ namespace Timeline.Tests.IntegratedTests public async Task UserListShouldHaveUniqueId() { using var client = await CreateDefaultClient(); - foreach (var user in await client.TestGetAsync>("users")) + foreach (var user in await client.TestGetAsync>("users")) { user.UniqueId.Should().NotBeNullOrWhiteSpace(); } @@ -22,14 +22,14 @@ namespace Timeline.Tests.IntegratedTests public async Task GetList() { using var client = await CreateDefaultClient(); - await client.TestGetAsync>("users"); + await client.TestGetAsync>("users"); } [Fact] public async Task Get() { using var client = await CreateDefaultClient(); - var user = await client.TestGetAsync($"users/admin"); + var user = await client.TestGetAsync($"users/admin"); user.Username.Should().Be("admin"); user.Nickname.Should().Be("administrator"); user.UniqueId.Should().NotBeNullOrEmpty(); @@ -55,8 +55,8 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateClientAsUser(); { - var body = await client.TestPatchAsync("users/user1", - new UserPatchRequest { Nickname = "aaa" }); + var body = await client.TestPatchAsync("users/user1", + new HttpUserPatchRequest { Nickname = "aaa" }); body.Nickname.Should().Be("aaa"); } @@ -73,8 +73,8 @@ namespace Timeline.Tests.IntegratedTests using var userClient = await CreateClientAsUser(); { - var body = await client.TestPatchAsync("users/user1", - new UserPatchRequest + var body = await client.TestPatchAsync("users/user1", + new HttpUserPatchRequest { Username = "newuser", Password = "newpw", @@ -91,7 +91,7 @@ namespace Timeline.Tests.IntegratedTests { var token = userClient.DefaultRequestHeaders.Authorization!.Parameter!; // Token should expire. - await userClient.TestPostAssertErrorAsync("token/verify", new VerifyTokenRequest() { Token = token }); + await userClient.TestPostAssertErrorAsync("token/verify", new HttpVerifyTokenRequest() { Token = token }); } { @@ -104,26 +104,26 @@ namespace Timeline.Tests.IntegratedTests public async Task Patch_NotExist() { using var client = await CreateClientAsAdministrator(); - await client.TestPatchAssertNotFoundAsync("users/usernotexist", new UserPatchRequest { }, errorCode: ErrorCodes.UserCommon.NotExist); + await client.TestPatchAssertNotFoundAsync("users/usernotexist", new HttpUserPatchRequest { }, errorCode: ErrorCodes.UserCommon.NotExist); } [Fact] public async Task Patch_InvalidModel() { using var client = await CreateClientAsAdministrator(); - await client.TestPatchAssertInvalidModelAsync("users/aaa!a", new UserPatchRequest { }); + await client.TestPatchAssertInvalidModelAsync("users/aaa!a", new HttpUserPatchRequest { }); } public static IEnumerable Patch_InvalidModel_Body_Data() { - yield return new[] { new UserPatchRequest { Username = "aaa!a" } }; - yield return new[] { new UserPatchRequest { Password = "" } }; - yield return new[] { new UserPatchRequest { Nickname = new string('a', 50) } }; + yield return new[] { new HttpUserPatchRequest { Username = "aaa!a" } }; + yield return new[] { new HttpUserPatchRequest { Password = "" } }; + yield return new[] { new HttpUserPatchRequest { Nickname = new string('a', 50) } }; } [Theory] [MemberData(nameof(Patch_InvalidModel_Body_Data))] - public async Task Patch_InvalidModel_Body(UserPatchRequest body) + public async Task Patch_InvalidModel_Body(HttpUserPatchRequest body) { using var client = await CreateClientAsAdministrator(); await client.TestPatchAssertInvalidModelAsync("users/user1", body); @@ -133,35 +133,35 @@ namespace Timeline.Tests.IntegratedTests public async Task Patch_UsernameConflict() { using var client = await CreateClientAsAdministrator(); - await client.TestPatchAssertErrorAsync("users/user1", new UserPatchRequest { Username = "admin" }, errorCode: ErrorCodes.UserController.UsernameConflict); + await client.TestPatchAssertErrorAsync("users/user1", new HttpUserPatchRequest { Username = "admin" }, errorCode: ErrorCodes.UserController.UsernameConflict); } [Fact] public async Task Patch_NoAuth_Unauthorized() { using var client = await CreateDefaultClient(); - await client.TestPatchAssertUnauthorizedAsync("users/user1", new UserPatchRequest { Nickname = "aaa" }); + await client.TestPatchAssertUnauthorizedAsync("users/user1", new HttpUserPatchRequest { Nickname = "aaa" }); } [Fact] public async Task Patch_User_Forbid() { using var client = await CreateClientAsUser(); - await client.TestPatchAssertForbiddenAsync("users/admin", new UserPatchRequest { Nickname = "aaa" }); + await client.TestPatchAssertForbiddenAsync("users/admin", new HttpUserPatchRequest { Nickname = "aaa" }); } [Fact] public async Task Patch_Username_Forbid() { using var client = await CreateClientAsUser(); - await client.TestPatchAssertForbiddenAsync("users/user1", new UserPatchRequest { Username = "aaa" }); + await client.TestPatchAssertForbiddenAsync("users/user1", new HttpUserPatchRequest { Username = "aaa" }); } [Fact] public async Task Patch_Password_Forbid() { using var client = await CreateClientAsUser(); - await client.TestPatchAssertForbiddenAsync("users/user1", new UserPatchRequest { Password = "aaa" }); + await client.TestPatchAssertForbiddenAsync("users/user1", new HttpUserPatchRequest { Password = "aaa" }); } [Fact] @@ -214,7 +214,7 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateClientAsAdministrator(); { - var body = await client.TestPostAsync(createUserUrl, new CreateUserRequest + var body = await client.TestPostAsync(createUserUrl, new HttpCreateUserRequest { Username = "aaa", Password = "bbb", @@ -233,15 +233,15 @@ namespace Timeline.Tests.IntegratedTests public static IEnumerable Op_CreateUser_InvalidModel_Data() { - yield return new[] { new CreateUserRequest { Username = "aaa" } }; - yield return new[] { new CreateUserRequest { Password = "bbb" } }; - yield return new[] { new CreateUserRequest { Username = "a!a", Password = "bbb" } }; - yield return new[] { new CreateUserRequest { Username = "aaa", Password = "" } }; + yield return new[] { new HttpCreateUserRequest { Username = "aaa" } }; + yield return new[] { new HttpCreateUserRequest { Password = "bbb" } }; + yield return new[] { new HttpCreateUserRequest { Username = "a!a", Password = "bbb" } }; + yield return new[] { new HttpCreateUserRequest { Username = "aaa", Password = "" } }; } [Theory] [MemberData(nameof(Op_CreateUser_InvalidModel_Data))] - public async Task Op_CreateUser_InvalidModel(CreateUserRequest body) + public async Task Op_CreateUser_InvalidModel(HttpCreateUserRequest body) { using var client = await CreateClientAsAdministrator(); await client.TestPostAssertInvalidModelAsync(createUserUrl, body); @@ -251,7 +251,7 @@ namespace Timeline.Tests.IntegratedTests public async Task Op_CreateUser_UsernameConflict() { using var client = await CreateClientAsAdministrator(); - await client.TestPostAssertErrorAsync(createUserUrl, new CreateUserRequest + await client.TestPostAssertErrorAsync(createUserUrl, new HttpCreateUserRequest { Username = "user1", Password = "bbb", @@ -262,7 +262,7 @@ namespace Timeline.Tests.IntegratedTests public async Task Op_CreateUser_NoAuth_Unauthorized() { using var client = await CreateDefaultClient(); - await client.TestPostAssertUnauthorizedAsync(createUserUrl, new CreateUserRequest + await client.TestPostAssertUnauthorizedAsync(createUserUrl, new HttpCreateUserRequest { Username = "aaa", Password = "bbb", @@ -273,7 +273,7 @@ namespace Timeline.Tests.IntegratedTests public async Task Op_CreateUser_User_Forbid() { using var client = await CreateClientAsUser(); - await client.TestPostAssertForbiddenAsync(createUserUrl, new CreateUserRequest + await client.TestPostAssertForbiddenAsync(createUserUrl, new HttpCreateUserRequest { Username = "aaa", Password = "bbb", @@ -286,8 +286,8 @@ namespace Timeline.Tests.IntegratedTests public async Task Op_ChangePassword() { using var client = await CreateClientAsUser(); - await client.TestPostAsync(changePasswordUrl, new ChangePasswordRequest { OldPassword = "user1pw", NewPassword = "newpw" }); - await client.TestPatchAssertUnauthorizedAsync("users/user1", new UserPatchRequest { }); + await client.TestPostAsync(changePasswordUrl, new HttpChangePasswordRequest { OldPassword = "user1pw", NewPassword = "newpw" }); + await client.TestPatchAssertUnauthorizedAsync("users/user1", new HttpUserPatchRequest { }); (await CreateClientWithCredential("user1", "newpw")).Dispose(); } @@ -303,21 +303,21 @@ namespace Timeline.Tests.IntegratedTests { using var client = await CreateClientAsUser(); await client.TestPostAssertInvalidModelAsync(changePasswordUrl, - new ChangePasswordRequest { OldPassword = oldPassword, NewPassword = newPassword }); + new HttpChangePasswordRequest { OldPassword = oldPassword, NewPassword = newPassword }); } [Fact] public async Task Op_ChangePassword_BadOldPassword() { using var client = await CreateClientAsUser(); - await client.TestPostAssertErrorAsync(changePasswordUrl, new ChangePasswordRequest { OldPassword = "???", NewPassword = "???" }, errorCode: ErrorCodes.UserController.ChangePassword_BadOldPassword); + await client.TestPostAssertErrorAsync(changePasswordUrl, new HttpChangePasswordRequest { OldPassword = "???", NewPassword = "???" }, errorCode: ErrorCodes.UserController.ChangePassword_BadOldPassword); } [Fact] public async Task Op_ChangePassword_NoAuth_Unauthorized() { using var client = await CreateDefaultClient(); - await client.TestPostAssertUnauthorizedAsync(changePasswordUrl, new ChangePasswordRequest { OldPassword = "???", NewPassword = "???" }); + await client.TestPostAssertUnauthorizedAsync(changePasswordUrl, new HttpChangePasswordRequest { OldPassword = "???", NewPassword = "???" }); } } } diff --git a/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs b/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs index fac0b6f3..98f03066 100644 --- a/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs +++ b/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs @@ -67,7 +67,7 @@ namespace Timeline.Tests.Services { var initTime = _clock.ForwardCurrentTime(); - void Check(Models.Timeline timeline) + void Check(TimelineInfo timeline) { timeline.NameLastModified.Should().Be(initTime); timeline.LastModified.Should().Be(_clock.GetCurrentTime()); diff --git a/BackEnd/Timeline/Controllers/TimelineController.cs b/BackEnd/Timeline/Controllers/TimelineController.cs index 0ffadc50..27b4b7a7 100644 --- a/BackEnd/Timeline/Controllers/TimelineController.cs +++ b/BackEnd/Timeline/Controllers/TimelineController.cs @@ -57,7 +57,7 @@ namespace Timeline.Controllers [HttpGet("timelines")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task>> TimelineList([FromQuery][Username] string? relate, [FromQuery][RegularExpression("(own)|(join)")] string? relateType, [FromQuery] string? visibility) + public async Task>> TimelineList([FromQuery][Username] string? relate, [FromQuery][RegularExpression("(own)|(join)")] string? relateType, [FromQuery] string? visibility) { List? visibilityFilter = null; if (visibility != null) @@ -109,7 +109,7 @@ namespace Timeline.Controllers } var timelines = await _service.GetTimelines(relationship, visibilityFilter); - var result = _mapper.Map>(timelines); + var result = _mapper.Map>(timelines); return result; } @@ -125,7 +125,7 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status304NotModified)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> TimelineGet([FromRoute][GeneralTimelineName] string name, [FromQuery] string? checkUniqueId, [FromQuery(Name = "ifModifiedSince")] DateTime? queryIfModifiedSince, [FromHeader(Name = "If-Modified-Since")] DateTime? headerIfModifiedSince) + public async Task> TimelineGet([FromRoute][GeneralTimelineName] string name, [FromQuery] string? checkUniqueId, [FromQuery(Name = "ifModifiedSince")] DateTime? queryIfModifiedSince, [FromHeader(Name = "If-Modified-Since")] DateTime? headerIfModifiedSince) { DateTime? ifModifiedSince = null; if (queryIfModifiedSince.HasValue) @@ -166,7 +166,7 @@ namespace Timeline.Controllers else { var timeline = await _service.GetTimeline(name); - var result = _mapper.Map(timeline); + var result = _mapper.Map(timeline); return result; } } @@ -182,16 +182,16 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task>> PostListGet([FromRoute][GeneralTimelineName] string name, [FromQuery] DateTime? modifiedSince, [FromQuery] bool? includeDeleted) + public async Task>> PostListGet([FromRoute][GeneralTimelineName] string name, [FromQuery] DateTime? modifiedSince, [FromQuery] bool? includeDeleted) { if (!UserHasAllTimelineManagementPermission && !await _service.HasReadPermission(name, this.GetOptionalUserId())) { return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); } - List posts = await _postService.GetPosts(name, modifiedSince, includeDeleted ?? false); + List posts = await _postService.GetPosts(name, modifiedSince, includeDeleted ?? false); - var result = _mapper.Map>(posts); + var result = _mapper.Map>(posts); return result; } @@ -247,7 +247,7 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task> PostPost([FromRoute][GeneralTimelineName] string name, [FromBody] TimelinePostCreateRequest body) + public async Task> PostPost([FromRoute][GeneralTimelineName] string name, [FromBody] HttpTimelinePostCreateRequest body) { var id = this.GetUserId(); if (!UserHasAllTimelineManagementPermission && !await _service.IsMemberOf(name, id)) @@ -257,7 +257,7 @@ namespace Timeline.Controllers var content = body.Content; - TimelinePost post; + TimelinePostInfo post; if (content.Type == TimelinePostContentTypes.Text) { @@ -299,7 +299,7 @@ namespace Timeline.Controllers return BadRequest(ErrorResponse.Common.CustomMessage_InvalidModel(Resources.Messages.TimelineController_ContentUnknownType)); } - var result = _mapper.Map(post); + var result = _mapper.Map(post); return result; } @@ -344,7 +344,7 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task> TimelinePatch([FromRoute][GeneralTimelineName] string name, [FromBody] TimelinePatchRequest body) + public async Task> TimelinePatch([FromRoute][GeneralTimelineName] string name, [FromBody] HttpTimelinePatchRequest body) { if (!UserHasAllTimelineManagementPermission && !(await _service.HasManagePermission(name, this.GetUserId()))) { @@ -352,7 +352,7 @@ namespace Timeline.Controllers } await _service.ChangeProperty(name, _mapper.Map(body)); var timeline = await _service.GetTimeline(name); - var result = _mapper.Map(timeline); + var result = _mapper.Map(timeline); return result; } @@ -423,14 +423,14 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public async Task> TimelineCreate([FromBody] TimelineCreateRequest body) + public async Task> TimelineCreate([FromBody] TimelineCreateRequest body) { var userId = this.GetUserId(); try { var timeline = await _service.CreateTimeline(body.Name, userId); - var result = _mapper.Map(timeline); + var result = _mapper.Map(timeline); return result; } catch (EntityAlreadyExistException e) when (e.EntityName == EntityNames.Timeline) @@ -474,7 +474,7 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task> TimelineOpChangeName([FromBody] TimelineChangeNameRequest body) + public async Task> TimelineOpChangeName([FromBody] HttpTimelineChangeNameRequest body) { if (!UserHasAllTimelineManagementPermission && !(await _service.HasManagePermission(body.OldName, this.GetUserId()))) { @@ -484,7 +484,7 @@ namespace Timeline.Controllers try { var timeline = await _service.ChangeTimelineName(body.OldName, body.NewName); - return Ok(_mapper.Map(timeline)); + return Ok(_mapper.Map(timeline)); } catch (EntityAlreadyExistException) { diff --git a/BackEnd/Timeline/Controllers/TokenController.cs b/BackEnd/Timeline/Controllers/TokenController.cs index 41ec21e6..c801b8cc 100644 --- a/BackEnd/Timeline/Controllers/TokenController.cs +++ b/BackEnd/Timeline/Controllers/TokenController.cs @@ -47,7 +47,7 @@ namespace Timeline.Controllers [AllowAnonymous] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task> Create([FromBody] CreateTokenRequest request) + public async Task> Create([FromBody] HttpCreateTokenRequest request) { void LogFailure(string reason, Exception? e = null) { @@ -71,10 +71,10 @@ namespace Timeline.Controllers ("Username", request.Username), ("Expire At", expireTime?.ToString(CultureInfo.CurrentCulture.DateTimeFormat) ?? "default") )); - return Ok(new CreateTokenResponse + return Ok(new HttpCreateTokenResponse { Token = result.Token, - User = _mapper.Map(result.User) + User = _mapper.Map(result.User) }); } catch (UserNotExistException e) @@ -97,7 +97,7 @@ namespace Timeline.Controllers [AllowAnonymous] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task> Verify([FromBody] VerifyTokenRequest request) + public async Task> Verify([FromBody] HttpVerifyTokenRequest request) { void LogFailure(string reason, Exception? e = null, params (string, object?)[] otherProperties) { @@ -113,9 +113,9 @@ namespace Timeline.Controllers var result = await _userTokenManager.VerifyToken(request.Token); _logger.LogInformation(Log.Format(LogVerifySuccess, ("Username", result.Username), ("Token", request.Token))); - return Ok(new VerifyTokenResponse + return Ok(new HttpVerifyTokenResponse { - User = _mapper.Map(result) + User = _mapper.Map(result) }); } catch (UserTokenTimeExpireException e) diff --git a/BackEnd/Timeline/Controllers/UserController.cs b/BackEnd/Timeline/Controllers/UserController.cs index 626a116f..3727da36 100644 --- a/BackEnd/Timeline/Controllers/UserController.cs +++ b/BackEnd/Timeline/Controllers/UserController.cs @@ -42,7 +42,7 @@ namespace Timeline.Controllers _mapper = mapper; } - private UserInfo ConvertToUserInfo(User user) => _mapper.Map(user); + private HttpUser ConvertToUserInfo(UserInfo user) => _mapper.Map(user); private bool UserHasUserManagementPermission => this.UserHasPermission(UserPermission.UserManagement); @@ -52,7 +52,7 @@ namespace Timeline.Controllers /// All user list. [HttpGet("users")] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task> List() + public async Task> List() { var users = await _userService.GetUsers(); var result = users.Select(u => ConvertToUserInfo(u)).ToArray(); @@ -67,7 +67,7 @@ namespace Timeline.Controllers [HttpGet("users/{username}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> Get([FromRoute][Username] string username) + public async Task> Get([FromRoute][Username] string username) { try { @@ -94,7 +94,7 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task> Patch([FromBody] UserPatchRequest body, [FromRoute][Username] string username) + public async Task> Patch([FromBody] HttpUserPatchRequest body, [FromRoute][Username] string username) { if (UserHasUserManagementPermission) { @@ -168,7 +168,7 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task> CreateUser([FromBody] CreateUserRequest body) + public async Task> CreateUser([FromBody] HttpCreateUserRequest body) { try { @@ -188,7 +188,7 @@ namespace Timeline.Controllers [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public async Task ChangePassword([FromBody] ChangePasswordRequest request) + public async Task ChangePassword([FromBody] HttpChangePasswordRequest request) { try { diff --git a/BackEnd/Timeline/Models/Http/Timeline.cs b/BackEnd/Timeline/Models/Http/Timeline.cs index a81b33f5..8e3831e1 100644 --- a/BackEnd/Timeline/Models/Http/Timeline.cs +++ b/BackEnd/Timeline/Models/Http/Timeline.cs @@ -11,7 +11,7 @@ namespace Timeline.Models.Http /// /// Info of post content. /// - public class TimelinePostContentInfo + public class HttpTimelinePostContent { /// /// Type of the post content. @@ -34,7 +34,7 @@ namespace Timeline.Models.Http /// /// Info of a post. /// - public class TimelinePostInfo + public class HttpTimelinePost { /// /// Post id. @@ -43,7 +43,7 @@ namespace Timeline.Models.Http /// /// Content of the post. May be null if post is deleted. /// - public TimelinePostContentInfo? Content { get; set; } + public HttpTimelinePostContent? Content { get; set; } /// /// True if post is deleted. /// @@ -55,7 +55,7 @@ namespace Timeline.Models.Http /// /// The author. May be null if the user has been deleted. /// - public UserInfo? Author { get; set; } = default!; + public HttpUser? Author { get; set; } = default!; /// /// Last updated time. /// @@ -65,7 +65,7 @@ namespace Timeline.Models.Http /// /// Info of a timeline. /// - public class TimelineInfo + public class HttpTimeline { /// /// Unique id. @@ -90,7 +90,7 @@ namespace Timeline.Models.Http /// /// Owner of the timeline. /// - public UserInfo Owner { get; set; } = default!; + public HttpUser Owner { get; set; } = default!; /// /// Visibility of the timeline. /// @@ -99,7 +99,7 @@ namespace Timeline.Models.Http /// /// Members of timeline. /// - public List Members { get; set; } = default!; + public List Members { get; set; } = default!; #pragma warning restore CA2227 // Collection properties should be read only /// /// Create time of timeline. @@ -114,14 +114,14 @@ namespace Timeline.Models.Http /// /// Related links. /// - public TimelineInfoLinks _links { get; set; } = default!; + public HttpTimelineLinks _links { get; set; } = default!; #pragma warning restore CA1707 // Identifiers should not contain underscores } /// /// Related links for timeline. /// - public class TimelineInfoLinks + public class HttpTimelineLinks { /// /// Self. @@ -133,23 +133,23 @@ namespace Timeline.Models.Http public string Posts { get; set; } = default!; } - public class TimelineInfoLinksValueResolver : IValueResolver + public class HttpTimelineLinksValueResolver : IValueResolver { private readonly IActionContextAccessor _actionContextAccessor; private readonly IUrlHelperFactory _urlHelperFactory; - public TimelineInfoLinksValueResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) + public HttpTimelineLinksValueResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) { _actionContextAccessor = actionContextAccessor; _urlHelperFactory = urlHelperFactory; } - public TimelineInfoLinks Resolve(Timeline source, TimelineInfo destination, TimelineInfoLinks destMember, ResolutionContext context) + public HttpTimelineLinks Resolve(TimelineInfo source, HttpTimeline destination, HttpTimelineLinks destMember, ResolutionContext context) { var actionContext = _actionContextAccessor.AssertActionContextForUrlFill(); var urlHelper = _urlHelperFactory.GetUrlHelper(actionContext); - return new TimelineInfoLinks + return new HttpTimelineLinks { Self = urlHelper.ActionLink(nameof(TimelineController.TimelineGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { source.Name }), Posts = urlHelper.ActionLink(nameof(TimelineController.PostListGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { source.Name }) @@ -157,18 +157,18 @@ namespace Timeline.Models.Http } } - public class TimelinePostContentResolver : IValueResolver + public class HttpTimelinePostContentResolver : IValueResolver { private readonly IActionContextAccessor _actionContextAccessor; private readonly IUrlHelperFactory _urlHelperFactory; - public TimelinePostContentResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) + public HttpTimelinePostContentResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) { _actionContextAccessor = actionContextAccessor; _urlHelperFactory = urlHelperFactory; } - public TimelinePostContentInfo? Resolve(TimelinePost source, TimelinePostInfo destination, TimelinePostContentInfo? destMember, ResolutionContext context) + public HttpTimelinePostContent? Resolve(TimelinePostInfo source, HttpTimelinePost destination, HttpTimelinePostContent? destMember, ResolutionContext context) { var actionContext = _actionContextAccessor.AssertActionContextForUrlFill(); var urlHelper = _urlHelperFactory.GetUrlHelper(actionContext); @@ -182,7 +182,7 @@ namespace Timeline.Models.Http if (sourceContent is TextTimelinePostContent textContent) { - return new TimelinePostContentInfo + return new HttpTimelinePostContent { Type = TimelinePostContentTypes.Text, Text = textContent.Text @@ -190,7 +190,7 @@ namespace Timeline.Models.Http } else if (sourceContent is ImageTimelinePostContent imageContent) { - return new TimelinePostContentInfo + return new HttpTimelinePostContent { Type = TimelinePostContentTypes.Image, Url = urlHelper.ActionLink( @@ -207,13 +207,12 @@ namespace Timeline.Models.Http } } - public class TimelineInfoAutoMapperProfile : Profile + public class HttpTimelineAutoMapperProfile : Profile { - public TimelineInfoAutoMapperProfile() + public HttpTimelineAutoMapperProfile() { - CreateMap().ForMember(u => u._links, opt => opt.MapFrom()); - CreateMap().ForMember(p => p.Content, opt => opt.MapFrom()); - CreateMap(); + CreateMap().ForMember(u => u._links, opt => opt.MapFrom()); + CreateMap().ForMember(p => p.Content, opt => opt.MapFrom()); } } } diff --git a/BackEnd/Timeline/Models/Http/TimelineController.cs b/BackEnd/Timeline/Models/Http/TimelineController.cs index 7bd141ed..42a926fd 100644 --- a/BackEnd/Timeline/Models/Http/TimelineController.cs +++ b/BackEnd/Timeline/Models/Http/TimelineController.cs @@ -1,4 +1,5 @@ -using System; +using AutoMapper; +using System; using System.ComponentModel.DataAnnotations; using Timeline.Models.Validation; @@ -7,7 +8,7 @@ namespace Timeline.Models.Http /// /// Content of post create request. /// - public class TimelinePostCreateRequestContent + public class HttpTimelinePostCreateRequestContent { /// /// Type of post content. @@ -24,13 +25,13 @@ namespace Timeline.Models.Http public string? Data { get; set; } } - public class TimelinePostCreateRequest + public class HttpTimelinePostCreateRequest { /// /// Content of the new post. /// [Required] - public TimelinePostCreateRequestContent Content { get; set; } = default!; + public HttpTimelinePostCreateRequestContent Content { get; set; } = default!; /// /// Time of the post. If not set, current time will be used. @@ -54,7 +55,7 @@ namespace Timeline.Models.Http /// /// Patch timeline request model. /// - public class TimelinePatchRequest + public class HttpTimelinePatchRequest { /// /// New title. Null for not change. @@ -75,7 +76,7 @@ namespace Timeline.Models.Http /// /// Change timeline name request model. /// - public class TimelineChangeNameRequest + public class HttpTimelineChangeNameRequest { /// /// Old name of timeline. @@ -90,4 +91,12 @@ namespace Timeline.Models.Http [TimelineName] public string NewName { get; set; } = default!; } + + public class HttpTimelineControllerAutoMapperProfile : Profile + { + public HttpTimelineControllerAutoMapperProfile() + { + CreateMap(); + } + } } diff --git a/BackEnd/Timeline/Models/Http/TokenController.cs b/BackEnd/Timeline/Models/Http/TokenController.cs index a42c44e5..a5cbba14 100644 --- a/BackEnd/Timeline/Models/Http/TokenController.cs +++ b/BackEnd/Timeline/Models/Http/TokenController.cs @@ -4,9 +4,9 @@ using Timeline.Controllers; namespace Timeline.Models.Http { /// - /// Request model for . + /// Request model for . /// - public class CreateTokenRequest + public class HttpCreateTokenRequest { /// /// The username. @@ -24,9 +24,9 @@ namespace Timeline.Models.Http } /// - /// Response model for . + /// Response model for . /// - public class CreateTokenResponse + public class HttpCreateTokenResponse { /// /// The token created. @@ -35,13 +35,13 @@ namespace Timeline.Models.Http /// /// The user owning the token. /// - public UserInfo User { get; set; } = default!; + public HttpUser User { get; set; } = default!; } /// - /// Request model for . + /// Request model for . /// - public class VerifyTokenRequest + public class HttpVerifyTokenRequest { /// /// The token to verify. @@ -50,13 +50,13 @@ namespace Timeline.Models.Http } /// - /// Response model for . + /// Response model for . /// - public class VerifyTokenResponse + public class HttpVerifyTokenResponse { /// /// The user owning the token. /// - public UserInfo User { get; set; } = default!; + public HttpUser User { get; set; } = default!; } } diff --git a/BackEnd/Timeline/Models/Http/User.cs b/BackEnd/Timeline/Models/Http/User.cs new file mode 100644 index 00000000..bdb40b9f --- /dev/null +++ b/BackEnd/Timeline/Models/Http/User.cs @@ -0,0 +1,105 @@ +using AutoMapper; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using System.Collections.Generic; +using Timeline.Controllers; +using Timeline.Services; + +namespace Timeline.Models.Http +{ + /// + /// Info of a user. + /// + public class HttpUser + { + /// + /// Unique id. + /// + public string UniqueId { get; set; } = default!; + /// + /// Username. + /// + public string Username { get; set; } = default!; + /// + /// Nickname. + /// + public string Nickname { get; set; } = default!; +#pragma warning disable CA2227 // Collection properties should be read only + /// + /// The permissions of the user. + /// + public List Permissions { get; set; } = default!; +#pragma warning restore CA2227 // Collection properties should be read only +#pragma warning disable CA1707 // Identifiers should not contain underscores + /// + /// Related links. + /// + public HttpUserLinks _links { get; set; } = default!; +#pragma warning restore CA1707 // Identifiers should not contain underscores + } + + /// + /// Related links for user. + /// + public class HttpUserLinks + { + /// + /// Self. + /// + public string Self { get; set; } = default!; + /// + /// Avatar url. + /// + public string Avatar { get; set; } = default!; + /// + /// Personal timeline url. + /// + public string Timeline { get; set; } = default!; + } + + public class HttpUserPermissionsValueConverter : ITypeConverter> + { + public List Convert(UserPermissions source, List destination, ResolutionContext context) + { + return source.ToStringList(); + } + } + + public class HttpUserLinksValueResolver : IValueResolver + { + private readonly IActionContextAccessor _actionContextAccessor; + private readonly IUrlHelperFactory _urlHelperFactory; + + public HttpUserLinksValueResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) + { + _actionContextAccessor = actionContextAccessor; + _urlHelperFactory = urlHelperFactory; + } + + public HttpUserLinks Resolve(UserInfo source, HttpUser destination, HttpUserLinks destMember, ResolutionContext context) + { + var actionContext = _actionContextAccessor.AssertActionContextForUrlFill(); + var urlHelper = _urlHelperFactory.GetUrlHelper(actionContext); + + var result = new HttpUserLinks + { + Self = urlHelper.ActionLink(nameof(UserController.Get), nameof(UserController)[0..^nameof(Controller).Length], new { destination.Username }), + Avatar = urlHelper.ActionLink(nameof(UserAvatarController.Get), nameof(UserAvatarController)[0..^nameof(Controller).Length], new { destination.Username }), + Timeline = urlHelper.ActionLink(nameof(TimelineController.TimelineGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { Name = "@" + destination.Username }) + }; + return result; + } + } + + public class HttpUserAutoMapperProfile : Profile + { + public HttpUserAutoMapperProfile() + { + CreateMap>() + .ConvertUsing(); + CreateMap() + .ForMember(u => u._links, opt => opt.MapFrom()); + } + } +} diff --git a/BackEnd/Timeline/Models/Http/UserController.cs b/BackEnd/Timeline/Models/Http/UserController.cs index 92a63874..1b4d09ec 100644 --- a/BackEnd/Timeline/Models/Http/UserController.cs +++ b/BackEnd/Timeline/Models/Http/UserController.cs @@ -7,9 +7,9 @@ using Timeline.Services; namespace Timeline.Models.Http { /// - /// Request model for . + /// Request model for . /// - public class UserPatchRequest + public class HttpUserPatchRequest { /// /// New username. Null if not change. Need to be administrator. @@ -31,9 +31,9 @@ namespace Timeline.Models.Http } /// - /// Request model for . + /// Request model for . /// - public class CreateUserRequest + public class HttpCreateUserRequest { /// /// Username of the new user. @@ -49,9 +49,9 @@ namespace Timeline.Models.Http } /// - /// Request model for . + /// Request model for . /// - public class ChangePasswordRequest + public class HttpChangePasswordRequest { /// /// Old password. @@ -66,11 +66,11 @@ namespace Timeline.Models.Http public string NewPassword { get; set; } = default!; } - public class UserControllerAutoMapperProfile : Profile + public class HttpUserControllerModelAutoMapperProfile : Profile { - public UserControllerAutoMapperProfile() + public HttpUserControllerModelAutoMapperProfile() { - CreateMap(); + CreateMap(); } } } diff --git a/BackEnd/Timeline/Models/Http/UserInfo.cs b/BackEnd/Timeline/Models/Http/UserInfo.cs deleted file mode 100644 index 0f865172..00000000 --- a/BackEnd/Timeline/Models/Http/UserInfo.cs +++ /dev/null @@ -1,105 +0,0 @@ -using AutoMapper; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Infrastructure; -using Microsoft.AspNetCore.Mvc.Routing; -using System.Collections.Generic; -using Timeline.Controllers; -using Timeline.Services; - -namespace Timeline.Models.Http -{ - /// - /// Info of a user. - /// - public class UserInfo - { - /// - /// Unique id. - /// - public string UniqueId { get; set; } = default!; - /// - /// Username. - /// - public string Username { get; set; } = default!; - /// - /// Nickname. - /// - public string Nickname { get; set; } = default!; -#pragma warning disable CA2227 // Collection properties should be read only - /// - /// The permissions of the user. - /// - public List Permissions { get; set; } = default!; -#pragma warning restore CA2227 // Collection properties should be read only -#pragma warning disable CA1707 // Identifiers should not contain underscores - /// - /// Related links. - /// - public UserInfoLinks _links { get; set; } = default!; -#pragma warning restore CA1707 // Identifiers should not contain underscores - } - - /// - /// Related links for user. - /// - public class UserInfoLinks - { - /// - /// Self. - /// - public string Self { get; set; } = default!; - /// - /// Avatar url. - /// - public string Avatar { get; set; } = default!; - /// - /// Personal timeline url. - /// - public string Timeline { get; set; } = default!; - } - - public class UserPermissionsValueConverter : ITypeConverter> - { - public List Convert(UserPermissions source, List destination, ResolutionContext context) - { - return source.ToStringList(); - } - } - - public class UserInfoLinksValueResolver : IValueResolver - { - private readonly IActionContextAccessor _actionContextAccessor; - private readonly IUrlHelperFactory _urlHelperFactory; - - public UserInfoLinksValueResolver(IActionContextAccessor actionContextAccessor, IUrlHelperFactory urlHelperFactory) - { - _actionContextAccessor = actionContextAccessor; - _urlHelperFactory = urlHelperFactory; - } - - public UserInfoLinks Resolve(User source, UserInfo destination, UserInfoLinks destMember, ResolutionContext context) - { - var actionContext = _actionContextAccessor.AssertActionContextForUrlFill(); - var urlHelper = _urlHelperFactory.GetUrlHelper(actionContext); - - var result = new UserInfoLinks - { - Self = urlHelper.ActionLink(nameof(UserController.Get), nameof(UserController)[0..^nameof(Controller).Length], new { destination.Username }), - Avatar = urlHelper.ActionLink(nameof(UserAvatarController.Get), nameof(UserAvatarController)[0..^nameof(Controller).Length], new { destination.Username }), - Timeline = urlHelper.ActionLink(nameof(TimelineController.TimelineGet), nameof(TimelineController)[0..^nameof(Controller).Length], new { Name = "@" + destination.Username }) - }; - return result; - } - } - - public class UserInfoAutoMapperProfile : Profile - { - public UserInfoAutoMapperProfile() - { - CreateMap>() - .ConvertUsing(); - CreateMap() - .ForMember(u => u._links, opt => opt.MapFrom()); - } - } -} diff --git a/BackEnd/Timeline/Models/Timeline.cs b/BackEnd/Timeline/Models/Timeline.cs deleted file mode 100644 index a5987577..00000000 --- a/BackEnd/Timeline/Models/Timeline.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Timeline.Models -{ - public enum TimelineVisibility - { - /// - /// All people including those without accounts. - /// - Public, - /// - /// Only people signed in. - /// - Register, - /// - /// Only member. - /// - Private - } - - public static class TimelinePostContentTypes - { - public const string Text = "text"; - public const string Image = "image"; - } - - public interface ITimelinePostContent - { - public string Type { get; } - } - - public class TextTimelinePostContent : ITimelinePostContent - { - public TextTimelinePostContent(string text) { Text = text; } - - public string Type { get; } = TimelinePostContentTypes.Text; - public string Text { get; set; } - } - - public class ImageTimelinePostContent : ITimelinePostContent - { - public ImageTimelinePostContent(string dataTag) { DataTag = dataTag; } - - public string Type { get; } = TimelinePostContentTypes.Image; - - /// - /// The tag of the data. The tag of the entry in DataManager. Also the etag (not quoted). - /// - public string DataTag { get; set; } - } - - public class TimelinePost - { - public TimelinePost(long id, ITimelinePostContent? content, DateTime time, User? author, DateTime lastUpdated, string timelineName) - { - Id = id; - Content = content; - Time = time; - Author = author; - LastUpdated = lastUpdated; - TimelineName = timelineName; - } - - public long Id { get; set; } - public ITimelinePostContent? Content { get; set; } - public bool Deleted => Content == null; - public DateTime Time { get; set; } - public User? Author { get; set; } - public DateTime LastUpdated { get; set; } - public string TimelineName { get; set; } - } - -#pragma warning disable CA1724 // Type names should not match namespaces - public class Timeline -#pragma warning restore CA1724 // Type names should not match namespaces - { - public string UniqueID { get; set; } = default!; - public string Name { get; set; } = default!; - public DateTime NameLastModified { get; set; } = default!; - public string Title { get; set; } = default!; - public string Description { get; set; } = default!; - public User Owner { get; set; } = default!; - public TimelineVisibility Visibility { get; set; } -#pragma warning disable CA2227 // Collection properties should be read only - public List Members { get; set; } = default!; -#pragma warning restore CA2227 // Collection properties should be read only - public DateTime CreateTime { get; set; } = default!; - public DateTime LastModified { get; set; } = default!; - } - - public class TimelineChangePropertyRequest - { - public string? Title { get; set; } - public string? Description { get; set; } - public TimelineVisibility? Visibility { get; set; } - } -} diff --git a/BackEnd/Timeline/Models/TimelineInfo.cs b/BackEnd/Timeline/Models/TimelineInfo.cs new file mode 100644 index 00000000..440f6b81 --- /dev/null +++ b/BackEnd/Timeline/Models/TimelineInfo.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; + +namespace Timeline.Models +{ + public enum TimelineVisibility + { + /// + /// All people including those without accounts. + /// + Public, + /// + /// Only people signed in. + /// + Register, + /// + /// Only member. + /// + Private + } + + public static class TimelinePostContentTypes + { + public const string Text = "text"; + public const string Image = "image"; + } + + public interface ITimelinePostContent + { + public string Type { get; } + } + + public class TextTimelinePostContent : ITimelinePostContent + { + public TextTimelinePostContent(string text) { Text = text; } + + public string Type { get; } = TimelinePostContentTypes.Text; + public string Text { get; set; } + } + + public class ImageTimelinePostContent : ITimelinePostContent + { + public ImageTimelinePostContent(string dataTag) { DataTag = dataTag; } + + public string Type { get; } = TimelinePostContentTypes.Image; + + /// + /// The tag of the data. The tag of the entry in DataManager. Also the etag (not quoted). + /// + public string DataTag { get; set; } + } + + public record TimelinePostInfo + { + public TimelinePostInfo() + { + + } + + public TimelinePostInfo(long id, ITimelinePostContent? content, DateTime time, UserInfo? author, DateTime lastUpdated, string timelineName) + { + Id = id; + Content = content; + Time = time; + Author = author; + LastUpdated = lastUpdated; + TimelineName = timelineName; + } + + public long Id { get; set; } + public ITimelinePostContent? Content { get; set; } + public bool Deleted => Content == null; + public DateTime Time { get; set; } + public UserInfo? Author { get; set; } + public DateTime LastUpdated { get; set; } + public string TimelineName { get; set; } = default!; + } + + public record TimelineInfo + { + public TimelineInfo() + { + + } + + public TimelineInfo( + string uniqueId, + string name, + DateTime nameLastModified, + string title, + string description, + UserInfo owner, + TimelineVisibility visibility, + List members, + DateTime createTime, + DateTime lastModified) + { + UniqueId = uniqueId; + Name = name; + NameLastModified = nameLastModified; + Title = title; + Description = description; + Owner = owner; + Visibility = visibility; + Members = members; + CreateTime = createTime; + LastModified = lastModified; + } + + public string UniqueId { get; set; } = default!; + public string Name { get; set; } = default!; + public DateTime NameLastModified { get; set; } = default!; + public string Title { get; set; } = default!; + public string Description { get; set; } = default!; + public UserInfo Owner { get; set; } = default!; + public TimelineVisibility Visibility { get; set; } +#pragma warning disable CA2227 // Collection properties should be read only + public List Members { get; set; } = default!; +#pragma warning restore CA2227 // Collection properties should be read only + public DateTime CreateTime { get; set; } = default!; + public DateTime LastModified { get; set; } = default!; + } + + public class TimelineChangePropertyRequest + { + public string? Title { get; set; } + public string? Description { get; set; } + public TimelineVisibility? Visibility { get; set; } + } +} diff --git a/BackEnd/Timeline/Models/User.cs b/BackEnd/Timeline/Models/User.cs deleted file mode 100644 index ae2afe85..00000000 --- a/BackEnd/Timeline/Models/User.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using Timeline.Services; - -namespace Timeline.Models -{ - public record User - { - public long Id { get; set; } - public string UniqueId { get; set; } = default!; - - public string Username { get; set; } = default!; - public string Nickname { get; set; } = default!; - - public UserPermissions Permissions { get; set; } = default!; - - public DateTime UsernameChangeTime { get; set; } - public DateTime CreateTime { get; set; } - public DateTime LastModified { get; set; } - public long Version { get; set; } - } -} diff --git a/BackEnd/Timeline/Models/UserInfo.cs b/BackEnd/Timeline/Models/UserInfo.cs new file mode 100644 index 00000000..058cc590 --- /dev/null +++ b/BackEnd/Timeline/Models/UserInfo.cs @@ -0,0 +1,48 @@ +using System; +using Timeline.Services; + +namespace Timeline.Models +{ + public record UserInfo + { + public UserInfo() + { + + } + + public UserInfo( + long id, + string uniqueId, + string username, + string nickname, + UserPermissions permissions, + DateTime usernameChangeTime, + DateTime createTime, + DateTime lastModified, + long version) + { + Id = id; + UniqueId = uniqueId; + Username = username; + Nickname = nickname; + Permissions = permissions; + UsernameChangeTime = usernameChangeTime; + CreateTime = createTime; + LastModified = lastModified; + Version = version; + } + + public long Id { get; set; } + public string UniqueId { get; set; } = default!; + + public string Username { get; set; } = default!; + public string Nickname { get; set; } = default!; + + public UserPermissions Permissions { get; set; } = default!; + + public DateTime UsernameChangeTime { get; set; } + public DateTime CreateTime { get; set; } + public DateTime LastModified { get; set; } + public long Version { get; set; } + } +} diff --git a/BackEnd/Timeline/Services/HighlightTimelineService.cs b/BackEnd/Timeline/Services/HighlightTimelineService.cs index 88ad4a4b..619bc33e 100644 --- a/BackEnd/Timeline/Services/HighlightTimelineService.cs +++ b/BackEnd/Timeline/Services/HighlightTimelineService.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Timeline.Entities; +using Timeline.Models; using Timeline.Services.Exceptions; namespace Timeline.Services @@ -14,7 +15,7 @@ namespace Timeline.Services /// Get all highlight timelines. /// /// A list of all highlight timelines. - Task> GetHighlightTimelines(); + Task> GetHighlightTimelines(); /// /// Add a timeline to highlight list. @@ -73,11 +74,11 @@ namespace Timeline.Services await _database.SaveChangesAsync(); } - public async Task> GetHighlightTimelines() + public async Task> GetHighlightTimelines() { var entities = await _database.HighlightTimelines.Select(t => new { t.Id }).ToListAsync(); - var result = new List(); + var result = new List(); foreach (var entity in entities) { diff --git a/BackEnd/Timeline/Services/TimelinePostService.cs b/BackEnd/Timeline/Services/TimelinePostService.cs index a1176a68..35513a36 100644 --- a/BackEnd/Timeline/Services/TimelinePostService.cs +++ b/BackEnd/Timeline/Services/TimelinePostService.cs @@ -39,7 +39,7 @@ namespace Timeline.Services /// Thrown when timeline with name does not exist. /// If it is a personal timeline, then inner exception is . /// - Task> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false); + Task> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false); /// /// Get the etag of data of a post. @@ -90,7 +90,7 @@ namespace Timeline.Services /// If it is a personal timeline, then inner exception is . /// /// Thrown if user of does not exist. - Task CreateTextPost(string timelineName, long authorId, string text, DateTime? time); + Task CreateTextPost(string timelineName, long authorId, string text, DateTime? time); /// /// Create a new image post in timeline. @@ -108,7 +108,7 @@ namespace Timeline.Services /// /// Thrown if user of does not exist. /// Thrown if data is not a image. Validated by . - Task CreateImagePost(string timelineName, long authorId, byte[] imageData, DateTime? time); + Task CreateImagePost(string timelineName, long authorId, byte[] imageData, DateTime? time); /// /// Delete a post. @@ -179,9 +179,9 @@ namespace Timeline.Services _clock = clock; } - private async Task MapTimelinePostFromEntity(TimelinePostEntity entity, string timelineName) + private async Task MapTimelinePostFromEntity(TimelinePostEntity entity, string timelineName) { - User? author = entity.AuthorId.HasValue ? await _userService.GetUser(entity.AuthorId.Value) : null; + UserInfo? author = entity.AuthorId.HasValue ? await _userService.GetUser(entity.AuthorId.Value) : null; ITimelinePostContent? content = null; @@ -197,7 +197,7 @@ namespace Timeline.Services }; } - return new TimelinePost( + return new TimelinePostInfo( id: entity.LocalId, author: author, content: content, @@ -207,7 +207,7 @@ namespace Timeline.Services ); } - public async Task> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false) + public async Task> GetPosts(string timelineName, DateTime? modifiedSince = null, bool includeDeleted = false) { modifiedSince = modifiedSince?.MyToUtc(); @@ -231,7 +231,7 @@ namespace Timeline.Services var postEntities = await query.ToListAsync(); - var posts = new List(); + var posts = new List(); foreach (var entity in postEntities) { posts.Add(await MapTimelinePostFromEntity(entity, timelineName)); @@ -309,7 +309,7 @@ namespace Timeline.Services }; } - public async Task CreateTextPost(string timelineName, long authorId, string text, DateTime? time) + public async Task CreateTextPost(string timelineName, long authorId, string text, DateTime? time) { time = time?.MyToUtc(); @@ -342,7 +342,7 @@ namespace Timeline.Services await _database.SaveChangesAsync(); - return new TimelinePost( + return new TimelinePostInfo( id: postEntity.LocalId, content: new TextTimelinePostContent(text), time: finalTime, @@ -352,7 +352,7 @@ namespace Timeline.Services ); } - public async Task CreateImagePost(string timelineName, long authorId, byte[] data, DateTime? time) + public async Task CreateImagePost(string timelineName, long authorId, byte[] data, DateTime? time) { time = time?.MyToUtc(); @@ -391,7 +391,7 @@ namespace Timeline.Services _database.TimelinePosts.Add(postEntity); await _database.SaveChangesAsync(); - return new TimelinePost( + return new TimelinePostInfo( id: postEntity.LocalId, content: new ImageTimelinePostContent(tag), time: finalTime, diff --git a/BackEnd/Timeline/Services/TimelineService.cs b/BackEnd/Timeline/Services/TimelineService.cs index b8ec354a..b65b3cf4 100644 --- a/BackEnd/Timeline/Services/TimelineService.cs +++ b/BackEnd/Timeline/Services/TimelineService.cs @@ -90,7 +90,7 @@ namespace Timeline.Services /// Thrown when timeline with name does not exist. /// If it is a personal timeline, then inner exception is . /// - Task GetTimeline(string timelineName); + Task GetTimeline(string timelineName); /// /// Get timeline by id. @@ -98,7 +98,7 @@ namespace Timeline.Services /// Id of timeline. /// The timeline. /// Thrown when timeline with given id does not exist. - Task GetTimelineById(long id); + Task GetTimelineById(long id); /// /// Set the properties of a timeline. @@ -113,8 +113,6 @@ namespace Timeline.Services /// Task ChangeProperty(string timelineName, TimelineChangePropertyRequest newProperties); - - /// /// Change member of timeline. /// @@ -174,7 +172,6 @@ namespace Timeline.Services /// Task HasReadPermission(string timelineName, long? visitorId); - /// /// Verify whether a user is member of a timeline. /// @@ -202,7 +199,7 @@ namespace Timeline.Services /// /// If user with related user id does not exist, empty list will be returned. /// - Task> GetTimelines(TimelineUserRelationship? relate = null, List? visibility = null); + Task> GetTimelines(TimelineUserRelationship? relate = null, List? visibility = null); /// /// Create a timeline. @@ -214,7 +211,7 @@ namespace Timeline.Services /// Thrown when timeline name is invalid. /// Thrown when the timeline already exists. /// Thrown when the owner user does not exist. - Task CreateTimeline(string timelineName, long ownerId); + Task CreateTimeline(string timelineName, long ownerId); /// /// Delete a timeline. @@ -238,7 +235,7 @@ namespace Timeline.Services /// /// You can only change name of general timeline. /// - Task ChangeTimelineName(string oldTimelineName, string newTimelineName); + Task ChangeTimelineName(string oldTimelineName, string newTimelineName); } public class TimelineService : BasicTimelineService, ITimelineService @@ -270,11 +267,11 @@ namespace Timeline.Services } /// Remember to include Members when query. - private async Task MapTimelineFromEntity(TimelineEntity entity) + private async Task MapTimelineFromEntity(TimelineEntity entity) { var owner = await _userService.GetUser(entity.OwnerId); - var members = new List(); + var members = new List(); foreach (var memberEntity in entity.Members) { members.Add(await _userService.GetUser(memberEntity.UserId)); @@ -282,19 +279,18 @@ namespace Timeline.Services var name = entity.Name ?? ("@" + owner.Username); - return new Models.Timeline - { - UniqueID = entity.UniqueId, - Name = name, - NameLastModified = entity.NameLastModified, - Title = string.IsNullOrEmpty(entity.Title) ? name : entity.Title, - Description = entity.Description ?? "", - Owner = owner, - Visibility = entity.Visibility, - Members = members, - CreateTime = entity.CreateTime, - LastModified = entity.LastModified - }; + return new TimelineInfo( + entity.UniqueId, + name, + entity.NameLastModified, + string.IsNullOrEmpty(entity.Title) ? name : entity.Title, + entity.Description ?? "", + owner, + entity.Visibility, + members, + entity.CreateTime, + entity.LastModified + ); } public async Task GetTimelineLastModifiedTime(string timelineName) @@ -321,7 +317,7 @@ namespace Timeline.Services return timelineEntity.UniqueId; } - public async Task GetTimeline(string timelineName) + public async Task GetTimeline(string timelineName) { if (timelineName == null) throw new ArgumentNullException(nameof(timelineName)); @@ -333,7 +329,7 @@ namespace Timeline.Services return await MapTimelineFromEntity(timelineEntity); } - public async Task GetTimelineById(long id) + public async Task GetTimelineById(long id) { var timelineEntity = await _database.Timelines.Where(t => t.Id == id).Include(t => t.Members).SingleOrDefaultAsync(); @@ -522,7 +518,7 @@ namespace Timeline.Services return await _database.TimelineMembers.AnyAsync(m => m.TimelineId == timelineId && m.UserId == userId); } - public async Task> GetTimelines(TimelineUserRelationship? relate = null, List? visibility = null) + public async Task> GetTimelines(TimelineUserRelationship? relate = null, List? visibility = null) { List entities; @@ -556,7 +552,7 @@ namespace Timeline.Services } } - var result = new List(); + var result = new List(); foreach (var entity in entities) { @@ -566,7 +562,7 @@ namespace Timeline.Services return result; } - public async Task CreateTimeline(string name, long owner) + public async Task CreateTimeline(string name, long owner) { if (name == null) throw new ArgumentNullException(nameof(name)); @@ -604,7 +600,7 @@ namespace Timeline.Services await _database.SaveChangesAsync(); } - public async Task ChangeTimelineName(string oldTimelineName, string newTimelineName) + public async Task ChangeTimelineName(string oldTimelineName, string newTimelineName) { if (oldTimelineName == null) throw new ArgumentNullException(nameof(oldTimelineName)); diff --git a/BackEnd/Timeline/Services/UserCredentialService.cs b/BackEnd/Timeline/Services/UserCredentialService.cs index e5c3581b..8aeef9ef 100644 --- a/BackEnd/Timeline/Services/UserCredentialService.cs +++ b/BackEnd/Timeline/Services/UserCredentialService.cs @@ -1,12 +1,10 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Timeline.Entities; using Timeline.Helpers; -using Timeline.Models; using Timeline.Models.Validation; using Timeline.Services.Exceptions; diff --git a/BackEnd/Timeline/Services/UserService.cs b/BackEnd/Timeline/Services/UserService.cs index 9395cc52..96068e44 100644 --- a/BackEnd/Timeline/Services/UserService.cs +++ b/BackEnd/Timeline/Services/UserService.cs @@ -32,13 +32,13 @@ namespace Timeline.Services /// The id of the user. /// The user info. /// Thrown when the user with given id does not exist. - Task GetUser(long id); + Task GetUser(long id); /// /// List all users. /// /// The user info of users. - Task> GetUsers(); + Task> GetUsers(); /// /// Create a user with given info. @@ -49,7 +49,7 @@ namespace Timeline.Services /// Thrown when or is null. /// Thrown when or is of bad format. /// Thrown when a user with given username already exists. - Task CreateUser(string username, string password); + Task CreateUser(string username, string password); /// /// Modify a user. @@ -62,7 +62,7 @@ namespace Timeline.Services /// /// Version will increase if password is changed. /// - Task ModifyUser(long id, ModifyUserParams? param); + Task ModifyUser(long id, ModifyUserParams? param); } public class UserService : BasicUserService, IUserService @@ -116,26 +116,23 @@ namespace Timeline.Services throw new EntityAlreadyExistException(EntityNames.User, ExceptionUsernameConflict); } - private async Task CreateUserFromEntity(UserEntity entity) + private async Task CreateUserFromEntity(UserEntity entity) { var permission = await _userPermissionService.GetPermissionsOfUserAsync(entity.Id); - return new User - { - UniqueId = entity.UniqueId, - Username = entity.Username, - Permissions = permission, - Nickname = string.IsNullOrEmpty(entity.Nickname) ? entity.Username : entity.Nickname, - Id = entity.Id, - Version = entity.Version, - CreateTime = entity.CreateTime, - UsernameChangeTime = entity.UsernameChangeTime, - LastModified = entity.LastModified - }; + return new UserInfo( + entity.Id, + entity.UniqueId, + entity.Username, + string.IsNullOrEmpty(entity.Nickname) ? entity.Username : entity.Nickname, + permission, + entity.UsernameChangeTime, + entity.CreateTime, + entity.LastModified, + entity.Version + ); } - - - public async Task GetUser(long id) + public async Task GetUser(long id) { var user = await _databaseContext.Users.Where(u => u.Id == id).SingleOrDefaultAsync(); @@ -145,9 +142,9 @@ namespace Timeline.Services return await CreateUserFromEntity(user); } - public async Task> GetUsers() + public async Task> GetUsers() { - List result = new(); + List result = new(); foreach (var entity in await _databaseContext.Users.ToArrayAsync()) { result.Add(await CreateUserFromEntity(entity)); @@ -155,7 +152,7 @@ namespace Timeline.Services return result; } - public async Task CreateUser(string username, string password) + public async Task CreateUser(string username, string password) { if (username == null) throw new ArgumentNullException(nameof(username)); @@ -183,7 +180,7 @@ namespace Timeline.Services return await CreateUserFromEntity(newEntity); } - public async Task ModifyUser(long id, ModifyUserParams? param) + public async Task ModifyUser(long id, ModifyUserParams? param) { if (param != null) { diff --git a/BackEnd/Timeline/Services/UserTokenManager.cs b/BackEnd/Timeline/Services/UserTokenManager.cs index 831329e6..b887b987 100644 --- a/BackEnd/Timeline/Services/UserTokenManager.cs +++ b/BackEnd/Timeline/Services/UserTokenManager.cs @@ -10,7 +10,7 @@ namespace Timeline.Services public class UserTokenCreateResult { public string Token { get; set; } = default!; - public User User { get; set; } = default!; + public UserInfo User { get; set; } = default!; } public interface IUserTokenManager @@ -38,7 +38,7 @@ namespace Timeline.Services /// Thrown when the token is of bad version. /// Thrown when the token is of bad format. /// Thrown when the user specified by the token does not exist. Usually the user had been deleted after the token was issued. - public Task VerifyToken(string token); + public Task VerifyToken(string token); } public class UserTokenManager : IUserTokenManager @@ -75,7 +75,7 @@ namespace Timeline.Services } - public async Task VerifyToken(string token) + public async Task VerifyToken(string token) { if (token == null) throw new ArgumentNullException(nameof(token)); -- cgit v1.2.3 From 3971aeb4b0d1a7566b6c9d3c88984c488fdf8074 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 27 Nov 2020 01:26:23 +0800 Subject: ... --- BackEnd/Timeline.Tests/Helpers/TestDatabase.cs | 2 +- .../Services/HighlightTimelineServiceTest.cs | 35 ++++++++++++++++++---- .../Services/TimelinePostServiceTest.cs | 2 +- .../Timeline.Tests/Services/TimelineServiceTest.cs | 2 +- .../Timeline/Entities/HighlightTimelineEntity.cs | 8 +++-- BackEnd/Timeline/Models/TimelineInfo.cs | 4 +-- BackEnd/Timeline/Models/UserInfo.cs | 2 +- .../Timeline/Services/HighlightTimelineService.cs | 6 ++-- BackEnd/Timeline/Services/UserPermissionService.cs | 33 ++++++++++++++++++-- BackEnd/Timeline/Services/UserService.cs | 6 ++-- 10 files changed, 79 insertions(+), 21 deletions(-) diff --git a/BackEnd/Timeline.Tests/Helpers/TestDatabase.cs b/BackEnd/Timeline.Tests/Helpers/TestDatabase.cs index 74db74aa..a71c2208 100644 --- a/BackEnd/Timeline.Tests/Helpers/TestDatabase.cs +++ b/BackEnd/Timeline.Tests/Helpers/TestDatabase.cs @@ -35,7 +35,7 @@ namespace Timeline.Tests.Helpers if (_createUser) { var passwordService = new PasswordService(); - var userService = new UserService(NullLogger.Instance, context, passwordService, new Clock(), new UserPermissionService(context)); + var userService = new UserService(NullLogger.Instance, context, passwordService, new UserPermissionService(context), new Clock()); var admin = await userService.CreateUser("admin", "adminpw"); await userService.ModifyUser(admin.Id, new ModifyUserParams() { Nickname = "administrator" }); diff --git a/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs b/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs index a4cd983d..8ba26613 100644 --- a/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs +++ b/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs @@ -1,18 +1,43 @@ -using Timeline.Services; +using FluentAssertions; +using Microsoft.Extensions.Logging.Abstractions; +using System.Threading.Tasks; +using Timeline.Services; +using Timeline.Tests.Helpers; +using Xunit; namespace Timeline.Tests.Services { public class HighlightTimelineServiceTest : DatabaseBasedTest { - private UserService _userService; - private TimelineService _timelineService; + private readonly TestClock _clock = new TestClock(); + private UserService _userService = default!; + private TimelineService _timelineService = default!; - private HighlightTimelineService _service; + private HighlightTimelineService _service = default!; protected override void OnDatabaseCreated() { - + _userService = new UserService(NullLogger.Instance, Database, new PasswordService(), new UserPermissionService(Database), _clock); + _timelineService = new TimelineService(Database, _userService, _clock); + _service = new HighlightTimelineService(Database, _userService, _timelineService, _clock); } + [Fact] + public async Task Should_Work() + { + { + var ht = await _service.GetHighlightTimelines(); + ht.Should().BeEmpty(); + } + + var userId = await _userService.GetUserIdByUsername("user"); + await _timelineService.CreateTimeline("tl", userId); + await _service.AddHighlightTimeline("tl", userId); + + { + var ht = await _service.GetHighlightTimelines(); + ht.Should().HaveCount(1).And.BeEquivalentTo(await _timelineService.GetTimeline("tl")); + } + } } } diff --git a/BackEnd/Timeline.Tests/Services/TimelinePostServiceTest.cs b/BackEnd/Timeline.Tests/Services/TimelinePostServiceTest.cs index 97512be5..7771ae0b 100644 --- a/BackEnd/Timeline.Tests/Services/TimelinePostServiceTest.cs +++ b/BackEnd/Timeline.Tests/Services/TimelinePostServiceTest.cs @@ -36,7 +36,7 @@ namespace Timeline.Tests.Services { _dataManager = new DataManager(Database, _eTagGenerator); _userPermissionService = new UserPermissionService(Database); - _userService = new UserService(NullLogger.Instance, Database, _passwordService, _clock, _userPermissionService); + _userService = new UserService(NullLogger.Instance, Database, _passwordService, _userPermissionService, _clock); _timelineService = new TimelineService(Database, _userService, _clock); _timelinePostService = new TimelinePostService(NullLogger.Instance, Database, _timelineService, _userService, _dataManager, _imageValidator, _clock); _userDeleteService = new UserDeleteService(NullLogger.Instance, Database, _timelinePostService); diff --git a/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs b/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs index 98f03066..70f54ede 100644 --- a/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs +++ b/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs @@ -26,7 +26,7 @@ namespace Timeline.Tests.Services protected override void OnDatabaseCreated() { _userPermissionService = new UserPermissionService(Database); - _userService = new UserService(NullLogger.Instance, Database, _passwordService, _clock, _userPermissionService); + _userService = new UserService(NullLogger.Instance, Database, _passwordService, _userPermissionService, _clock); _timelineService = new TimelineService(Database, _userService, _clock); } diff --git a/BackEnd/Timeline/Entities/HighlightTimelineEntity.cs b/BackEnd/Timeline/Entities/HighlightTimelineEntity.cs index 0a38c8a6..3378a175 100644 --- a/BackEnd/Timeline/Entities/HighlightTimelineEntity.cs +++ b/BackEnd/Timeline/Entities/HighlightTimelineEntity.cs @@ -1,10 +1,11 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace Timeline.Entities { [Table("highlight_timelines")] - public record HighlightTimelineEntity + public class HighlightTimelineEntity { [Key, Column("id"), DatabaseGenerated(DatabaseGeneratedOption.Identity)] public long Id { get; set; } @@ -20,5 +21,8 @@ namespace Timeline.Entities [ForeignKey(nameof(OperatorId))] public UserEntity? Operator { get; set; } + + [Column("add_time")] + public DateTime AddTime { get; set; } } } diff --git a/BackEnd/Timeline/Models/TimelineInfo.cs b/BackEnd/Timeline/Models/TimelineInfo.cs index 440f6b81..649af274 100644 --- a/BackEnd/Timeline/Models/TimelineInfo.cs +++ b/BackEnd/Timeline/Models/TimelineInfo.cs @@ -50,7 +50,7 @@ namespace Timeline.Models public string DataTag { get; set; } } - public record TimelinePostInfo + public class TimelinePostInfo { public TimelinePostInfo() { @@ -76,7 +76,7 @@ namespace Timeline.Models public string TimelineName { get; set; } = default!; } - public record TimelineInfo + public class TimelineInfo { public TimelineInfo() { diff --git a/BackEnd/Timeline/Models/UserInfo.cs b/BackEnd/Timeline/Models/UserInfo.cs index 058cc590..e8d57def 100644 --- a/BackEnd/Timeline/Models/UserInfo.cs +++ b/BackEnd/Timeline/Models/UserInfo.cs @@ -3,7 +3,7 @@ using Timeline.Services; namespace Timeline.Models { - public record UserInfo + public class UserInfo { public UserInfo() { diff --git a/BackEnd/Timeline/Services/HighlightTimelineService.cs b/BackEnd/Timeline/Services/HighlightTimelineService.cs index 619bc33e..0f4e5488 100644 --- a/BackEnd/Timeline/Services/HighlightTimelineService.cs +++ b/BackEnd/Timeline/Services/HighlightTimelineService.cs @@ -46,12 +46,14 @@ namespace Timeline.Services private readonly DatabaseContext _database; private readonly IBasicUserService _userService; private readonly ITimelineService _timelineService; + private readonly IClock _clock; - public HighlightTimelineService(DatabaseContext database, IBasicUserService userService, ITimelineService timelineService) + public HighlightTimelineService(DatabaseContext database, IBasicUserService userService, ITimelineService timelineService, IClock clock) { _database = database; _userService = userService; _timelineService = timelineService; + _clock = clock; } public async Task AddHighlightTimeline(string timelineName, long? operatorId) @@ -70,7 +72,7 @@ namespace Timeline.Services if (alreadyIs) return; - _database.HighlightTimelines.Add(new HighlightTimelineEntity { TimelineId = timelineId, OperatorId = operatorId }); + _database.HighlightTimelines.Add(new HighlightTimelineEntity { TimelineId = timelineId, OperatorId = operatorId, AddTime = _clock.GetCurrentTime() }); await _database.SaveChangesAsync(); } diff --git a/BackEnd/Timeline/Services/UserPermissionService.cs b/BackEnd/Timeline/Services/UserPermissionService.cs index 9683000a..bd7cd6aa 100644 --- a/BackEnd/Timeline/Services/UserPermissionService.cs +++ b/BackEnd/Timeline/Services/UserPermissionService.cs @@ -28,7 +28,7 @@ namespace Timeline.Services /// /// Represents a user's permissions. /// - public class UserPermissions : IEnumerable + public class UserPermissions : IEnumerable, IEquatable { public static UserPermissions AllPermissions { get; } = new UserPermissions(Enum.GetValues()); @@ -49,10 +49,10 @@ namespace Timeline.Services public UserPermissions(IEnumerable permissions) { if (permissions == null) throw new ArgumentNullException(nameof(permissions)); - _permissions = new HashSet(permissions); + _permissions = new SortedSet(permissions); } - private readonly HashSet _permissions = new(); + private readonly SortedSet _permissions = new(); /// /// Check if a permission is contained in the list. @@ -108,6 +108,33 @@ namespace Timeline.Services { return ((IEnumerable)_permissions).GetEnumerator(); } + + public bool Equals(UserPermissions? other) + { + if (other == null) + return false; + + return _permissions.SequenceEqual(other._permissions); + } + + public override bool Equals(object? obj) + { + return Equals(obj as UserPermissions); + } + + public override int GetHashCode() + { + int result = 0; + foreach (var permission in Enum.GetValues()) + { + if (_permissions.Contains(permission)) + { + result += 1; + } + result <<= 1; + } + return result; + } } public interface IUserPermissionService diff --git a/BackEnd/Timeline/Services/UserService.cs b/BackEnd/Timeline/Services/UserService.cs index 96068e44..c99e86b0 100644 --- a/BackEnd/Timeline/Services/UserService.cs +++ b/BackEnd/Timeline/Services/UserService.cs @@ -17,7 +17,7 @@ namespace Timeline.Services /// /// Null means not change. /// - public record ModifyUserParams + public class ModifyUserParams { public string? Username { get; set; } public string? Password { get; set; } @@ -78,13 +78,13 @@ namespace Timeline.Services private readonly UsernameValidator _usernameValidator = new UsernameValidator(); private readonly NicknameValidator _nicknameValidator = new NicknameValidator(); - public UserService(ILogger logger, DatabaseContext databaseContext, IPasswordService passwordService, IClock clock, IUserPermissionService userPermissionService) : base(databaseContext) + public UserService(ILogger logger, DatabaseContext databaseContext, IPasswordService passwordService, IUserPermissionService userPermissionService, IClock clock) : base(databaseContext) { _logger = logger; - _clock = clock; _databaseContext = databaseContext; _passwordService = passwordService; _userPermissionService = userPermissionService; + _clock = clock; } private void CheckUsernameFormat(string username, string? paramName) -- cgit v1.2.3 From d934c1273bc20533683eaad858a1c499c7729a28 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 17 Dec 2020 20:08:33 +0800 Subject: ... --- BackEnd/Timeline.Tests/Helpers/TestDatabase.cs | 13 +- .../Timeline.Tests/Services/DatabaseBasedTest.cs | 10 +- .../Services/HighlightTimelineServiceTest.cs | 49 +++ .../Timeline/Entities/HighlightTimelineEntity.cs | 3 + ...0201217093401_AddHighlightTimelines.Designer.cs | 451 +++++++++++++++++++++ .../20201217093401_AddHighlightTimelines.cs | 55 +++ .../Migrations/DatabaseContextModelSnapshot.cs | 49 +++ .../Timeline/Services/HighlightTimelineService.cs | 85 +++- 8 files changed, 706 insertions(+), 9 deletions(-) create mode 100644 BackEnd/Timeline/Migrations/20201217093401_AddHighlightTimelines.Designer.cs create mode 100644 BackEnd/Timeline/Migrations/20201217093401_AddHighlightTimelines.cs diff --git a/BackEnd/Timeline.Tests/Helpers/TestDatabase.cs b/BackEnd/Timeline.Tests/Helpers/TestDatabase.cs index a71c2208..00164835 100644 --- a/BackEnd/Timeline.Tests/Helpers/TestDatabase.cs +++ b/BackEnd/Timeline.Tests/Helpers/TestDatabase.cs @@ -1,11 +1,14 @@ using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using System; using System.Threading.Tasks; using Timeline.Entities; using Timeline.Migrations; using Timeline.Services; using Xunit; +using Xunit.Abstractions; namespace Timeline.Tests.Helpers { @@ -54,12 +57,14 @@ namespace Timeline.Tests.Helpers public SqliteConnection Connection { get; } - public DatabaseContext CreateContext() + public DatabaseContext CreateContext(ITestOutputHelper? testOutputHelper = null) { - var options = new DbContextOptionsBuilder() - .UseSqlite(Connection).Options; + var optionsBuilder = new DbContextOptionsBuilder() + .UseSqlite(Connection); - return new DatabaseContext(options); + if (testOutputHelper != null) optionsBuilder.LogTo(testOutputHelper.WriteLine).EnableDetailedErrors().EnableSensitiveDataLogging(); + + return new DatabaseContext(optionsBuilder.Options); } } } diff --git a/BackEnd/Timeline.Tests/Services/DatabaseBasedTest.cs b/BackEnd/Timeline.Tests/Services/DatabaseBasedTest.cs index 3bb6ebb5..90fb6463 100644 --- a/BackEnd/Timeline.Tests/Services/DatabaseBasedTest.cs +++ b/BackEnd/Timeline.Tests/Services/DatabaseBasedTest.cs @@ -2,6 +2,7 @@ using Timeline.Entities; using Timeline.Tests.Helpers; using Xunit; +using Xunit.Abstractions; namespace Timeline.Tests.Services { @@ -10,15 +11,20 @@ namespace Timeline.Tests.Services protected TestDatabase TestDatabase { get; } protected DatabaseContext Database { get; private set; } = default!; - protected DatabaseBasedTest(bool databaseCreateUsers = true) + 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(); + Database = TestDatabase.CreateContext(_testOutputHelper); await OnDatabaseCreatedAsync(); OnDatabaseCreated(); } diff --git a/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs b/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs index 8ba26613..dca070c6 100644 --- a/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs +++ b/BackEnd/Timeline.Tests/Services/HighlightTimelineServiceTest.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Timeline.Services; using Timeline.Tests.Helpers; using Xunit; +using Xunit.Abstractions; namespace Timeline.Tests.Services { @@ -15,6 +16,12 @@ namespace Timeline.Tests.Services private HighlightTimelineService _service = default!; + public HighlightTimelineServiceTest(ITestOutputHelper testOutputHelper) + : base(testOutputHelper) + { + + } + protected override void OnDatabaseCreated() { _userService = new UserService(NullLogger.Instance, Database, new PasswordService(), new UserPermissionService(Database), _clock); @@ -39,5 +46,47 @@ namespace Timeline.Tests.Services ht.Should().HaveCount(1).And.BeEquivalentTo(await _timelineService.GetTimeline("tl")); } } + + [Fact] + public async Task NewOne_Should_BeAtLast() + { + var userId = await _userService.GetUserIdByUsername("user"); + await _timelineService.CreateTimeline("t1", userId); + await _service.AddHighlightTimeline("t1", userId); + + await _timelineService.CreateTimeline("t2", userId); + await _service.AddHighlightTimeline("t2", userId); + + var ht = await _service.GetHighlightTimelines(); + + ht.Should().HaveCount(2); + ht[0].Name.Should().Be("t1"); + ht[1].Name.Should().Be("t2"); + } + + [Fact] + public async Task Multiple_Should_Work() + { + var userId = await _userService.GetUserIdByUsername("user"); + await _timelineService.CreateTimeline("t1", userId); + await _service.AddHighlightTimeline("t1", userId); + + await _timelineService.CreateTimeline("t2", userId); + await _service.AddHighlightTimeline("t2", userId); + + await _timelineService.CreateTimeline("t3", userId); + await _service.AddHighlightTimeline("t3", userId); + + await _service.MoveHighlightTimeline("t3", 2); + (await _service.GetHighlightTimelines())[1].Name.Should().Be("t3"); + + await _service.MoveHighlightTimeline("t1", 3); + (await _service.GetHighlightTimelines())[2].Name.Should().Be("t1"); + + await _service.RemoveHighlightTimeline("t2", userId); + await _service.RemoveHighlightTimeline("t1", userId); + await _service.RemoveHighlightTimeline("t3", userId); + (await _service.GetHighlightTimelines()).Should().BeEmpty(); + } } } diff --git a/BackEnd/Timeline/Entities/HighlightTimelineEntity.cs b/BackEnd/Timeline/Entities/HighlightTimelineEntity.cs index 3378a175..35bf6af3 100644 --- a/BackEnd/Timeline/Entities/HighlightTimelineEntity.cs +++ b/BackEnd/Timeline/Entities/HighlightTimelineEntity.cs @@ -24,5 +24,8 @@ namespace Timeline.Entities [Column("add_time")] public DateTime AddTime { get; set; } + + [Column("order")] + public long Order { get; set; } } } diff --git a/BackEnd/Timeline/Migrations/20201217093401_AddHighlightTimelines.Designer.cs b/BackEnd/Timeline/Migrations/20201217093401_AddHighlightTimelines.Designer.cs new file mode 100644 index 00000000..6cc591fa --- /dev/null +++ b/BackEnd/Timeline/Migrations/20201217093401_AddHighlightTimelines.Designer.cs @@ -0,0 +1,451 @@ +// +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("20201217093401_AddHighlightTimelines")] + partial class AddHighlightTimelines + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "5.0.0"); + + modelBuilder.Entity("Timeline.Entities.DataEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Data") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("data"); + + b.Property("Ref") + .HasColumnType("INTEGER") + .HasColumnName("ref"); + + b.Property("Tag") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("tag"); + + b.HasKey("Id"); + + b.HasIndex("Tag") + .IsUnique(); + + b.ToTable("data"); + }); + + modelBuilder.Entity("Timeline.Entities.HighlightTimelineEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("AddTime") + .HasColumnType("TEXT") + .HasColumnName("add_time"); + + b.Property("OperatorId") + .HasColumnType("INTEGER") + .HasColumnName("operator_id"); + + b.Property("Order") + .HasColumnType("INTEGER") + .HasColumnName("order"); + + b.Property("TimelineId") + .HasColumnType("INTEGER") + .HasColumnName("timeline_id"); + + b.HasKey("Id"); + + b.HasIndex("OperatorId"); + + b.HasIndex("TimelineId"); + + b.ToTable("highlight_timelines"); + }); + + modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Key") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("key"); + + b.HasKey("Id"); + + b.ToTable("jwt_token"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("CreateTime") + .HasColumnType("TEXT") + .HasColumnName("create_time"); + + b.Property("CurrentPostLocalId") + .HasColumnType("INTEGER") + .HasColumnName("current_post_local_id"); + + b.Property("Description") + .HasColumnType("TEXT") + .HasColumnName("description"); + + b.Property("LastModified") + .HasColumnType("TEXT") + .HasColumnName("last_modified"); + + b.Property("Name") + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.Property("NameLastModified") + .HasColumnType("TEXT") + .HasColumnName("name_last_modified"); + + b.Property("OwnerId") + .HasColumnType("INTEGER") + .HasColumnName("owner"); + + b.Property("Title") + .HasColumnType("TEXT") + .HasColumnName("title"); + + b.Property("UniqueId") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("unique_id") + .HasDefaultValueSql("lower(hex(randomblob(16)))"); + + b.Property("Visibility") + .HasColumnType("INTEGER") + .HasColumnName("visibility"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("timelines"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("TimelineId") + .HasColumnType("INTEGER") + .HasColumnName("timeline"); + + b.Property("UserId") + .HasColumnType("INTEGER") + .HasColumnName("user"); + + b.HasKey("Id"); + + b.HasIndex("TimelineId"); + + b.HasIndex("UserId"); + + b.ToTable("timeline_members"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("AuthorId") + .HasColumnType("INTEGER") + .HasColumnName("author"); + + b.Property("Content") + .HasColumnType("TEXT") + .HasColumnName("content"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("content_type"); + + b.Property("ExtraContent") + .HasColumnType("TEXT") + .HasColumnName("extra_content"); + + b.Property("LastUpdated") + .HasColumnType("TEXT") + .HasColumnName("last_updated"); + + b.Property("LocalId") + .HasColumnType("INTEGER") + .HasColumnName("local_id"); + + b.Property("Time") + .HasColumnType("TEXT") + .HasColumnName("time"); + + b.Property("TimelineId") + .HasColumnType("INTEGER") + .HasColumnName("timeline"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("TimelineId"); + + b.ToTable("timeline_posts"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("DataTag") + .HasColumnType("TEXT") + .HasColumnName("data_tag"); + + b.Property("LastModified") + .HasColumnType("TEXT") + .HasColumnName("last_modified"); + + b.Property("Type") + .HasColumnType("TEXT") + .HasColumnName("type"); + + b.Property("UserId") + .HasColumnType("INTEGER") + .HasColumnName("user"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_avatars"); + }); + + modelBuilder.Entity("Timeline.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("CreateTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("create_time") + .HasDefaultValueSql("datetime('now', 'utc')"); + + b.Property("LastModified") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("last_modified") + .HasDefaultValueSql("datetime('now', 'utc')"); + + b.Property("Nickname") + .HasColumnType("TEXT") + .HasColumnName("nickname"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("password"); + + b.Property("UniqueId") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("unique_id") + .HasDefaultValueSql("lower(hex(randomblob(16)))"); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("username"); + + b.Property("UsernameChangeTime") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasColumnName("username_change_time") + .HasDefaultValueSql("datetime('now', 'utc')"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0L) + .HasColumnName("version"); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Timeline.Entities.UserPermissionEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("Permission") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("permission"); + + b.Property("UserId") + .HasColumnType("INTEGER") + .HasColumnName("user_id"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("user_permission"); + }); + + modelBuilder.Entity("Timeline.Entities.HighlightTimelineEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Operator") + .WithMany() + .HasForeignKey("OperatorId"); + + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany() + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Operator"); + + b.Navigation("Timeline"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Owner") + .WithMany("Timelines") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Members") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithMany("TimelinesJoined") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Timeline"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Author") + .WithMany("TimelinePosts") + .HasForeignKey("AuthorId"); + + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany("Posts") + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + + b.Navigation("Timeline"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithOne("Avatar") + .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Timeline.Entities.UserPermissionEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithMany("Permissions") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.Navigation("Members"); + + b.Navigation("Posts"); + }); + + modelBuilder.Entity("Timeline.Entities.UserEntity", b => + { + b.Navigation("Avatar"); + + b.Navigation("Permissions"); + + b.Navigation("TimelinePosts"); + + b.Navigation("Timelines"); + + b.Navigation("TimelinesJoined"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/BackEnd/Timeline/Migrations/20201217093401_AddHighlightTimelines.cs b/BackEnd/Timeline/Migrations/20201217093401_AddHighlightTimelines.cs new file mode 100644 index 00000000..e838615e --- /dev/null +++ b/BackEnd/Timeline/Migrations/20201217093401_AddHighlightTimelines.cs @@ -0,0 +1,55 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Timeline.Migrations +{ + public partial class AddHighlightTimelines : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "highlight_timelines", + columns: table => new + { + id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + timeline_id = table.Column(type: "INTEGER", nullable: false), + operator_id = table.Column(type: "INTEGER", nullable: true), + add_time = table.Column(type: "TEXT", nullable: false), + order = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_highlight_timelines", x => x.id); + table.ForeignKey( + name: "FK_highlight_timelines_timelines_timeline_id", + column: x => x.timeline_id, + principalTable: "timelines", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_highlight_timelines_users_operator_id", + column: x => x.operator_id, + principalTable: "users", + principalColumn: "id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_highlight_timelines_operator_id", + table: "highlight_timelines", + column: "operator_id"); + + migrationBuilder.CreateIndex( + name: "IX_highlight_timelines_timeline_id", + table: "highlight_timelines", + column: "timeline_id"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "highlight_timelines"); + } + } +} diff --git a/BackEnd/Timeline/Migrations/DatabaseContextModelSnapshot.cs b/BackEnd/Timeline/Migrations/DatabaseContextModelSnapshot.cs index 2f0f75a2..ea3378dc 100644 --- a/BackEnd/Timeline/Migrations/DatabaseContextModelSnapshot.cs +++ b/BackEnd/Timeline/Migrations/DatabaseContextModelSnapshot.cs @@ -45,6 +45,38 @@ namespace Timeline.Migrations b.ToTable("data"); }); + modelBuilder.Entity("Timeline.Entities.HighlightTimelineEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("id"); + + b.Property("AddTime") + .HasColumnType("TEXT") + .HasColumnName("add_time"); + + b.Property("OperatorId") + .HasColumnType("INTEGER") + .HasColumnName("operator_id"); + + b.Property("Order") + .HasColumnType("INTEGER") + .HasColumnName("order"); + + b.Property("TimelineId") + .HasColumnType("INTEGER") + .HasColumnName("timeline_id"); + + b.HasKey("Id"); + + b.HasIndex("OperatorId"); + + b.HasIndex("TimelineId"); + + b.ToTable("highlight_timelines"); + }); + modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => { b.Property("Id") @@ -306,6 +338,23 @@ namespace Timeline.Migrations b.ToTable("user_permission"); }); + modelBuilder.Entity("Timeline.Entities.HighlightTimelineEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Operator") + .WithMany() + .HasForeignKey("OperatorId"); + + b.HasOne("Timeline.Entities.TimelineEntity", "Timeline") + .WithMany() + .HasForeignKey("TimelineId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Operator"); + + b.Navigation("Timeline"); + }); + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => { b.HasOne("Timeline.Entities.UserEntity", "Owner") diff --git a/BackEnd/Timeline/Services/HighlightTimelineService.cs b/BackEnd/Timeline/Services/HighlightTimelineService.cs index 0f4e5488..ea3e4c7e 100644 --- a/BackEnd/Timeline/Services/HighlightTimelineService.cs +++ b/BackEnd/Timeline/Services/HighlightTimelineService.cs @@ -9,10 +9,25 @@ using Timeline.Services.Exceptions; namespace Timeline.Services { + + [Serializable] + public class InvalidHighlightTimelineException : Exception + { + public InvalidHighlightTimelineException() { } + public InvalidHighlightTimelineException(string message) : base(message) { } + public InvalidHighlightTimelineException(string message, Exception inner) : base(message, inner) { } + protected InvalidHighlightTimelineException( + System.Runtime.Serialization.SerializationInfo info, + System.Runtime.Serialization.StreamingContext context) : base(info, context) { } + } + + /// + /// Service that controls highlight timeline. + /// public interface IHighlightTimelineService { /// - /// Get all highlight timelines. + /// Get all highlight timelines in order. /// /// A list of all highlight timelines. Task> GetHighlightTimelines(); @@ -39,6 +54,21 @@ namespace Timeline.Services /// Thrown when timeline with given name does not exist. /// Thrown when user with given operator id does not exist. Task RemoveHighlightTimeline(string timelineName, long? operatorId); + + /// + /// Move a highlight timeline to a new position. + /// + /// The timeline name. + /// The new position. Starts at 1. + /// Thrown when is null. + /// Thrown when is not a valid timeline name. + /// Thrown when timeline with given name does not exist. + /// Thrown when given timeline is not a highlight timeline. + /// + /// If is smaller than 1. Then move the timeline to head. + /// If is bigger than total count. Then move the timeline to tail. + /// + Task MoveHighlightTimeline(string timelineName, long newPosition); } public class HighlightTimelineService : IHighlightTimelineService @@ -72,13 +102,13 @@ namespace Timeline.Services if (alreadyIs) return; - _database.HighlightTimelines.Add(new HighlightTimelineEntity { TimelineId = timelineId, OperatorId = operatorId, AddTime = _clock.GetCurrentTime() }); + _database.HighlightTimelines.Add(new HighlightTimelineEntity { TimelineId = timelineId, OperatorId = operatorId, AddTime = _clock.GetCurrentTime(), Order = await _database.HighlightTimelines.CountAsync() + 1 }); await _database.SaveChangesAsync(); } public async Task> GetHighlightTimelines() { - var entities = await _database.HighlightTimelines.Select(t => new { t.Id }).ToListAsync(); + var entities = await _database.HighlightTimelines.OrderBy(t => t.Order).Select(t => new { t.Id }).ToListAsync(); var result = new List(); @@ -106,10 +136,59 @@ namespace Timeline.Services if (entity == null) return false; + await using var transaction = await _database.Database.BeginTransactionAsync(); + + var order = entity.Order; + _database.HighlightTimelines.Remove(entity); await _database.SaveChangesAsync(); + await _database.Database.ExecuteSqlRawAsync("UPDATE highlight_timelines SET `order` = `order` - 1 WHERE `order` > {0}", order); + + await transaction.CommitAsync(); + return true; } + + public async Task MoveHighlightTimeline(string timelineName, long newPosition) + { + if (timelineName == null) + throw new ArgumentNullException(nameof(timelineName)); + + var timelineId = await _timelineService.GetTimelineIdByName(timelineName); + + var entity = await _database.HighlightTimelines.SingleOrDefaultAsync(t => t.TimelineId == timelineId); + + if (entity == null) throw new InvalidHighlightTimelineException("You can't move a non-highlight timeline."); + + var oldPosition = entity.Order; + + if (newPosition < 1) + { + newPosition = 1; + } + else + { + var totalCount = await _database.HighlightTimelines.CountAsync(); + if (newPosition > totalCount) newPosition = totalCount; + } + + if (oldPosition == newPosition) return; + + await using var transaction = await _database.Database.BeginTransactionAsync(); + + if (newPosition > oldPosition) + { + await _database.Database.ExecuteSqlRawAsync("UPDATE highlight_timelines SET `order` = `order` - 1 WHERE `order` BETWEEN {0} AND {1}", oldPosition + 1, newPosition); + await _database.Database.ExecuteSqlRawAsync("UPDATE highlight_timelines SET `order` = {0} WHERE id = {1}", newPosition, entity.Id); + } + else + { + await _database.Database.ExecuteSqlRawAsync("UPDATE highlight_timelines SET `order` = `order` + 1 WHERE `order` BETWEEN {0} AND {1}", newPosition, oldPosition - 1); + await _database.Database.ExecuteSqlRawAsync("UPDATE highlight_timelines SET `order` = {0} WHERE id = {1}", newPosition, entity.Id); + } + + await transaction.CommitAsync(); + } } } -- cgit v1.2.3 From 1a56df5e91788f0e04cad7d94542ab0189033502 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 17 Dec 2020 20:24:22 +0800 Subject: ... --- .../Controllers/HighlightTimelineController.cs | 63 ++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 BackEnd/Timeline/Controllers/HighlightTimelineController.cs diff --git a/BackEnd/Timeline/Controllers/HighlightTimelineController.cs b/BackEnd/Timeline/Controllers/HighlightTimelineController.cs new file mode 100644 index 00000000..3819bfc4 --- /dev/null +++ b/BackEnd/Timeline/Controllers/HighlightTimelineController.cs @@ -0,0 +1,63 @@ +using AutoMapper; +using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System.Threading.Tasks; +using Timeline.Auth; +using Timeline.Models.Http; +using Timeline.Models.Validation; +using Timeline.Services; +using Timeline.Services.Exceptions; + +namespace Timeline.Controllers +{ + /// + /// Api related to highlight timeline. + /// + [ApiController] + [ProducesErrorResponseType(typeof(CommonResponse))] + [Route("highlights")] + public class HighlightTimelineController : Controller + { + private readonly IHighlightTimelineService _service; + private readonly IMapper _mapper; + + public HighlightTimelineController(IHighlightTimelineService service, IMapper mapper) + { + _service = service; + _mapper = mapper; + } + + /// + /// Get all highlight timelines. + /// + /// Highlight timeline list. + [HttpGet] + [ProducesResponseType(200)] + public async Task>> List() + { + var t = await _service.GetHighlightTimelines(); + return _mapper.Map>(t); + } + + /// + /// Add a timeline to highlight list. + /// + /// + [HttpPut("{timeline}")] + [PermissionAuthorize(UserPermission.HighlightTimelineManagement)] + [ProducesResponseType(200)] + [ProducesResponseType(400)] + public async Task Put([GeneralTimelineName] string timeline) + { + try + { + await _service.AddHighlightTimeline(timeline, this.GetUserId()); + return Ok(); + } + catch (TimelineNotExistException) + { + return BadRequest(ErrorResponse.TimelineController.NotExist()); + } + } + } +} -- cgit v1.2.3 From b9e55a05730cf4ede8dd5bd7a6f9befe5bc3580e Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 17 Dec 2020 23:09:24 +0800 Subject: ... --- BackEnd/Timeline.ErrorCodes/ErrorCodes.cs | 5 +++ .../Controllers/HighlightTimelineController.cs | 52 ++++++++++++++++++++-- BackEnd/Timeline/Models/Http/HighlightTimeline.cs | 17 +++++++ 3 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 BackEnd/Timeline/Models/Http/HighlightTimeline.cs diff --git a/BackEnd/Timeline.ErrorCodes/ErrorCodes.cs b/BackEnd/Timeline.ErrorCodes/ErrorCodes.cs index 90c4ed99..a8519216 100644 --- a/BackEnd/Timeline.ErrorCodes/ErrorCodes.cs +++ b/BackEnd/Timeline.ErrorCodes/ErrorCodes.cs @@ -63,6 +63,11 @@ public const int PostNotExist = 1_104_05_01; public const int PostNoData = 1_104_05_02; } + + public static class HighlightTimelineController + { + public const int NonHighlight = 1_105_01_01; + } } } diff --git a/BackEnd/Timeline/Controllers/HighlightTimelineController.cs b/BackEnd/Timeline/Controllers/HighlightTimelineController.cs index 3819bfc4..0b6e1665 100644 --- a/BackEnd/Timeline/Controllers/HighlightTimelineController.cs +++ b/BackEnd/Timeline/Controllers/HighlightTimelineController.cs @@ -15,7 +15,6 @@ namespace Timeline.Controllers /// [ApiController] [ProducesErrorResponseType(typeof(CommonResponse))] - [Route("highlights")] public class HighlightTimelineController : Controller { private readonly IHighlightTimelineService _service; @@ -31,7 +30,7 @@ namespace Timeline.Controllers /// Get all highlight timelines. /// /// Highlight timeline list. - [HttpGet] + [HttpGet("highlights")] [ProducesResponseType(200)] public async Task>> List() { @@ -42,8 +41,8 @@ namespace Timeline.Controllers /// /// Add a timeline to highlight list. /// - /// - [HttpPut("{timeline}")] + /// The timeline name. + [HttpPut("highlights/{timeline}")] [PermissionAuthorize(UserPermission.HighlightTimelineManagement)] [ProducesResponseType(200)] [ProducesResponseType(400)] @@ -59,5 +58,50 @@ namespace Timeline.Controllers return BadRequest(ErrorResponse.TimelineController.NotExist()); } } + + /// + /// Remove a timeline from highlight list. + /// + /// Timeline name. + [HttpDelete("highlights/{timeline}")] + [PermissionAuthorize(UserPermission.HighlightTimelineManagement)] + [ProducesResponseType(200)] + [ProducesResponseType(400)] + public async Task Delete([GeneralTimelineName] string timeline) + { + try + { + await _service.RemoveHighlightTimeline(timeline, this.GetUserId()); + return Ok(); + } + catch (TimelineNotExistException) + { + return BadRequest(ErrorResponse.TimelineController.NotExist()); + } + } + + /// + /// Move a highlight position. + /// + [HttpPost("highlightop/move")] + [PermissionAuthorize(UserPermission.HighlightTimelineManagement)] + [ProducesResponseType(200)] + [ProducesResponseType(400)] + public async Task Move([FromBody] HttpHighlightTimelineMoveRequest body) + { + try + { + await _service.MoveHighlightTimeline(body.Timeline, body.NewPosition!.Value); + return Ok(); + } + catch (TimelineNotExistException) + { + return BadRequest(ErrorResponse.TimelineController.NotExist()); + } + catch (InvalidHighlightTimelineException) + { + return BadRequest(new CommonResponse(ErrorCodes.HighlightTimelineController.NonHighlight, "Can't move a non-highlight timeline.")); + } + } } } diff --git a/BackEnd/Timeline/Models/Http/HighlightTimeline.cs b/BackEnd/Timeline/Models/Http/HighlightTimeline.cs new file mode 100644 index 00000000..e5aed068 --- /dev/null +++ b/BackEnd/Timeline/Models/Http/HighlightTimeline.cs @@ -0,0 +1,17 @@ +using System.ComponentModel.DataAnnotations; +using Timeline.Models.Validation; + +namespace Timeline.Models.Http +{ + /// + /// Move highlight timeline request body model. + /// + public class HttpHighlightTimelineMoveRequest + { + [GeneralTimelineName] + public string Timeline { get; set; } = default!; + + [Required] + public long? NewPosition { get; set; } + } +} -- cgit v1.2.3 From 5cbc306fc82bf14157ced4ff8b935191d20dae5d Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 17 Dec 2020 23:44:40 +0800 Subject: ... --- .../IntegratedTests/HighlightTimelineTest.cs | 91 ++++++++++++++++++++++ .../IntegratedTests/HttpClientTestExtensions.cs | 5 ++ .../Timeline/Services/HighlightTimelineService.cs | 4 +- BackEnd/Timeline/Startup.cs | 2 + 4 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 BackEnd/Timeline.Tests/IntegratedTests/HighlightTimelineTest.cs diff --git a/BackEnd/Timeline.Tests/IntegratedTests/HighlightTimelineTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/HighlightTimelineTest.cs new file mode 100644 index 00000000..d4b4d55d --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/HighlightTimelineTest.cs @@ -0,0 +1,91 @@ +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Models.Http; +using Xunit; + +namespace Timeline.Tests.IntegratedTests +{ + public class HighlightTimelineTest : IntegratedTestBase + { + [Fact] + public async Task PermissionTest() + { + using var client = await CreateClientAsUser(); + + await client.TestPutAssertForbiddenAsync("highlights/@user1"); + await client.TestDeleteAssertForbiddenAsync("highlights/@user1"); + await client.TestPostAssertForbiddenAsync("highlightop/move", new HttpHighlightTimelineMoveRequest { Timeline = "aaa", NewPosition = 1 }); + } + + [Fact] + public async Task InvalidModel() + { + using var client = await CreateClientAsAdministrator(); + + await client.TestPutAssertInvalidModelAsync("highlights/!!!"); + await client.TestDeleteAssertInvalidModelAsync("highlights/!!!"); + await client.TestPostAssertInvalidModelAsync("highlightop/move", new HttpHighlightTimelineMoveRequest { Timeline = null!, NewPosition = 1 }); + await client.TestPostAssertInvalidModelAsync("highlightop/move", new HttpHighlightTimelineMoveRequest { Timeline = "aaa", NewPosition = null }); + } + + [Fact] + public async Task ShouldWork() + { + { + using var client1 = await CreateClientAsUser(); + await client1.TestPostAsync("timelines", new TimelineCreateRequest { Name = "t1" }); + } + + using var client = await CreateClientAsAdministrator(); + + { + var h = await client.TestGetAsync>("highlights"); + h.Should().BeEmpty(); + } + + await client.TestPutAsync("highlights/@user1"); + + { + var h = await client.TestGetAsync>("highlights"); + h.Should().HaveCount(1); + h[0].Name.Should().Be("@user1"); + } + + await client.TestPutAsync("highlights/t1"); + + { + var h = await client.TestGetAsync>("highlights"); + h.Should().HaveCount(2); + h[0].Name.Should().Be("@user1"); + h[1].Name.Should().Be("t1"); + } + + await client.TestPostAsync("highlightop/move", new HttpHighlightTimelineMoveRequest { Timeline = "@user1", NewPosition = 2 }); + + { + var h = await client.TestGetAsync>("highlights"); + h.Should().HaveCount(2); + h[0].Name.Should().Be("t1"); + h[1].Name.Should().Be("@user1"); + } + + await client.TestDeleteAsync("highlights/@user1"); + + { + var h = await client.TestGetAsync>("highlights"); + h.Should().HaveCount(1); + h[0].Name.Should().Be("t1"); + } + + await client.TestDeleteAsync("highlights/t1"); + + { + var h = await client.TestGetAsync>("highlights"); + h.Should().BeEmpty(); + } + } + } +} diff --git a/BackEnd/Timeline.Tests/IntegratedTests/HttpClientTestExtensions.cs b/BackEnd/Timeline.Tests/IntegratedTests/HttpClientTestExtensions.cs index 7cff0c39..ec517362 100644 --- a/BackEnd/Timeline.Tests/IntegratedTests/HttpClientTestExtensions.cs +++ b/BackEnd/Timeline.Tests/IntegratedTests/HttpClientTestExtensions.cs @@ -212,6 +212,11 @@ namespace Timeline.Tests.IntegratedTests await client.TestJsonSendAssertForbiddenAsync(HttpMethod.Post, url, jsonBody, errorCode, headerSetup); } + public static async Task TestPutAssertForbiddenAsync(this HttpClient client, string url, object? jsonBody = null, int? errorCode = null, HeaderSetup? headerSetup = null) + { + await client.TestJsonSendAssertForbiddenAsync(HttpMethod.Put, url, jsonBody, errorCode, headerSetup); + } + public static async Task TestPatchAssertForbiddenAsync(this HttpClient client, string url, object? jsonBody = null, int? errorCode = null, HeaderSetup? headerSetup = null) { await client.TestJsonSendAssertForbiddenAsync(HttpMethod.Patch, url, jsonBody, errorCode, headerSetup); diff --git a/BackEnd/Timeline/Services/HighlightTimelineService.cs b/BackEnd/Timeline/Services/HighlightTimelineService.cs index ea3e4c7e..b19efe21 100644 --- a/BackEnd/Timeline/Services/HighlightTimelineService.cs +++ b/BackEnd/Timeline/Services/HighlightTimelineService.cs @@ -108,13 +108,13 @@ namespace Timeline.Services public async Task> GetHighlightTimelines() { - var entities = await _database.HighlightTimelines.OrderBy(t => t.Order).Select(t => new { t.Id }).ToListAsync(); + var entities = await _database.HighlightTimelines.OrderBy(t => t.Order).Select(t => new { t.TimelineId }).ToListAsync(); var result = new List(); foreach (var entity in entities) { - result.Add(await _timelineService.GetTimelineById(entity.Id)); + result.Add(await _timelineService.GetTimelineById(entity.TimelineId)); } return result; diff --git a/BackEnd/Timeline/Startup.cs b/BackEnd/Timeline/Startup.cs index bf34f9e2..d20fc54b 100644 --- a/BackEnd/Timeline/Startup.cs +++ b/BackEnd/Timeline/Startup.cs @@ -101,6 +101,8 @@ namespace Timeline services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddDbContext((services, options) => { var pathProvider = services.GetRequiredService(); -- cgit v1.2.3