aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2020-06-14 16:34:42 +0800
committercrupest <crupest@outlook.com>2020-06-14 16:34:42 +0800
commit53a0483ef11cce939b1df2f288563c888a1d0567 (patch)
treee998f8cdaf38ccd2c4725b16386f56b7162d7942
parent5d467d950fd4078146709470084cbfae331b8b10 (diff)
downloadtimeline-53a0483ef11cce939b1df2f288563c888a1d0567.tar.gz
timeline-53a0483ef11cce939b1df2f288563c888a1d0567.tar.bz2
timeline-53a0483ef11cce939b1df2f288563c888a1d0567.zip
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.
-rw-r--r--Timeline.ErrorCodes/ErrorCodes.cs1
-rw-r--r--Timeline.Tests/Helpers/TestApplication.cs2
-rw-r--r--Timeline.Tests/IntegratedTests/FrontEndTest.cs34
-rw-r--r--Timeline.Tests/IntegratedTests/IntegratedTestBase.cs30
-rw-r--r--Timeline.Tests/IntegratedTests/UnknownEndpointTest.cs26
-rw-r--r--Timeline/Configs/ApplicationConfiguration.cs1
-rw-r--r--Timeline/Migrations/20200614061237_AddTimelineUniqueId.cs47
-rw-r--r--Timeline/MockClientApp/index.html10
-rw-r--r--Timeline/Models/Http/ErrorResponse.cs10
-rw-r--r--Timeline/Properties/launchSettings.json8
-rw-r--r--Timeline/Resources/Messages.Designer.cs9
-rw-r--r--Timeline/Resources/Messages.resx3
-rw-r--r--Timeline/Routes/UnknownEndpointMiddleware.cs39
-rw-r--r--Timeline/Startup.cs29
-rw-r--r--Timeline/Timeline.csproj7
15 files changed, 235 insertions, 21 deletions
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<string, string>
{
- [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<Startup> 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<HttpClient> CreateDefaultClient()
+ public Task<HttpClient> 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<HttpClient> CreateClientWithCredential(string username, string password)
+ public async Task<HttpClient> 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<HttpClient> CreateClientAs(int userNumber)
+ public Task<HttpClient> 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<HttpClient> CreateClientAsAdministrator()
+ public Task<HttpClient> CreateClientAsAdministrator(bool setApiBase = true)
{
- return CreateClientAs(0);
+ return CreateClientAs(0, setApiBase);
}
- public Task<HttpClient> CreateClientAsUser()
+ public Task<HttpClient> 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<Startup> 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 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>Mock Client App</title>
+</head>
+<body>
+ This is a mock client app for testing.
+</body>
+</html>
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
@@ -151,6 +151,15 @@ namespace Timeline.Resources {
}
/// <summary>
+ /// Looks up a localized string similar to The api endpoint you request is unknown. You might get the wrong api entry..
+ /// </summary>
+ internal static string Common_UnknownEndpoint {
+ get {
+ return ResourceManager.GetString("Common_UnknownEndpoint", resourceCulture);
+ }
+ }
+
+ /// <summary>
/// Looks up a localized string similar to Unknown type of post content..
/// </summary>
internal static string TimelineController_ContentUnknownType {
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 @@
<data name="Common_InvalidModel" xml:space="preserve">
<value>Model is of bad format.</value>
</data>
+ <data name="Common_UnknownEndpoint" xml:space="preserve">
+ <value>The api endpoint you request is unknown. You might get the wrong api entry.</value>
+ </data>
<data name="TimelineController_ContentUnknownType" xml:space="preserve">
<value>Unknown type of post content.</value>
</data>
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<bool?>(ApplicationConfiguration.DisableFrontEndKey) ?? false;
+ useMockFrontEnd = Configuration.GetValue<bool?>(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<bool?>(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 @@
<Content Include="default-avatar.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
+ <Content Include="MockClientApp\index.html">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </Content>
</ItemGroup>
<ItemGroup>
@@ -69,6 +72,10 @@
</Target>
<ItemGroup>
+ <None Remove="MockClientApp\index.html" />
+ </ItemGroup>
+
+ <ItemGroup>
<Compile Update="Resources\Authentication\AuthHandler.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>