From ddfd4cedaf696e6ad19f40bec4db0f9d0e28dc65 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 18 Jun 2020 16:21:39 +0800 Subject: Add last modified info to timeline. --- Timeline/Entities/TimelineEntity.cs | 6 + ...00618064936_TimelineAddModifiedTime.Designer.cs | 314 +++++++++++++++++++++ .../20200618064936_TimelineAddModifiedTime.cs | 57 ++++ .../Migrations/DatabaseContextModelSnapshot.cs | 10 +- Timeline/Models/Timeline.cs | 3 + Timeline/Services/TimelineService.cs | 22 +- 6 files changed, 400 insertions(+), 12 deletions(-) create mode 100644 Timeline/Migrations/20200618064936_TimelineAddModifiedTime.Designer.cs create mode 100644 Timeline/Migrations/20200618064936_TimelineAddModifiedTime.cs (limited to 'Timeline') diff --git a/Timeline/Entities/TimelineEntity.cs b/Timeline/Entities/TimelineEntity.cs index 1159cbfe..b084a572 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("name_last_modified")] + public DateTime NameLastModified { get; set; } + [Column("description")] public string? Description { get; set; } @@ -38,6 +41,9 @@ namespace Timeline.Entities [Column("create_time")] public DateTime CreateTime { get; set; } + [Column("last_modified")] + public DateTime LastModified { get; set; } + [Column("current_post_local_id")] public long CurrentPostLocalId { get; set; } diff --git a/Timeline/Migrations/20200618064936_TimelineAddModifiedTime.Designer.cs b/Timeline/Migrations/20200618064936_TimelineAddModifiedTime.Designer.cs new file mode 100644 index 00000000..fd10dfa9 --- /dev/null +++ b/Timeline/Migrations/20200618064936_TimelineAddModifiedTime.Designer.cs @@ -0,0 +1,314 @@ +// +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("20200618064936_TimelineAddModifiedTime")] + partial class TimelineAddModifiedTime + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.5"); + + 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("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("Nickname") + .HasColumnName("nickname") + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnName("password") + .HasColumnType("TEXT"); + + b.Property("Roles") + .IsRequired() + .HasColumnName("roles") + .HasColumnType("TEXT"); + + b.Property("Username") + .IsRequired() + .HasColumnName("username") + .HasColumnType("TEXT"); + + 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") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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/20200618064936_TimelineAddModifiedTime.cs b/Timeline/Migrations/20200618064936_TimelineAddModifiedTime.cs new file mode 100644 index 00000000..c277fe39 --- /dev/null +++ b/Timeline/Migrations/20200618064936_TimelineAddModifiedTime.cs @@ -0,0 +1,57 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Timeline.Migrations +{ + public partial class TimelineAddModifiedTime : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + var currentTime = new DateTimeToStringConverter().ConvertToProvider(DateTime.Now); + + migrationBuilder.Sql( +@$" +PRAGMA foreign_keys=OFF; + +BEGIN TRANSACTION; + +CREATE TABLE new_timelines ( + id INTEGER NOT NULL CONSTRAINT PK_timelines PRIMARY KEY AUTOINCREMENT, + unique_id TEXT NOT NULL DEFAULT (lower(hex(randomblob(16)))), + name TEXT NULL, + name_last_modified TEXT NOT NULL, + description TEXT NULL, + owner INTEGER NOT NULL, + visibility INTEGER NOT NULL, + create_time TEXT NOT NULL, + last_modified TEXT NOT NULL, + current_post_local_id INTEGER NOT NULL DEFAULT 0, + CONSTRAINT FK_timelines_users_owner FOREIGN KEY (owner) REFERENCES users (id) ON DELETE CASCADE +); + +INSERT INTO new_timelines (id, unique_id, name, name_last_modified, description, owner, visibility, create_time, last_modified, current_post_local_id) + SELECT id, unique_id, name, '{currentTime}', description, owner, visibility, create_time, '{currentTime}', current_post_local_id FROM timelines; + +DROP TABLE timelines; + +ALTER TABLE new_timelines + RENAME TO timelines; + +CREATE INDEX IX_timelines_owner ON timelines (owner); + +PRAGMA foreign_key_check; + +COMMIT TRANSACTION; + +PRAGMA foreign_keys=ON; +" + , true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + + } + } +} diff --git a/Timeline/Migrations/DatabaseContextModelSnapshot.cs b/Timeline/Migrations/DatabaseContextModelSnapshot.cs index ea0b58c6..e24023d1 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.4"); + .HasAnnotation("ProductVersion", "3.1.5"); modelBuilder.Entity("Timeline.Entities.DataEntity", b => { @@ -81,10 +81,18 @@ namespace Timeline.Migrations .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"); diff --git a/Timeline/Models/Timeline.cs b/Timeline/Models/Timeline.cs index d4b3e849..3701ed35 100644 --- a/Timeline/Models/Timeline.cs +++ b/Timeline/Models/Timeline.cs @@ -72,12 +72,15 @@ namespace Timeline.Models { public string UniqueID { get; set; } = default!; public string Name { get; set; } = default!; + public DateTime NameLastModified { 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 diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index d232f3e1..ba5576d1 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -356,22 +356,30 @@ namespace Timeline.Services { UniqueID = entity.UniqueId, Name = entity.Name ?? ("@" + owner.Username), + NameLastModified = entity.NameLastModified, Description = entity.Description ?? "", Owner = owner, Visibility = entity.Visibility, - Members = members + Members = members, + CreateTime = entity.CreateTime, + LastModified = entity.LastModified }; } private TimelineEntity CreateNewTimelineEntity(string? name, long ownerId) { + var currentTime = _clock.GetCurrentTime(); + return new TimelineEntity { Name = name, + NameLastModified = currentTime, OwnerId = ownerId, Visibility = TimelineVisibility.Register, - CreateTime = _clock.GetCurrentTime(), + CreateTime = currentTime, + LastModified = currentTime, CurrentPostLocalId = 0, + Members = new List() }; } @@ -930,15 +938,7 @@ namespace Timeline.Services if (conflict) throw new EntityAlreadyExistException(EntityNames.Timeline, null, ExceptionTimelineNameConflict); - var newEntity = new TimelineEntity - { - CurrentPostLocalId = 0, - Name = name, - OwnerId = owner, - Visibility = TimelineVisibility.Register, - CreateTime = _clock.GetCurrentTime(), - Members = new List() - }; + var newEntity = CreateNewTimelineEntity(name, user.Id!.Value); _database.Timelines.Add(newEntity); await _database.SaveChangesAsync(); -- cgit v1.2.3 From 5c1b88ce2df221d6a021b9246c952dbf5ee58550 Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 18 Jun 2020 17:13:08 +0800 Subject: feat(back): Timeline service add last modified. --- Timeline.Tests/Helpers/TestClock.cs | 20 ++++++++++ Timeline.Tests/Services/TimelineServiceTest.cs | 53 ++++++++++++++----------- Timeline/Services/TimelineService.cs | 55 +++++++++++++++++++------- 3 files changed, 91 insertions(+), 37 deletions(-) (limited to 'Timeline') diff --git a/Timeline.Tests/Helpers/TestClock.cs b/Timeline.Tests/Helpers/TestClock.cs index 7febc0fe..de7d0eb7 100644 --- a/Timeline.Tests/Helpers/TestClock.cs +++ b/Timeline.Tests/Helpers/TestClock.cs @@ -19,5 +19,25 @@ namespace Timeline.Tests.Helpers { _currentTime = mockTime; } + + public DateTime SetMockCurrentTime() + { + var time = new DateTime(2000, 1, 1, 1, 1, 1); + _currentTime = time; + return time; + } + + public DateTime ForwardCurrentTime() + { + return ForwardCurrentTime(TimeSpan.FromDays(1)); + } + + public DateTime ForwardCurrentTime(TimeSpan timeSpan) + { + if (_currentTime == null) + return SetMockCurrentTime(); + _currentTime.Value.Add(timeSpan); + return _currentTime.Value; + } } } diff --git a/Timeline.Tests/Services/TimelineServiceTest.cs b/Timeline.Tests/Services/TimelineServiceTest.cs index f7a94dfc..cb2ade61 100644 --- a/Timeline.Tests/Services/TimelineServiceTest.cs +++ b/Timeline.Tests/Services/TimelineServiceTest.cs @@ -1,18 +1,20 @@ using Castle.Core.Logging; using FluentAssertions; +using FluentAssertions.Xml; using Microsoft.Extensions.Logging.Abstractions; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Timeline.Entities; +using Timeline.Models; using Timeline.Services; using Timeline.Tests.Helpers; using Xunit; namespace Timeline.Tests.Services { - public class TimelineServiceTest : IAsyncLifetime + public class TimelineServiceTest : IAsyncLifetime, IDisposable { private TestDatabase _testDatabase = new TestDatabase(); @@ -48,40 +50,45 @@ namespace Timeline.Tests.Services public async Task DisposeAsync() { await _testDatabase.DisposeAsync(); + await _databaseContext.DisposeAsync(); } - [Fact] - public async Task PersonalTimeline_LastModified() + public void Dispose() { - var mockTime = new DateTime(2000, 1, 1, 1, 1, 1); - - _clock.SetCurrentTime(mockTime); - - var timeline = await _timelineService.GetTimeline("@user"); - - timeline.NameLastModified.Should().Be(mockTime); - timeline.LastModified.Should().Be(mockTime); + _eTagGenerator.Dispose(); } - [Fact] - public async Task OrdinaryTimeline_LastModified() + [Theory] + [InlineData("@user")] + [InlineData("tl")] + public async Task Timeline_LastModified(string timelineName) { - var mockTime = new DateTime(2000, 1, 1, 1, 1, 1); - - _clock.SetCurrentTime(mockTime); + _clock.ForwardCurrentTime(); + void Check(Models.Timeline timeline) { - var timeline = await _timelineService.CreateTimeline("tl", await _userService.GetUserIdByUsername("user")); - - timeline.NameLastModified.Should().Be(mockTime); - timeline.LastModified.Should().Be(mockTime); + timeline.NameLastModified.Should().Be(_clock.GetCurrentTime()); + timeline.LastModified.Should().Be(_clock.GetCurrentTime()); } + async Task GetAndCheck() { - var timeline = await _timelineService.GetTimeline("tl"); - timeline.NameLastModified.Should().Be(mockTime); - timeline.LastModified.Should().Be(mockTime); + Check(await _timelineService.GetTimeline(timelineName)); } + + var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); + if (!isPersonal) + Check(await _timelineService.CreateTimeline(timelineName, await _userService.GetUserIdByUsername("user"))); + + await GetAndCheck(); + + _clock.ForwardCurrentTime(); + await _timelineService.ChangeProperty(timelineName, new TimelineChangePropertyRequest { Visibility = TimelineVisibility.Public }); + await GetAndCheck(); + + _clock.ForwardCurrentTime(); + await _timelineService.ChangeMember(timelineName, new List { "admin" }, null); + await GetAndCheck(); } } } diff --git a/Timeline/Services/TimelineService.cs b/Timeline/Services/TimelineService.cs index ba5576d1..6c1e91c6 100644 --- a/Timeline/Services/TimelineService.cs +++ b/Timeline/Services/TimelineService.cs @@ -15,6 +15,23 @@ using static Timeline.Resources.Services.TimelineService; namespace Timeline.Services { + public static class TimelineHelper + { + public static string ExtractTimelineName(string name, out bool isPersonal) + { + if (name.StartsWith("@", StringComparison.OrdinalIgnoreCase)) + { + isPersonal = true; + return name.Substring(1); + } + else + { + isPersonal = false; + return name; + } + } + } + public enum TimelineUserRelationshipType { Own = 0b1, @@ -383,19 +400,7 @@ namespace Timeline.Services }; } - private static string ExtractTimelineName(string name, out bool isPersonal) - { - if (name.StartsWith("@", StringComparison.OrdinalIgnoreCase)) - { - isPersonal = true; - return name.Substring(1); - } - else - { - isPersonal = false; - return name; - } - } + // Get timeline id by name. If it is a personal timeline and it does not exist, it will be created. // @@ -407,7 +412,7 @@ namespace Timeline.Services // It follows all timeline-related function common interface contracts. private async Task FindTimelineId(string timelineName) { - timelineName = ExtractTimelineName(timelineName, out var isPersonal); + timelineName = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); if (isPersonal) { @@ -713,16 +718,26 @@ namespace Timeline.Services var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); + var changed = false; + if (newProperties.Description != null) { + changed = true; timelineEntity.Description = newProperties.Description; } if (newProperties.Visibility.HasValue) { + changed = true; timelineEntity.Visibility = newProperties.Visibility.Value; } + if (changed) + { + var currentTime = _clock.GetCurrentTime(); + timelineEntity.LastModified = currentTime; + } + await _database.SaveChangesAsync(); } @@ -768,8 +783,17 @@ namespace Timeline.Services simplifiedAdd.Remove(u); simplifiedRemove.Remove(u); } + + if (simplifiedAdd.Count == 0) + simplifiedAdd = null; + + if (simplifiedRemove.Count == 0) + simplifiedRemove = null; } + if (simplifiedAdd == null && simplifiedRemove == null) + return; + var timelineId = await FindTimelineId(timelineName); async Task?> CheckExistenceAndGetId(List? list) @@ -799,6 +823,9 @@ namespace Timeline.Services _database.TimelineMembers.RemoveRange(membersToRemove); } + var timelineEntity = await _database.Timelines.Where(t => t.Id == timelineId).SingleAsync(); + timelineEntity.LastModified = _clock.GetCurrentTime(); + await _database.SaveChangesAsync(); } -- cgit v1.2.3 From c5c50332aebd9a3e7d5d2c9163f0cad6c6ba241b Mon Sep 17 00:00:00 2001 From: crupest Date: Thu, 18 Jun 2020 17:49:02 +0800 Subject: feat(back): Add last modified to timeline. --- .../IntegratedTests/IntegratedTestBase.cs | 3 +- Timeline.Tests/IntegratedTests/TimelineTest.cs | 72 ++++++++++++++++++---- Timeline/Models/Http/Timeline.cs | 3 + 3 files changed, 65 insertions(+), 13 deletions(-) (limited to 'Timeline') diff --git a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs b/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs index b5aec512..7cf27297 100644 --- a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs +++ b/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.TestHost; +using FluentAssertions; +using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; diff --git a/Timeline.Tests/IntegratedTests/TimelineTest.cs b/Timeline.Tests/IntegratedTests/TimelineTest.cs index b6a56e94..6736fecd 100644 --- a/Timeline.Tests/IntegratedTests/TimelineTest.cs +++ b/Timeline.Tests/IntegratedTests/TimelineTest.cs @@ -78,10 +78,12 @@ namespace Timeline.Tests.IntegratedTests return $"timelines/t{id}/{(subpath ?? "")}"; } + public delegate string TimelineUrlGenerator(int userId, string subpath = null); + public static IEnumerable TimelineUrlGeneratorData() { - yield return new[] { new Func(GeneratePersonalTimelineUrl) }; - yield return new[] { new Func(GenerateOrdinaryTimelineUrl) }; + yield return new[] { new TimelineUrlGenerator(GeneratePersonalTimelineUrl) }; + yield return new[] { new TimelineUrlGenerator(GenerateOrdinaryTimelineUrl) }; } private static string GeneratePersonalTimelineUrlByName(string name, string subpath = null) @@ -545,7 +547,7 @@ namespace Timeline.Tests.IntegratedTests [Theory] [MemberData(nameof(TimelineUrlGeneratorData))] - public async Task Description_Should_Work(Func generator) + public async Task Description_Should_Work(TimelineUrlGenerator generator) { using var client = await CreateClientAsUser(); @@ -585,7 +587,7 @@ namespace Timeline.Tests.IntegratedTests [Theory] [MemberData(nameof(TimelineUrlGeneratorData))] - public async Task Member_Should_Work(Func generator) + public async Task Member_Should_Work(TimelineUrlGenerator generator) { var getUrl = generator(1, null); using var client = await CreateClientAsUser(); @@ -682,7 +684,7 @@ namespace Timeline.Tests.IntegratedTests [Theory] [MemberData(nameof(TimelineUrlGeneratorData))] - public async Task Visibility_Test(Func generator) + public async Task Visibility_Test(TimelineUrlGenerator generator) { var userUrl = generator(1, "posts"); var adminUrl = generator(0, "posts"); @@ -765,7 +767,7 @@ namespace Timeline.Tests.IntegratedTests [Theory] [MemberData(nameof(TimelineUrlGeneratorData))] - public async Task Permission_Post_Create(Func generator) + public async Task Permission_Post_Create(TimelineUrlGenerator generator) { using (var client = await CreateClientAsUser()) { @@ -817,7 +819,7 @@ namespace Timeline.Tests.IntegratedTests [Theory] [MemberData(nameof(TimelineUrlGeneratorData))] - public async Task Permission_Post_Delete(Func generator) + public async Task Permission_Post_Delete(TimelineUrlGenerator generator) { async Task CreatePost(int userNumber) { @@ -885,7 +887,7 @@ namespace Timeline.Tests.IntegratedTests [Theory] [MemberData(nameof(TimelineUrlGeneratorData))] - public async Task TextPost_ShouldWork(Func generator) + public async Task TextPost_ShouldWork(TimelineUrlGenerator generator) { { using var client = await CreateClientAsUser(); @@ -963,7 +965,7 @@ namespace Timeline.Tests.IntegratedTests [Theory] [MemberData(nameof(TimelineUrlGeneratorData))] - public async Task GetPost_Should_Ordered(Func generator) + public async Task GetPost_Should_Ordered(TimelineUrlGenerator generator) { using var client = await CreateClientAsUser(); @@ -991,7 +993,7 @@ namespace Timeline.Tests.IntegratedTests [Theory] [MemberData(nameof(TimelineUrlGeneratorData))] - public async Task CreatePost_InvalidModel(Func generator) + public async Task CreatePost_InvalidModel(TimelineUrlGenerator generator) { using var client = await CreateClientAsUser(); @@ -1035,7 +1037,7 @@ namespace Timeline.Tests.IntegratedTests [Theory] [MemberData(nameof(TimelineUrlGeneratorData))] - public async Task ImagePost_ShouldWork(Func generator) + public async Task ImagePost_ShouldWork(TimelineUrlGenerator generator) { var imageData = ImageHelper.CreatePngWithSize(100, 200); @@ -1119,7 +1121,7 @@ namespace Timeline.Tests.IntegratedTests [Theory] [MemberData(nameof(TimelineUrlGeneratorData))] - public async Task ImagePost_400(Func generator) + public async Task ImagePost_400(TimelineUrlGenerator generator) { using var client = await CreateClientAsUser(); @@ -1145,5 +1147,51 @@ namespace Timeline.Tests.IntegratedTests .And.HaveCommonBody(ErrorCodes.TimelineController.PostNoData); } } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task LastModified(TimelineUrlGenerator generator) + { + using var client = await CreateClientAsUser(); + + DateTime lastModified; + + { + var res = await client.GetAsync(generator(1)); + lastModified = res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.LastModified; + } + + await Task.Delay(1000); + + { + var res = await client.PatchAsJsonAsync(generator(1), new TimelinePatchRequest { Description = "123" }); + lastModified = res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.LastModified.Should().BeAfter(lastModified).And.Subject.Value; + } + + { + var res = await client.GetAsync(generator(1)); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.LastModified.Should().Be(lastModified); + } + + await Task.Delay(1000); + + { + var res = await client.PutAsync(generator(1, "members/user2"), null); + res.Should().HaveStatusCode(200); + } + + { + var res = await client.GetAsync(generator(1)); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.LastModified.Should().BeAfter(lastModified); + } + } } } diff --git a/Timeline/Models/Http/Timeline.cs b/Timeline/Models/Http/Timeline.cs index a942db1e..80e6e69d 100644 --- a/Timeline/Models/Http/Timeline.cs +++ b/Timeline/Models/Http/Timeline.cs @@ -28,12 +28,15 @@ namespace Timeline.Models.Http { public string UniqueId { get; set; } = default!; public string Name { get; set; } = default!; + public DateTime NameLastModifed { 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!; #pragma warning disable CA1707 // Identifiers should not contain underscores public TimelineInfoLinks _links { get; set; } = default!; -- cgit v1.2.3