From 5e02abc95cc8419b378917d7053d63092046b2cf Mon Sep 17 00:00:00 2001 From: crupest Date: Sun, 14 Jun 2020 16:34:42 +0800 Subject: Many many bugs fix. 1. Add a way to test front end with mock page. 2. Unknown api returns 400 but not frontend page. 3. Fix a big bug that cause all data loss in database migration. --- Timeline.ErrorCodes/ErrorCodes.cs | 1 + Timeline.Tests/Helpers/TestApplication.cs | 2 +- Timeline.Tests/IntegratedTests/FrontEndTest.cs | 34 ++++++++++++++++ .../IntegratedTests/IntegratedTestBase.cs | 30 ++++++++------ .../IntegratedTests/UnknownEndpointTest.cs | 26 ++++++++++++ Timeline/Configs/ApplicationConfiguration.cs | 1 + .../20200614061237_AddTimelineUniqueId.cs | 47 +++++++++++++++++++++- Timeline/MockClientApp/index.html | 10 +++++ Timeline/Models/Http/ErrorResponse.cs | 10 +++++ Timeline/Properties/launchSettings.json | 8 ++++ Timeline/Resources/Messages.Designer.cs | 9 +++++ Timeline/Resources/Messages.resx | 3 ++ Timeline/Routes/UnknownEndpointMiddleware.cs | 39 ++++++++++++++++++ Timeline/Startup.cs | 29 +++++++++---- Timeline/Timeline.csproj | 7 ++++ 15 files changed, 235 insertions(+), 21 deletions(-) create mode 100644 Timeline.Tests/IntegratedTests/FrontEndTest.cs create mode 100644 Timeline.Tests/IntegratedTests/UnknownEndpointTest.cs create mode 100644 Timeline/MockClientApp/index.html create mode 100644 Timeline/Routes/UnknownEndpointMiddleware.cs diff --git a/Timeline.ErrorCodes/ErrorCodes.cs b/Timeline.ErrorCodes/ErrorCodes.cs index 0af36383..4637242a 100644 --- a/Timeline.ErrorCodes/ErrorCodes.cs +++ b/Timeline.ErrorCodes/ErrorCodes.cs @@ -12,6 +12,7 @@ { public const int InvalidModel = 1_000_0001; public const int Forbid = 1_000_0002; + public const int UnknownEndpoint = 1_000_0003; public static class Header { diff --git a/Timeline.Tests/Helpers/TestApplication.cs b/Timeline.Tests/Helpers/TestApplication.cs index 6e0a4ca6..abdb0a60 100644 --- a/Timeline.Tests/Helpers/TestApplication.cs +++ b/Timeline.Tests/Helpers/TestApplication.cs @@ -54,7 +54,7 @@ namespace Timeline.Tests.Helpers { config.AddInMemoryCollection(new Dictionary { - [ApplicationConfiguration.DisableFrontEndKey] = "true", + [ApplicationConfiguration.UseMockFrontEndKey] = "true", ["WorkDir"] = WorkDir }); }); diff --git a/Timeline.Tests/IntegratedTests/FrontEndTest.cs b/Timeline.Tests/IntegratedTests/FrontEndTest.cs new file mode 100644 index 00000000..a00d41b1 --- /dev/null +++ b/Timeline.Tests/IntegratedTests/FrontEndTest.cs @@ -0,0 +1,34 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.Testing; +using System.Net.Mime; +using System.Threading.Tasks; +using Timeline.Tests.Helpers; +using Xunit; + +namespace Timeline.Tests.IntegratedTests +{ + public class FrontEndTest : IntegratedTestBase + { + public FrontEndTest(WebApplicationFactory factory) : base(factory) + { + } + + [Fact] + public async Task Index() + { + using var client = await CreateDefaultClient(false); + var res = await client.GetAsync("index.html"); + res.Should().HaveStatusCode(200); + res.Content.Headers.ContentType.MediaType.Should().Be(MediaTypeNames.Text.Html); + } + + [Fact] + public async Task Fallback() + { + using var client = await CreateDefaultClient(false); + var res = await client.GetAsync("aaaaaaaaaaaaaaa"); + res.Should().HaveStatusCode(200); + res.Content.Headers.ContentType.MediaType.Should().Be(MediaTypeNames.Text.Html); + } + } +} diff --git a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs b/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs index e42483bd..01544828 100644 --- a/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs +++ b/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs @@ -117,17 +117,23 @@ namespace Timeline.Tests.IntegratedTests await TestApp.DisposeAsync(); } - public Task CreateDefaultClient() + public Task CreateDefaultClient(bool setApiBase = true) { var client = Factory.CreateDefaultClient(); - client.BaseAddress = new Uri(client.BaseAddress, "api/"); + if (setApiBase) + { + client.BaseAddress = new Uri(client.BaseAddress, "api/"); + } return Task.FromResult(client); } - public async Task CreateClientWithCredential(string username, string password) + public async Task CreateClientWithCredential(string username, string password, bool setApiBase = true) { var client = Factory.CreateDefaultClient(); - client.BaseAddress = new Uri(client.BaseAddress, "api/"); + if (setApiBase) + { + client.BaseAddress = new Uri(client.BaseAddress, "api/"); + } var response = await client.PostAsJsonAsync("token/create", new CreateTokenRequest { Username = username, Password = password }); var token = response.Should().HaveStatusCode(200) @@ -136,24 +142,24 @@ namespace Timeline.Tests.IntegratedTests return client; } - public Task CreateClientAs(int userNumber) + public Task CreateClientAs(int userNumber, bool setApiBase = true) { if (userNumber < 0) - return CreateDefaultClient(); + return CreateDefaultClient(setApiBase); if (userNumber == 0) - return CreateClientWithCredential("admin", "adminpw"); + return CreateClientWithCredential("admin", "adminpw", setApiBase); else - return CreateClientWithCredential($"user{userNumber}", $"user{userNumber}pw"); + return CreateClientWithCredential($"user{userNumber}", $"user{userNumber}pw", setApiBase); } - public Task CreateClientAsAdministrator() + public Task CreateClientAsAdministrator(bool setApiBase = true) { - return CreateClientAs(0); + return CreateClientAs(0, setApiBase); } - public Task CreateClientAsUser() + public Task CreateClientAsUser(bool setApiBase = true) { - return CreateClientAs(1); + return CreateClientAs(1, setApiBase); } } } diff --git a/Timeline.Tests/IntegratedTests/UnknownEndpointTest.cs b/Timeline.Tests/IntegratedTests/UnknownEndpointTest.cs new file mode 100644 index 00000000..40f818a7 --- /dev/null +++ b/Timeline.Tests/IntegratedTests/UnknownEndpointTest.cs @@ -0,0 +1,26 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Mvc.Testing; +using System.Threading.Tasks; +using Timeline.Models.Http; +using Timeline.Tests.Helpers; +using Xunit; + +namespace Timeline.Tests.IntegratedTests +{ + public class UnknownEndpointTest : IntegratedTestBase + { + public UnknownEndpointTest(WebApplicationFactory factory) : base(factory) + { + } + + [Fact] + public async Task UnknownEndpoint() + { + using var client = await CreateDefaultClient(); + var res = await client.GetAsync("unknownEndpoint"); + res.Should().HaveStatusCode(400) + .And.HaveCommonBody() + .Which.Code.Should().Be(ErrorCodes.Common.UnknownEndpoint); + } + } +} diff --git a/Timeline/Configs/ApplicationConfiguration.cs b/Timeline/Configs/ApplicationConfiguration.cs index fec7f06c..c84327d7 100644 --- a/Timeline/Configs/ApplicationConfiguration.cs +++ b/Timeline/Configs/ApplicationConfiguration.cs @@ -7,5 +7,6 @@ public const string DatabaseFileName = "timeline.db"; public const string DisableFrontEndKey = "DisableFrontEnd"; public const string FrontEndProxyOnlyKey = "FrontEndProxyOnly"; + public const string UseMockFrontEndKey = "UseMockFrontEnd"; } } diff --git a/Timeline/Migrations/20200614061237_AddTimelineUniqueId.cs b/Timeline/Migrations/20200614061237_AddTimelineUniqueId.cs index 80e90dbf..1fc3de18 100644 --- a/Timeline/Migrations/20200614061237_AddTimelineUniqueId.cs +++ b/Timeline/Migrations/20200614061237_AddTimelineUniqueId.cs @@ -9,6 +9,8 @@ namespace Timeline.Migrations migrationBuilder.Sql( @" ALTER TABLE timelines RENAME TO timelines_backup; +ALTER TABLE timeline_members RENAME TO timeline_members_backup; +ALTER TABLE timeline_posts RENAME TO timeline_posts_backup; CREATE TABLE timelines ( id INTEGER NOT NULL CONSTRAINT PK_timelines PRIMARY KEY AUTOINCREMENT, @@ -21,10 +23,53 @@ CREATE TABLE timelines ( CONSTRAINT FK_timelines_users_owner FOREIGN KEY (owner) REFERENCES users (id) ON DELETE CASCADE ); +CREATE TABLE timeline_members ( + id INTEGER NOT NULL + CONSTRAINT PK_timeline_members PRIMARY KEY AUTOINCREMENT, + user INTEGER NOT NULL, + timeline INTEGER NOT NULL, + CONSTRAINT FK_timeline_members_timelines_timeline FOREIGN KEY ( + timeline + ) + REFERENCES timelines (id) ON DELETE CASCADE, + CONSTRAINT FK_timeline_members_users_user FOREIGN KEY ( + user + ) + REFERENCES users (id) ON DELETE CASCADE +); + +CREATE TABLE timeline_posts ( + id INTEGER NOT NULL + CONSTRAINT PK_timeline_posts PRIMARY KEY AUTOINCREMENT, + timeline INTEGER NOT NULL, + author INTEGER NOT NULL, + content TEXT, + time TEXT NOT NULL, + last_updated TEXT NOT NULL, + local_id INTEGER NOT NULL + DEFAULT 0, + content_type TEXT NOT NULL + DEFAULT '', + extra_content TEXT, + CONSTRAINT FK_timeline_posts_users_author FOREIGN KEY ( + author + ) + REFERENCES users (id) ON DELETE CASCADE, + CONSTRAINT FK_timeline_posts_timelines_timeline FOREIGN KEY ( + timeline + ) + REFERENCES timelines (id) ON DELETE CASCADE +); + + INSERT INTO timelines (id, name, description, owner, visibility, create_time) SELECT id, name, description, owner, visibility, create_time FROM timelines_backup; - +INSERT INTO timeline_members SELECT * FROM timeline_members_backup; +INSERT INTO timeline_posts SELECT * FROM timeline_posts_backup; + DROP TABLE timelines_backup; +DROP TABLE timeline_members_backup; +DROP TABLE timeline_posts_backup; " ); } diff --git a/Timeline/MockClientApp/index.html b/Timeline/MockClientApp/index.html new file mode 100644 index 00000000..03cf371e --- /dev/null +++ b/Timeline/MockClientApp/index.html @@ -0,0 +1,10 @@ + + + + + Mock Client App + + + This is a mock client app for testing. + + diff --git a/Timeline/Models/Http/ErrorResponse.cs b/Timeline/Models/Http/ErrorResponse.cs index bb9c44df..9a4d190a 100644 --- a/Timeline/Models/Http/ErrorResponse.cs +++ b/Timeline/Models/Http/ErrorResponse.cs @@ -30,6 +30,16 @@ namespace Timeline.Models.Http return new CommonResponse(ErrorCodes.Common.Forbid, string.Format(message, formatArgs)); } + public static CommonResponse UnknownEndpoint(params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.UnknownEndpoint, string.Format(Common_UnknownEndpoint, formatArgs)); + } + + public static CommonResponse CustomMessage_UnknownEndpoint(string message, params object?[] formatArgs) + { + return new CommonResponse(ErrorCodes.Common.UnknownEndpoint, string.Format(message, formatArgs)); + } + public static class Header { diff --git a/Timeline/Properties/launchSettings.json b/Timeline/Properties/launchSettings.json index 4baafa62..d23d132f 100644 --- a/Timeline/Properties/launchSettings.json +++ b/Timeline/Properties/launchSettings.json @@ -8,6 +8,14 @@ "ASPNETCORE_WORKDIR": "D:\\timeline-development" } }, + "Timeline-MockFrontEnd": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_USEMOCKFRONTEND": "true", + "ASPNETCORE_WORKDIR": "D:\\timeline-development" + } + }, "Timeline-Staging": { "commandName": "Project", "environmentVariables": { diff --git a/Timeline/Resources/Messages.Designer.cs b/Timeline/Resources/Messages.Designer.cs index 40c4a1ce..bb654ce6 100644 --- a/Timeline/Resources/Messages.Designer.cs +++ b/Timeline/Resources/Messages.Designer.cs @@ -150,6 +150,15 @@ namespace Timeline.Resources { } } + /// + /// Looks up a localized string similar to The api endpoint you request is unknown. You might get the wrong api entry.. + /// + internal static string Common_UnknownEndpoint { + get { + return ResourceManager.GetString("Common_UnknownEndpoint", resourceCulture); + } + } + /// /// Looks up a localized string similar to Unknown type of post content.. /// diff --git a/Timeline/Resources/Messages.resx b/Timeline/Resources/Messages.resx index 8d5543fe..2bbf494e 100644 --- a/Timeline/Resources/Messages.resx +++ b/Timeline/Resources/Messages.resx @@ -147,6 +147,9 @@ Model is of bad format. + + The api endpoint you request is unknown. You might get the wrong api entry. + Unknown type of post content. diff --git a/Timeline/Routes/UnknownEndpointMiddleware.cs b/Timeline/Routes/UnknownEndpointMiddleware.cs new file mode 100644 index 00000000..25ec563c --- /dev/null +++ b/Timeline/Routes/UnknownEndpointMiddleware.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using System; +using System.Net.Mime; +using System.Text.Json; +using Timeline.Models.Http; + +namespace Timeline.Routes +{ + public static class UnknownEndpointMiddleware + { + public static void Attach(IApplicationBuilder app) + { + app.Use(async (context, next) => + { + if (context.GetEndpoint() != null) + { + await next(); + return; + } + + if (context.Request.Path.StartsWithSegments("/api", StringComparison.OrdinalIgnoreCase)) + { + context.Response.StatusCode = StatusCodes.Status400BadRequest; + context.Response.ContentType = MediaTypeNames.Application.Json; + + var body = JsonSerializer.SerializeToUtf8Bytes(ErrorResponse.Common.UnknownEndpoint(), new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + + context.Response.ContentLength = body.Length; + await context.Response.Body.WriteAsync(body); + await context.Response.CompleteAsync(); + return; + } + + await next(); + }); + } + } +} diff --git a/Timeline/Startup.cs b/Timeline/Startup.cs index 35c47712..918f025a 100644 --- a/Timeline/Startup.cs +++ b/Timeline/Startup.cs @@ -24,6 +24,7 @@ namespace Timeline public class Startup { private readonly bool disableFrontEnd; + private readonly bool useMockFrontEnd; public Startup(IConfiguration configuration, IWebHostEnvironment environment) { @@ -31,6 +32,7 @@ namespace Timeline Configuration = configuration; disableFrontEnd = Configuration.GetValue(ApplicationConfiguration.DisableFrontEndKey) ?? false; + useMockFrontEnd = Configuration.GetValue(ApplicationConfiguration.UseMockFrontEndKey) ?? false; } public IWebHostEnvironment Environment { get; } @@ -90,12 +92,23 @@ namespace Timeline options.UseSqlite($"Data Source={pathProvider.GetDatabaseFilePath()}"); }); - if (!disableFrontEnd && !Environment.IsDevelopment()) + if (!disableFrontEnd) { - services.AddSpaStaticFiles(config => + if (useMockFrontEnd) { - config.RootPath = "ClientApp/dist"; - }); + services.AddSpaStaticFiles(config => + { + config.RootPath = "MockClientApp"; + }); + + } + else if (!Environment.IsDevelopment()) // In development, we don't want to serve dist. Or it will take precedence than front end dev server. + { + services.AddSpaStaticFiles(config => + { + config.RootPath = "ClientApp/dist"; + }); + } } } @@ -120,7 +133,7 @@ namespace Timeline app.UseRouting(); - if (!disableFrontEnd && !Environment.IsDevelopment()) + if (!disableFrontEnd && (useMockFrontEnd || !Environment.IsDevelopment())) { app.UseSpaStaticFiles(new StaticFileOptions { @@ -136,13 +149,15 @@ namespace Timeline endpoints.MapControllers(); }); + UnknownEndpointMiddleware.Attach(app); + if (!disableFrontEnd) { app.UseSpa(spa => { - spa.Options.SourcePath = "ClientApp"; + spa.Options.SourcePath = useMockFrontEnd ? "MockClientApp" : "ClientApp"; - if (Environment.IsDevelopment()) + if (!useMockFrontEnd && Environment.IsDevelopment()) { if (Configuration.GetValue(ApplicationConfiguration.FrontEndProxyOnlyKey) ?? false) { diff --git a/Timeline/Timeline.csproj b/Timeline/Timeline.csproj index 53fd3b71..dfaecf02 100644 --- a/Timeline/Timeline.csproj +++ b/Timeline/Timeline.csproj @@ -20,6 +20,9 @@ PreserveNewest + + PreserveNewest + @@ -68,6 +71,10 @@ + + + + True -- cgit v1.2.3