From d5cc18a9ea161b28e1a7acd67a0c4558ba265800 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 27 Aug 2020 00:47:52 +0800 Subject: Database add title to timeline. --- Timeline/Entities/TimelineEntity.cs | 3 + .../20200826164553_TimelineAddTitle.Designer.cs | 341 +++++++++++++++++++++ .../Migrations/20200826164553_TimelineAddTitle.cs | 22 ++ .../Migrations/DatabaseContextModelSnapshot.cs | 6 +- 4 files changed, 371 insertions(+), 1 deletion(-) create mode 100644 Timeline/Migrations/20200826164553_TimelineAddTitle.Designer.cs create mode 100644 Timeline/Migrations/20200826164553_TimelineAddTitle.cs diff --git a/Timeline/Entities/TimelineEntity.cs b/Timeline/Entities/TimelineEntity.cs index b084a572..3e592673 100644 --- a/Timeline/Entities/TimelineEntity.cs +++ b/Timeline/Entities/TimelineEntity.cs @@ -23,6 +23,9 @@ namespace Timeline.Entities [Column("name")] public string? Name { get; set; } + [Column("title")] + public string? Title { get; set; } + [Column("name_last_modified")] public DateTime NameLastModified { get; set; } diff --git a/Timeline/Migrations/20200826164553_TimelineAddTitle.Designer.cs b/Timeline/Migrations/20200826164553_TimelineAddTitle.Designer.cs new file mode 100644 index 00000000..f2279f3b --- /dev/null +++ b/Timeline/Migrations/20200826164553_TimelineAddTitle.Designer.cs @@ -0,0 +1,341 @@ +// +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("20200826164553_TimelineAddTitle")] + partial class TimelineAddTitle + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.7"); + + modelBuilder.Entity("Timeline.Entities.DataEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Data") + .IsRequired() + .HasColumnName("data") + .HasColumnType("BLOB"); + + b.Property("Ref") + .HasColumnName("ref") + .HasColumnType("INTEGER"); + + b.Property("Tag") + .IsRequired() + .HasColumnName("tag") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Tag") + .IsUnique(); + + b.ToTable("data"); + }); + + modelBuilder.Entity("Timeline.Entities.JwtTokenEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasColumnName("key") + .HasColumnType("BLOB"); + + b.HasKey("Id"); + + b.ToTable("jwt_token"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("CreateTime") + .HasColumnName("create_time") + .HasColumnType("TEXT"); + + b.Property("CurrentPostLocalId") + .HasColumnName("current_post_local_id") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnName("description") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnName("last_modified") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnName("name") + .HasColumnType("TEXT"); + + b.Property("NameLastModified") + .HasColumnName("name_last_modified") + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnName("owner") + .HasColumnType("INTEGER"); + + b.Property("Title") + .HasColumnName("title") + .HasColumnType("TEXT"); + + b.Property("UniqueId") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnName("unique_id") + .HasColumnType("TEXT") + .HasDefaultValueSql("lower(hex(randomblob(16)))"); + + b.Property("Visibility") + .HasColumnName("visibility") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OwnerId"); + + b.ToTable("timelines"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineMemberEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("TimelineId"); + + b.HasIndex("UserId"); + + b.ToTable("timeline_members"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelinePostEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnName("author") + .HasColumnType("INTEGER"); + + b.Property("Content") + .HasColumnName("content") + .HasColumnType("TEXT"); + + b.Property("ContentType") + .IsRequired() + .HasColumnName("content_type") + .HasColumnType("TEXT"); + + b.Property("ExtraContent") + .HasColumnName("extra_content") + .HasColumnType("TEXT"); + + b.Property("LastUpdated") + .HasColumnName("last_updated") + .HasColumnType("TEXT"); + + b.Property("LocalId") + .HasColumnName("local_id") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnName("time") + .HasColumnType("TEXT"); + + b.Property("TimelineId") + .HasColumnName("timeline") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId"); + + b.HasIndex("TimelineId"); + + b.ToTable("timeline_posts"); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("DataTag") + .HasColumnName("data_tag") + .HasColumnType("TEXT"); + + b.Property("LastModified") + .HasColumnName("last_modified") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnName("type") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnName("user") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("user_avatars"); + }); + + modelBuilder.Entity("Timeline.Entities.UserEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id") + .HasColumnType("INTEGER"); + + b.Property("CreateTime") + .ValueGeneratedOnAdd() + .HasColumnName("create_time") + .HasColumnType("TEXT") + .HasDefaultValueSql("datetime('now', 'utc')"); + + b.Property("LastModified") + .ValueGeneratedOnAdd() + .HasColumnName("last_modified") + .HasColumnType("TEXT") + .HasDefaultValueSql("datetime('now', 'utc')"); + + b.Property("Nickname") + .HasColumnName("nickname") + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnName("password") + .HasColumnType("TEXT"); + + b.Property("Roles") + .IsRequired() + .HasColumnName("roles") + .HasColumnType("TEXT"); + + b.Property("UniqueId") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnName("unique_id") + .HasColumnType("TEXT") + .HasDefaultValueSql("lower(hex(randomblob(16)))"); + + b.Property("Username") + .IsRequired() + .HasColumnName("username") + .HasColumnType("TEXT"); + + b.Property("UsernameChangeTime") + .ValueGeneratedOnAdd() + .HasColumnName("username_change_time") + .HasColumnType("TEXT") + .HasDefaultValueSql("datetime('now', 'utc')"); + + b.Property("Version") + .ValueGeneratedOnAdd() + .HasColumnName("version") + .HasColumnType("INTEGER") + .HasDefaultValue(0L); + + b.HasKey("Id"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("users"); + }); + + modelBuilder.Entity("Timeline.Entities.TimelineEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "Owner") + .WithMany("Timelines") + .HasForeignKey("OwnerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + 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(); + }); + + 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(); + }); + + modelBuilder.Entity("Timeline.Entities.UserAvatarEntity", b => + { + b.HasOne("Timeline.Entities.UserEntity", "User") + .WithOne("Avatar") + .HasForeignKey("Timeline.Entities.UserAvatarEntity", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Timeline/Migrations/20200826164553_TimelineAddTitle.cs b/Timeline/Migrations/20200826164553_TimelineAddTitle.cs new file mode 100644 index 00000000..7e8c498b --- /dev/null +++ b/Timeline/Migrations/20200826164553_TimelineAddTitle.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Timeline.Migrations +{ + public partial class TimelineAddTitle : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "title", + table: "timelines", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "title", + table: "timelines"); + } + } +} diff --git a/Timeline/Migrations/DatabaseContextModelSnapshot.cs b/Timeline/Migrations/DatabaseContextModelSnapshot.cs index 3066dcc0..65ae6c9a 100644 --- a/Timeline/Migrations/DatabaseContextModelSnapshot.cs +++ b/Timeline/Migrations/DatabaseContextModelSnapshot.cs @@ -14,7 +14,7 @@ namespace Timeline.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "3.1.5"); + .HasAnnotation("ProductVersion", "3.1.7"); modelBuilder.Entity("Timeline.Entities.DataEntity", b => { @@ -97,6 +97,10 @@ namespace Timeline.Migrations .HasColumnName("owner") .HasColumnType("INTEGER"); + b.Property("Title") + .HasColumnName("title") + .HasColumnType("TEXT"); + b.Property("UniqueId") .IsRequired() .ValueGeneratedOnAdd() -- cgit v1.2.3 From 7936281edc1ac592cd318b8dccb37f3c4e7334e8 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 27 Aug 2020 01:13:31 +0800 Subject: Timeline title feature. --- Timeline.Tests/IntegratedTests/TimelineTest.cs | 29 ++++++++++++++++++++++++++ Timeline.Tests/Services/TimelineServiceTest.cs | 27 ++++++++++++++++++++++++ Timeline/Models/Http/Timeline.cs | 4 ++++ Timeline/Models/Http/TimelineController.cs | 5 +++++ Timeline/Models/Timeline.cs | 2 ++ Timeline/Services/TimelineService.cs | 11 +++++++++- 6 files changed, 77 insertions(+), 1 deletion(-) diff --git a/Timeline.Tests/IntegratedTests/TimelineTest.cs b/Timeline.Tests/IntegratedTests/TimelineTest.cs index 3b4b1754..302b2195 100644 --- a/Timeline.Tests/IntegratedTests/TimelineTest.cs +++ b/Timeline.Tests/IntegratedTests/TimelineTest.cs @@ -1407,5 +1407,34 @@ namespace Timeline.Tests.IntegratedTests .Which.Should().BeEquivalentTo(timeline); } } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task Title(TimelineUrlGenerator urlGenerator) + { + using var client = await CreateClientAsUser(); + + { + var res = await client.GetAsync(urlGenerator(1)); + var timeline = res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which; + timeline.Title.Should().Be(timeline.Name); + } + + { + var res = await client.PatchAsJsonAsync(urlGenerator(1), new TimelinePatchRequest { Title = "atitle" }); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Title.Should().Be("atitle"); + } + + { + var res = await client.GetAsync(urlGenerator(1)); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Title.Should().Be("atitle"); + } + } } } diff --git a/Timeline.Tests/Services/TimelineServiceTest.cs b/Timeline.Tests/Services/TimelineServiceTest.cs index 36e5ed0c..558ec597 100644 --- a/Timeline.Tests/Services/TimelineServiceTest.cs +++ b/Timeline.Tests/Services/TimelineServiceTest.cs @@ -271,5 +271,32 @@ namespace Timeline.Tests.Services posts.Should().HaveCount(4); } } + + [Theory] + [InlineData("@admin")] + [InlineData("tl")] + public async Task Title(string timelineName) + { + var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); + if (!isPersonal) + await _timelineService.CreateTimeline(timelineName, await _userService.GetUserIdByUsername("user")); + + { + var timeline = await _timelineService.GetTimeline(timelineName); + timeline.Title.Should().Be(timelineName); + } + + { + await _timelineService.ChangeProperty(timelineName, new TimelineChangePropertyRequest { Title = null }); + var timeline = await _timelineService.GetTimeline(timelineName); + timeline.Title.Should().Be(timelineName); + } + + { + await _timelineService.ChangeProperty(timelineName, new TimelineChangePropertyRequest { Title = "atitle" }); + var timeline = await _timelineService.GetTimeline(timelineName); + timeline.Title.Should().Be("atitle"); + } + } } } diff --git a/Timeline/Models/Http/Timeline.cs b/Timeline/Models/Http/Timeline.cs index 6498fa74..3596af18 100644 --- a/Timeline/Models/Http/Timeline.cs +++ b/Timeline/Models/Http/Timeline.cs @@ -68,6 +68,10 @@ namespace Timeline.Models.Http /// public string UniqueId { get; set; } = default!; /// + /// Title. + /// + public string Title { get; set; } = default!; + /// /// Name of timeline. /// public string Name { get; set; } = default!; diff --git a/Timeline/Models/Http/TimelineController.cs b/Timeline/Models/Http/TimelineController.cs index aad361ee..95bae3e6 100644 --- a/Timeline/Models/Http/TimelineController.cs +++ b/Timeline/Models/Http/TimelineController.cs @@ -56,6 +56,11 @@ namespace Timeline.Models.Http /// public class TimelinePatchRequest { + /// + /// New title. Null for not change. + /// + public string? Title { get; set; } + /// /// New description. Null for not change. /// diff --git a/Timeline/Models/Timeline.cs b/Timeline/Models/Timeline.cs index 34c253a0..42906053 100644 --- a/Timeline/Models/Timeline.cs +++ b/Timeline/Models/Timeline.cs @@ -74,6 +74,7 @@ namespace Timeline.Models 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; } @@ -86,6 +87,7 @@ namespace Timeline.Models public class TimelineChangePropertyRequest { + public string? Title { get; set; } public string? Description { get; set; } public TimelineVisibility? Visibility { get; set; } } diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index 01f7f5fd..2f0bf2c5 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -405,11 +405,14 @@ namespace Timeline.Services members.Add(await _userService.GetUserById(memberEntity.UserId)); } + var name = entity.Name ?? ("@" + owner.Username); + return new Models.Timeline { UniqueID = entity.UniqueId, - Name = entity.Name ?? ("@" + owner.Username), + Name = name, NameLastModified = entity.NameLastModified, + Title = string.IsNullOrEmpty(entity.Title) ? name : entity.Title, Description = entity.Description ?? "", Owner = owner, Visibility = entity.Visibility, @@ -834,6 +837,12 @@ namespace Timeline.Services var changed = false; + if (newProperties.Title != null) + { + changed = true; + timelineEntity.Title = newProperties.Title; + } + if (newProperties.Description != null) { changed = true; -- cgit v1.2.3 From c10dc9e221317ea39f9ce0f172670ffbe94e6f8a Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 27 Aug 2020 23:20:14 +0800 Subject: Timeline service add change timeline name api. --- Timeline.Tests/Services/TimelineServiceTest.cs | 19 ++++++++++ Timeline/Services/TimelineService.cs | 50 ++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/Timeline.Tests/Services/TimelineServiceTest.cs b/Timeline.Tests/Services/TimelineServiceTest.cs index 558ec597..3883cda9 100644 --- a/Timeline.Tests/Services/TimelineServiceTest.cs +++ b/Timeline.Tests/Services/TimelineServiceTest.cs @@ -298,5 +298,24 @@ namespace Timeline.Tests.Services timeline.Title.Should().Be("atitle"); } } + + [Fact] + public async Task ChangeName() + { + _clock.ForwardCurrentTime(); + + await _timelineService.CreateTimeline("tl", await _userService.GetUserIdByUsername("user")); + + var time = _clock.ForwardCurrentTime(); + + await _timelineService.ChangeTimelineName("tl", "newtl"); + + { + var timeline = await _timelineService.GetTimeline("newtl"); + timeline.Name.Should().Be("newtl"); + timeline.LastModified.Should().Be(time); + timeline.NameLastModified.Should().Be(time); + } + } } } diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index 2f0bf2c5..0a3a2076 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Timeline.Entities; using Timeline.Helpers; @@ -357,6 +358,21 @@ namespace Timeline.Services /// Thrown when timeline name is invalid. /// Thrown when the timeline does not exist. Task DeleteTimeline(string timelineName); + + /// + /// Change name of a timeline. + /// + /// The old timeline name. + /// The new timeline name. + /// The new timeline info. + /// Thrown when or is null. + /// Thrown when or is of invalid format. + /// Thrown when timeline does not exist. + /// Thrown when a timeline with new name already exists. + /// + /// You can only change name of general timeline. + /// + Task ChangeTimelineName(string oldTimelineName, string newTimelineName); } public class TimelineService : ITimelineService @@ -1111,5 +1127,39 @@ namespace Timeline.Services _database.Timelines.Remove(entity); await _database.SaveChangesAsync(); } + + public async Task ChangeTimelineName(string oldTimelineName, string newTimelineName) + { + if (oldTimelineName == null) + throw new ArgumentNullException(nameof(oldTimelineName)); + if (newTimelineName == null) + throw new ArgumentNullException(nameof(newTimelineName)); + + ValidateTimelineName(oldTimelineName, nameof(oldTimelineName)); + ValidateTimelineName(newTimelineName, nameof(newTimelineName)); + + var entity = await _database.Timelines.Where(t => t.Name == oldTimelineName).SingleOrDefaultAsync(); + + if (entity == null) + throw new TimelineNotExistException(oldTimelineName); + + if (oldTimelineName == newTimelineName) + return await MapTimelineFromEntity(entity); + + var conflict = await _database.Timelines.AnyAsync(t => t.Name == newTimelineName); + + if (conflict) + throw new EntityAlreadyExistException(EntityNames.Timeline, null, ExceptionTimelineNameConflict); + + var now = _clock.GetCurrentTime(); + + entity.Name = newTimelineName; + entity.NameLastModified = now; + entity.LastModified = now; + + await _database.SaveChangesAsync(); + + return await MapTimelineFromEntity(entity); + } } } -- cgit v1.2.3 From 12410a51fb2e5f55e8d83415bc3c4053a146ce3b Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 28 Aug 2020 00:01:04 +0800 Subject: Add change timeline name api to timeline controller. --- Timeline.Tests/IntegratedTests/TimelineTest.cs | 61 +++++++++++++++++++++++--- Timeline.Tests/Services/TimelineServiceTest.cs | 8 ++++ Timeline/Controllers/TimelineController.cs | 26 +++++++++++ Timeline/Filters/Timeline.cs | 13 ++++-- Timeline/Models/Http/TimelineController.cs | 19 ++++++++ Timeline/Services/TimelineService.cs | 3 +- 6 files changed, 120 insertions(+), 10 deletions(-) diff --git a/Timeline.Tests/IntegratedTests/TimelineTest.cs b/Timeline.Tests/IntegratedTests/TimelineTest.cs index 302b2195..ac4f41a2 100644 --- a/Timeline.Tests/IntegratedTests/TimelineTest.cs +++ b/Timeline.Tests/IntegratedTests/TimelineTest.cs @@ -488,7 +488,7 @@ namespace Timeline.Tests.IntegratedTests { var res = await client.DeleteAsync("timelines/t1"); - res.Should().HaveStatusCode(HttpStatusCode.NotFound); + res.Should().HaveStatusCode(400); } } } @@ -545,15 +545,15 @@ namespace Timeline.Tests.IntegratedTests } { var res = await client.PatchAsJsonAsync(generator("notexist", null), new TimelinePatchRequest { }); - res.Should().HaveStatusCode(404).And.HaveCommonBody(errorCode); + res.Should().HaveStatusCode(400).And.HaveCommonBody(errorCode); } { var res = await client.PutAsync(generator("notexist", "members/user1"), null); - res.Should().HaveStatusCode(404).And.HaveCommonBody(errorCode); + res.Should().HaveStatusCode(400).And.HaveCommonBody(errorCode); } { var res = await client.DeleteAsync(generator("notexist", "members/user1")); - res.Should().HaveStatusCode(404).And.HaveCommonBody(errorCode); + res.Should().HaveStatusCode(400).And.HaveCommonBody(errorCode); } { var res = await client.GetAsync(generator("notexist", "posts")); @@ -561,11 +561,11 @@ namespace Timeline.Tests.IntegratedTests } { var res = await client.PostAsJsonAsync(generator("notexist", "posts"), TimelineHelper.TextPostCreateRequest("aaa")); - res.Should().HaveStatusCode(404).And.HaveCommonBody(errorCode); + res.Should().HaveStatusCode(400).And.HaveCommonBody(errorCode); } { var res = await client.DeleteAsync(generator("notexist", "posts/123")); - res.Should().HaveStatusCode(404).And.HaveCommonBody(errorCode); + res.Should().HaveStatusCode(400).And.HaveCommonBody(errorCode); } { var res = await client.GetAsync(generator("notexist", "posts/123/data")); @@ -1436,5 +1436,54 @@ namespace Timeline.Tests.IntegratedTests .Which.Title.Should().Be("atitle"); } } + + [Fact] + public async Task ChangeName() + { + { + using var client = await CreateDefaultClient(); + var res = await client.PostAsJsonAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "t1", NewName = "tttttttt" }); + res.Should().HaveStatusCode(401); + } + + { + using var client = await CreateClientAs(2); + var res = await client.PostAsJsonAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "t1", NewName = "tttttttt" }); + res.Should().HaveStatusCode(403); + } + + using (var client = await CreateClientAsUser()) + { + { + var res = await client.PostAsJsonAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "!!!", NewName = "tttttttt" }); + res.Should().BeInvalidModel(); + } + + { + var res = await client.PostAsJsonAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "ttt", NewName = "!!!!" }); + res.Should().BeInvalidModel(); + } + + { + var res = await client.PostAsJsonAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "ttttt", NewName = "tttttttt" }); + res.Should().HaveStatusCode(400).And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.TimelineController.NotExist); + } + + { + var res = await client.PostAsJsonAsync("timelineop/changename", new TimelineChangeNameRequest { OldName = "t1", NewName = "newt" }); + res.Should().HaveStatusCode(200).And.HaveJsonBody().Which.Name.Should().Be("newt"); + } + + { + var res = await client.GetAsync("timelines/t1"); + res.Should().HaveStatusCode(404); + } + + { + var res = await client.GetAsync("timelines/newt"); + res.Should().HaveStatusCode(200).And.HaveJsonBody().Which.Name.Should().Be("newt"); + } + } + } } } diff --git a/Timeline.Tests/Services/TimelineServiceTest.cs b/Timeline.Tests/Services/TimelineServiceTest.cs index 3883cda9..5a774b78 100644 --- a/Timeline.Tests/Services/TimelineServiceTest.cs +++ b/Timeline.Tests/Services/TimelineServiceTest.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Timeline.Entities; using Timeline.Models; using Timeline.Services; +using Timeline.Services.Exceptions; using Timeline.Tests.Helpers; using Xunit; @@ -304,7 +305,14 @@ namespace Timeline.Tests.Services { _clock.ForwardCurrentTime(); + await _timelineService.Awaiting(s => s.ChangeTimelineName("!!!", "newtl")).Should().ThrowAsync(); + await _timelineService.Awaiting(s => s.ChangeTimelineName("tl", "!!!")).Should().ThrowAsync(); + await _timelineService.Awaiting(s => s.ChangeTimelineName("tl", "newtl")).Should().ThrowAsync(); + await _timelineService.CreateTimeline("tl", await _userService.GetUserIdByUsername("user")); + await _timelineService.CreateTimeline("tl2", await _userService.GetUserIdByUsername("user")); + + await _timelineService.Awaiting(s => s.ChangeTimelineName("tl", "tl2")).Should().ThrowAsync(); var time = _clock.ForwardCurrentTime(); diff --git a/Timeline/Controllers/TimelineController.cs b/Timeline/Controllers/TimelineController.cs index 90b50bbb..9a3147ea 100644 --- a/Timeline/Controllers/TimelineController.cs +++ b/Timeline/Controllers/TimelineController.cs @@ -308,6 +308,7 @@ namespace Timeline.Controllers [HttpDelete("timelines/{name}/posts/{id}")] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task> PostDelete([FromRoute][GeneralTimelineName] string name, [FromRoute] long id) @@ -336,6 +337,7 @@ namespace Timeline.Controllers [HttpPatch("timelines/{name}")] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status401Unauthorized)] [ProducesResponseType(StatusCodes.Status403Forbidden)] public async Task> TimelinePatch([FromRoute][GeneralTimelineName] string name, [FromBody] TimelinePatchRequest body) @@ -461,5 +463,29 @@ namespace Timeline.Controllers return CommonDeleteResponse.NotExist(); } } + + [HttpPost("timelineop/changename")] + [Authorize] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public async Task> TimelineOpChangeName([FromBody] TimelineChangeNameRequest body) + { + if (!this.IsAdministrator() && !(await _service.HasManagePermission(body.OldName, this.GetUserId()))) + { + return StatusCode(StatusCodes.Status403Forbidden, ErrorResponse.Common.Forbid()); + } + + try + { + var timeline = await _service.ChangeTimelineName(body.OldName, body.NewName); + return Ok(_mapper.Map(timeline)); + } + catch (EntityAlreadyExistException) + { + return BadRequest(ErrorResponse.TimelineController.NameConflict()); + } + } } } diff --git a/Timeline/Filters/Timeline.cs b/Timeline/Filters/Timeline.cs index 90b87223..6a730ee7 100644 --- a/Timeline/Filters/Timeline.cs +++ b/Timeline/Filters/Timeline.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Timeline.Models.Http; using Timeline.Services.Exceptions; @@ -13,11 +14,17 @@ namespace Timeline.Filters { if (e.InnerException is UserNotExistException) { - context.Result = new NotFoundObjectResult(ErrorResponse.UserCommon.NotExist()); + if (HttpMethods.IsGet(context.HttpContext.Request.Method)) + context.Result = new NotFoundObjectResult(ErrorResponse.UserCommon.NotExist()); + else + context.Result = new BadRequestObjectResult(ErrorResponse.UserCommon.NotExist()); } else { - context.Result = new NotFoundObjectResult(ErrorResponse.TimelineController.NotExist()); + if (HttpMethods.IsGet(context.HttpContext.Request.Method)) + context.Result = new NotFoundObjectResult(ErrorResponse.TimelineController.NotExist()); + else + context.Result = new BadRequestObjectResult(ErrorResponse.TimelineController.NotExist()); } } } diff --git a/Timeline/Models/Http/TimelineController.cs b/Timeline/Models/Http/TimelineController.cs index 95bae3e6..7bd141ed 100644 --- a/Timeline/Models/Http/TimelineController.cs +++ b/Timeline/Models/Http/TimelineController.cs @@ -71,4 +71,23 @@ namespace Timeline.Models.Http /// public TimelineVisibility? Visibility { get; set; } } + + /// + /// Change timeline name request model. + /// + public class TimelineChangeNameRequest + { + /// + /// Old name of timeline. + /// + [Required] + [TimelineName] + public string OldName { get; set; } = default!; + /// + /// New name of timeline. + /// + [Required] + [TimelineName] + public string NewName { get; set; } = default!; + } } diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index 0a3a2076..4bcae596 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -411,6 +411,7 @@ namespace Timeline.Services } } + /// Remember to include Members when query. private async Task MapTimelineFromEntity(TimelineEntity entity) { var owner = await _userService.GetUserById(entity.OwnerId); @@ -1138,7 +1139,7 @@ namespace Timeline.Services ValidateTimelineName(oldTimelineName, nameof(oldTimelineName)); ValidateTimelineName(newTimelineName, nameof(newTimelineName)); - var entity = await _database.Timelines.Where(t => t.Name == oldTimelineName).SingleOrDefaultAsync(); + var entity = await _database.Timelines.Include(t => t.Members).Where(t => t.Name == oldTimelineName).SingleOrDefaultAsync(); if (entity == null) throw new TimelineNotExistException(oldTimelineName); -- cgit v1.2.3 From c73388256aad039239cf3977d7b079e3f9323258 Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 31 Aug 2020 22:22:57 +0800 Subject: Now uesr avatar put api returns etag. --- Timeline.Tests/Helpers/HttpResponseExtensions.cs | 35 ++++++++++++++++++++++++ Timeline.Tests/IntegratedTests/UserAvatarTest.cs | 25 +++++++++++++++++ Timeline/Controllers/UserAvatarController.cs | 6 +++- Timeline/Services/UserAvatarService.cs | 14 +++++----- 4 files changed, 72 insertions(+), 8 deletions(-) create mode 100644 Timeline.Tests/Helpers/HttpResponseExtensions.cs diff --git a/Timeline.Tests/Helpers/HttpResponseExtensions.cs b/Timeline.Tests/Helpers/HttpResponseExtensions.cs new file mode 100644 index 00000000..2bd497f1 --- /dev/null +++ b/Timeline.Tests/Helpers/HttpResponseExtensions.cs @@ -0,0 +1,35 @@ +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Timeline.Models.Converters; +using Timeline.Models.Http; + +namespace Timeline.Tests.Helpers +{ + public static class HttpResponseExtensions + { + public static JsonSerializerOptions JsonSerializerOptions { get; } + + static HttpResponseExtensions() + { + JsonSerializerOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + JsonSerializerOptions.Converters.Add(new JsonDateTimeConverter()); + } + + public static async Task ReadBodyAsJsonAsync(this HttpResponseMessage response) + { + var stream = await response.Content.ReadAsStreamAsync(); + return await JsonSerializer.DeserializeAsync(stream, JsonSerializerOptions); + } + + public static Task ReadBodyAsCommonResponseAsync(this HttpResponseMessage response) + { + return response.ReadBodyAsJsonAsync(); + } + } +} diff --git a/Timeline.Tests/IntegratedTests/UserAvatarTest.cs b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs index 507b05ba..f2796005 100644 --- a/Timeline.Tests/IntegratedTests/UserAvatarTest.cs +++ b/Timeline.Tests/IntegratedTests/UserAvatarTest.cs @@ -10,6 +10,7 @@ using System.IO; using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Net.Mime; using System.Threading.Tasks; using Timeline.Models.Http; using Timeline.Services; @@ -222,5 +223,29 @@ namespace Timeline.Tests.IntegratedTests } } } + + [Fact] + public async Task AvatarPutReturnETag() + { + using var client = await CreateClientAsUser(); + + EntityTagHeaderValue etag; + + { + var image = ImageHelper.CreatePngWithSize(100, 100); + var res = await client.PutByteArrayAsync("users/user1/avatar", image, PngFormat.Instance.DefaultMimeType); + res.Should().HaveStatusCode(200); + etag = res.Headers.ETag; + etag.Should().NotBeNull(); + etag.Tag.Should().NotBeNullOrEmpty(); + } + + { + var res = await client.GetAsync("users/user1/avatar"); + res.Should().HaveStatusCode(200); + res.Headers.ETag.Should().Be(etag); + res.Headers.ETag.Tag.Should().Be(etag.Tag); + } + } } } \ No newline at end of file diff --git a/Timeline/Controllers/UserAvatarController.cs b/Timeline/Controllers/UserAvatarController.cs index 97c4bdb8..bc4afa30 100644 --- a/Timeline/Controllers/UserAvatarController.cs +++ b/Timeline/Controllers/UserAvatarController.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; using System; using System.Threading.Tasks; using Timeline.Auth; @@ -105,7 +106,7 @@ namespace Timeline.Controllers try { - await _service.SetAvatar(id, new Avatar + var etag = await _service.SetAvatar(id, new Avatar { Data = body.Data, Type = body.ContentType @@ -113,6 +114,9 @@ namespace Timeline.Controllers _logger.LogInformation(Log.Format(LogPutSuccess, ("Username", username), ("Mime Type", Request.ContentType))); + + Response.Headers.Append("ETag", new EntityTagHeaderValue($"\"{etag}\"").ToString()); + return Ok(); } catch (ImageException e) diff --git a/Timeline/Services/UserAvatarService.cs b/Timeline/Services/UserAvatarService.cs index 2bf8bddc..b41c45fd 100644 --- a/Timeline/Services/UserAvatarService.cs +++ b/Timeline/Services/UserAvatarService.cs @@ -71,9 +71,10 @@ namespace Timeline.Services /// /// The id of the user to set avatar for. /// The avatar. Can be null to delete the saved avatar. + /// The etag of the avatar. /// Thrown if any field in is null when is not null. /// Thrown if avatar is of bad format. - Task SetAvatar(long id, Avatar? avatar); + Task SetAvatar(long id, Avatar? avatar); } // TODO! : Make this configurable. @@ -199,7 +200,7 @@ namespace Timeline.Services return defaultAvatar; } - public async Task SetAvatar(long id, Avatar? avatar) + public async Task SetAvatar(long id, Avatar? avatar) { if (avatar != null) { @@ -213,11 +214,7 @@ namespace Timeline.Services if (avatar == null) { - if (avatarEntity == null || avatarEntity.DataTag == null) - { - return; - } - else + if (avatarEntity != null && avatarEntity.DataTag != null) { await _dataManager.FreeEntry(avatarEntity.DataTag); avatarEntity.DataTag = null; @@ -226,6 +223,7 @@ namespace Timeline.Services await _database.SaveChangesAsync(); _logger.LogInformation(Resources.Services.UserAvatarService.LogUpdateEntity); } + return await _defaultUserAvatarProvider.GetDefaultAvatarETag(); } else { @@ -250,6 +248,8 @@ namespace Timeline.Services { await _dataManager.FreeEntry(oldTag); } + + return avatarEntity.DataTag; } } } -- cgit v1.2.3 From ff90e2819a1c0b7d1b605b45edaaaee7527c05b1 Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 31 Aug 2020 22:39:18 +0800 Subject: Post info now contain data etag. --- Timeline.Tests/IntegratedTests/TimelineTest.cs | 34 ++++++++++++++++++++++++++ Timeline/Models/Http/Timeline.cs | 7 +++++- Timeline/Models/Timeline.cs | 4 +++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/Timeline.Tests/IntegratedTests/TimelineTest.cs b/Timeline.Tests/IntegratedTests/TimelineTest.cs index ac4f41a2..ec46b96a 100644 --- a/Timeline.Tests/IntegratedTests/TimelineTest.cs +++ b/Timeline.Tests/IntegratedTests/TimelineTest.cs @@ -1485,5 +1485,39 @@ namespace Timeline.Tests.IntegratedTests } } } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task PostDataETag(TimelineUrlGenerator urlGenerator) + { + using var client = await CreateClientAsUser(); + + long id; + string etag; + + { + var res = await client.PostAsJsonAsync(urlGenerator(1, "posts"), new TimelinePostCreateRequest + { + Content = new TimelinePostCreateRequestContent + { + Type = TimelinePostContentTypes.Image, + Data = Convert.ToBase64String(ImageHelper.CreatePngWithSize(100, 50)) + } + }); + res.Should().HaveStatusCode(200); + var body = await res.ReadBodyAsJsonAsync(); + body.Content.ETag.Should().NotBeNullOrEmpty(); + + id = body.Id; + etag = body.Content.ETag; + } + + { + var res = await client.GetAsync(urlGenerator(1, $"posts/{id}/data")); + res.Should().HaveStatusCode(200); + res.Headers.ETag.Should().NotBeNull(); + res.Headers.ETag.ToString().Should().Be(etag); + } + } } } diff --git a/Timeline/Models/Http/Timeline.cs b/Timeline/Models/Http/Timeline.cs index 3596af18..a81b33f5 100644 --- a/Timeline/Models/Http/Timeline.cs +++ b/Timeline/Models/Http/Timeline.cs @@ -25,6 +25,10 @@ namespace Timeline.Models.Http /// If post is of image type. This is the image url. /// public string? Url { get; set; } + /// + /// If post has data (currently it means it's a image post), this is the data etag. + /// + public string? ETag { get; set; } } /// @@ -192,7 +196,8 @@ namespace Timeline.Models.Http Url = urlHelper.ActionLink( action: nameof(TimelineController.PostDataGet), controller: nameof(TimelineController)[0..^nameof(Controller).Length], - values: new { Name = source.TimelineName, Id = source.Id }) + values: new { Name = source.TimelineName, Id = source.Id }), + ETag = $"\"{imageContent.DataTag}\"" }; } else diff --git a/Timeline/Models/Timeline.cs b/Timeline/Models/Timeline.cs index 42906053..a5987577 100644 --- a/Timeline/Models/Timeline.cs +++ b/Timeline/Models/Timeline.cs @@ -43,6 +43,10 @@ namespace Timeline.Models 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; } } -- cgit v1.2.3