From 05ccb4d8f1bbe3fb64e117136b4a89bcfb0b0b33 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 27 Oct 2020 19:21:35 +0800 Subject: Split front and back end. --- BackEnd/Timeline.Tests/ErrorCodeTest.cs | 53 + BackEnd/Timeline.Tests/GlobalSuppressions.cs | 16 + .../Helpers/AsyncFunctionAssertionsExtensions.cs | 16 + BackEnd/Timeline.Tests/Helpers/CacheTestHelper.cs | 64 + .../Timeline.Tests/Helpers/HttpClientExtensions.cs | 51 + .../Helpers/HttpResponseExtensions.cs | 35 + BackEnd/Timeline.Tests/Helpers/ImageHelper.cs | 26 + .../Helpers/ParameterInfoAssertions.cs | 60 + BackEnd/Timeline.Tests/Helpers/ReflectionHelper.cs | 13 + .../Timeline.Tests/Helpers/ResponseAssertions.cs | 172 ++ BackEnd/Timeline.Tests/Helpers/TestApplication.cs | 72 + BackEnd/Timeline.Tests/Helpers/TestClock.cs | 43 + BackEnd/Timeline.Tests/Helpers/TestDatabase.cs | 76 + .../IntegratedTests/AuthorizationTest.cs | 52 + .../Timeline.Tests/IntegratedTests/FrontEndTest.cs | 29 + .../IntegratedTests/IntegratedTestBase.cs | 164 ++ .../Timeline.Tests/IntegratedTests/TimelineTest.cs | 1523 +++++++++++++++ .../Timeline.Tests/IntegratedTests/TokenTest.cs | 165 ++ .../IntegratedTests/UnknownEndpointTest.cs | 21 + .../IntegratedTests/UserAvatarTest.cs | 251 +++ BackEnd/Timeline.Tests/IntegratedTests/UserTest.cs | 447 +++++ BackEnd/Timeline.Tests/PasswordGenerator.cs | 23 + .../Timeline.Tests/Properties/launchSettings.json | 2 + .../Timeline.Tests/Services/TimelineServiceTest.cs | 329 ++++ BackEnd/Timeline.Tests/Timeline.Tests.csproj | 34 + .../Timeline.Tests/UsernameValidatorUnitTest.cs | 78 + BackEnd/Timeline.Tests/coverletArgs.runsettings | 13 + BackEnd/Timeline.Tests/packages.lock.json | 2040 ++++++++++++++++++++ 28 files changed, 5868 insertions(+) create mode 100644 BackEnd/Timeline.Tests/ErrorCodeTest.cs create mode 100644 BackEnd/Timeline.Tests/GlobalSuppressions.cs create mode 100644 BackEnd/Timeline.Tests/Helpers/AsyncFunctionAssertionsExtensions.cs create mode 100644 BackEnd/Timeline.Tests/Helpers/CacheTestHelper.cs create mode 100644 BackEnd/Timeline.Tests/Helpers/HttpClientExtensions.cs create mode 100644 BackEnd/Timeline.Tests/Helpers/HttpResponseExtensions.cs create mode 100644 BackEnd/Timeline.Tests/Helpers/ImageHelper.cs create mode 100644 BackEnd/Timeline.Tests/Helpers/ParameterInfoAssertions.cs create mode 100644 BackEnd/Timeline.Tests/Helpers/ReflectionHelper.cs create mode 100644 BackEnd/Timeline.Tests/Helpers/ResponseAssertions.cs create mode 100644 BackEnd/Timeline.Tests/Helpers/TestApplication.cs create mode 100644 BackEnd/Timeline.Tests/Helpers/TestClock.cs create mode 100644 BackEnd/Timeline.Tests/Helpers/TestDatabase.cs create mode 100644 BackEnd/Timeline.Tests/IntegratedTests/AuthorizationTest.cs create mode 100644 BackEnd/Timeline.Tests/IntegratedTests/FrontEndTest.cs create mode 100644 BackEnd/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs create mode 100644 BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs create mode 100644 BackEnd/Timeline.Tests/IntegratedTests/TokenTest.cs create mode 100644 BackEnd/Timeline.Tests/IntegratedTests/UnknownEndpointTest.cs create mode 100644 BackEnd/Timeline.Tests/IntegratedTests/UserAvatarTest.cs create mode 100644 BackEnd/Timeline.Tests/IntegratedTests/UserTest.cs create mode 100644 BackEnd/Timeline.Tests/PasswordGenerator.cs create mode 100644 BackEnd/Timeline.Tests/Properties/launchSettings.json create mode 100644 BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs create mode 100644 BackEnd/Timeline.Tests/Timeline.Tests.csproj create mode 100644 BackEnd/Timeline.Tests/UsernameValidatorUnitTest.cs create mode 100644 BackEnd/Timeline.Tests/coverletArgs.runsettings create mode 100644 BackEnd/Timeline.Tests/packages.lock.json (limited to 'BackEnd/Timeline.Tests') diff --git a/BackEnd/Timeline.Tests/ErrorCodeTest.cs b/BackEnd/Timeline.Tests/ErrorCodeTest.cs new file mode 100644 index 00000000..258ebf4e --- /dev/null +++ b/BackEnd/Timeline.Tests/ErrorCodeTest.cs @@ -0,0 +1,53 @@ +using FluentAssertions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Timeline.Models.Http; +using Xunit; +using Xunit.Abstractions; + +namespace Timeline.Tests +{ + public class ErrorCodeTest + { + private readonly ITestOutputHelper _output; + + public ErrorCodeTest(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void ShouldWork() + { + var errorCodes = new Dictionary(); + + void RecursiveCheckErrorCode(Type type) + { + foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy) + .Where(fi => fi.IsLiteral && !fi.IsInitOnly && fi.FieldType == typeof(int))) + { + var name = type.FullName + "." + field.Name; + var value = (int)field.GetRawConstantValue(); + _output.WriteLine($"Find error code {name} , value is {value}."); + + value.Should().BeInRange(1000_0000, 9999_9999, "Error code should have exactly 8 digits."); + + errorCodes.Should().NotContainKey(value, + "identical error codes are found and conflict paths are {0} and {1}", + name, errorCodes.GetValueOrDefault(value)); + + errorCodes.Add(value, name); + } + + foreach (var nestedType in type.GetNestedTypes()) + { + RecursiveCheckErrorCode(nestedType); + } + } + + RecursiveCheckErrorCode(typeof(ErrorCodes)); + } + } +} diff --git a/BackEnd/Timeline.Tests/GlobalSuppressions.cs b/BackEnd/Timeline.Tests/GlobalSuppressions.cs new file mode 100644 index 00000000..0f873033 --- /dev/null +++ b/BackEnd/Timeline.Tests/GlobalSuppressions.cs @@ -0,0 +1,16 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2007:Consider calling ConfigureAwait on the awaited task", Justification = "This is not a UI application.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Tests name have underscores.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Test may catch all exceptions.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "Test classes can be nested.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "This is redundant.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1063:Implement IDisposable Correctly", Justification = "Test classes do not need to implement it that way.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA1816:Dispose methods should call SuppressFinalize", Justification = "Test classes do not need to implement it that way.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2234:Pass system uri objects instead of strings", Justification = "I really don't understand this rule.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Globalization", "CA1303:Do not pass literals as localized parameters", Justification = "Tests do not need make strings resources.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1054:Uri parameters should not be strings", Justification = "That's unnecessary.")] +[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1056:Uri properties should not be strings", Justification = "That's unnecessary.")] diff --git a/BackEnd/Timeline.Tests/Helpers/AsyncFunctionAssertionsExtensions.cs b/BackEnd/Timeline.Tests/Helpers/AsyncFunctionAssertionsExtensions.cs new file mode 100644 index 00000000..b78309c0 --- /dev/null +++ b/BackEnd/Timeline.Tests/Helpers/AsyncFunctionAssertionsExtensions.cs @@ -0,0 +1,16 @@ +using FluentAssertions; +using FluentAssertions.Primitives; +using FluentAssertions.Specialized; +using System; +using System.Threading.Tasks; + +namespace Timeline.Tests.Helpers +{ + public static class AsyncFunctionAssertionsExtensions + { + public static async Task> ThrowAsync(this AsyncFunctionAssertions assertions, Type exceptionType, string because = "", params object[] becauseArgs) + { + return (await assertions.ThrowAsync(because, becauseArgs)).Which.Should().BeAssignableTo(exceptionType); + } + } +} diff --git a/BackEnd/Timeline.Tests/Helpers/CacheTestHelper.cs b/BackEnd/Timeline.Tests/Helpers/CacheTestHelper.cs new file mode 100644 index 00000000..b3709a28 --- /dev/null +++ b/BackEnd/Timeline.Tests/Helpers/CacheTestHelper.cs @@ -0,0 +1,64 @@ +using FluentAssertions; +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; +using Timeline.Models.Http; + +namespace Timeline.Tests.Helpers +{ + public static class CacheTestHelper + { + public static async Task TestCache(HttpClient client, string getUrl) + { + EntityTagHeaderValue eTag; + { + var res = await client.GetAsync(getUrl); + res.Should().HaveStatusCode(200); + var cacheControlHeader = res.Headers.CacheControl; + cacheControlHeader.NoCache.Should().BeTrue(); + cacheControlHeader.NoStore.Should().BeFalse(); + cacheControlHeader.Private.Should().BeTrue(); + cacheControlHeader.Public.Should().BeFalse(); + cacheControlHeader.MustRevalidate.Should().BeTrue(); + cacheControlHeader.MaxAge.Should().NotBeNull().And.Be(TimeSpan.FromDays(14)); + eTag = res.Headers.ETag; + } + + { + using var request = new HttpRequestMessage() + { + RequestUri = new Uri(client.BaseAddress, getUrl), + Method = HttpMethod.Get, + }; + request.Headers.TryAddWithoutValidation("If-None-Match", "\"dsdfd"); + var res = await client.SendAsync(request); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.HaveCommonBody(ErrorCodes.Common.Header.IfNonMatch_BadFormat); + } + + { + using var request = new HttpRequestMessage() + { + RequestUri = new Uri(client.BaseAddress, getUrl), + Method = HttpMethod.Get, + }; + request.Headers.TryAddWithoutValidation("If-None-Match", "\"aaa\""); + var res = await client.SendAsync(request); + res.Should().HaveStatusCode(HttpStatusCode.OK); + } + + { + using var request = new HttpRequestMessage() + { + RequestUri = new Uri(client.BaseAddress, getUrl), + Method = HttpMethod.Get, + }; + request.Headers.Add("If-None-Match", eTag.ToString()); + var res = await client.SendAsync(request); + res.Should().HaveStatusCode(HttpStatusCode.NotModified); + } + } + } +} diff --git a/BackEnd/Timeline.Tests/Helpers/HttpClientExtensions.cs b/BackEnd/Timeline.Tests/Helpers/HttpClientExtensions.cs new file mode 100644 index 00000000..6513bbe7 --- /dev/null +++ b/BackEnd/Timeline.Tests/Helpers/HttpClientExtensions.cs @@ -0,0 +1,51 @@ +using Newtonsoft.Json; +using System; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Net.Mime; +using System.Text; +using System.Threading.Tasks; + +namespace Timeline.Tests.Helpers +{ + public static class HttpClientExtensions + { + public static Task PatchAsJsonAsync(this HttpClient client, string url, T body) + { + return client.PatchAsJsonAsync(new Uri(url, UriKind.RelativeOrAbsolute), body); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")] + public static Task PatchAsJsonAsync(this HttpClient client, Uri url, T body) + { + return client.PatchAsync(url, new StringContent( + JsonConvert.SerializeObject(body), Encoding.UTF8, MediaTypeNames.Application.Json)); + } + + public static Task PutByteArrayAsync(this HttpClient client, string url, byte[] body, string mimeType) + { + return client.PutByteArrayAsync(new Uri(url, UriKind.RelativeOrAbsolute), body, mimeType); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")] + public static Task PutByteArrayAsync(this HttpClient client, Uri url, byte[] body, string mimeType) + { + var content = new ByteArrayContent(body); + content.Headers.ContentLength = body.Length; + content.Headers.ContentType = new MediaTypeHeaderValue(mimeType); + return client.PutAsync(url, content); + } + + public static Task PutStringAsync(this HttpClient client, string url, string body, string mimeType = null) + { + return client.PutStringAsync(new Uri(url, UriKind.RelativeOrAbsolute), body, mimeType); + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope")] + public static Task PutStringAsync(this HttpClient client, Uri url, string body, string mimeType = null) + { + var content = new StringContent(body, Encoding.UTF8, mimeType ?? MediaTypeNames.Text.Plain); + return client.PutAsync(url, content); + } + } +} diff --git a/BackEnd/Timeline.Tests/Helpers/HttpResponseExtensions.cs b/BackEnd/Timeline.Tests/Helpers/HttpResponseExtensions.cs new file mode 100644 index 00000000..2bd497f1 --- /dev/null +++ b/BackEnd/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/BackEnd/Timeline.Tests/Helpers/ImageHelper.cs b/BackEnd/Timeline.Tests/Helpers/ImageHelper.cs new file mode 100644 index 00000000..9bed0917 --- /dev/null +++ b/BackEnd/Timeline.Tests/Helpers/ImageHelper.cs @@ -0,0 +1,26 @@ +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.PixelFormats; +using System.IO; + +namespace Timeline.Tests.Helpers +{ + public static class ImageHelper + { + public static byte[] CreatePngWithSize(int width, int height) + { + using var image = new Image(width, height); + using var stream = new MemoryStream(); + image.SaveAsPng(stream); + return stream.ToArray(); + } + + public static byte[] CreateImageWithSize(int width, int height, IImageFormat format) + { + using var image = new Image(width, height); + using var stream = new MemoryStream(); + image.Save(stream, format); + return stream.ToArray(); + } + } +} diff --git a/BackEnd/Timeline.Tests/Helpers/ParameterInfoAssertions.cs b/BackEnd/Timeline.Tests/Helpers/ParameterInfoAssertions.cs new file mode 100644 index 00000000..d3e5a41e --- /dev/null +++ b/BackEnd/Timeline.Tests/Helpers/ParameterInfoAssertions.cs @@ -0,0 +1,60 @@ +using FluentAssertions; +using FluentAssertions.Execution; +using FluentAssertions.Formatting; +using FluentAssertions.Primitives; +using System; +using System.Reflection; + +namespace Timeline.Tests.Helpers +{ + public class ParameterInfoValueFormatter : IValueFormatter + { + public bool CanHandle(object value) + { + return value is ParameterInfo; + } + + public string Format(object value, FormattingContext context, FormatChild formatChild) + { + var param = (ParameterInfo)value; + return $"{param.Member.DeclaringType.FullName}.{param.Member.Name}#{param.Name}"; + } + } + + public class ParameterInfoAssertions : ReferenceTypeAssertions + { + static ParameterInfoAssertions() + { + Formatter.AddFormatter(new ParameterInfoValueFormatter()); + } + + public ParameterInfoAssertions(ParameterInfo parameterInfo) + { + Subject = parameterInfo; + } + + protected override string Identifier => "parameter"; + + public AndWhichConstraint BeDecoratedWith(string because = "", params object[] becauseArgs) + where TAttribute : Attribute + { + var attribute = Subject.GetCustomAttribute(false); + + Execute.Assertion + .BecauseOf(because, becauseArgs) + .ForCondition(attribute != null) + .FailWith("Expected {0} {1} to be decorated with {2}{reason}, but that attribute was not found.", + Identifier, Subject, typeof(TAttribute).FullName); + + return new AndWhichConstraint(this, attribute); + } + } + + public static class ParameterInfoAssertionExtensions + { + public static ParameterInfoAssertions Should(this ParameterInfo parameterInfo) + { + return new ParameterInfoAssertions(parameterInfo); + } + } +} diff --git a/BackEnd/Timeline.Tests/Helpers/ReflectionHelper.cs b/BackEnd/Timeline.Tests/Helpers/ReflectionHelper.cs new file mode 100644 index 00000000..3f6036e3 --- /dev/null +++ b/BackEnd/Timeline.Tests/Helpers/ReflectionHelper.cs @@ -0,0 +1,13 @@ +using System.Linq; +using System.Reflection; + +namespace Timeline.Tests.Helpers +{ + public static class ReflectionHelper + { + public static ParameterInfo GetParameter(this MethodInfo methodInfo, string name) + { + return methodInfo.GetParameters().Where(p => p.Name == name).Single(); + } + } +} diff --git a/BackEnd/Timeline.Tests/Helpers/ResponseAssertions.cs b/BackEnd/Timeline.Tests/Helpers/ResponseAssertions.cs new file mode 100644 index 00000000..024732f5 --- /dev/null +++ b/BackEnd/Timeline.Tests/Helpers/ResponseAssertions.cs @@ -0,0 +1,172 @@ +using FluentAssertions; +using FluentAssertions.Execution; +using FluentAssertions.Formatting; +using FluentAssertions.Primitives; +using System; +using System.Globalization; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using Timeline.Models.Converters; +using Timeline.Models.Http; + +namespace Timeline.Tests.Helpers +{ + public class HttpResponseMessageValueFormatter : IValueFormatter + { + public bool CanHandle(object value) + { + return value is HttpResponseMessage; + } + + public string Format(object value, FormattingContext context, FormatChild formatChild) + { + string newline = context.UseLineBreaks ? Environment.NewLine : ""; + string padding = new string('\t', context.Depth); + + var res = (HttpResponseMessage)value; + + var builder = new StringBuilder(); + builder.Append($"{newline}{padding} Status Code: {res.StatusCode} ; Body: "); + + try + { + var task = res.Content.ReadAsStringAsync(); + task.Wait(); + var body = task.Result; + if (body.Length > 40) + { + body = body[0..40] + " ..."; + } + builder.Append(body); + } + catch (AggregateException) + { + builder.Append("NOT A STRING."); + } + + return builder.ToString(); + } + } + + public class HttpResponseMessageAssertions + : ReferenceTypeAssertions + { + static HttpResponseMessageAssertions() + { + Formatter.AddFormatter(new HttpResponseMessageValueFormatter()); + } + + public HttpResponseMessageAssertions(HttpResponseMessage instance) + { + Subject = instance; + } + + protected override string Identifier => "HttpResponseMessage"; + + public AndConstraint HaveStatusCode(int expected, string because = "", params object[] becauseArgs) + { + return HaveStatusCode((HttpStatusCode)expected, because, becauseArgs); + } + + public AndConstraint HaveStatusCode(HttpStatusCode expected, string because = "", params object[] becauseArgs) + { + Execute.Assertion.BecauseOf(because, becauseArgs) + .ForCondition(Subject.StatusCode == expected) + .FailWith("Expected status code of {context:HttpResponseMessage} to be {0}{reason}, but found {1}.", expected, Subject.StatusCode); + return new AndConstraint(this); + } + + public AndWhichConstraint HaveJsonBody(string because = "", params object[] becauseArgs) + { + var a = Execute.Assertion.BecauseOf(because, becauseArgs); + string body; + try + { + var task = Subject.Content.ReadAsStringAsync(); + task.Wait(); + body = task.Result; + } + catch (AggregateException e) + { + a.FailWith("Expected response body of {context:HttpResponseMessage} to be json string{reason}, but failed to read it or it was not a string. Exception is {0}.", e.InnerExceptions); + return new AndWhichConstraint(this, null); + } + + + try + { + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + options.Converters.Add(new JsonStringEnumConverter()); + options.Converters.Add(new JsonDateTimeConverter()); + + var result = JsonSerializer.Deserialize(body, options); + + return new AndWhichConstraint(this, result); + } + catch (JsonException e) + { + a.FailWith("Expected response body of {context:HttpResponseMessage} to be json string{reason}, but failed to deserialize it. Exception is {0}.", e); + return new AndWhichConstraint(this, null); + } + } + } + + public static class AssertionResponseExtensions + { + public static HttpResponseMessageAssertions Should(this HttpResponseMessage instance) + { + return new HttpResponseMessageAssertions(instance); + } + + public static AndWhichConstraint HaveCommonBody(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) + { + return assertions.HaveJsonBody(because, becauseArgs); + } + + public static void HaveCommonBody(this HttpResponseMessageAssertions assertions, int code, string message = null, params object[] messageArgs) + { + message = string.IsNullOrEmpty(message) ? "" : ", " + string.Format(CultureInfo.CurrentCulture, message, messageArgs); + var body = assertions.HaveCommonBody("Response body should be CommonResponse{0}", message).Which; + body.Code.Should().Be(code, "Response body code is not the specified one{0}", message); + } + + public static AndWhichConstraint> HaveCommonDataBody(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs) + { + return assertions.HaveJsonBody>(because, becauseArgs); + } + + public static void BePut(this HttpResponseMessageAssertions assertions, bool create, string because = "", params object[] becauseArgs) + { + var body = assertions.HaveStatusCode(create ? 201 : 200, because, becauseArgs) + .And.HaveJsonBody(because, becauseArgs) + .Which; + body.Code.Should().Be(0); + body.Data.Create.Should().Be(create); + } + + public static void BeDelete(this HttpResponseMessageAssertions assertions, bool delete, string because = "", params object[] becauseArgs) + { + var body = assertions.HaveStatusCode(200, because, becauseArgs) + .And.HaveJsonBody(because, becauseArgs) + .Which; + body.Code.Should().Be(0); + body.Data.Delete.Should().Be(delete); + } + + public static void BeInvalidModel(this HttpResponseMessageAssertions assertions, string message = null) + { + message = string.IsNullOrEmpty(message) ? "" : ", " + message; + assertions.HaveStatusCode(400, "Invalid Model Error must have 400 status code{0}", message) + .And.HaveCommonBody("Invalid Model Error must have CommonResponse body{0}", message) + .Which.Code.Should().Be(ErrorCodes.Common.InvalidModel, + "Invalid Model Error must have code {0} in body{1}", + ErrorCodes.Common.InvalidModel, message); + } + } +} diff --git a/BackEnd/Timeline.Tests/Helpers/TestApplication.cs b/BackEnd/Timeline.Tests/Helpers/TestApplication.cs new file mode 100644 index 00000000..684ffe2c --- /dev/null +++ b/BackEnd/Timeline.Tests/Helpers/TestApplication.cs @@ -0,0 +1,72 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Timeline.Configs; +using Timeline.Entities; +using Xunit; + +namespace Timeline.Tests.Helpers +{ + public class TestApplication : IAsyncLifetime + { + public TestDatabase Database { get; } + + public IHost Host { get; private set; } + + public string WorkDir { get; private set; } + + public TestApplication() + { + Database = new TestDatabase(false); + } + + public async Task InitializeAsync() + { + await Database.InitializeAsync(); + + WorkDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(WorkDir); + + Host = await Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder() + .ConfigureAppConfiguration((context, config) => + { + config.AddInMemoryCollection(new Dictionary + { + [ApplicationConfiguration.UseMockFrontEndKey] = "true", + ["WorkDir"] = WorkDir + }); + }) + .ConfigureServices(services => + { + services.AddDbContext(options => + { + options.UseSqlite(Database.Connection); + }); + }) + .ConfigureWebHost(webBuilder => + { + webBuilder + .UseTestServer() + .UseStartup(); + }) + .StartAsync(); + } + + public async Task DisposeAsync() + { + await Host.StopAsync(); + Host.Dispose(); + + Directory.Delete(WorkDir, true); + + await Database.DisposeAsync(); + } + } +} diff --git a/BackEnd/Timeline.Tests/Helpers/TestClock.cs b/BackEnd/Timeline.Tests/Helpers/TestClock.cs new file mode 100644 index 00000000..34adb245 --- /dev/null +++ b/BackEnd/Timeline.Tests/Helpers/TestClock.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Timeline.Services; + +namespace Timeline.Tests.Helpers +{ + public class TestClock : IClock + { + private DateTime? _currentTime; + + public DateTime GetCurrentTime() + { + return _currentTime ?? DateTime.UtcNow; + } + + public void SetCurrentTime(DateTime? mockTime) + { + _currentTime = mockTime; + } + + public DateTime SetMockCurrentTime() + { + var time = new DateTime(3000, 1, 1, 1, 1, 1, DateTimeKind.Utc); + _currentTime = time; + return time; + } + + public DateTime ForwardCurrentTime() + { + return ForwardCurrentTime(TimeSpan.FromDays(1)); + } + + public DateTime ForwardCurrentTime(TimeSpan timeSpan) + { + if (_currentTime == null) + return SetMockCurrentTime(); + _currentTime += timeSpan; + return _currentTime.Value; + } + } +} diff --git a/BackEnd/Timeline.Tests/Helpers/TestDatabase.cs b/BackEnd/Timeline.Tests/Helpers/TestDatabase.cs new file mode 100644 index 00000000..f0c26180 --- /dev/null +++ b/BackEnd/Timeline.Tests/Helpers/TestDatabase.cs @@ -0,0 +1,76 @@ +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging.Abstractions; +using System.Threading.Tasks; +using Timeline.Entities; +using Timeline.Migrations; +using Timeline.Models; +using Timeline.Services; +using Xunit; + +namespace Timeline.Tests.Helpers +{ + public class TestDatabase : IAsyncLifetime + { + private readonly bool _createUser; + + public TestDatabase(bool createUser = true) + { + _createUser = createUser; + Connection = new SqliteConnection("Data Source=:memory:;"); + } + + public async Task InitializeAsync() + { + await Connection.OpenAsync(); + + using (var context = CreateContext()) + { + await context.Database.EnsureCreatedAsync(); + context.JwtToken.Add(new JwtTokenEntity + { + Key = JwtTokenGenerateHelper.GenerateKey() + }); + await context.SaveChangesAsync(); + + if (_createUser) + { + var passwordService = new PasswordService(); + var userService = new UserService(NullLogger.Instance, context, passwordService, new Clock()); + + await userService.CreateUser(new User + { + Username = "admin", + Password = "adminpw", + Administrator = true, + Nickname = "administrator" + }); + + await userService.CreateUser(new User + { + Username = "user", + Password = "userpw", + Administrator = false, + Nickname = "imuser" + }); + } + } + } + + public async Task DisposeAsync() + { + await Connection.CloseAsync(); + await Connection.DisposeAsync(); + } + + public SqliteConnection Connection { get; } + + public DatabaseContext CreateContext() + { + var options = new DbContextOptionsBuilder() + .UseSqlite(Connection).Options; + + return new DatabaseContext(options); + } + } +} diff --git a/BackEnd/Timeline.Tests/IntegratedTests/AuthorizationTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/AuthorizationTest.cs new file mode 100644 index 00000000..38071394 --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/AuthorizationTest.cs @@ -0,0 +1,52 @@ +using FluentAssertions; +using System.Net; +using System.Threading.Tasks; +using Timeline.Tests.Helpers; +using Xunit; + +namespace Timeline.Tests.IntegratedTests +{ + public class AuthorizationTest : IntegratedTestBase + { + private const string BaseUrl = "testing/auth/"; + private const string AuthorizeUrl = BaseUrl + "Authorize"; + private const string UserUrl = BaseUrl + "User"; + private const string AdminUrl = BaseUrl + "Admin"; + + [Fact] + public async Task UnauthenticationTest() + { + using var client = await CreateDefaultClient(); + var response = await client.GetAsync(AuthorizeUrl); + response.Should().HaveStatusCode(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task AuthenticationTest() + { + using var client = await CreateClientAsUser(); + var response = await client.GetAsync(AuthorizeUrl); + response.Should().HaveStatusCode(HttpStatusCode.OK); + } + + [Fact] + public async Task UserAuthorizationTest() + { + using var client = await CreateClientAsUser(); + var response1 = await client.GetAsync(UserUrl); + response1.Should().HaveStatusCode(HttpStatusCode.OK); + var response2 = await client.GetAsync(AdminUrl); + response2.Should().HaveStatusCode(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task AdminAuthorizationTest() + { + using var client = await CreateClientAsAdministrator(); + var response1 = await client.GetAsync(UserUrl); + response1.Should().HaveStatusCode(HttpStatusCode.OK); + var response2 = await client.GetAsync(AdminUrl); + response2.Should().HaveStatusCode(HttpStatusCode.OK); + } + } +} diff --git a/BackEnd/Timeline.Tests/IntegratedTests/FrontEndTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/FrontEndTest.cs new file mode 100644 index 00000000..39a6e545 --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/FrontEndTest.cs @@ -0,0 +1,29 @@ +using FluentAssertions; +using System.Net.Mime; +using System.Threading.Tasks; +using Timeline.Tests.Helpers; +using Xunit; + +namespace Timeline.Tests.IntegratedTests +{ + public class FrontEndTest : IntegratedTestBase + { + [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/BackEnd/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs b/BackEnd/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs new file mode 100644 index 00000000..7cf27297 --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/IntegratedTestBase.cs @@ -0,0 +1,164 @@ +using FluentAssertions; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Timeline.Models; +using Timeline.Models.Converters; +using Timeline.Models.Http; +using Timeline.Services; +using Timeline.Tests.Helpers; +using Xunit; + +namespace Timeline.Tests.IntegratedTests +{ + public abstract class IntegratedTestBase : IAsyncLifetime + { + protected TestApplication TestApp { get; } + + public IReadOnlyList UserInfos { get; private set; } + + private readonly int _userCount; + + public IntegratedTestBase() : this(1) + { + + } + + public IntegratedTestBase(int userCount) + { + if (userCount < 0) + throw new ArgumentOutOfRangeException(nameof(userCount), userCount, "User count can't be negative."); + + _userCount = userCount; + + TestApp = new TestApplication(); + } + + protected virtual Task OnInitializeAsync() + { + return Task.CompletedTask; + } + + protected virtual Task OnDisposeAsync() + { + return Task.CompletedTask; + } + + protected virtual void OnDispose() + { + + } + + public async Task InitializeAsync() + { + await TestApp.InitializeAsync(); + + using (var scope = TestApp.Host.Services.CreateScope()) + { + var users = new List() + { + new User + { + Username = "admin", + Password = "adminpw", + Administrator = true, + Nickname = "administrator" + } + }; + + for (int i = 1; i <= _userCount; i++) + { + users.Add(new User + { + Username = $"user{i}", + Password = $"user{i}pw", + Administrator = false, + Nickname = $"imuser{i}" + }); + } + + var userInfoList = new List(); + + var userService = scope.ServiceProvider.GetRequiredService(); + foreach (var user in users) + { + await userService.CreateUser(user); + } + + using var client = await CreateDefaultClient(); + var options = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + options.Converters.Add(new JsonStringEnumConverter()); + options.Converters.Add(new JsonDateTimeConverter()); + foreach (var user in users) + { + var s = await client.GetStringAsync($"users/{user.Username}"); + userInfoList.Add(JsonSerializer.Deserialize(s, options)); + } + + UserInfos = userInfoList; + } + + await OnInitializeAsync(); + } + + public async Task DisposeAsync() + { + await OnDisposeAsync(); + OnDispose(); + await TestApp.DisposeAsync(); + } + + public Task CreateDefaultClient(bool setApiBase = true) + { + var client = TestApp.Host.GetTestServer().CreateClient(); + if (setApiBase) + { + client.BaseAddress = new Uri(client.BaseAddress, "api/"); + } + return Task.FromResult(client); + } + + public async Task CreateClientWithCredential(string username, string password, bool setApiBase = true) + { + var client = TestApp.Host.GetTestServer().CreateClient(); + 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) + .And.HaveJsonBody().Which.Token; + client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token); + return client; + } + + public Task CreateClientAs(int userNumber, bool setApiBase = true) + { + if (userNumber < 0) + return CreateDefaultClient(setApiBase); + if (userNumber == 0) + return CreateClientWithCredential("admin", "adminpw", setApiBase); + else + return CreateClientWithCredential($"user{userNumber}", $"user{userNumber}pw", setApiBase); + } + + public Task CreateClientAsAdministrator(bool setApiBase = true) + { + return CreateClientAs(0, setApiBase); + } + + public Task CreateClientAsUser(bool setApiBase = true) + { + return CreateClientAs(1, setApiBase); + } + } +} diff --git a/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs new file mode 100644 index 00000000..ec46b96a --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/TimelineTest.cs @@ -0,0 +1,1523 @@ +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Png; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Timeline.Entities; +using Timeline.Models; +using Timeline.Models.Http; +using Timeline.Tests.Helpers; +using Xunit; + +namespace Timeline.Tests.IntegratedTests +{ + public static class TimelineHelper + { + public static TimelinePostContentInfo TextPostContent(string text) + { + return new TimelinePostContentInfo + { + Type = "text", + Text = text + }; + } + + public static TimelinePostCreateRequest TextPostCreateRequest(string text, DateTime? time = null) + { + return new TimelinePostCreateRequest + { + Content = new TimelinePostCreateRequestContent + { + Type = "text", + Text = text + }, + Time = time + }; + } + } + + public class TimelineTest : IntegratedTestBase + { + public TimelineTest() : base(3) + { + } + + protected override async Task OnInitializeAsync() + { + await CreateTestTimelines(); + } + + private List _testTimelines; + + private async Task CreateTestTimelines() + { + _testTimelines = new List(); + for (int i = 0; i <= 3; i++) + { + var client = await CreateClientAs(i); + var res = await client.PostAsJsonAsync("timelines", new TimelineCreateRequest { Name = $"t{i}" }); + var timelineInfo = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + _testTimelines.Add(timelineInfo); + } + } + + private static string CalculateUrlTail(string subpath, ICollection> query) + { + StringBuilder result = new StringBuilder(); + if (subpath != null) + { + if (!subpath.StartsWith("/", StringComparison.OrdinalIgnoreCase)) + result.Append('/'); + result.Append(subpath); + } + + if (query != null && query.Count != 0) + { + result.Append('?'); + foreach (var (key, value, index) in query.Select((pair, index) => (pair.Key, pair.Value, index))) + { + result.Append(WebUtility.UrlEncode(key)); + result.Append('='); + result.Append(WebUtility.UrlEncode(value)); + if (index != query.Count - 1) + result.Append('&'); + } + } + + return result.ToString(); + } + + private static string GeneratePersonalTimelineUrl(int id, string subpath = null, ICollection> query = null) + { + return $"timelines/@{(id == 0 ? "admin" : ("user" + id))}{CalculateUrlTail(subpath, query)}"; + } + + private static string GenerateOrdinaryTimelineUrl(int id, string subpath = null, ICollection> query = null) + { + return $"timelines/t{id}{CalculateUrlTail(subpath, query)}"; + } + + public delegate string TimelineUrlGenerator(int userId, string subpath = null, ICollection> query = null); + + public static IEnumerable TimelineUrlGeneratorData() + { + yield return new[] { new TimelineUrlGenerator(GeneratePersonalTimelineUrl) }; + yield return new[] { new TimelineUrlGenerator(GenerateOrdinaryTimelineUrl) }; + } + + private static string GeneratePersonalTimelineUrlByName(string name, string subpath = null) + { + return $"timelines/@{name}{(subpath == null ? "" : "/" + subpath)}"; + } + + private static string GenerateOrdinaryTimelineUrlByName(string name, string subpath = null) + { + return $"timelines/{name}{(subpath == null ? "" : "/" + subpath)}"; + } + + public static IEnumerable TimelineUrlByNameGeneratorData() + { + yield return new[] { new Func(GeneratePersonalTimelineUrlByName) }; + yield return new[] { new Func(GenerateOrdinaryTimelineUrlByName) }; + } + + [Fact] + public async Task TimelineGet_Should_Work() + { + using var client = await CreateDefaultClient(); + { + var res = await client.GetAsync("timelines/@user1"); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + body.Owner.Should().BeEquivalentTo(UserInfos[1]); + body.Visibility.Should().Be(TimelineVisibility.Register); + body.Description.Should().Be(""); + body.Members.Should().NotBeNull().And.BeEmpty(); + var links = body._links; + links.Should().NotBeNull(); + links.Self.Should().EndWith("timelines/@user1"); + links.Posts.Should().EndWith("timelines/@user1/posts"); + } + + { + var res = await client.GetAsync("timelines/t1"); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + body.Owner.Should().BeEquivalentTo(UserInfos[1]); + body.Visibility.Should().Be(TimelineVisibility.Register); + body.Description.Should().Be(""); + body.Members.Should().NotBeNull().And.BeEmpty(); + var links = body._links; + links.Should().NotBeNull(); + links.Self.Should().EndWith("timelines/t1"); + links.Posts.Should().EndWith("timelines/t1/posts"); + } + } + + [Fact] + public async Task TimelineList() + { + TimelineInfo user1Timeline; + + var client = await CreateDefaultClient(); + + { + var res = await client.GetAsync("timelines/@user1"); + user1Timeline = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + } + + { + var testResult = new List(); + testResult.Add(user1Timeline); + testResult.AddRange(_testTimelines); + + var res = await client.GetAsync("timelines"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody>() + .Which.Should().BeEquivalentTo(testResult); + } + } + + [Fact] + public async Task TimelineList_WithQuery() + { + var testResultRelate = new List(); + var testResultOwn = new List(); + var testResultJoin = new List(); + var testResultOwnPrivate = new List(); + var testResultRelatePublic = new List(); + var testResultRelateRegister = new List(); + var testResultJoinPrivate = new List(); + var testResultPublic = new List(); + + { + var client = await CreateClientAsUser(); + + { + var res = await client.PutAsync("timelines/@user1/members/user3", null); + res.Should().HaveStatusCode(200); + } + + { + var res = await client.PutAsync("timelines/t1/members/user3", null); + res.Should().HaveStatusCode(200); + } + + { + var res = await client.PatchAsJsonAsync("timelines/@user1", new TimelinePatchRequest { Visibility = TimelineVisibility.Public }); + res.Should().HaveStatusCode(200); + } + + { + var res = await client.PatchAsJsonAsync("timelines/t1", new TimelinePatchRequest { Visibility = TimelineVisibility.Register }); + res.Should().HaveStatusCode(200); + } + + { + var res = await client.GetAsync("timelines/@user1"); + var timeline = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + testResultRelate.Add(timeline); + testResultJoin.Add(timeline); + testResultRelatePublic.Add(timeline); + testResultPublic.Add(timeline); + } + + { + var res = await client.GetAsync("timelines/t1"); + var timeline = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + testResultRelate.Add(timeline); + testResultJoin.Add(timeline); + testResultRelateRegister.Add(timeline); + } + } + + { + var client = await CreateClientAs(2); + + { + var res = await client.PutAsync("timelines/@user2/members/user3", null); + res.Should().HaveStatusCode(200); + } + + { + var res = await client.PutAsync("timelines/t2/members/user3", null); + res.Should().HaveStatusCode(200); + } + + { + var res = await client.PatchAsJsonAsync("timelines/@user2", new TimelinePatchRequest { Visibility = TimelineVisibility.Register }); + res.Should().HaveStatusCode(200); + } + + { + var res = await client.PatchAsJsonAsync("timelines/t2", new TimelinePatchRequest { Visibility = TimelineVisibility.Private }); + res.Should().HaveStatusCode(200); + } + + { + var res = await client.GetAsync("timelines/@user2"); + var timeline = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + testResultRelate.Add(timeline); + testResultJoin.Add(timeline); + testResultRelateRegister.Add(timeline); + } + + { + var res = await client.GetAsync("timelines/t2"); + var timeline = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + testResultRelate.Add(timeline); + testResultJoin.Add(timeline); + testResultJoinPrivate.Add(timeline); + } + } + + { + var client = await CreateClientAs(3); + + { + var res = await client.PatchAsJsonAsync("timelines/@user3", new TimelinePatchRequest { Visibility = TimelineVisibility.Private }); + res.Should().HaveStatusCode(200); + } + + { + var res = await client.PatchAsJsonAsync("timelines/t3", new TimelinePatchRequest { Visibility = TimelineVisibility.Register }); + res.Should().HaveStatusCode(200); + } + + { + var res = await client.GetAsync("timelines/@user3"); + var timeline = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + testResultRelate.Add(timeline); + testResultOwn.Add(timeline); + testResultOwnPrivate.Add(timeline); + } + + { + var res = await client.GetAsync("timelines/t3"); + var timeline = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + testResultRelate.Add(timeline); + testResultOwn.Add(timeline); + testResultRelateRegister.Add(timeline); + } + } + + { + var client = await CreateClientAs(3); + { + var res = await client.GetAsync("timelines?relate=user3"); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody>() + .Which; + body.Should().BeEquivalentTo(testResultRelate); + } + + { + var res = await client.GetAsync("timelines?relate=user3&relateType=own"); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody>() + .Which; + body.Should().BeEquivalentTo(testResultOwn); + } + + { + var res = await client.GetAsync("timelines?relate=user3&visibility=public"); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody>() + .Which; + body.Should().BeEquivalentTo(testResultRelatePublic); + } + + { + var res = await client.GetAsync("timelines?relate=user3&visibility=register"); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody>() + .Which; + body.Should().BeEquivalentTo(testResultRelateRegister); + } + + { + var res = await client.GetAsync("timelines?relate=user3&relateType=join&visibility=private"); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody>() + .Which; + body.Should().BeEquivalentTo(testResultJoinPrivate); + } + + { + var res = await client.GetAsync("timelines?relate=user3&relateType=own&visibility=private"); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody>() + .Which; + body.Should().BeEquivalentTo(testResultOwnPrivate); + } + } + + { + var client = await CreateDefaultClient(); + { + var res = await client.GetAsync("timelines?visibility=public"); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody>() + .Which; + body.Should().BeEquivalentTo(testResultPublic); + } + } + } + + [Fact] + public async Task TimelineList_InvalidModel() + { + var client = await CreateClientAsUser(); + + { + var res = await client.GetAsync("timelines?relate=us!!"); + res.Should().BeInvalidModel(); + } + + { + var res = await client.GetAsync("timelines?relateType=aaa"); + res.Should().BeInvalidModel(); + } + + { + var res = await client.GetAsync("timelines?visibility=aaa"); + res.Should().BeInvalidModel(); + } + } + + [Fact] + public async Task TimelineCreate_Should_Work() + { + { + using var client = await CreateDefaultClient(); + var res = await client.PostAsJsonAsync("timelines", new TimelineCreateRequest { Name = "aaa" }); + res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); + } + + using (var client = await CreateClientAsUser()) + { + { + var res = await client.PostAsJsonAsync("timelines", new TimelineCreateRequest { Name = "!!!" }); + res.Should().BeInvalidModel(); + } + + TimelineInfo timelineInfo; + { + var res = await client.PostAsJsonAsync("timelines", new TimelineCreateRequest { Name = "aaa" }); + timelineInfo = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + } + + { + var res = await client.GetAsync("timelines/aaa"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(timelineInfo); + } + + { + var res = await client.PostAsJsonAsync("timelines", new TimelineCreateRequest { Name = "aaa" }); + res.Should().HaveStatusCode(400) + .And.HaveCommonBody(ErrorCodes.TimelineController.NameConflict); + } + } + } + + [Fact] + public async Task TimelineDelete_Should_Work() + { + { + using var client = await CreateDefaultClient(); + var res = await client.DeleteAsync("timelines/t1"); + res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); + } + + { + using var client = await CreateClientAs(2); + var res = await client.DeleteAsync("timelines/t1"); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden); + } + + { + using var client = await CreateClientAsAdministrator(); + + { + var res = await client.DeleteAsync("timelines/!!!"); + res.Should().BeInvalidModel(); + } + + { + var res = await client.DeleteAsync("timelines/t2"); + res.Should().BeDelete(true); + } + + { + var res = await client.DeleteAsync("timelines/t2"); + res.Should().BeDelete(false); + } + } + + { + using var client = await CreateClientAs(1); + + { + var res = await client.DeleteAsync("timelines/!!!"); + res.Should().BeInvalidModel(); + } + + { + var res = await client.DeleteAsync("timelines/t1"); + res.Should().BeDelete(true); + } + + { + var res = await client.DeleteAsync("timelines/t1"); + res.Should().HaveStatusCode(400); + } + } + } + + [Theory] + [MemberData(nameof(TimelineUrlByNameGeneratorData))] + public async Task InvalidModel_BadName(Func generator) + { + using var client = await CreateClientAsAdministrator(); + { + var res = await client.GetAsync(generator("aaa!!!", null)); + res.Should().BeInvalidModel(); + } + { + var res = await client.PatchAsJsonAsync(generator("aaa!!!", null), new TimelinePatchRequest { }); + res.Should().BeInvalidModel(); + } + { + var res = await client.PutAsync(generator("aaa!!!", "members/user1"), null); + res.Should().BeInvalidModel(); + } + { + var res = await client.DeleteAsync(generator("aaa!!!", "members/user1")); + res.Should().BeInvalidModel(); + } + { + var res = await client.GetAsync(generator("aaa!!!", "posts")); + res.Should().BeInvalidModel(); + } + { + var res = await client.PostAsJsonAsync(generator("aaa!!!", "posts"), TimelineHelper.TextPostCreateRequest("aaa")); + res.Should().BeInvalidModel(); + } + { + var res = await client.DeleteAsync(generator("aaa!!!", "posts/123")); + res.Should().BeInvalidModel(); + } + { + var res = await client.GetAsync(generator("aaa!!!", "posts/123/data")); + res.Should().BeInvalidModel(); + } + } + + [Theory] + [MemberData(nameof(TimelineUrlByNameGeneratorData))] + public async Task Ordinary_NotFound(Func generator) + { + var errorCode = generator == GenerateOrdinaryTimelineUrlByName ? ErrorCodes.TimelineController.NotExist : ErrorCodes.UserCommon.NotExist; + + using var client = await CreateClientAsAdministrator(); + { + var res = await client.GetAsync(generator("notexist", null)); + res.Should().HaveStatusCode(404).And.HaveCommonBody(errorCode); + } + { + var res = await client.PatchAsJsonAsync(generator("notexist", null), new TimelinePatchRequest { }); + res.Should().HaveStatusCode(400).And.HaveCommonBody(errorCode); + } + { + var res = await client.PutAsync(generator("notexist", "members/user1"), null); + res.Should().HaveStatusCode(400).And.HaveCommonBody(errorCode); + } + { + var res = await client.DeleteAsync(generator("notexist", "members/user1")); + res.Should().HaveStatusCode(400).And.HaveCommonBody(errorCode); + } + { + var res = await client.GetAsync(generator("notexist", "posts")); + res.Should().HaveStatusCode(404).And.HaveCommonBody(errorCode); + } + { + var res = await client.PostAsJsonAsync(generator("notexist", "posts"), TimelineHelper.TextPostCreateRequest("aaa")); + res.Should().HaveStatusCode(400).And.HaveCommonBody(errorCode); + } + { + var res = await client.DeleteAsync(generator("notexist", "posts/123")); + res.Should().HaveStatusCode(400).And.HaveCommonBody(errorCode); + } + { + var res = await client.GetAsync(generator("notexist", "posts/123/data")); + res.Should().HaveStatusCode(404).And.HaveCommonBody(errorCode); + } + } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task Description_Should_Work(TimelineUrlGenerator generator) + { + using var client = await CreateClientAsUser(); + + async Task AssertDescription(string description) + { + var res = await client.GetAsync(generator(1, null)); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Description.Should().Be(description); + } + + const string mockDescription = "haha"; + + await AssertDescription(""); + { + var res = await client.PatchAsJsonAsync(generator(1, null), + new TimelinePatchRequest { Description = mockDescription }); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which.Description.Should().Be(mockDescription); + await AssertDescription(mockDescription); + } + { + var res = await client.PatchAsJsonAsync(generator(1, null), + new TimelinePatchRequest { Description = null }); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which.Description.Should().Be(mockDescription); + await AssertDescription(mockDescription); + } + { + var res = await client.PatchAsJsonAsync(generator(1, null), + new TimelinePatchRequest { Description = "" }); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which.Description.Should().Be(""); + await AssertDescription(""); + } + } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task Member_Should_Work(TimelineUrlGenerator generator) + { + var getUrl = generator(1, null); + using var client = await CreateClientAsUser(); + + async Task AssertMembers(IList members) + { + var res = await client.GetAsync(getUrl); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Members.Should().NotBeNull().And.BeEquivalentTo(members); + } + + async Task AssertEmptyMembers() + { + var res = await client.GetAsync(getUrl); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Members.Should().NotBeNull().And.BeEmpty(); + } + + await AssertEmptyMembers(); + { + var res = await client.PutAsync(generator(1, "members/usernotexist"), null); + res.Should().HaveStatusCode(400) + .And.HaveCommonBody(ErrorCodes.TimelineController.MemberPut_NotExist); + } + await AssertEmptyMembers(); + { + var res = await client.PutAsync(generator(1, "members/user2"), null); + res.Should().HaveStatusCode(200); + } + await AssertMembers(new List { UserInfos[2] }); + { + var res = await client.DeleteAsync(generator(1, "members/user2")); + res.Should().BeDelete(true); + } + await AssertEmptyMembers(); + { + var res = await client.DeleteAsync(generator(1, "members/aaa")); + res.Should().BeDelete(false); + } + await AssertEmptyMembers(); + } + + public static IEnumerable Permission_Timeline_Data() + { + yield return new object[] { new TimelineUrlGenerator(GenerateOrdinaryTimelineUrl), -1, 200, 401, 401, 401, 401 }; + yield return new object[] { new TimelineUrlGenerator(GenerateOrdinaryTimelineUrl), 1, 200, 200, 403, 200, 403 }; + yield return new object[] { new TimelineUrlGenerator(GenerateOrdinaryTimelineUrl), 0, 200, 200, 200, 200, 200 }; + yield return new object[] { new TimelineUrlGenerator(GeneratePersonalTimelineUrl), -1, 200, 401, 401, 401, 401 }; + yield return new object[] { new TimelineUrlGenerator(GeneratePersonalTimelineUrl), 1, 200, 200, 403, 200, 403 }; + yield return new object[] { new TimelineUrlGenerator(GeneratePersonalTimelineUrl), 0, 200, 200, 200, 200, 200 }; + } + + [Theory] + [MemberData(nameof(Permission_Timeline_Data))] + public async Task Permission_Timeline(TimelineUrlGenerator generator, int userNumber, int get, int opPatchUser, int opPatchAdmin, int opMemberUser, int opMemberAdmin) + { + using var client = await CreateClientAs(userNumber); + { + var res = await client.GetAsync("timelines/t1"); + res.Should().HaveStatusCode(get); + } + + { + var res = await client.PatchAsJsonAsync(generator(1, null), new TimelinePatchRequest { Description = "hahaha" }); + res.Should().HaveStatusCode(opPatchUser); + } + + { + var res = await client.PatchAsJsonAsync(generator(0, null), new TimelinePatchRequest { Description = "hahaha" }); + res.Should().HaveStatusCode(opPatchAdmin); + } + + { + var res = await client.PutAsync(generator(1, "members/user2"), null); + res.Should().HaveStatusCode(opMemberUser); + } + + { + var res = await client.DeleteAsync(generator(1, "members/user2")); + res.Should().HaveStatusCode(opMemberUser); + } + + { + var res = await client.PutAsync(generator(0, "members/user2"), null); + res.Should().HaveStatusCode(opMemberAdmin); + } + + { + var res = await client.DeleteAsync(generator(0, "members/user2")); + res.Should().HaveStatusCode(opMemberAdmin); + } + } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task Visibility_Test(TimelineUrlGenerator generator) + { + var userUrl = generator(1, "posts"); + var adminUrl = generator(0, "posts"); + { + + using var client = await CreateClientAsUser(); + using var content = new StringContent(@"{""visibility"":""abcdefg""}", System.Text.Encoding.UTF8, System.Net.Mime.MediaTypeNames.Application.Json); + var res = await client.PatchAsync(generator(1, null), content); + res.Should().BeInvalidModel(); + } + { // default visibility is registered + { + using var client = await CreateDefaultClient(); + var res = await client.GetAsync(userUrl); + res.Should().HaveStatusCode(403); + } + + { + using var client = await CreateClientAsUser(); + var res = await client.GetAsync(adminUrl); + res.Should().HaveStatusCode(200); + } + } + + { // change visibility to public + { + using var client = await CreateClientAsUser(); + var res = await client.PatchAsJsonAsync(generator(1, null), + new TimelinePatchRequest { Visibility = TimelineVisibility.Public }); + res.Should().HaveStatusCode(200); + } + { + using var client = await CreateDefaultClient(); + var res = await client.GetAsync(userUrl); + res.Should().HaveStatusCode(200); + } + } + + { // change visibility to private + { + using var client = await CreateClientAsAdministrator(); + { + var res = await client.PatchAsJsonAsync(generator(1, null), + new TimelinePatchRequest { Visibility = TimelineVisibility.Private }); + res.Should().HaveStatusCode(200); + } + { + var res = await client.PatchAsJsonAsync(generator(0, null), + new TimelinePatchRequest { Visibility = TimelineVisibility.Private }); + res.Should().HaveStatusCode(200); + } + } + { + using var client = await CreateDefaultClient(); + var res = await client.GetAsync(userUrl); + res.Should().HaveStatusCode(403); + } + { // user can't read admin's + using var client = await CreateClientAsUser(); + var res = await client.GetAsync(adminUrl); + res.Should().HaveStatusCode(403); + } + { // admin can read user's + using var client = await CreateClientAsAdministrator(); + var res = await client.GetAsync(userUrl); + res.Should().HaveStatusCode(200); + } + { // add member + using var client = await CreateClientAsAdministrator(); + var res = await client.PutAsync(generator(0, "members/user1"), null); + res.Should().HaveStatusCode(200); + } + { // now user can read admin's + using var client = await CreateClientAsUser(); + var res = await client.GetAsync(adminUrl); + res.Should().HaveStatusCode(200); + } + } + } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task Permission_Post_Create(TimelineUrlGenerator generator) + { + using (var client = await CreateClientAsUser()) + { + var res = await client.PutAsync(generator(1, "members/user2"), null); + res.Should().HaveStatusCode(200); + } + + using (var client = await CreateDefaultClient()) + { + { // no auth should get 401 + var res = await client.PostAsJsonAsync(generator(1, "posts"), + TimelineHelper.TextPostCreateRequest("aaa")); + res.Should().HaveStatusCode(401); + } + } + + using (var client = await CreateClientAsUser()) + { + { // post self's + var res = await client.PostAsJsonAsync(generator(1, "posts"), + TimelineHelper.TextPostCreateRequest("aaa")); + res.Should().HaveStatusCode(200); + } + { // post other not as a member should get 403 + var res = await client.PostAsJsonAsync(generator(0, "posts"), + TimelineHelper.TextPostCreateRequest("aaa")); + res.Should().HaveStatusCode(403); + } + } + + using (var client = await CreateClientAsAdministrator()) + { + { // post as admin + var res = await client.PostAsJsonAsync(generator(1, "posts"), + TimelineHelper.TextPostCreateRequest("aaa")); + res.Should().HaveStatusCode(200); + } + } + + using (var client = await CreateClientAs(2)) + { + { // post as member + var res = await client.PostAsJsonAsync(generator(1, "posts"), + TimelineHelper.TextPostCreateRequest("aaa")); + res.Should().HaveStatusCode(200); + } + } + } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task Permission_Post_Delete(TimelineUrlGenerator generator) + { + async Task CreatePost(int userNumber) + { + using var client = await CreateClientAs(userNumber); + var res = await client.PostAsJsonAsync(generator(1, "posts"), + TimelineHelper.TextPostCreateRequest("aaa")); + return res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Id; + } + + using (var client = await CreateClientAsUser()) + { + { + var res = await client.PutAsync(generator(1, "members/user2"), null); + res.Should().HaveStatusCode(200); + } + { + var res = await client.PutAsync(generator(1, "members/user3"), null); + res.Should().HaveStatusCode(200); + } + } + + { // no auth should get 401 + using var client = await CreateDefaultClient(); + var res = await client.DeleteAsync(generator(1, "posts/12")); + res.Should().HaveStatusCode(401); + } + + { // self can delete self + var postId = await CreatePost(1); + using var client = await CreateClientAsUser(); + var res = await client.DeleteAsync(generator(1, $"posts/{postId}")); + res.Should().HaveStatusCode(200); + } + + { // admin can delete any + var postId = await CreatePost(1); + using var client = await CreateClientAsAdministrator(); + var res = await client.DeleteAsync(generator(1, $"posts/{postId}")); + res.Should().HaveStatusCode(200); + } + + { // owner can delete other + var postId = await CreatePost(2); + using var client = await CreateClientAsUser(); + var res = await client.DeleteAsync(generator(1, $"posts/{postId}")); + res.Should().HaveStatusCode(200); + } + + { // author can delete self + var postId = await CreatePost(2); + using var client = await CreateClientAs(2); + var res = await client.DeleteAsync(generator(1, $"posts/{postId}")); + res.Should().HaveStatusCode(200); + } + + { // otherwise is forbidden + var postId = await CreatePost(2); + using var client = await CreateClientAs(3); + var res = await client.DeleteAsync(generator(1, $"posts/{postId}")); + res.Should().HaveStatusCode(403); + } + } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task TextPost_ShouldWork(TimelineUrlGenerator generator) + { + { + using var client = await CreateClientAsUser(); + { + var res = await client.GetAsync(generator(1, "posts")); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().NotBeNull().And.BeEmpty(); + } + { + var res = await client.PostAsJsonAsync(generator(1, "posts"), + TimelineHelper.TextPostCreateRequest(null)); + res.Should().BeInvalidModel(); + } + const string mockContent = "aaa"; + TimelinePostInfo createRes; + { + var res = await client.PostAsJsonAsync(generator(1, "posts"), + TimelineHelper.TextPostCreateRequest(mockContent)); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which; + body.Should().NotBeNull(); + body.Content.Should().BeEquivalentTo(TimelineHelper.TextPostContent(mockContent)); + body.Author.Should().BeEquivalentTo(UserInfos[1]); + body.Deleted.Should().BeFalse(); + createRes = body; + } + { + var res = await client.GetAsync(generator(1, "posts")); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().NotBeNull().And.BeEquivalentTo(createRes); + } + const string mockContent2 = "bbb"; + var mockTime2 = DateTime.UtcNow.AddDays(-1); + TimelinePostInfo createRes2; + { + var res = await client.PostAsJsonAsync(generator(1, "posts"), + TimelineHelper.TextPostCreateRequest(mockContent2, mockTime2)); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which; + body.Should().NotBeNull(); + body.Content.Should().BeEquivalentTo(TimelineHelper.TextPostContent(mockContent2)); + body.Author.Should().BeEquivalentTo(UserInfos[1]); + body.Time.Should().BeCloseTo(mockTime2, 1000); + body.Deleted.Should().BeFalse(); + createRes2 = body; + } + { + var res = await client.GetAsync(generator(1, "posts")); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().NotBeNull().And.BeEquivalentTo(createRes, createRes2); + } + { + var res = await client.DeleteAsync(generator(1, $"posts/{createRes.Id}")); + res.Should().BeDelete(true); + } + { + var res = await client.DeleteAsync(generator(1, $"posts/{createRes.Id}")); + res.Should().BeDelete(false); + } + { + var res = await client.DeleteAsync(generator(1, "posts/30000")); + res.Should().BeDelete(false); + } + { + var res = await client.GetAsync(generator(1, "posts")); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().NotBeNull().And.BeEquivalentTo(createRes2); + } + } + } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task GetPost_Should_Ordered(TimelineUrlGenerator generator) + { + using var client = await CreateClientAsUser(); + + async Task CreatePost(DateTime time) + { + var res = await client.PostAsJsonAsync(generator(1, "posts"), + TimelineHelper.TextPostCreateRequest("aaa", time)); + return res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Id; + } + + var now = DateTime.UtcNow; + var id0 = await CreatePost(now.AddDays(1)); + var id1 = await CreatePost(now.AddDays(-1)); + var id2 = await CreatePost(now); + + { + var res = await client.GetAsync(generator(1, "posts")); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Select(p => p.Id).Should().Equal(id1, id2, id0); + } + } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task CreatePost_InvalidModel(TimelineUrlGenerator generator) + { + using var client = await CreateClientAsUser(); + + { + var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = null }); + res.Should().BeInvalidModel(); + } + + { + var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = null } }); + res.Should().BeInvalidModel(); + } + + { + var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "hahaha" } }); + res.Should().BeInvalidModel(); + } + + { + var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "text", Text = null } }); + res.Should().BeInvalidModel(); + } + + { + var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "image", Data = null } }); + res.Should().BeInvalidModel(); + } + + { + // image not base64 + var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "image", Data = "!!!" } }); + res.Should().BeInvalidModel(); + } + + { + // image base64 not image + var res = await client.PostAsJsonAsync(generator(1, "posts"), new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Type = "image", Data = Convert.ToBase64String(new byte[] { 0x01, 0x02, 0x03 }) } }); + res.Should().BeInvalidModel(); + } + } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task ImagePost_ShouldWork(TimelineUrlGenerator generator) + { + var imageData = ImageHelper.CreatePngWithSize(100, 200); + + long postId; + string postImageUrl; + + void AssertPostContent(TimelinePostContentInfo content) + { + content.Type.Should().Be(TimelinePostContentTypes.Image); + content.Url.Should().EndWith(generator(1, $"posts/{postId}/data")); + content.Text.Should().Be(null); + } + + using var client = await CreateClientAsUser(); + + { + var res = await client.PostAsJsonAsync(generator(1, "posts"), + new TimelinePostCreateRequest + { + Content = new TimelinePostCreateRequestContent + { + Type = TimelinePostContentTypes.Image, + Data = Convert.ToBase64String(imageData) + } + }); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + postId = body.Id; + postImageUrl = body.Content.Url; + AssertPostContent(body.Content); + } + + { + var res = await client.GetAsync(generator(1, "posts")); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + body.Should().HaveCount(1); + var post = body[0]; + post.Id.Should().Be(postId); + AssertPostContent(post.Content); + } + + { + var res = await client.GetAsync(generator(1, $"posts/{postId}/data")); + res.Content.Headers.ContentType.MediaType.Should().Be("image/png"); + var data = await res.Content.ReadAsByteArrayAsync(); + var image = Image.Load(data, out var format); + image.Width.Should().Be(100); + image.Height.Should().Be(200); + format.Name.Should().Be(PngFormat.Instance.Name); + } + + { + await CacheTestHelper.TestCache(client, generator(1, $"posts/{postId}/data")); + } + + { + var res = await client.DeleteAsync(generator(1, $"posts/{postId}")); + res.Should().BeDelete(true); + } + + { + var res = await client.DeleteAsync(generator(1, $"posts/{postId}")); + res.Should().BeDelete(false); + } + + { + var res = await client.GetAsync(generator(1, "posts")); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().BeEmpty(); + } + + { + using var scope = TestApp.Host.Services.CreateScope(); + var database = scope.ServiceProvider.GetRequiredService(); + var count = await database.Data.CountAsync(); + count.Should().Be(0); + } + } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task ImagePost_400(TimelineUrlGenerator generator) + { + using var client = await CreateClientAsUser(); + + { + var res = await client.GetAsync(generator(1, "posts/11234/data")); + res.Should().HaveStatusCode(404) + .And.HaveCommonBody(ErrorCodes.TimelineController.PostNotExist); + } + + long postId; + { + var res = await client.PostAsJsonAsync(generator(1, "posts"), + TimelineHelper.TextPostCreateRequest("aaa")); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which; + postId = body.Id; + } + + { + var res = await client.GetAsync(generator(1, $"posts/{postId}/data")); + res.Should().HaveStatusCode(400) + .And.HaveCommonBody(ErrorCodes.TimelineController.PostNoData); + } + } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task Timeline_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); + } + } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task Post_ModifiedSince(TimelineUrlGenerator generator) + { + using var client = await CreateClientAsUser(); + + var postContentList = new List { "a", "b", "c", "d" }; + var posts = new List(); + + foreach (var content in postContentList) + { + var res = await client.PostAsJsonAsync(generator(1, "posts"), + new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } }); + var post = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + posts.Add(post); + await Task.Delay(1000); + } + + { + var res = await client.DeleteAsync(generator(1, $"posts/{posts[2].Id}")); + res.Should().BeDelete(true); + } + + { + var res = await client.GetAsync(generator(1, "posts", + new Dictionary { { "modifiedSince", posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture) } })); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody>() + .Which.Should().HaveCount(2) + .And.Subject.Select(p => p.Content.Text).Should().Equal("b", "d"); + } + } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task PostList_IncludeDeleted(TimelineUrlGenerator urlGenerator) + { + using var client = await CreateClientAsUser(); + + var postContentList = new List { "a", "b", "c", "d" }; + var posts = new List(); + + foreach (var content in postContentList) + { + var res = await client.PostAsJsonAsync(urlGenerator(1, "posts"), + new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } }); + posts.Add(res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which); + } + + foreach (var id in new long[] { posts[0].Id, posts[2].Id }) + { + var res = await client.DeleteAsync(urlGenerator(1, $"posts/{id}")); + res.Should().BeDelete(true); + } + + { + var res = await client.GetAsync(urlGenerator(1, "posts", new Dictionary { ["includeDeleted"] = "true" })); + posts = res.Should().HaveStatusCode(200) + .And.HaveJsonBody>() + .Which; + posts.Should().HaveCount(4); + posts.Select(p => p.Deleted).Should().Equal(true, false, true, false); + posts.Select(p => p.Content == null).Should().Equal(true, false, true, false); + } + } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task Post_ModifiedSince_And_IncludeDeleted(TimelineUrlGenerator urlGenerator) + { + using var client = await CreateClientAsUser(); + + var postContentList = new List { "a", "b", "c", "d" }; + var posts = new List(); + + foreach (var (content, index) in postContentList.Select((v, i) => (v, i))) + { + var res = await client.PostAsJsonAsync(urlGenerator(1, "posts"), + new TimelinePostCreateRequest { Content = new TimelinePostCreateRequestContent { Text = content, Type = TimelinePostContentTypes.Text } }); + var post = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + posts.Add(post); + await Task.Delay(1000); + } + + { + var res = await client.DeleteAsync(urlGenerator(1, $"posts/{posts[2].Id}")); + res.Should().BeDelete(true); + } + + { + + var res = await client.GetAsync(urlGenerator(1, "posts", + new Dictionary { + { "modifiedSince", posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture) }, + { "includeDeleted", "true" } + })); + posts = res.Should().HaveStatusCode(200) + .And.HaveJsonBody>() + .Which; + posts.Should().HaveCount(3); + posts.Select(p => p.Deleted).Should().Equal(false, true, false); + posts.Select(p => p.Content == null).Should().Equal(false, true, false); + } + } + + [Theory] + [MemberData(nameof(TimelineUrlGeneratorData))] + public async Task Timeline_Get_IfModifiedSince_And_CheckUniqueId(TimelineUrlGenerator urlGenerator) + { + using var client = await CreateClientAsUser(); + + DateTime lastModifiedTime; + TimelineInfo timeline; + string uniqueId; + + { + var res = await client.GetAsync(urlGenerator(1)); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + timeline = body; + lastModifiedTime = body.LastModified; + uniqueId = body.UniqueId; + } + + { + using var req = new HttpRequestMessage + { + RequestUri = new Uri(client.BaseAddress, urlGenerator(1)), + Method = HttpMethod.Get, + }; + req.Headers.IfModifiedSince = lastModifiedTime.AddSeconds(1); + var res = await client.SendAsync(req); + res.Should().HaveStatusCode(304); + } + + { + using var req = new HttpRequestMessage + { + RequestUri = new Uri(client.BaseAddress, urlGenerator(1)), + Method = HttpMethod.Get, + }; + req.Headers.IfModifiedSince = lastModifiedTime.AddSeconds(-1); + var res = await client.SendAsync(req); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(timeline); + } + + { + var res = await client.GetAsync(urlGenerator(1, null, + new Dictionary { { "ifModifiedSince", lastModifiedTime.AddSeconds(1).ToString("s", CultureInfo.InvariantCulture) } })); + res.Should().HaveStatusCode(304); + } + + { + var res = await client.GetAsync(urlGenerator(1, null, + new Dictionary { { "ifModifiedSince", lastModifiedTime.AddSeconds(-1).ToString("s", CultureInfo.InvariantCulture) } })); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(timeline); + } + + { + var res = await client.GetAsync(urlGenerator(1, null, + new Dictionary { { "ifModifiedSince", lastModifiedTime.AddSeconds(1).ToString("s", CultureInfo.InvariantCulture) }, + {"checkUniqueId", uniqueId } })); + res.Should().HaveStatusCode(304); + } + + { + var testUniqueId = (uniqueId[0] == 'a' ? "b" : "a") + uniqueId[1..]; + var res = await client.GetAsync(urlGenerator(1, null, + new Dictionary { { "ifModifiedSince", lastModifiedTime.AddSeconds(1).ToString("s", CultureInfo.InvariantCulture) }, + {"checkUniqueId", testUniqueId } })); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .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"); + } + } + + [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"); + } + } + } + + [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/BackEnd/Timeline.Tests/IntegratedTests/TokenTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/TokenTest.cs new file mode 100644 index 00000000..480d66cd --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/TokenTest.cs @@ -0,0 +1,165 @@ +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Timeline.Models; +using Timeline.Models.Http; +using Timeline.Services; +using Timeline.Tests.Helpers; +using Xunit; + +namespace Timeline.Tests.IntegratedTests +{ + public class TokenTest : IntegratedTestBase + { + private const string CreateTokenUrl = "token/create"; + private const string VerifyTokenUrl = "token/verify"; + + private static async Task CreateUserTokenAsync(HttpClient client, string username, string password, int? expireOffset = null) + { + var response = await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest { Username = username, Password = password, Expire = expireOffset }); + return response.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + } + + public static IEnumerable CreateToken_InvalidModel_Data() + { + yield return new[] { null, "p", null }; + yield return new[] { "u", null, null }; + yield return new object[] { "u", "p", 2000 }; + yield return new object[] { "u", "p", -1 }; + } + + [Theory] + [MemberData(nameof(CreateToken_InvalidModel_Data))] + public async Task CreateToken_InvalidModel(string username, string password, int expire) + { + using var client = await CreateDefaultClient(); + (await client.PostAsJsonAsync(CreateTokenUrl, new CreateTokenRequest + { + Username = username, + Password = password, + Expire = expire + })).Should().BeInvalidModel(); + } + + public static IEnumerable CreateToken_UserCredential_Data() + { + yield return new[] { "usernotexist", "p" }; + yield return new[] { "user1", "???" }; + } + + [Theory] + [MemberData(nameof(CreateToken_UserCredential_Data))] + public async void CreateToken_UserCredential(string username, string password) + { + using var client = await CreateDefaultClient(); + var response = await client.PostAsJsonAsync(CreateTokenUrl, + new CreateTokenRequest { Username = username, Password = password }); + response.Should().HaveStatusCode(400) + .And.HaveCommonBody() + .Which.Code.Should().Be(ErrorCodes.TokenController.Create_BadCredential); + } + + [Fact] + public async Task CreateToken_Success() + { + using var client = await CreateDefaultClient(); + var response = await client.PostAsJsonAsync(CreateTokenUrl, + new CreateTokenRequest { Username = "user1", Password = "user1pw" }); + var body = response.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + body.Token.Should().NotBeNullOrWhiteSpace(); + body.User.Should().BeEquivalentTo(UserInfos[1]); + } + + [Fact] + public async Task VerifyToken_InvalidModel() + { + using var client = await CreateDefaultClient(); + (await client.PostAsJsonAsync(VerifyTokenUrl, + new VerifyTokenRequest { Token = null })).Should().BeInvalidModel(); + } + + [Fact] + public async Task VerifyToken_BadFormat() + { + using var client = await CreateDefaultClient(); + var response = await client.PostAsJsonAsync(VerifyTokenUrl, + new VerifyTokenRequest { Token = "bad token hahaha" }); + response.Should().HaveStatusCode(400) + .And.HaveCommonBody() + .Which.Code.Should().Be(ErrorCodes.TokenController.Verify_BadFormat); + } + + [Fact] + public async Task VerifyToken_OldVersion() + { + using var client = await CreateDefaultClient(); + var token = (await CreateUserTokenAsync(client, "user1", "user1pw")).Token; + + using (var scope = TestApp.Host.Services.CreateScope()) // UserService is scoped. + { + // create a user for test + var userService = scope.ServiceProvider.GetRequiredService(); + await userService.ModifyUser("user1", new User { Password = "user1pw" }); + } + + (await client.PostAsJsonAsync(VerifyTokenUrl, + new VerifyTokenRequest { Token = token })) + .Should().HaveStatusCode(400) + .And.HaveCommonBody() + .Which.Code.Should().Be(ErrorCodes.TokenController.Verify_OldVersion); + } + + [Fact] + public async Task VerifyToken_UserNotExist() + { + using var client = await CreateDefaultClient(); + var token = (await CreateUserTokenAsync(client, "user1", "user1pw")).Token; + + using (var scope = TestApp.Host.Services.CreateScope()) // UserDeleteService is scoped. + { + var userService = scope.ServiceProvider.GetRequiredService(); + await userService.DeleteUser("user1"); + } + + (await client.PostAsJsonAsync(VerifyTokenUrl, + new VerifyTokenRequest { Token = token })) + .Should().HaveStatusCode(400) + .And.HaveCommonBody() + .Which.Code.Should().Be(ErrorCodes.TokenController.Verify_UserNotExist); + } + + //[Fact] + //public async Task VerifyToken_Expired() + //{ + // using (var client = await CreateClientWithNoAuth()) + // { + // // I can only control the token expired time but not current time + // // because verify logic is encapsuled in other library. + // var mockClock = _factory.GetTestClock(); + // mockClock.MockCurrentTime = DateTime.Now - TimeSpan.FromDays(2); + // var token = (await client.CreateUserTokenAsync(MockUsers.UserUsername, MockUsers.UserPassword, 1)).Token; + // var response = await client.PostAsJsonAsync(VerifyTokenUrl, + // new VerifyTokenRequest { Token = token }); + // response.Should().HaveStatusCodeBadRequest() + // .And.Should().HaveBodyAsCommonResponseWithCode(TokenController.ErrorCodes.Verify_Expired); + // mockClock.MockCurrentTime = null; + // } + //} + + [Fact] + public async Task VerifyToken_Success() + { + using var client = await CreateDefaultClient(); + var createTokenResult = await CreateUserTokenAsync(client, "user1", "user1pw"); + var response = await client.PostAsJsonAsync(VerifyTokenUrl, + new VerifyTokenRequest { Token = createTokenResult.Token }); + response.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.User.Should().BeEquivalentTo(UserInfos[1]); + } + } +} diff --git a/BackEnd/Timeline.Tests/IntegratedTests/UnknownEndpointTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/UnknownEndpointTest.cs new file mode 100644 index 00000000..732232e2 --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/UnknownEndpointTest.cs @@ -0,0 +1,21 @@ +using FluentAssertions; +using System.Threading.Tasks; +using Timeline.Models.Http; +using Timeline.Tests.Helpers; +using Xunit; + +namespace Timeline.Tests.IntegratedTests +{ + public class UnknownEndpointTest : IntegratedTestBase + { + [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/BackEnd/Timeline.Tests/IntegratedTests/UserAvatarTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/UserAvatarTest.cs new file mode 100644 index 00000000..f2796005 --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/UserAvatarTest.cs @@ -0,0 +1,251 @@ +using FluentAssertions; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Png; +using System.Collections.Generic; +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; +using Timeline.Tests.Helpers; +using Xunit; + +namespace Timeline.Tests.IntegratedTests +{ + public class UserAvatarTest : IntegratedTestBase + { + [Fact] + public async Task Test() + { + Avatar mockAvatar = new Avatar + { + Data = ImageHelper.CreatePngWithSize(100, 100), + Type = PngFormat.Instance.DefaultMimeType + }; + + using (var client = await CreateClientAsUser()) + { + { + var res = await client.GetAsync("users/usernotexist/avatar"); + res.Should().HaveStatusCode(404) + .And.HaveCommonBody() + .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); + } + + var env = TestApp.Host.Services.GetRequiredService(); + var defaultAvatarData = await File.ReadAllBytesAsync(Path.Combine(env.ContentRootPath, "default-avatar.png")); + + async Task GetReturnDefault(string username = "user1") + { + var res = await client.GetAsync($"users/{username}/avatar"); + res.Should().HaveStatusCode(200); + res.Content.Headers.ContentType.MediaType.Should().Be("image/png"); + var body = await res.Content.ReadAsByteArrayAsync(); + body.Should().Equal(defaultAvatarData); + } + + { + var res = await client.GetAsync("users/user1/avatar"); + res.Should().HaveStatusCode(200); + res.Content.Headers.ContentType.MediaType.Should().Be("image/png"); + var body = await res.Content.ReadAsByteArrayAsync(); + body.Should().Equal(defaultAvatarData); + } + + await CacheTestHelper.TestCache(client, "users/user1/avatar"); + + await GetReturnDefault("admin"); + + { + using var content = new ByteArrayContent(new[] { (byte)0x00 }); + content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); + var res = await client.PutAsync("users/user1/avatar", content); + res.Should().BeInvalidModel(); + } + + { + using var content = new ByteArrayContent(new[] { (byte)0x00 }); + content.Headers.ContentLength = 1; + var res = await client.PutAsync("users/user1/avatar", content); + res.Should().BeInvalidModel(); + } + + { + using var content = new ByteArrayContent(new[] { (byte)0x00 }); + content.Headers.ContentLength = 0; + content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); + var res = await client.PutAsync("users/user1/avatar", content); + res.Should().BeInvalidModel(); + } + + { + var res = await client.PutByteArrayAsync("users/user1/avatar", new[] { (byte)0x00 }, "image/notaccept"); + res.Should().HaveStatusCode(HttpStatusCode.UnsupportedMediaType); + } + + { + using var content = new ByteArrayContent(new[] { (byte)0x00 }); + content.Headers.ContentLength = 1000 * 1000 * 11; + content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); + var res = await client.PutAsync("users/user1/avatar", content); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Content.TooBig); + } + + { + using var content = new ByteArrayContent(new[] { (byte)0x00 }); + content.Headers.ContentLength = 2; + content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); + var res = await client.PutAsync("users/user1/avatar", content); + res.Should().BeInvalidModel(); + } + + { + using var content = new ByteArrayContent(new[] { (byte)0x00, (byte)0x01 }); + content.Headers.ContentLength = 1; + content.Headers.ContentType = new MediaTypeHeaderValue("image/png"); + var res = await client.PutAsync("users/user1/avatar", content); + res.Should().BeInvalidModel(); + } + + { + var res = await client.PutByteArrayAsync("users/user1/avatar", new[] { (byte)0x00 }, "image/png"); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.UserAvatar.BadFormat_CantDecode); + } + + { + var res = await client.PutByteArrayAsync("users/user1/avatar", mockAvatar.Data, "image/jpeg"); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.UserAvatar.BadFormat_UnmatchedFormat); + } + + { + var res = await client.PutByteArrayAsync("users/user1/avatar", ImageHelper.CreatePngWithSize(100, 200), "image/png"); + res.Should().HaveStatusCode(HttpStatusCode.BadRequest) + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.UserAvatar.BadFormat_BadSize); + } + + { + var res = await client.PutByteArrayAsync("users/user1/avatar", mockAvatar.Data, mockAvatar.Type); + res.Should().HaveStatusCode(HttpStatusCode.OK); + + var res2 = await client.GetAsync("users/user1/avatar"); + res2.Should().HaveStatusCode(200); + res2.Content.Headers.ContentType.MediaType.Should().Be(mockAvatar.Type); + var body = await res2.Content.ReadAsByteArrayAsync(); + body.Should().Equal(mockAvatar.Data); + } + + IEnumerable<(string, IImageFormat)> formats = new (string, IImageFormat)[] + { + ("image/jpeg", JpegFormat.Instance), + ("image/gif", GifFormat.Instance), + ("image/png", PngFormat.Instance), + }; + + foreach ((var mimeType, var format) in formats) + { + var res = await client.PutByteArrayAsync("users/user1/avatar", ImageHelper.CreateImageWithSize(100, 100, format), mimeType); + res.Should().HaveStatusCode(HttpStatusCode.OK); + } + + { + var res = await client.PutByteArrayAsync("users/admin/avatar", new[] { (byte)0x00 }, "image/png"); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden) + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Forbid); + } + + { + var res = await client.DeleteAsync("users/admin/avatar"); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden) + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.Common.Forbid); + } + + for (int i = 0; i < 2; i++) // double delete should work. + { + var res = await client.DeleteAsync("users/user1/avatar"); + res.Should().HaveStatusCode(200); + await GetReturnDefault(); + } + } + + // Authorization check. + using (var client = await CreateClientAsAdministrator()) + { + { + var res = await client.PutByteArrayAsync("users/user1/avatar", mockAvatar.Data, mockAvatar.Type); + res.Should().HaveStatusCode(HttpStatusCode.OK); + } + + { + var res = await client.DeleteAsync("users/user1/avatar"); + res.Should().HaveStatusCode(HttpStatusCode.OK); + } + + { + var res = await client.PutByteArrayAsync("users/usernotexist/avatar", new[] { (byte)0x00 }, "image/png"); + res.Should().HaveStatusCode(400) + .And.HaveCommonBody() + .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); + } + + { + var res = await client.DeleteAsync("users/usernotexist/avatar"); + res.Should().HaveStatusCode(400) + .And.HaveCommonBody().Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); + } + } + + // bad username check + using (var client = await CreateClientAsAdministrator()) + { + { + var res = await client.GetAsync("users/u!ser/avatar"); + res.Should().BeInvalidModel(); + } + + { + var res = await client.PutByteArrayAsync("users/u!ser/avatar", ImageHelper.CreatePngWithSize(100, 100), "image/png"); + res.Should().BeInvalidModel(); + } + + { + var res = await client.DeleteAsync("users/u!ser/avatar"); + res.Should().BeInvalidModel(); + } + } + } + + [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/BackEnd/Timeline.Tests/IntegratedTests/UserTest.cs b/BackEnd/Timeline.Tests/IntegratedTests/UserTest.cs new file mode 100644 index 00000000..9dfcc6a5 --- /dev/null +++ b/BackEnd/Timeline.Tests/IntegratedTests/UserTest.cs @@ -0,0 +1,447 @@ +using FluentAssertions; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Timeline.Models.Http; +using Timeline.Tests.Helpers; +using Xunit; + +namespace Timeline.Tests.IntegratedTests +{ + public class UserTest : IntegratedTestBase + { + [Fact] + public void UserListShouldHaveUniqueId() + { + foreach (var user in UserInfos) + { + user.UniqueId.Should().NotBeNullOrWhiteSpace(); + } + } + + [Fact] + public async Task GetList_NoAuth() + { + using var client = await CreateDefaultClient(); + var res = await client.GetAsync("users"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(UserInfos); + } + + [Fact] + public async Task GetList_User() + { + using var client = await CreateClientAsUser(); + var res = await client.GetAsync("users"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(UserInfos); + } + + [Fact] + public async Task GetList_Admin() + { + using var client = await CreateClientAsAdministrator(); + var res = await client.GetAsync("users"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(UserInfos); + } + + [Fact] + public async Task Get_NoAuth() + { + using var client = await CreateDefaultClient(); + var res = await client.GetAsync($"users/admin"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(UserInfos[0]); + } + + [Fact] + public async Task Get_User() + { + using var client = await CreateClientAsUser(); + var res = await client.GetAsync($"users/admin"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(UserInfos[0]); + } + + [Fact] + public async Task Get_Admin() + { + using var client = await CreateClientAsAdministrator(); + var res = await client.GetAsync($"users/user1"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Should().BeEquivalentTo(UserInfos[1]); + } + + [Fact] + public async Task Get_InvalidModel() + { + using var client = await CreateClientAsUser(); + var res = await client.GetAsync("users/aaa!a"); + res.Should().BeInvalidModel(); + } + + [Fact] + public async Task Get_404() + { + using var client = await CreateClientAsUser(); + var res = await client.GetAsync("users/usernotexist"); + res.Should().HaveStatusCode(404) + .And.HaveCommonBody(ErrorCodes.UserCommon.NotExist); + } + + [Fact] + public async Task Patch_User() + { + using var client = await CreateClientAsUser(); + { + var res = await client.PatchAsJsonAsync("users/user1", + new UserPatchRequest { Nickname = "aaa" }); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Nickname.Should().Be("aaa"); + } + + { + var res = await client.GetAsync("users/user1"); + res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which.Nickname.Should().Be("aaa"); + } + } + + [Fact] + public async Task Patch_Admin() + { + using var client = await CreateClientAsAdministrator(); + using var userClient = await CreateClientAsUser(); + + { + var res = await client.PatchAsJsonAsync("users/user1", + new UserPatchRequest + { + Username = "newuser", + Password = "newpw", + Administrator = true, + Nickname = "aaa" + }); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which; + body.Administrator.Should().Be(true); + body.Nickname.Should().Be("aaa"); + } + + { + var res = await client.GetAsync("users/newuser"); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody() + .Which; + body.Administrator.Should().Be(true); + body.Nickname.Should().Be("aaa"); + } + + { + // Token should expire. + var res = await userClient.GetAsync("testing/auth/Authorize"); + res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); + } + + { + // Check password. + (await CreateClientWithCredential("newuser", "newpw")).Dispose(); + } + } + + [Fact] + public async Task Patch_NotExist() + { + using var client = await CreateClientAsAdministrator(); + var res = await client.PatchAsJsonAsync("users/usernotexist", new UserPatchRequest { }); + res.Should().HaveStatusCode(404) + .And.HaveCommonBody() + .Which.Code.Should().Be(ErrorCodes.UserCommon.NotExist); + } + + [Fact] + public async Task Patch_InvalidModel() + { + using var client = await CreateClientAsAdministrator(); + var res = await client.PatchAsJsonAsync("users/aaa!a", new UserPatchRequest { }); + res.Should().BeInvalidModel(); + } + + public static IEnumerable Patch_InvalidModel_Body_Data() + { + yield return new[] { new UserPatchRequest { Username = "aaa!a" } }; + yield return new[] { new UserPatchRequest { Password = "" } }; + yield return new[] { new UserPatchRequest { Nickname = new string('a', 50) } }; + } + + [Theory] + [MemberData(nameof(Patch_InvalidModel_Body_Data))] + public async Task Patch_InvalidModel_Body(UserPatchRequest body) + { + using var client = await CreateClientAsAdministrator(); + var res = await client.PatchAsJsonAsync("users/user1", body); + res.Should().BeInvalidModel(); + } + + [Fact] + public async Task Patch_UsernameConflict() + { + using var client = await CreateClientAsAdministrator(); + var res = await client.PatchAsJsonAsync("users/user1", new UserPatchRequest { Username = "admin" }); + res.Should().HaveStatusCode(400) + .And.HaveCommonBody(ErrorCodes.UserController.UsernameConflict); + } + + [Fact] + public async Task Patch_NoAuth_Unauthorized() + { + using var client = await CreateDefaultClient(); + var res = await client.PatchAsJsonAsync("users/user1", new UserPatchRequest { Nickname = "aaa" }); + res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task Patch_User_Forbid() + { + using var client = await CreateClientAsUser(); + var res = await client.PatchAsJsonAsync("users/admin", new UserPatchRequest { Nickname = "aaa" }); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task Patch_Username_Forbid() + { + using var client = await CreateClientAsUser(); + var res = await client.PatchAsJsonAsync("users/user1", new UserPatchRequest { Username = "aaa" }); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task Patch_Password_Forbid() + { + using var client = await CreateClientAsUser(); + var res = await client.PatchAsJsonAsync("users/user1", new UserPatchRequest { Password = "aaa" }); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task Patch_Administrator_Forbid() + { + using var client = await CreateClientAsUser(); + var res = await client.PatchAsJsonAsync("users/user1", new UserPatchRequest { Administrator = true }); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden); + } + + [Fact] + public async Task Delete_Deleted() + { + using var client = await CreateClientAsAdministrator(); + { + var res = await client.DeleteAsync("users/user1"); + res.Should().BeDelete(true); + } + + { + var res = await client.GetAsync("users/user1"); + res.Should().HaveStatusCode(404); + } + } + + [Fact] + public async Task Delete_NotExist() + { + using var client = await CreateClientAsAdministrator(); + var res = await client.DeleteAsync("users/usernotexist"); + res.Should().BeDelete(false); + } + + [Fact] + public async Task Delete_InvalidModel() + { + using var client = await CreateClientAsAdministrator(); + var res = await client.DeleteAsync("users/aaa!a"); + res.Should().BeInvalidModel(); + } + + [Fact] + public async Task Delete_NoAuth_Unauthorized() + { + using var client = await CreateDefaultClient(); + var res = await client.DeleteAsync("users/aaa!a"); + res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); + } + + [Fact] + public async Task Delete_User_Forbid() + { + using var client = await CreateClientAsUser(); + var res = await client.DeleteAsync("users/aaa!a"); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden); + } + + private const string createUserUrl = "userop/createuser"; + + [Fact] + public async Task Op_CreateUser() + { + using var client = await CreateClientAsAdministrator(); + { + var res = await client.PostAsJsonAsync(createUserUrl, new CreateUserRequest + { + Username = "aaa", + Password = "bbb", + Administrator = true, + Nickname = "ccc" + }); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + body.Username.Should().Be("aaa"); + body.Nickname.Should().Be("ccc"); + body.Administrator.Should().BeTrue(); + } + { + var res = await client.GetAsync("users/aaa"); + var body = res.Should().HaveStatusCode(200) + .And.HaveJsonBody().Which; + body.Username.Should().Be("aaa"); + body.Nickname.Should().Be("ccc"); + body.Administrator.Should().BeTrue(); + } + { + // Test password. + (await CreateClientWithCredential("aaa", "bbb")).Dispose(); + } + } + + public static IEnumerable Op_CreateUser_InvalidModel_Data() + { + yield return new[] { new CreateUserRequest { Username = "aaa", Password = "bbb" } }; + yield return new[] { new CreateUserRequest { Username = "aaa", Administrator = true } }; + yield return new[] { new CreateUserRequest { Password = "bbb", Administrator = true } }; + yield return new[] { new CreateUserRequest { Username = "a!a", Password = "bbb", Administrator = true } }; + yield return new[] { new CreateUserRequest { Username = "aaa", Password = "", Administrator = true } }; + yield return new[] { new CreateUserRequest { Username = "aaa", Password = "bbb", Administrator = true, Nickname = new string('a', 40) } }; + } + + [Theory] + [MemberData(nameof(Op_CreateUser_InvalidModel_Data))] + public async Task Op_CreateUser_InvalidModel(CreateUserRequest body) + { + using var client = await CreateClientAsAdministrator(); + { + var res = await client.PostAsJsonAsync(createUserUrl, body); + res.Should().BeInvalidModel(); + } + } + + [Fact] + public async Task Op_CreateUser_UsernameConflict() + { + using var client = await CreateClientAsAdministrator(); + { + var res = await client.PostAsJsonAsync(createUserUrl, new CreateUserRequest + { + Username = "user1", + Password = "bbb", + Administrator = false + }); + res.Should().HaveStatusCode(400) + .And.HaveCommonBody(ErrorCodes.UserController.UsernameConflict); + } + } + + [Fact] + public async Task Op_CreateUser_NoAuth_Unauthorized() + { + using var client = await CreateDefaultClient(); + { + var res = await client.PostAsJsonAsync(createUserUrl, new CreateUserRequest + { + Username = "aaa", + Password = "bbb", + Administrator = false + }); + res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); + } + } + + [Fact] + public async Task Op_CreateUser_User_Forbid() + { + using var client = await CreateClientAsUser(); + { + var res = await client.PostAsJsonAsync(createUserUrl, new CreateUserRequest + { + Username = "aaa", + Password = "bbb", + Administrator = false + }); + res.Should().HaveStatusCode(HttpStatusCode.Forbidden); + } + } + + private const string changePasswordUrl = "userop/changepassword"; + + [Fact] + public async Task Op_ChangePassword() + { + using var client = await CreateClientAsUser(); + { + var res = await client.PostAsJsonAsync(changePasswordUrl, + new ChangePasswordRequest { OldPassword = "user1pw", NewPassword = "newpw" }); + res.Should().HaveStatusCode(200); + } + { + var res = await client.PatchAsJsonAsync("users/user1", new UserPatchRequest { }); + res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); + } + { + (await CreateClientWithCredential("user1", "newpw")).Dispose(); + } + } + + public static IEnumerable Op_ChangePassword_InvalidModel_Data() + { + yield return new[] { null, "ppp" }; + yield return new[] { "ppp", null }; + } + + [Theory] + [MemberData(nameof(Op_ChangePassword_InvalidModel_Data))] + public async Task Op_ChangePassword_InvalidModel(string oldPassword, string newPassword) + { + using var client = await CreateClientAsUser(); + var res = await client.PostAsJsonAsync(changePasswordUrl, + new ChangePasswordRequest { OldPassword = oldPassword, NewPassword = newPassword }); + res.Should().BeInvalidModel(); + } + + [Fact] + public async Task Op_ChangePassword_BadOldPassword() + { + using var client = await CreateClientAsUser(); + var res = await client.PostAsJsonAsync(changePasswordUrl, new ChangePasswordRequest { OldPassword = "???", NewPassword = "???" }); + res.Should().HaveStatusCode(400) + .And.HaveCommonBody(ErrorCodes.UserController.ChangePassword_BadOldPassword); + } + + [Fact] + public async Task Op_ChangePassword_NoAuth_Unauthorized() + { + using var client = await CreateDefaultClient(); + var res = await client.PostAsJsonAsync(changePasswordUrl, new ChangePasswordRequest { OldPassword = "???", NewPassword = "???" }); + res.Should().HaveStatusCode(HttpStatusCode.Unauthorized); + } + } +} diff --git a/BackEnd/Timeline.Tests/PasswordGenerator.cs b/BackEnd/Timeline.Tests/PasswordGenerator.cs new file mode 100644 index 00000000..863439b5 --- /dev/null +++ b/BackEnd/Timeline.Tests/PasswordGenerator.cs @@ -0,0 +1,23 @@ +using Timeline.Services; +using Xunit; +using Xunit.Abstractions; + +namespace Timeline.Tests +{ + public class PasswordGenerator + { + private readonly ITestOutputHelper _output; + + public PasswordGenerator(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + public void Generate() + { + var service = new PasswordService(); + _output.WriteLine(service.HashPassword("crupest")); + } + } +} diff --git a/BackEnd/Timeline.Tests/Properties/launchSettings.json b/BackEnd/Timeline.Tests/Properties/launchSettings.json new file mode 100644 index 00000000..f3ee419d --- /dev/null +++ b/BackEnd/Timeline.Tests/Properties/launchSettings.json @@ -0,0 +1,2 @@ +{ +} diff --git a/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs b/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs new file mode 100644 index 00000000..5a774b78 --- /dev/null +++ b/BackEnd/Timeline.Tests/Services/TimelineServiceTest.cs @@ -0,0 +1,329 @@ +using FluentAssertions; +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.Services.Exceptions; +using Timeline.Tests.Helpers; +using Xunit; + +namespace Timeline.Tests.Services +{ + public class TimelineServiceTest : IAsyncLifetime, IDisposable + { + private readonly TestDatabase _testDatabase = new TestDatabase(); + + private DatabaseContext _databaseContext; + + private readonly PasswordService _passwordService = new PasswordService(); + + private readonly ETagGenerator _eTagGenerator = new ETagGenerator(); + + private readonly ImageValidator _imageValidator = new ImageValidator(); + + private readonly TestClock _clock = new TestClock(); + + private DataManager _dataManager; + + private UserService _userService; + + private TimelineService _timelineService; + + private UserDeleteService _userDeleteService; + + public TimelineServiceTest() + { + } + + public async Task InitializeAsync() + { + await _testDatabase.InitializeAsync(); + _databaseContext = _testDatabase.CreateContext(); + _dataManager = new DataManager(_databaseContext, _eTagGenerator); + _userService = new UserService(NullLogger.Instance, _databaseContext, _passwordService, _clock); + _timelineService = new TimelineService(NullLogger.Instance, _databaseContext, _dataManager, _userService, _imageValidator, _clock); + _userDeleteService = new UserDeleteService(NullLogger.Instance, _databaseContext, _timelineService); + } + + public async Task DisposeAsync() + { + await _testDatabase.DisposeAsync(); + await _databaseContext.DisposeAsync(); + } + + public void Dispose() + { + _eTagGenerator.Dispose(); + } + + [Theory] + [InlineData("@user")] + [InlineData("tl")] + public async Task Timeline_GetLastModified(string timelineName) + { + var time = _clock.ForwardCurrentTime(); + + var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); + if (!isPersonal) + await _timelineService.CreateTimeline(timelineName, await _userService.GetUserIdByUsername("user")); + + var t = await _timelineService.GetTimelineLastModifiedTime(timelineName); + + t.Should().Be(time); + } + + [Theory] + [InlineData("@user")] + [InlineData("tl")] + public async Task Timeline_GetUnqiueId(string timelineName) + { + var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); + if (!isPersonal) + await _timelineService.CreateTimeline(timelineName, await _userService.GetUserIdByUsername("user")); + + var uniqueId = await _timelineService.GetTimelineUniqueId(timelineName); + + uniqueId.Should().NotBeNullOrEmpty(); + } + + [Theory] + [InlineData("@user")] + [InlineData("tl")] + public async Task Timeline_LastModified(string timelineName) + { + var initTime = _clock.ForwardCurrentTime(); + + void Check(Models.Timeline timeline) + { + timeline.NameLastModified.Should().Be(initTime); + timeline.LastModified.Should().Be(_clock.GetCurrentTime()); + } + + async Task GetAndCheck() + { + 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(); + } + + [Theory] + [InlineData("@user")] + [InlineData("tl")] + public async Task GetPosts_ModifiedSince(string timelineName) + { + _clock.ForwardCurrentTime(); + + var userId = await _userService.GetUserIdByUsername("user"); + + var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); + if (!isPersonal) + await _timelineService.CreateTimeline(timelineName, userId); + + var postContentList = new string[] { "a", "b", "c", "d" }; + + DateTime testPoint = new DateTime(); + + foreach (var (content, index) in postContentList.Select((v, i) => (v, i))) + { + var t = _clock.ForwardCurrentTime(); + if (index == 1) + testPoint = t; + await _timelineService.CreateTextPost(timelineName, userId, content, null); + } + + var posts = await _timelineService.GetPosts(timelineName, testPoint); + posts.Should().HaveCount(3) + .And.Subject.Select(p => (p.Content as TextTimelinePostContent).Text).Should().Equal(postContentList.Skip(1)); + } + + [Theory] + [InlineData("@user")] + [InlineData("tl")] + public async Task GetPosts_IncludeDeleted(string timelineName) + { + var userId = await _userService.GetUserIdByUsername("user"); + + var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); + if (!isPersonal) + await _timelineService.CreateTimeline(timelineName, userId); + + var postContentList = new string[] { "a", "b", "c", "d" }; + + foreach (var content in postContentList) + { + await _timelineService.CreateTextPost(timelineName, userId, content, null); + } + + var posts = await _timelineService.GetPosts(timelineName); + posts.Should().HaveCount(4); + posts.Select(p => p.Deleted).Should().Equal(Enumerable.Repeat(false, posts.Count)); + posts.Select(p => ((TextTimelinePostContent)p.Content).Text).Should().Equal(postContentList); + + foreach (var id in new long[] { posts[0].Id, posts[2].Id }) + { + await _timelineService.DeletePost(timelineName, id); + } + + posts = await _timelineService.GetPosts(timelineName); + posts.Should().HaveCount(2); + posts.Select(p => p.Deleted).Should().Equal(Enumerable.Repeat(false, posts.Count)); + posts.Select(p => ((TextTimelinePostContent)p.Content).Text).Should().Equal(new string[] { "b", "d" }); + + posts = await _timelineService.GetPosts(timelineName, includeDeleted: true); + posts.Should().HaveCount(4); + posts.Select(p => p.Deleted).Should().Equal(new bool[] { true, false, true, false }); + posts.Where(p => !p.Deleted).Select(p => ((TextTimelinePostContent)p.Content).Text).Should().Equal(new string[] { "b", "d" }); + } + + [Theory] + [InlineData("@admin")] + [InlineData("tl")] + public async Task GetPosts_ModifiedSince_UsernameChange(string timelineName) + { + var time1 = _clock.ForwardCurrentTime(); + + var userId = await _userService.GetUserIdByUsername("user"); + + var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); + if (!isPersonal) + await _timelineService.CreateTimeline(timelineName, userId); + + var postContentList = new string[] { "a", "b", "c", "d" }; + + foreach (var (content, index) in postContentList.Select((v, i) => (v, i))) + { + await _timelineService.CreateTextPost(timelineName, userId, content, null); + } + + var time2 = _clock.ForwardCurrentTime(); + + { + var posts = await _timelineService.GetPosts(timelineName, time2); + posts.Should().HaveCount(0); + } + + { + await _userService.ModifyUser(userId, new User { Nickname = "haha" }); + var posts = await _timelineService.GetPosts(timelineName, time2); + posts.Should().HaveCount(0); + } + + { + await _userService.ModifyUser(userId, new User { Username = "haha" }); + var posts = await _timelineService.GetPosts(timelineName, time2); + posts.Should().HaveCount(4); + } + } + + [Theory] + [InlineData("@admin")] + [InlineData("tl")] + public async Task GetPosts_ModifiedSince_UserDelete(string timelineName) + { + var time1 = _clock.ForwardCurrentTime(); + + var userId = await _userService.GetUserIdByUsername("user"); + var adminId = await _userService.GetUserIdByUsername("admin"); + + var _ = TimelineHelper.ExtractTimelineName(timelineName, out var isPersonal); + if (!isPersonal) + await _timelineService.CreateTimeline(timelineName, adminId); + + var postContentList = new string[] { "a", "b", "c", "d" }; + + foreach (var (content, index) in postContentList.Select((v, i) => (v, i))) + { + await _timelineService.CreateTextPost(timelineName, userId, content, null); + } + + var time2 = _clock.ForwardCurrentTime(); + + { + var posts = await _timelineService.GetPosts(timelineName, time2); + posts.Should().HaveCount(0); + } + + await _userDeleteService.DeleteUser("user"); + + { + var posts = await _timelineService.GetPosts(timelineName, time2); + posts.Should().HaveCount(0); + } + + { + var posts = await _timelineService.GetPosts(timelineName, time2, true); + posts.Should().HaveCount(4); + } + } + + [Theory] + [InlineData("@admin")] + [InlineData("tl")] + 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"); + } + } + + [Fact] + public async Task ChangeName() + { + _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(); + + 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/BackEnd/Timeline.Tests/Timeline.Tests.csproj b/BackEnd/Timeline.Tests/Timeline.Tests.csproj new file mode 100644 index 00000000..973e0fc0 --- /dev/null +++ b/BackEnd/Timeline.Tests/Timeline.Tests.csproj @@ -0,0 +1,34 @@ + + + + netcoreapp3.1 + + 8.0 + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + diff --git a/BackEnd/Timeline.Tests/UsernameValidatorUnitTest.cs b/BackEnd/Timeline.Tests/UsernameValidatorUnitTest.cs new file mode 100644 index 00000000..5b568adf --- /dev/null +++ b/BackEnd/Timeline.Tests/UsernameValidatorUnitTest.cs @@ -0,0 +1,78 @@ +using FluentAssertions; +using Timeline.Models.Validation; +using Timeline.Tests.Helpers; +using Xunit; + +namespace Timeline.Tests +{ + public class UsernameValidatorUnitTest : IClassFixture + { + private readonly UsernameValidator _validator; + + public UsernameValidatorUnitTest(UsernameValidator validator) + { + _validator = validator; + } + + private string FailAndMessage(string username) + { + var (result, message) = _validator.Validate(username); + result.Should().BeFalse(); + return message; + } + + [Fact] + public void NotString() + { + var (result, message) = _validator.Validate(123); + result.Should().BeFalse(); + message.Should().ContainEquivalentOf("type"); + } + + [Fact] + public void Empty() + { + FailAndMessage("").Should().ContainEquivalentOf("empty"); + } + + [Theory] + [InlineData("!")] + [InlineData("!abc")] + [InlineData("ab c")] + [InlineData("ab c!")] // This is a chinese ! . + public void BadCharactor(string value) + { + FailAndMessage(value).Should().ContainEquivalentOf("invalid") + .And.ContainEquivalentOf("character"); + } + + [Fact] + public void TooLong() + { + FailAndMessage(new string('a', 40)).Should().ContainEquivalentOf("long"); + } + + [Fact(Skip = "Currently name can't be longer than 26. So this will print message of too long.")] + public void UniqueId() + { + FailAndMessage("e4c80127d092d9b2fc19c5e04612d4c0").Should().ContainEquivalentOf("unique id"); + } + + [Theory] + [InlineData(null)] + [InlineData("abc")] + [InlineData("-abc")] + [InlineData("_abc")] + [InlineData("abc-")] + [InlineData("abc_")] + [InlineData("a-bc")] + [InlineData("a-b-c")] + [InlineData("a-b_c")] + [InlineData("a-你好_c")] + public void Success(string value) + { + var (result, _) = _validator.Validate(value); + result.Should().BeTrue(); + } + } +} diff --git a/BackEnd/Timeline.Tests/coverletArgs.runsettings b/BackEnd/Timeline.Tests/coverletArgs.runsettings new file mode 100644 index 00000000..24cd1822 --- /dev/null +++ b/BackEnd/Timeline.Tests/coverletArgs.runsettings @@ -0,0 +1,13 @@ + + + + + + + + [xunit.*]*,[Timeline]Timeline.Migrations.* + + + + + diff --git a/BackEnd/Timeline.Tests/packages.lock.json b/BackEnd/Timeline.Tests/packages.lock.json new file mode 100644 index 00000000..7150a222 --- /dev/null +++ b/BackEnd/Timeline.Tests/packages.lock.json @@ -0,0 +1,2040 @@ +{ + "version": 1, + "dependencies": { + ".NETCoreApp,Version=v3.1": { + "coverlet.collector": { + "type": "Direct", + "requested": "[1.3.0, )", + "resolved": "1.3.0", + "contentHash": "t8pnf5SX2ya0RX4vjoxsbhDMQCZJcpPun2neHKJ4FouMmObylo25FvoOydvf3Bl+l+IzWw7u2vjEeCBHnleB9g==" + }, + "FluentAssertions": { + "type": "Direct", + "requested": "[5.10.3, )", + "resolved": "5.10.3", + "contentHash": "gVPEVp1hLVqcv+7Q2wiDf7kqCNn7+bQcQ0jbJ2mcRT6CeRoZl1tNkqvzSIhvekyldDptk77j1b03MXTTRIqqpg==", + "dependencies": { + "System.Configuration.ConfigurationManager": "4.4.0" + } + }, + "JunitXml.TestLogger": { + "type": "Direct", + "requested": "[2.1.78, )", + "resolved": "2.1.78", + "contentHash": "4y4FSfKWxlked8ilQdqBBSeRMf5jD/Hkvyp744hc54yQcABLt4rR2Q+4hNqAqrSo+mhwAlusj2rpXpN/5TICCA==" + }, + "Microsoft.AspNet.WebApi.Client": { + "type": "Direct", + "requested": "[5.2.7, )", + "resolved": "5.2.7", + "contentHash": "/76fAHknzvFqbznS6Uj2sOyE9rJB3PltY+f53TH8dX9RiGhk02EhuFCWljSj5nnqKaTsmma8DFR50OGyQ4yJ1g==", + "dependencies": { + "Newtonsoft.Json": "10.0.1", + "Newtonsoft.Json.Bson": "1.0.1" + } + }, + "Microsoft.AspNetCore.TestHost": { + "type": "Direct", + "requested": "[3.1.9, )", + "resolved": "3.1.9", + "contentHash": "0DBtfgmM2yS4h0v+gS4JHRX4nuyQmW7Yi5/G4yB5KelA2dDXPsAiipw9z47B1jVEs9QZdOwSqPQm2R/owl2TnA==", + "dependencies": { + "System.IO.Pipelines": "4.7.3" + } + }, + "Microsoft.CodeAnalysis.FxCopAnalyzers": { + "type": "Direct", + "requested": "[3.3.0, )", + "resolved": "3.3.0", + "contentHash": "k3Icqx8kc+NrHImuiB8Jc/wd32Xeyd2B/7HOR5Qu9pyKzXQ4ikPeBAwzG2FSTuYhyIuNWvwL5k9yYBbbVz6w9w==", + "dependencies": { + "Microsoft.CodeAnalysis.VersionCheckAnalyzer": "[3.3.0]", + "Microsoft.CodeQuality.Analyzers": "[3.3.0]", + "Microsoft.NetCore.Analyzers": "[3.3.0]", + "Microsoft.NetFramework.Analyzers": "[3.3.0]" + } + }, + "Microsoft.NET.Test.Sdk": { + "type": "Direct", + "requested": "[16.7.1, )", + "resolved": "16.7.1", + "contentHash": "7T3XYuLT2CRMZXwlp8p4cEEf6y7VifxTdKwYNzCYp31CN4iyrcDKneIJvNTo0YVnTxJn+CSlGVlUnZHUlAwt9A==", + "dependencies": { + "Microsoft.CodeCoverage": "16.7.1", + "Microsoft.TestPlatform.TestHost": "16.7.1" + } + }, + "Moq": { + "type": "Direct", + "requested": "[4.14.7, )", + "resolved": "4.14.7", + "contentHash": "z1jwY3lL3d4l+92cdSnhRDUUco68HiRNfLKB9r9/PLP5lrN+ZL1Qtt3brVGVB8iY+ioBXhlFue2JtycBczE8Pw==", + "dependencies": { + "Castle.Core": "4.4.0", + "System.Threading.Tasks.Extensions": "4.5.1" + } + }, + "xunit": { + "type": "Direct", + "requested": "[2.4.1, )", + "resolved": "2.4.1", + "contentHash": "XNR3Yz9QTtec16O0aKcO6+baVNpXmOnPUxDkCY97J+8krUYxPvXT1szYYEUdKk4sB8GOI2YbAjRIOm8ZnXRfzQ==", + "dependencies": { + "xunit.analyzers": "0.10.0", + "xunit.assert": "[2.4.1]", + "xunit.core": "[2.4.1]" + } + }, + "xunit.runner.visualstudio": { + "type": "Direct", + "requested": "[2.4.3, )", + "resolved": "2.4.3", + "contentHash": "kZZSmOmKA8OBlAJaquPXnJJLM9RwQ27H7BMVqfMLUcTi9xHinWGJiWksa3D4NEtz0wZ/nxd2mogObvBgJKCRhQ==" + }, + "AutoMapper": { + "type": "Transitive", + "resolved": "10.1.1", + "contentHash": "uMgbqOdu9ZG5cIOty0C85hzzayBH2i9BthnS5FlMqKtMSHDv4ts81a2jS1VFaDBVhlBeIqJ/kQKjQY95BZde9w==", + "dependencies": { + "Microsoft.CSharp": "4.7.0", + "System.Reflection.Emit": "4.7.0" + } + }, + "AutoMapper.Extensions.Microsoft.DependencyInjection": { + "type": "Transitive", + "resolved": "8.1.0", + "contentHash": "dQyGCAYcHbGuimVvCMu4Ea2S1oYOlgO9XfVdClmY5wgygJMZoS57emPzH0qNfknmtzMm4QbDO9i237W5IDjU1A==", + "dependencies": { + "AutoMapper": "[10.1.0, 11.0.0)", + "Microsoft.Extensions.DependencyInjection.Abstractions": "3.0.0", + "Microsoft.Extensions.Options": "3.0.0" + } + }, + "Castle.Core": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "b5rRL5zeaau1y/5hIbI+6mGw3cwun16YjkHZnV9RRT5UyUIFsgLmNXJ0YnIN9p8Hw7K7AbG1q1UclQVU3DinAQ==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "System.Collections.Specialized": "4.3.0", + "System.ComponentModel": "4.3.0", + "System.ComponentModel.TypeConverter": "4.3.0", + "System.Diagnostics.TraceSource": "4.3.0", + "System.Dynamic.Runtime": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Xml.XmlDocument": "4.3.0" + } + }, + "Microsoft.AspNetCore.Authorization": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "cN2KJkfHcKwh82c9WGx4Tqfd2h5HflU/Mu5vYLMHON8WahHU9hE32ciIXcEIoKLNpu+zs1u1cN/qxcKTdqu89w==", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "1.0.2", + "Microsoft.Extensions.Options": "1.0.2", + "System.Security.Claims": "4.0.1" + } + }, + "Microsoft.AspNetCore.Hosting.Abstractions": { + "type": "Transitive", + "resolved": "1.0.4", + "contentHash": "ybY8FOkdNfBPB5PLv1JO+It/94ftBzGUI1WqU4XySbIWyhw2TPmmKAUuO9uvJoR0qpsFup8FJz6trsBcBITg9w==", + "dependencies": { + "Microsoft.AspNetCore.Hosting.Server.Abstractions": "1.0.4", + "Microsoft.AspNetCore.Http.Abstractions": "1.0.3", + "Microsoft.Extensions.Configuration.Abstractions": "1.0.2", + "Microsoft.Extensions.DependencyInjection.Abstractions": "1.0.2", + "Microsoft.Extensions.FileProviders.Abstractions": "1.0.1", + "Microsoft.Extensions.Logging.Abstractions": "1.0.2" + } + }, + "Microsoft.AspNetCore.Hosting.Server.Abstractions": { + "type": "Transitive", + "resolved": "1.0.4", + "contentHash": "XUiQPe/CflK1i0Voo9S6/G1iQh00gQ6sMqi3LRtKeceBbO6AOostaAUdhjyME92MapI4VFNl+Z+/KXUlMAExJQ==", + "dependencies": { + "Microsoft.AspNetCore.Http.Features": "1.0.3", + "Microsoft.Extensions.Configuration.Abstractions": "1.0.2" + } + }, + "Microsoft.AspNetCore.Http": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "kfNOIGGgVtMzsSWZzXBqz5zsdo8ssBa90YHzZt95N8ARGXoolSaBHy6yBoMm/XcpbXM+m/x1fixTTMIWMgzJdQ==", + "dependencies": { + "Microsoft.AspNetCore.Http.Abstractions": "1.0.3", + "Microsoft.AspNetCore.WebUtilities": "1.0.3", + "Microsoft.Extensions.ObjectPool": "1.0.1", + "Microsoft.Extensions.Options": "1.0.2", + "Microsoft.Net.Http.Headers": "1.0.3", + "System.Buffers": "4.0.0", + "System.Threading": "4.0.11" + } + }, + "Microsoft.AspNetCore.Http.Abstractions": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "nnjvAf7ag6P0DyD/0nhRGjLpv+3DkPU0juF8aQh46X8uF4kzjJdrh65oL+4PVOu3K6BgSg6OVUs0QC0SE0FRtg==", + "dependencies": { + "Microsoft.AspNetCore.Http.Features": "1.0.3", + "System.Globalization.Extensions": "4.0.1", + "System.Linq.Expressions": "4.1.1", + "System.Reflection.TypeExtensions": "4.1.0", + "System.Runtime.InteropServices": "4.1.0", + "System.Text.Encodings.Web": "4.0.1" + } + }, + "Microsoft.AspNetCore.Http.Extensions": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "+7Sd+14nexIJqcB4S1Eur9kzeMZ5CBtrxkei+PNbD78fg8vO3+TcCgrl5SBNTsUB/VJAfD/s0fgs5t+hHRj2Pg==", + "dependencies": { + "Microsoft.AspNetCore.Http.Abstractions": "1.0.3", + "Microsoft.Extensions.FileProviders.Abstractions": "1.0.1", + "Microsoft.Net.Http.Headers": "1.0.3", + "System.Buffers": "4.0.0", + "System.IO.FileSystem": "4.0.1" + } + }, + "Microsoft.AspNetCore.Http.Features": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "Ihq57tseNyPbJTmFXY4jQ4JkxLP0lh45VRwocQci/sFx+qcJGvWB+sJJ2/YPLy/qTWFAEfNAcswuY3OsNH9Gwg==", + "dependencies": { + "Microsoft.Extensions.Primitives": "1.0.1", + "System.Collections": "4.0.11", + "System.ComponentModel": "4.0.1", + "System.Linq": "4.1.0", + "System.Net.Primitives": "4.0.11", + "System.Net.WebSockets": "4.0.0", + "System.Runtime.Extensions": "4.1.0", + "System.Security.Claims": "4.0.1", + "System.Security.Cryptography.X509Certificates": "4.1.0", + "System.Security.Principal": "4.0.1" + } + }, + "Microsoft.AspNetCore.JsonPatch": { + "type": "Transitive", + "resolved": "1.0.0", + "contentHash": "WVaSVS+dDlWCR/qerHnBxU9tIeJ9GMA3M5tg4cxH7/cJYZZLnr2zvaFHGB+cRRNCKKTJ0pFRxT7ES8knhgAAaA==", + "dependencies": { + "Microsoft.CSharp": "4.0.1", + "Newtonsoft.Json": "9.0.1", + "System.Collections.Concurrent": "4.0.12", + "System.ComponentModel.TypeConverter": "4.1.0", + "System.Diagnostics.Debug": "4.0.11", + "System.Globalization": "4.0.11", + "System.Linq": "4.1.0", + "System.Reflection.Extensions": "4.0.1", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime.Extensions": "4.1.0", + "System.Runtime.Serialization.Primitives": "4.1.1", + "System.Text.Encoding.Extensions": "4.0.11" + } + }, + "Microsoft.AspNetCore.Mvc.Abstractions": { + "type": "Transitive", + "resolved": "1.0.4", + "contentHash": "Isqgif1nuB+um86cEkpL8KnoxFCUCXBsbs9PuiuzElvlSiv4Ek3LvtrSUcbivekDDfys8CDbJhxwEI7WKJieAQ==", + "dependencies": { + "Microsoft.AspNetCore.Routing.Abstractions": "1.0.4", + "Microsoft.CSharp": "4.0.1", + "Microsoft.Net.Http.Headers": "1.0.3", + "System.ComponentModel.TypeConverter": "4.1.0", + "System.Reflection.Extensions": "4.0.1", + "System.Text.Encoding.Extensions": "4.0.11" + } + }, + "Microsoft.AspNetCore.Mvc.ApiExplorer": { + "type": "Transitive", + "resolved": "1.0.4", + "contentHash": "ujCFTM42U2WKUBhdaoLoiI+wVHgYhrmDrkl5+hWJ7EJW4fhp42w4cRZ97tjuveWr+M6JZjpS0q+7PVofQzFUiw==", + "dependencies": { + "Microsoft.AspNetCore.Mvc.Core": "1.0.4" + } + }, + "Microsoft.AspNetCore.Mvc.Core": { + "type": "Transitive", + "resolved": "1.0.4", + "contentHash": "1ukcttN1+T82hWXE8WS5kawkruolKI6LPVqVI4rTzN16kFszS/UqTrcwSUEnmTRpmWgFo665V3c2GpdQ9B6znw==", + "dependencies": { + "Microsoft.AspNetCore.Authorization": "1.0.3", + "Microsoft.AspNetCore.Hosting.Abstractions": "1.0.3", + "Microsoft.AspNetCore.Http": "1.0.3", + "Microsoft.AspNetCore.Mvc.Abstractions": "1.0.4", + "Microsoft.AspNetCore.Routing": "1.0.4", + "Microsoft.Extensions.DependencyModel": "1.0.0", + "Microsoft.Extensions.FileProviders.Abstractions": "1.0.1", + "Microsoft.Extensions.Logging.Abstractions": "1.0.2", + "Microsoft.Extensions.PlatformAbstractions": "1.0.0", + "System.Buffers": "4.0.0", + "System.Diagnostics.DiagnosticSource": "4.0.0", + "System.Text.Encoding": "4.0.11" + } + }, + "Microsoft.AspNetCore.Mvc.Formatters.Json": { + "type": "Transitive", + "resolved": "1.0.4", + "contentHash": "i8WWK2GwlBHfOL+d+kknJWPks6DS9tbN6nfJZU4yb+/wfUAYd311B2CIHzdat3IewubnK1TYONwrhQcs2FbLeA==", + "dependencies": { + "Microsoft.AspNetCore.JsonPatch": "1.0.0", + "Microsoft.AspNetCore.Mvc.Core": "1.0.4" + } + }, + "Microsoft.AspNetCore.NodeServices": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "bbd3FlSPWiRQrIcBLa5TaOvo4gjmmiNMkxA8VmZ6u0eIpS0Yj35/eTopaGdtzqwlqj5jXbdRoib1MruXuPaW8A==", + "dependencies": { + "Microsoft.Extensions.Logging.Console": "3.1.9", + "Newtonsoft.Json": "12.0.2" + } + }, + "Microsoft.AspNetCore.Routing": { + "type": "Transitive", + "resolved": "1.0.4", + "contentHash": "mdIF3ckRothHWuCSFkk6YXACj5zxi5qM+cEAHjcpP04/wCHUoV0gGVnW+HI+LyFXE6JUwu2zXn5tfsCpW0U+SA==", + "dependencies": { + "Microsoft.AspNetCore.Http.Extensions": "1.0.3", + "Microsoft.AspNetCore.Routing.Abstractions": "1.0.4", + "Microsoft.Extensions.Logging.Abstractions": "1.0.2", + "Microsoft.Extensions.ObjectPool": "1.0.1", + "Microsoft.Extensions.Options": "1.0.2", + "System.Collections": "4.0.11", + "System.Text.RegularExpressions": "4.1.0" + } + }, + "Microsoft.AspNetCore.Routing.Abstractions": { + "type": "Transitive", + "resolved": "1.0.4", + "contentHash": "GHxVt6LlXHFsCUd2Un+/vY1tBTXxnogfbDO0b8G5EGmkapSK+dOGOLJviscxQkp338Uabs081JEIdkRymI5GXA==", + "dependencies": { + "Microsoft.AspNetCore.Http.Abstractions": "1.0.3", + "System.Collections.Concurrent": "4.0.12", + "System.Reflection.Extensions": "4.0.1", + "System.Threading.Tasks": "4.0.11" + } + }, + "Microsoft.AspNetCore.SpaServices": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "Fb+N2ZyF1wNrGeWggT+Ovv6W8AAVxfi4V/SnuEsBOR+nmkFhty9zyh6IDRRS98GJK6OE3adqqPbWMtJqbxYnNA==", + "dependencies": { + "Microsoft.AspNetCore.NodeServices": "3.1.9" + } + }, + "Microsoft.AspNetCore.SpaServices.Extensions": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "ciy2GCvRnh9C22laArLsaItS+72U6Hqf4nDYShdvFgcen2ZV+NNSitb/B3vsmFfIPM8m4mf2x4T+vZ6OlI5XaA==", + "dependencies": { + "Microsoft.AspNetCore.SpaServices": "3.1.9", + "Microsoft.Extensions.FileProviders.Physical": "3.1.9" + } + }, + "Microsoft.AspNetCore.StaticFiles": { + "type": "Transitive", + "resolved": "1.0.4", + "contentHash": "2pNvwewAazhaaCdw2CGUvIcDrNQMlqP57JgBDf3v+pRj1rZ29HVnpvkX6a+TrmRYlJNmmxHOKEt468uE/gDcFw==", + "dependencies": { + "Microsoft.AspNetCore.Hosting.Abstractions": "1.0.4", + "Microsoft.AspNetCore.Http.Extensions": "1.0.3", + "Microsoft.Extensions.FileProviders.Abstractions": "1.0.1", + "Microsoft.Extensions.Logging.Abstractions": "1.0.2", + "Microsoft.Extensions.WebEncoders": "1.0.3" + } + }, + "Microsoft.AspNetCore.WebUtilities": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "snSGNs5EEisqivDjDiskFkFyu+DV2Ib9sMPOBQKtoFwI5H1W5YNB/rIVqDZQL16zj/uzdwwxrdE/5xhkVyf6gQ==", + "dependencies": { + "Microsoft.Extensions.Primitives": "1.0.1", + "System.Buffers": "4.0.0", + "System.Collections": "4.0.11", + "System.IO": "4.1.0", + "System.IO.FileSystem": "4.0.1", + "System.Text.Encodings.Web": "4.0.1" + } + }, + "Microsoft.Bcl.AsyncInterfaces": { + "type": "Transitive", + "resolved": "1.1.1", + "contentHash": "yuvf07qFWFqtK3P/MRkEKLhn5r2UbSpVueRziSqj0yJQIKFwG1pq9mOayK3zE5qZCTs0CbrwL9M6R8VwqyGy2w==" + }, + "Microsoft.Bcl.HashCode": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "J2G1k+u5unBV+aYcwxo94ip16Rkp65pgWFb0R6zwJipzWNMgvqlWeuI7/+R+e8bob66LnSG+llLJ+z8wI94cHg==" + }, + "Microsoft.CodeAnalysis.VersionCheckAnalyzer": { + "type": "Transitive", + "resolved": "3.3.0", + "contentHash": "xjLM3DRFZMan3nQyBQEM1mBw6VqQybi4iMJhMFW6Ic1E1GCvqJR3ABOwEL7WtQjDUzxyrGld9bASnAos7G/Xyg==" + }, + "Microsoft.CodeCoverage": { + "type": "Transitive", + "resolved": "16.7.1", + "contentHash": "PhSppbk+kvAyD9yGJIcBRJ/XYwY+21YK88l22PGTtixaxNdjnx1idVKh88LCGwKaTL8HhlnQ41VmBiBdZJzIQw==" + }, + "Microsoft.CodeQuality.Analyzers": { + "type": "Transitive", + "resolved": "3.3.0", + "contentHash": "zZ3miq6u22UFQKhfJyLnVEJ+DgeOopLh3eKJnKAcOetPP2hiv3wa7kHZlBDeTvtqJQiAQhAVbttket8XxjN1zw==" + }, + "Microsoft.CSharp": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA==" + }, + "Microsoft.Data.Sqlite.Core": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "+u4PeT1npi2EzhxGc5r1Z2z73zuXw+TlKVZm44WQhNCUw4LtUVDaxGSpUhrjW+X4snBCBfr4kT/uJyKnL4R4og==", + "dependencies": { + "SQLitePCLRaw.core": "2.0.2" + } + }, + "Microsoft.DotNet.PlatformAbstractions": { + "type": "Transitive", + "resolved": "3.1.6", + "contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg==" + }, + "Microsoft.EntityFrameworkCore": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "u3A2W0BvAuAF2jgW+WX+C+Sh8sMGX5Kl1hdA0gu6A/XSrZQoW/BUP4a/q2n3iitDGndaorqjAKx+Spb9gBto+w==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "1.1.1", + "Microsoft.Bcl.HashCode": "1.1.0", + "Microsoft.EntityFrameworkCore.Abstractions": "3.1.9", + "Microsoft.EntityFrameworkCore.Analyzers": "3.1.9", + "Microsoft.Extensions.Caching.Memory": "3.1.9", + "Microsoft.Extensions.DependencyInjection": "3.1.9", + "Microsoft.Extensions.Logging": "3.1.9", + "System.Collections.Immutable": "1.7.1", + "System.ComponentModel.Annotations": "4.7.0", + "System.Diagnostics.DiagnosticSource": "4.7.1" + } + }, + "Microsoft.EntityFrameworkCore.Abstractions": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "IR6Y4RJVlw0QXdWXjF3Kx9s1QLiicJus+BFBKr43lBtriV20j3yrWMoaZ9W1AUUgnicZXpXVcNfklqtmwb9Sxw==" + }, + "Microsoft.EntityFrameworkCore.Analyzers": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "eXGyx/Lb1fiiKtnIStdxGrfBSSQg8oZytE10f1T/2xAx12W9dKB9U9fg05cwNCDC0S2CXILsmZHYaGqCSXVAqQ==" + }, + "Microsoft.EntityFrameworkCore.Relational": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "7fhWuSfrCYlv/hvOX5OhbFJF/G9f8sifqTrJiYnAYLDOvNizwv7t9tFPD8JwaF3zM2S54O5/Vni2NxvwzSaW2w==", + "dependencies": { + "Microsoft.EntityFrameworkCore": "3.1.9" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "sMFCWv/1UcsFQZeGQcbfPbEZKZ1oKZqWZXTbc7PEZVMIXu82nbavstdNQ84x5IBXJkxl8iW3zjChb/FRBr5uLQ==", + "dependencies": { + "Microsoft.EntityFrameworkCore.Sqlite.Core": "3.1.9", + "SQLitePCLRaw.bundle_e_sqlite3": "2.0.2" + } + }, + "Microsoft.EntityFrameworkCore.Sqlite.Core": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "Da6h8LdpJwKc1az9DMWt2Mt6gHXPRZqwiumV1Zx0AuM3EThyokVDzBGy2sti0AcBhcQMLJHPEr5R9xuiWvaYYQ==", + "dependencies": { + "Microsoft.Data.Sqlite.Core": "3.1.9", + "Microsoft.DotNet.PlatformAbstractions": "3.1.6", + "Microsoft.EntityFrameworkCore.Relational": "3.1.9", + "Microsoft.Extensions.DependencyModel": "3.1.6" + } + }, + "Microsoft.Extensions.ApiDescription.Server": { + "type": "Transitive", + "resolved": "3.0.0", + "contentHash": "LH4OE/76F6sOCslif7+Xh3fS/wUUrE5ryeXAMcoCnuwOQGT5Smw0p57IgDh/pHgHaGz/e+AmEQb7pRgb++wt0w==" + }, + "Microsoft.Extensions.Caching.Abstractions": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "/2QsPAsUZD4qvftZkUKHRRRryPDXWh606/iNXPLrulwHLMr9JNsKBJWVqylT3qU92nJok5VoqSblkY9mSyxFyg==", + "dependencies": { + "Microsoft.Extensions.Primitives": "3.1.9" + } + }, + "Microsoft.Extensions.Caching.Memory": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "/JrVMVetX/kpJQUIlJ6NLQ3zbF0yyryXpo4+uFCqYIUZzgmWk8DS/zSKcyj1tQ3410+vhDEAPngxC+hg0IlJeg==", + "dependencies": { + "Microsoft.Extensions.Caching.Abstractions": "3.1.9", + "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.9", + "Microsoft.Extensions.Logging.Abstractions": "3.1.9", + "Microsoft.Extensions.Options": "3.1.9" + } + }, + "Microsoft.Extensions.Configuration": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "lqdkOGNeTMKG981Q7yWGlRiFbIlsRwTlMMiybT+WOzUCFBS/wc25tZgh7Wm/uRoBbWefgvokzmnea7ZjmFedmA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "3.1.9" + } + }, + "Microsoft.Extensions.Configuration.Abstractions": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "vOJxPKczaHpXeZFrxARxYwsEulhEouXc5aZGgMdkhV/iEXX9/pfjqKk76rTG+4CsJjHV+G/4eMhvOIaQMHENNA==", + "dependencies": { + "Microsoft.Extensions.Primitives": "3.1.9" + } + }, + "Microsoft.Extensions.Configuration.Binder": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "BG6HcT7tARYakftqfQu+cLksgIWG1NdxMY+igI12hdZrUK+WjS973NiRyuao/U9yyTeM9NPwRnC61hCmG3G3jg==", + "dependencies": { + "Microsoft.Extensions.Configuration": "3.1.9" + } + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "ORqfrAACcvTInie1oGola5uky344/PiNfgayTPuZWV4WnSfIQZJQm/ZLpGshJE3h7TqwYaYElGazK/yaM2bFLA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.9" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "8PkcaPwiTPOhqshoY4+rQUbz86X6YpLDLUqXOezh7L2A3pgpBmeBBByYIffofBlvQxDdQ0zB2DkWjbZWyCxRWg==" + }, + "Microsoft.Extensions.DependencyModel": { + "type": "Transitive", + "resolved": "3.1.6", + "contentHash": "/UlDKULIVkLQYn1BaHcy/rc91ApDxJb7T75HcCbGdqwvxhnRQRKM2di1E70iCPMF9zsr6f4EgQTotBGxFIfXmw==", + "dependencies": { + "System.Text.Json": "4.7.2" + } + }, + "Microsoft.Extensions.FileProviders.Abstractions": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "Q4SGwEFZKiZbpzPgdGbQUULxtcH1zXMOwCPKSm6QwVcOCGshf3QLfBh+O/GyFH4B0RfZ16nKyeW1mMONlRyjUw==", + "dependencies": { + "Microsoft.Extensions.Primitives": "3.1.9" + } + }, + "Microsoft.Extensions.FileProviders.Embedded": { + "type": "Transitive", + "resolved": "1.0.1", + "contentHash": "nSEa8bH3fVdTYGqK4twOKLxxgKIW3cz9g9mrzhPh/CmdvGJWKRTIlBIZi7lz+lqNQpxean5vbAo84R/mU+JpGA==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "1.0.1", + "System.Runtime.Extensions": "4.1.0" + } + }, + "Microsoft.Extensions.FileProviders.Physical": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "HWDSsblTCQp7EEJJmnLzttIhFGzDu+DGqBbOvGCdFT0+pkCuBkn3EiWpEEcm5WMTO5njmsbLSK9ZuUUf2zPsFg==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "3.1.9", + "Microsoft.Extensions.FileSystemGlobbing": "3.1.9" + } + }, + "Microsoft.Extensions.FileSystemGlobbing": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "5bnewG1aBiSESPNwcXGIxDDRN95uqdy+fqZZ8Z63Et5rRNlAwAfXHOrg+FTht7UjHobjvtjzquMCbAWhWEPHIw==" + }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "+V3i0jCQCO6IIOf6e+fL0SqrZd2x/Krug9EEL1JHa9R03RsbEpltCtjVY5hxedyuyuQKwvLoR12sCfu/9XEUAw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Binder": "3.1.9", + "Microsoft.Extensions.DependencyInjection": "3.1.9", + "Microsoft.Extensions.Logging.Abstractions": "3.1.9", + "Microsoft.Extensions.Options": "3.1.9" + } + }, + "Microsoft.Extensions.Logging.Abstractions": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "W5fbF8qVR9SMVVJqDQLIR7meWbev6Pu/lbrm7LDNr4Sp7HOotr4k2UULTdFSXOi5aoDdkQZpWnq0ZSpjrR3tjg==" + }, + "Microsoft.Extensions.Logging.Configuration": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "hv6XsGgikrbkolBJdF1usl9R/nrliC5mifMqHMEY9zWcCLwNkXMJiS8p0lbosrnpVAMi4PbNx39DB51Dqscd0w==", + "dependencies": { + "Microsoft.Extensions.Logging": "3.1.9", + "Microsoft.Extensions.Options.ConfigurationExtensions": "3.1.9" + } + }, + "Microsoft.Extensions.Logging.Console": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "8Dusl1rkDivmvLrwj6QAo917xMHPiDBzG3IG3agiyDdtsC/fRp+1VN5iIN+O09PtEaMged2OLA6wCDwfSTSTZw==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "3.1.9", + "Microsoft.Extensions.Logging": "3.1.9", + "Microsoft.Extensions.Logging.Configuration": "3.1.9" + } + }, + "Microsoft.Extensions.ObjectPool": { + "type": "Transitive", + "resolved": "1.0.1", + "contentHash": "pJMOnxuqmG37OjccfvtqVoo3bQGoN+0EJUzzp7+2uxSdioER82caAk6Yi/z5aysapn5XENNIIa7SaYnYKSS69A==", + "dependencies": { + "System.Diagnostics.Debug": "4.0.11", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime.Extensions": "4.1.0", + "System.Threading": "4.0.11" + } + }, + "Microsoft.Extensions.Options": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "EIb3G1DL+Rl9MvJR7LjI1wCy2nfTN4y8MflbOftn1HLYQBj/Rwl8kUbGTrSFE01c99Wm4ETjWVsjqKcpFvhPng==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.9", + "Microsoft.Extensions.Primitives": "3.1.9" + } + }, + "Microsoft.Extensions.Options.ConfigurationExtensions": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "u5jh7RW+Ev81YqK1ZoBG0lftp2MA9xqXiTiRL46XzaPj2ScNUyiVbzcVY0fPbE27UOpT2hj+yPzRSOMIIo55UA==", + "dependencies": { + "Microsoft.Extensions.Configuration.Abstractions": "3.1.9", + "Microsoft.Extensions.Configuration.Binder": "3.1.9", + "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.9", + "Microsoft.Extensions.Options": "3.1.9" + } + }, + "Microsoft.Extensions.PlatformAbstractions": { + "type": "Transitive", + "resolved": "1.0.0", + "contentHash": "zyjUzrOmuevOAJpIo3Mt5GmpALVYCVdLZ99keMbmCxxgQH7oxzU58kGHzE6hAgYEiWsdfMJLjVR7r+vSmaJmtg==", + "dependencies": { + "System.AppContext": "4.1.0", + "System.Reflection": "4.1.0", + "System.Reflection.Extensions": "4.0.1", + "System.Reflection.TypeExtensions": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime.Extensions": "4.1.0" + } + }, + "Microsoft.Extensions.Primitives": { + "type": "Transitive", + "resolved": "3.1.9", + "contentHash": "IrHecH0eGG7/XoeEtv++oLg/sJHRNyeCqlA9RhAo6ig4GpOTjtDr32sBMYuuLtUq8ALahneWkrOzoBAwJ4L4iA==" + }, + "Microsoft.Extensions.WebEncoders": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "TClNvczWRxF6bVPhn5EK3Y3QNi5jTP68Qur+5Fk+MQLPeBI18WN7X145DDJ6bFeNOwgdCHl73lHs5uZp9ish1A==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "1.0.2", + "Microsoft.Extensions.Options": "1.0.2", + "System.Text.Encodings.Web": "4.0.1" + } + }, + "Microsoft.IdentityModel.JsonWebTokens": { + "type": "Transitive", + "resolved": "6.8.0", + "contentHash": "+7JIww64PkMt7NWFxoe4Y/joeF7TAtA/fQ0b2GFGcagzB59sKkTt/sMZWR6aSZht5YC7SdHi3W6yM1yylRGJCQ==", + "dependencies": { + "Microsoft.IdentityModel.Tokens": "6.8.0" + } + }, + "Microsoft.IdentityModel.Logging": { + "type": "Transitive", + "resolved": "6.8.0", + "contentHash": "Rfh/p4MaN4gkmhPxwbu8IjrmoDncGfHHPh1sTnc0AcM/Oc39/fzC9doKNWvUAjzFb8LqA6lgZyblTrIsX/wDXg==" + }, + "Microsoft.IdentityModel.Tokens": { + "type": "Transitive", + "resolved": "6.8.0", + "contentHash": "gTqzsGcmD13HgtNePPcuVHZ/NXWmyV+InJgalW/FhWpII1D7V1k0obIseGlWMeA4G+tZfeGMfXr0klnWbMR/mQ==", + "dependencies": { + "Microsoft.CSharp": "4.5.0", + "Microsoft.IdentityModel.Logging": "6.8.0", + "System.Security.Cryptography.Cng": "4.5.0" + } + }, + "Microsoft.Net.Http.Headers": { + "type": "Transitive", + "resolved": "1.0.3", + "contentHash": "2F8USh4hR5xppvaxtw2EStX74Ih+HhRj7aQD1uaB9JmTGy478F7t4VU+IdZXauEDrvS7LYAyyhmOExsUFK3PAw==", + "dependencies": { + "System.Buffers": "4.0.0", + "System.Collections": "4.0.11", + "System.Diagnostics.Contracts": "4.0.1", + "System.Globalization": "4.0.11", + "System.Linq": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime.Extensions": "4.1.0", + "System.Text.Encoding": "4.0.11" + } + }, + "Microsoft.NetCore.Analyzers": { + "type": "Transitive", + "resolved": "3.3.0", + "contentHash": "6qptTHUu1Wfszuf83NhU0IoAb4j7YWOpJs6oc6S4G/nI6aGGWKH/Xi5Vs9L/8lrI74ijEEzPcIwafSQW5ASHtA==" + }, + "Microsoft.NETCore.Platforms": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" + }, + "Microsoft.NETCore.Targets": { + "type": "Transitive", + "resolved": "1.1.0", + "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" + }, + "Microsoft.NetFramework.Analyzers": { + "type": "Transitive", + "resolved": "3.3.0", + "contentHash": "JTfMic5fEFWICePbr7GXOGPranqS9Qxu2U/BZEcnnGbK1SFW8TxRyGp6O1L52xsbfOdqmzjc0t5ubhDrjj+Xpg==" + }, + "Microsoft.TestPlatform.ObjectModel": { + "type": "Transitive", + "resolved": "16.7.1", + "contentHash": "FL+VpAC/nCCzj80MwX6L8gJD06u2m1SKcQQLAymDLFqNtgtI9h3J5n0mVN+s18qcMzybsmO9GK7rMuHYx11KMg==", + "dependencies": { + "NuGet.Frameworks": "5.0.0" + } + }, + "Microsoft.TestPlatform.TestHost": { + "type": "Transitive", + "resolved": "16.7.1", + "contentHash": "mv7MnBDtqwQAjoH+AphE+Tu0dsF6x/c7Zs8umkb2McbvNALJdfBuWJQbiXGWqhNq7k8eMmnkNO6klJz4pkgekw==", + "dependencies": { + "Microsoft.TestPlatform.ObjectModel": "16.7.1", + "Newtonsoft.Json": "9.0.1" + } + }, + "Microsoft.Win32.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "9ZQKCWxH7Ijp9BfahvL2Zyf1cJIk8XYLF6Yjzr2yi0b2cOut/HQ31qf1ThHAgCc3WiZMdnWcfJCgN82/0UunxA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "Namotion.Reflection": { + "type": "Transitive", + "resolved": "1.0.14", + "contentHash": "wuJGiFvGfehH2w7jAhMbCJt0/rvUuHyqSZn0sMhNTviDfBZRyX8LFlR/ndQcofkGWulPDfH5nKYTeGXE8xBHPA==", + "dependencies": { + "Microsoft.CSharp": "4.3.0" + } + }, + "NETStandard.Library": { + "type": "Transitive", + "resolved": "1.6.1", + "contentHash": "WcSp3+vP+yHNgS8EV5J7pZ9IRpeDuARBPN28by8zqff1wJQXm26PVU8L3/fYLBJVU7BtDyqNVWq2KlCVvSSR4A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.Win32.Primitives": "4.3.0", + "System.AppContext": "4.3.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Console": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.Compression.ZipFile": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.Net.Http": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Net.Sockets": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.InteropServices.RuntimeInformation": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Timer": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0", + "System.Xml.XDocument": "4.3.0" + } + }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "12.0.2", + "contentHash": "rTK0s2EKlfHsQsH6Yx2smvcTCeyoDNgCW7FEYyV01drPlh2T243PR2DiDXqtC5N4GDm4Ma/lkxfW5a/4793vbA==" + }, + "Newtonsoft.Json.Bson": { + "type": "Transitive", + "resolved": "1.0.1", + "contentHash": "5PYT/IqQ+UK31AmZiSS102R6EsTo+LGTSI8bp7WAUqDKaF4wHXD8U9u4WxTI1vc64tYi++8p3dk3WWNqPFgldw==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "Newtonsoft.Json": "10.0.1" + } + }, + "NJsonSchema": { + "type": "Transitive", + "resolved": "10.2.1", + "contentHash": "/BtWbYTusyoSgQkCB4eYijMfZotB/rfASDsl1k9evlkm5vlOP4s4Y09TOzBChU77d/qUABVYL1Xf+TB8E0Wfpw==", + "dependencies": { + "Namotion.Reflection": "1.0.14", + "Newtonsoft.Json": "9.0.1" + } + }, + "NSwag.Annotations": { + "type": "Transitive", + "resolved": "13.8.2", + "contentHash": "/GO+35CjPYQTPS5/Q8udM5JAMEWVo8JsrkV2Uw3OW4/AJU9iOS7t6WJid6ZlkpLMjnW7oex9mvJ2EZNE4eOG/Q==" + }, + "NSwag.AspNetCore": { + "type": "Transitive", + "resolved": "13.8.2", + "contentHash": "SNGlVSZoMyywBWueZBxl3B/nfaIM0fAcuNhTD/cfMKUn3Cn/Oi8d45HZY5vAPqczvppTbk4cZXyVwWDOfgiPbA==", + "dependencies": { + "Microsoft.AspNetCore.Mvc.Core": "1.0.4", + "Microsoft.AspNetCore.Mvc.Formatters.Json": "1.0.4", + "Microsoft.AspNetCore.StaticFiles": "1.0.4", + "Microsoft.Extensions.ApiDescription.Server": "3.0.0", + "Microsoft.Extensions.FileProviders.Embedded": "1.0.1", + "NSwag.Annotations": "13.8.2", + "NSwag.Core": "13.8.2", + "NSwag.Generation": "13.8.2", + "NSwag.Generation.AspNetCore": "13.8.2", + "System.IO.FileSystem": "4.3.0", + "System.Xml.XPath.XDocument": "4.0.1" + } + }, + "NSwag.Core": { + "type": "Transitive", + "resolved": "13.8.2", + "contentHash": "Hm6pU9qFJuXLo3b27+JTXztfeuI/15Ob1sDsfUu4rchN0+bMogtn8Lia8KVbcalw/M+hXc0rWTFp5ueP23e+iA==", + "dependencies": { + "NJsonSchema": "10.2.1", + "Newtonsoft.Json": "9.0.1" + } + }, + "NSwag.Generation": { + "type": "Transitive", + "resolved": "13.8.2", + "contentHash": "LBIrpHFRZeMMbqL1hdyGb7r8v+T52aOCARxwfAmzE+MlOHVpjsIxyNSXht9EzBFMbSH0tj7CK2Ugo7bm+zUssg==", + "dependencies": { + "NJsonSchema": "10.2.1", + "NSwag.Core": "13.8.2", + "Newtonsoft.Json": "9.0.1" + } + }, + "NSwag.Generation.AspNetCore": { + "type": "Transitive", + "resolved": "13.8.2", + "contentHash": "0ydVv6OidspZ/MS6qmU8hswGtXwq5YZPg+2a2PHGD6jNp2Fef4j1wC3xa3hplDAq7cK+BgpyDKtvj9+X01+P5g==", + "dependencies": { + "Microsoft.AspNetCore.Mvc.ApiExplorer": "1.0.4", + "Microsoft.AspNetCore.Mvc.Core": "1.0.4", + "Microsoft.AspNetCore.Mvc.Formatters.Json": "1.0.4", + "NJsonSchema": "10.2.1", + "NSwag.Generation": "13.8.2" + } + }, + "NuGet.Frameworks": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "c5JVjuVAm4f7E9Vj+v09Z9s2ZsqFDjBpcsyS3M9xRo0bEdm/LVZSzLxxNvfvAwRiiE8nwe1h2G4OwiwlzFKXlA==" + }, + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "HdSSp5MnJSsg08KMfZThpuLPJpPwE5hBXvHwoKWosyHHfe8Mh5WKT0ylEOf6yNzX6Ngjxe4Whkafh5q7Ymac4Q==" + }, + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "+yH1a49wJMy8Zt4yx5RhJrxO/DBDByAiCzNwiETI+1S4mPdCu0OY4djdciC7Vssk0l22wQaDLrXxXkp+3+7bVA==" + }, + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c3YNH1GQJbfIPJeCnr4avseugSqPrxwIqzthYyZDN6EuOyNOzq+y2KSUfRcXauya1sF4foESTgwM5e1A8arAKw==" + }, + "runtime.native.System": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "c/qWt2LieNZIj1jGnVNsE2Kl23Ya2aSTBuXMD6V7k9KWr6l16Tqdwq+hJScEpWER9753NWC8h96PaVNY5Ld7Jw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "INBPonS5QPEgn7naufQFXJEp3zX6L4bwHgJ/ZH78aBTpeNfQMtf7C6VrAFhlq2xxWBveIOWyFzQjJ8XzHMhdOQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZVuZJqnnegJhd2k/PtAbbIcZ3aZeITq3sj06oKfMBSfphW3HDmk/t4ObvbOk/JA/swGR0LNqMksAh/f7gpTROg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DloMk88juo0OuOWr56QG7MNchmafTLYWvABy36izkrLI5VledI0rq28KGs1i9wbpeT9NPQrx/wTf8U2vazqQ3Q==", + "dependencies": { + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": "4.3.0" + } + }, + "runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "NS1U+700m4KFRHR5o4vo9DSlTmlCKu/u7dtE5sUHVIPB+xpXxYQvgBgA6wEIeCz6Yfn0Z52/72WYsToCEPJnrw==", + "dependencies": { + "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.23-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.fedora.24-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0", + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "runtime.opensuse.13.2-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "b3pthNgxxFcD+Pc0WSEoC0+md3MyhRS6aCEeenvNE3Fdw1HyJ18ZhRFVJJzIeR/O/jpxPboB805Ho0T3Ul7w8A==" + }, + "runtime.opensuse.42.1-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KeLz4HClKf+nFS7p/6Fi/CqyLXh81FpiGzcmuS8DGi9lUqSnZ6Es23/gv2O+1XVGfrbNmviF7CckBpavkBoIFQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.Apple": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kVXCuMTrTlxq4XOOMAysuNwsXWpYeboGddNGpIgNSZmv1b6r/s/DPk0fYMB7Q5Qo4bY68o48jt4T4y5BVecbCQ==" + }, + "runtime.osx.10.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X7IdhILzr4ROXd8mI1BUCQMSHSQwelUlBjF1JyTKCjXaOGn2fB4EKBxQbCK2VjO3WaWIdlXZL3W6TiIVnrhX4g==" + }, + "runtime.rhel.7-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "nyFNiCk/r+VOiIqreLix8yN+q3Wga9+SE8BCgkf+2BwEKiNx6DyvFjCgkfV743/grxv8jHJ8gUK4XEQw7yzRYg==" + }, + "runtime.ubuntu.14.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ytoewC6wGorL7KoCAvRfsgoJPJbNq+64k2SqW6JcOAebWsFUvCCYgfzQMrnpvPiEl4OrblUlhF2ji+Q1+SVLrQ==" + }, + "runtime.ubuntu.16.04-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "I8bKw2I8k58Wx7fMKQJn2R8lamboCAiHfHeV/pS65ScKWMMI0+wJkLYlEKvgW1D/XvSl/221clBoR2q9QNNM7A==" + }, + "runtime.ubuntu.16.10-x64.runtime.native.System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VB5cn/7OzUfzdnC8tqAIMQciVLiq2epm2NrAm1E9OjNRyG4lVhfR61SMcLizejzQP8R8Uf/0l5qOIbUEi+RdEg==" + }, + "SixLabors.ImageSharp": { + "type": "Transitive", + "resolved": "1.0.1", + "contentHash": "DjLoFNdUfsDP7RhPpr5hcUhl1XiejqBML9uDWuOUwCkc0Y+sG9IJLLbqSOi9XeoWqPviwdcDm1F8nKdF0qTYIQ==" + }, + "SQLitePCLRaw.bundle_e_sqlite3": { + "type": "Transitive", + "resolved": "2.0.2", + "contentHash": "OVPI/nh5AqfLCIKhAYqjCa6AHhc7oKApGcGM3UhMRSerFiBx58nSpGwxVFdMgjOCWZR+fA49nzsnKlWp5hFo8w==", + "dependencies": { + "SQLitePCLRaw.core": "2.0.2", + "SQLitePCLRaw.lib.e_sqlite3": "2.0.2", + "SQLitePCLRaw.provider.dynamic_cdecl": "2.0.2" + } + }, + "SQLitePCLRaw.core": { + "type": "Transitive", + "resolved": "2.0.2", + "contentHash": "TFSBX426OelS1tkaVC254NVVlrJIe9YLhWPkEvuqJj2104QpmDmEYOhfdfDJD1E/2SmqDhoRw1ek5cQHj8olcQ==", + "dependencies": { + "System.Memory": "4.5.3" + } + }, + "SQLitePCLRaw.lib.e_sqlite3": { + "type": "Transitive", + "resolved": "2.0.2", + "contentHash": "S+Tsqe/M7wsc+9HeediI6UHtBKf2X586aRwhi1aBVLGe0WxkAo52O9ZxwEy/v8XMLefcrEMupd2e9CDlIT6QCw==" + }, + "SQLitePCLRaw.provider.dynamic_cdecl": { + "type": "Transitive", + "resolved": "2.0.2", + "contentHash": "ZSwacbKJUsxJEZxwT23uZVrGbaIvXcADZDz5Sr66fikO5eehdcceDncjzwzTzWfW13di8gpTpstx3WJSt/Ci5Q==", + "dependencies": { + "SQLitePCLRaw.core": "2.0.2" + } + }, + "System.AppContext": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "fKC+rmaLfeIzUhagxY17Q9siv/sPrjjKcfNg1Ic8IlQkZLipo8ljcaZQu4VtI4Jqbzjc2VTjzGLF6WmsRXAEgA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Buffers": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ratu44uTIHgeBeI0dE8DWvmXVBSo4u7ozRZZHOMmK/JPpYyo0dAfgSiHlpiObMQ5lEtEyIXA40sKRYg5J6A8uQ==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Collections": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3Dcj85/TBdVpL5Zr+gEEBUuFe2icOnLalmEh9hfck1PTYbbyWuZgh4fmm2ysCLTrqLQw6t3TgTyJ+VLp+Qb+Lw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Collections.Concurrent": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ztl69Xp0Y/UXCL+3v3tEU+lIy+bvjKNUmopn1wep/a291pVPK7dxBd6T7WnlQqRog+d1a/hSsgRsmFnIBKTPLQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "1.7.1", + "contentHash": "B43Zsz5EfMwyEbnObwRxW5u85fzJma3lrDeGcSAV1qkhSRTNY5uXAByTn9h9ddNdhM+4/YoLc/CI43umjwIl9Q==" + }, + "System.Collections.NonGeneric": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "prtjIEMhGUnQq6RnPEYLpFt8AtLbp9yq2zxOSrY7KJJZrw25Fi97IzBqY7iqssbM61Ek5b8f3MG/sG1N2sN5KA==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Collections.Specialized": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Epx8PoVZR0iuOnJJDzp7pWvdfMMOAvpUo95pC4ScH2mJuXkKA2Y4aR3cG9qt2klHgSons1WFh4kcGW7cSXvrxg==", + "dependencies": { + "System.Collections.NonGeneric": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Extensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.ComponentModel": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VyGn1jGRZVfxnh8EdvDCi71v3bMXrsu8aYJOwoV7SNDLVhiEqwP86pPMyRGsDsxhXAm2b3o9OIqeETfN5qfezw==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.ComponentModel.Annotations": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "0YFqjhp/mYkDGpU0Ye1GjE53HMp9UVfGN7seGpAMttAC0C40v5gw598jCgpbBLMmCo0E5YRLBv5Z2doypO49ZQ==" + }, + "System.ComponentModel.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "j8GUkCpM8V4d4vhLIIoBLGey2Z5bCkMVNjEZseyAlm4n5arcsJOeI3zkUP+zvZgzsbLTYh4lYeP/ZD/gdIAPrw==", + "dependencies": { + "System.ComponentModel": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.ComponentModel.TypeConverter": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "16pQ6P+EdhcXzPiEK4kbA953Fu0MNG2ovxTZU81/qsCd1zPRsKc3uif5NgvllCY598k6bI0KUyKW8fanlfaDQg==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Collections.NonGeneric": "4.3.0", + "System.Collections.Specialized": "4.3.0", + "System.ComponentModel": "4.3.0", + "System.ComponentModel.Primitives": "4.3.0", + "System.Globalization": "4.3.0", + "System.Linq": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Configuration.ConfigurationManager": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "gWwQv/Ug1qWJmHCmN17nAbxJYmQBM/E94QxKLksvUiiKB1Ld3Sc/eK1lgmbSjDFxkQhVuayI/cGFZhpBSodLrg==", + "dependencies": { + "System.Security.Cryptography.ProtectedData": "4.4.0" + } + }, + "System.Console": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "DHDrIxiqk1h03m6khKWV2X8p/uvN79rgSqpilL6uzpmSfxfU5ng8VcPtW4qsDsQDHiTv6IPV9TmD5M/vElPNLg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Diagnostics.Contracts": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "HvQQjy712vnlpPxaloZYkuE78Gn353L0SJLJVeLcNASeg9c4qla2a1Xq8I7B3jZoDzKPtHTkyVO7AZ5tpeQGuA==", + "dependencies": { + "System.Runtime": "4.1.0" + } + }, + "System.Diagnostics.Debug": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "ZUhUOdqmaG5Jk3Xdb8xi5kIyQYAA4PnTNlHx1mu9ZY3qv4ELIdKbnL/akbGaKi2RnNUWaZsAs31rvzFdewTj2g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.DiagnosticSource": { + "type": "Transitive", + "resolved": "4.7.1", + "contentHash": "j81Lovt90PDAq8kLpaJfJKV/rWdWuEk6jfV+MBkee33vzYLEUsy4gXK8laa9V2nZlLM9VM9yA/OOQxxPEJKAMw==" + }, + "System.Diagnostics.Tools": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "UUvkJfSYJMM6x527dJg2VyWPSRqIVB0Z7dbjHst1zmwTXz5CcXSYJFWRpuigfbO1Lf7yfZiIaEUesfnl/g5EyA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Diagnostics.TraceSource": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VnYp1NxGx8Ww731y2LJ1vpfb/DKVNKEZ8Jsh5SgQTZREL/YpWRArgh9pI8CDLmgHspZmLL697CaLvH85qQpRiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0" + } + }, + "System.Diagnostics.Tracing": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rswfv0f/Cqkh78rA5S8eN8Neocz234+emGCtTF3lxPY96F+mmmUen6tbn0glN6PMvlKQb9bPAY5e9u7fgPTkKw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Dynamic.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "SNVi1E/vfWUAs/WYKhE9+qlS6KqK0YVhnlT0HQtr8pMIA8YX3lwy3uPMownDwdYISBdmAF/2holEIldVp85Wag==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Linq": "4.3.0", + "System.Linq.Expressions": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Globalization": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Calendars": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GUlBtdOWT4LTV3I+9/PJW+56AnnChTaOqqTLFtdmype/L500M2LIyXgmtd9X2P2VOkmJd5c67H5SaC2QcL1bFA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Globalization.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "FhKmdR6MPG+pxow6wGtNAWdZh7noIOpdD5TwQ3CprzgIE1bBBoim0vbR1+AWsWjQmU7zXHgQo4TWSP6lCeiWcQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0" + } + }, + "System.IdentityModel.Tokens.Jwt": { + "type": "Transitive", + "resolved": "6.8.0", + "contentHash": "5tBCjAub2Bhd5qmcd0WhR5s354e4oLYa//kOWrkX+6/7ZbDDJjMTfwLSOiZ/MMpWdE4DWPLOfTLOq/juj9CKzA==", + "dependencies": { + "Microsoft.IdentityModel.JsonWebTokens": "6.8.0", + "Microsoft.IdentityModel.Tokens": "6.8.0" + } + }, + "System.IO": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.Compression": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YHndyoiV90iu4iKG115ibkhrG+S3jBm8Ap9OwoUAzO5oPDAWcr0SFwQFm0HjM8WkEZWo0zvLTyLmbvTkW1bXgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Buffers": "4.3.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.IO.Compression": "4.3.0" + } + }, + "System.IO.Compression.ZipFile": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "G4HwjEsgIwy3JFBduZ9quBkAu+eUwjIdJleuNSgmUojbH6O3mlvEIme+GHx/cLlTAPcrnnL7GqvB9pTlWRfhOg==", + "dependencies": { + "System.Buffers": "4.3.0", + "System.IO": "4.3.0", + "System.IO.Compression": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.IO.FileSystem": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "3wEMARTnuio+ulnvi+hkRNROYwa1kylvYahhcLk4HSoVdl+xxTFVeVlYOfLwrDPImGls0mDqbMhrza8qnWPTdA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.IO.FileSystem.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "6QOb2XFLch7bEc4lIcJH49nJN2HV+OC3fHDgsLVsBVBk3Y4hFAnOBGzJ2lUu7CyDDFo9IBWkSsnbkT6IBwwiMw==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.IO.Pipelines": { + "type": "Transitive", + "resolved": "4.7.3", + "contentHash": "zykThu9scJyg2Yeg27GMZCbjzniIsmjtNP5x6kQCd/8rEeKXRy20fP2NOMS7xQ+0pS/E85LZQA+K1aoQLxiUdw==" + }, + "System.Linq": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5DbqIUpsDp0dFftytzuMmc0oeMdQwjcP/EWxsksIz/w1TcFRkZ3yKKz0PqiYFMmEwPSWw+qNVqD7PJ889JzHbw==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Linq.Expressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "PGKkrd2khG4CnlyJwxwwaWWiSiWFNBGlgXvJpeO0xCXrZ89ODrQ6tjEWS/kOqZ8GwEOUATtKtzp1eRgmYNfclg==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Linq": "4.3.0", + "System.ObjectModel": "4.3.0", + "System.Reflection": "4.3.0", + "System.Reflection.Emit": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Emit.Lightweight": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Reflection.TypeExtensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Memory": { + "type": "Transitive", + "resolved": "4.5.3", + "contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA==" + }, + "System.Net.Http": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "sYg+FtILtRQuYWSIAuNOELwVuVsxVyJGWQyOnlAzhV4xvhyFnON1bAzYYC+jjRW8JREM45R0R5Dgi8MTC5sEwA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.DiagnosticSource": "4.3.0", + "System.Diagnostics.Tracing": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Extensions": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Security.Cryptography.X509Certificates": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Net.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "qOu+hDwFwoZPbzPvwut2qATe3ygjeQBDQj91xlsaqGFQUI5i4ZnZb8yyQuLGpDGivEPIt8EJkd1BVzVoP31FXA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Net.Sockets": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "m6icV6TqQOAdgt5N/9I5KNpjom/5NFtkmGseEH+AK/hny8XrytLH3+b5M8zL/Ycg3fhIocFpUMyl/wpFnVRvdw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Net.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Net.WebSockets": { + "type": "Transitive", + "resolved": "4.0.0", + "contentHash": "2KJo8hir6Edi9jnMDAMhiJoI691xRBmKcbNpwjrvpIMOCTYOtBpSsSEGBxBDV7PKbasJNaFp1+PZz1D7xS41Hg==", + "dependencies": { + "Microsoft.Win32.Primitives": "4.0.1", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Threading.Tasks": "4.0.11" + } + }, + "System.ObjectModel": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "bdX+80eKv9bN6K4N+d77OankKHGn6CH711a6fcOpMQu2Fckp/Ft4L/kW9WznHpyR0NRAvJutzOMHNNlBGvxQzQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Reflection": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "VR4kk8XLKebQ4MZuKuIni/7oh+QGFmZW3qORd1GvBq/8026OpW501SzT/oypwiQl4TvT8ErnReh/NzY9u+C6wQ==" + }, + "System.Reflection.Emit.ILGeneration": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "59tBslAk9733NXLrUJrwNZEzbMAcu8k344OYo+wfSVygcgZ9lgBdGIzH/nrg3LYhXceynyvTc8t5/GD4Ri0/ng==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Emit.Lightweight": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "oadVHGSMsTmZsAF864QYN1t1QzZjIcuKU3l2S9cZOwDdDueNTrqq1yRj7koFfIGEnKpt6NjpL3rOzRhs4ryOgA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Emit.ILGeneration": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "rJkrJD3kBI5B712aRu4DpSIiHRtr6QlfZSQsb0hYHrDCZORXCFjQfoipo2LaMUHoT9i1B7j7MnfaEKWDFmFQNQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Reflection.TypeExtensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7u6ulLcZbyxB5Gq0nMkQttcdBTx57ibzw+4IOXEfR+sXYQoHvjW5LTLyNr8O22UIMrqYbchJQJnos4eooYzYJA==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Resources.ResourceManager": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Globalization": "4.3.0", + "System.Reflection": "4.3.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0" + } + }, + "System.Runtime.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.Handles": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "OKiSUN7DmTWeYb3l51A7EYaeNMnvxwE249YtZz7yooT4gOZhmTjIn48KgSsw2k2lYdLgTKNJw/ZIfSElwDRVgg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Runtime.InteropServices": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "uv1ynXqiMK8mp1GM3jDqPCFN66eJ5w5XNomaK2XD+TuCroNTLFGeZ+WCmBMcBDyTFKou3P6cR6J/QsaqDp7fGQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Reflection": "4.3.0", + "System.Reflection.Primitives": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Handles": "4.3.0" + } + }, + "System.Runtime.InteropServices.RuntimeInformation": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "cbz4YJMqRDR7oLeMRbdYv7mYzc++17lNhScCX0goO2XpGWdvAt60CGN+FHdePUEHCe/Jy9jUlvNAiNdM+7jsOw==", + "dependencies": { + "System.Reflection": "4.3.0", + "System.Reflection.Extensions": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0" + } + }, + "System.Runtime.Numerics": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "yMH+MfdzHjy17l2KESnPiF2dwq7T+xLnSJar7slyimAkUh/gTrS9/UQOtv7xarskJ2/XDSNvfLGOBQPjL7PaHQ==", + "dependencies": { + "System.Globalization": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0" + } + }, + "System.Runtime.Serialization.Primitives": { + "type": "Transitive", + "resolved": "4.1.1", + "contentHash": "HZ6Du5QrTG8MNJbf4e4qMO3JRAkIboGT5Fk804uZtg3Gq516S7hAqTm2UZKUHa7/6HUGdVy3AqMQKbns06G/cg==", + "dependencies": { + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0" + } + }, + "System.Security.Claims": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "4Jlp0OgJLS/Voj1kyFP6MJlIYp3crgfH8kNQk2p7+4JYfc1aAmh9PZyAMMbDhuoolGNtux9HqSOazsioRiDvCw==", + "dependencies": { + "System.Collections": "4.0.11", + "System.Globalization": "4.0.11", + "System.IO": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Security.Principal": "4.0.1" + } + }, + "System.Security.Cryptography.Algorithms": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "W1kd2Y8mYSCgc3ULTAZ0hOP2dSdG5YauTb1089T0/kRcN2MpSAW1izOFROrJgxSlMn3ArsgHXagigyi+ibhevg==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.Apple": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Cng": { + "type": "Transitive", + "resolved": "4.5.0", + "contentHash": "WG3r7EyjUe9CMPFSs6bty5doUqT+q9pbI80hlNzo2SkPkZ4VTuZkGWjpp77JB8+uaL4DFPRdBsAY+DX3dBK92A==" + }, + "System.Security.Cryptography.Csp": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "X4s/FCkEUnRGnwR3aSfVIkldBmtURMhmexALNTwpjklzxWU7yjMk7GHLKOZTNkgnWnE0q7+BCf9N2LVRWxewaA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0" + } + }, + "System.Security.Cryptography.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "1DEWjZZly9ae9C79vFwqaO5kaOlI5q+3/55ohmq/7dpDyDfc8lYe7YVxJUZ5MF/NtbkRjwFRo14yM4OEo9EmDw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Collections.Concurrent": "4.3.0", + "System.Linq": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.OpenSsl": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "h4CEgOgv5PKVF/HwaHzJRiVboL2THYCou97zpmhjghx5frc7fIvlkY1jL+lnIQyChrJDMNEXS6r7byGif8Cy4w==", + "dependencies": { + "System.Collections": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Cryptography.Primitives": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "7bDIyVFNL/xKeFHjhobUAQqSpJq9YTOpbEs6mR233Et01STBMXNAc/V+BM6dwYGc95gVh/Zf+iVXWzj3mE8DWg==", + "dependencies": { + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Threading": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Security.Cryptography.ProtectedData": { + "type": "Transitive", + "resolved": "4.4.0", + "contentHash": "cJV7ScGW7EhatRsjehfvvYVBvtiSMKgN8bOVI0bQhnF5bU7vnHVIsH49Kva7i7GWaWYvmEzkYVk1TC+gZYBEog==" + }, + "System.Security.Cryptography.X509Certificates": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "t2Tmu6Y2NtJ2um0RtcuhP7ZdNNxXEgUm2JeoA/0NvlMjAhKCnM1NX07TDl3244mVp3QU6LPEhT3HTtH1uF7IYw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.Globalization.Calendars": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.Handles": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Runtime.Numerics": "4.3.0", + "System.Security.Cryptography.Algorithms": "4.3.0", + "System.Security.Cryptography.Cng": "4.3.0", + "System.Security.Cryptography.Csp": "4.3.0", + "System.Security.Cryptography.Encoding": "4.3.0", + "System.Security.Cryptography.OpenSsl": "4.3.0", + "System.Security.Cryptography.Primitives": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "runtime.native.System": "4.3.0", + "runtime.native.System.Net.Http": "4.3.0", + "runtime.native.System.Security.Cryptography.OpenSsl": "4.3.0" + } + }, + "System.Security.Principal": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "On+SKhXY5rzxh/S8wlH1Rm0ogBlu7zyHNxeNBiXauNrhHRXAe9EuX8Yl5IOzLPGU5Z4kLWHMvORDOCG8iu9hww==", + "dependencies": { + "System.Runtime": "4.1.0" + } + }, + "System.Text.Encoding": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Text.Encoding.Extensions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "YVMK0Bt/A43RmwizJoZ22ei2nmrhobgeiYwFzC4YAN+nue8RF6djXDMog0UCn+brerQoYVyaS+ghy9P/MUVcmw==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0", + "System.Text.Encoding": "4.3.0" + } + }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "GgJDO6/1bW6kkttxIiPK2jsqllQ3ifaeeBAJJrcoJq0lAclIZsAZZdEqi6JHq+QLZXL2UsjyWb8K8EOH7nOSPw==", + "dependencies": { + "System.Diagnostics.Debug": "4.0.11", + "System.IO": "4.1.0", + "System.Reflection": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Threading": "4.0.11" + } + }, + "System.Text.Json": { + "type": "Transitive", + "resolved": "4.7.2", + "contentHash": "TcMd95wcrubm9nHvJEQs70rC0H/8omiSGGpU4FQ/ZA1URIqD4pjmFJh2Mfv1yH1eHgJDWTi2hMDXwTET+zOOyg==" + }, + "System.Text.RegularExpressions": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "RpT2DA+L660cBt1FssIE9CAGpLFdFPuheB7pLpKpn6ZXNby7jDERe8Ua/Ne2xGiwLVG2JOqziiaVCGDon5sKFA==", + "dependencies": { + "System.Runtime": "4.3.0" + } + }, + "System.Threading": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "VkUS0kOBcUf3Wwm0TSbrevDDZ6BlM+b/HRiapRFWjM5O0NS0LviG0glKmFK+hhPDd1XFeSdU1GmlLhb2CoVpIw==", + "dependencies": { + "System.Runtime": "4.3.0", + "System.Threading.Tasks": "4.3.0" + } + }, + "System.Threading.Tasks": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.5.1", + "contentHash": "WSKUTtLhPR8gllzIWO2x6l4lmAIfbyMAiTlyXAis4QBDonXK4b4S6F8zGARX4/P8wH3DH+sLdhamCiHn+fTU1A==" + }, + "System.Threading.Timer": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "Z6YfyYTCg7lOZjJzBjONJTFKGN9/NIYKSxhU5GRd+DTwHSZyvWp1xuI5aR+dLg+ayyC5Xv57KiY4oJ0tMO89fQ==", + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0", + "Microsoft.NETCore.Targets": "1.1.0", + "System.Runtime": "4.3.0" + } + }, + "System.Xml.ReaderWriter": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "GrprA+Z0RUXaR4N7/eW71j1rgMnEnEVlgii49GZyAjTH7uliMnrOU3HNFBr6fEDBCJCIdlVNq9hHbaDR621XBA==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.IO.FileSystem": "4.3.0", + "System.IO.FileSystem.Primitives": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Runtime.InteropServices": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Text.Encoding.Extensions": "4.3.0", + "System.Text.RegularExpressions": "4.3.0", + "System.Threading.Tasks": "4.3.0", + "System.Threading.Tasks.Extensions": "4.3.0" + } + }, + "System.Xml.XDocument": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "5zJ0XDxAIg8iy+t4aMnQAu0MqVbqyvfoUVl1yDV61xdo3Vth45oA2FoY4pPkxYAH5f8ixpmTqXeEIya95x0aCQ==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Diagnostics.Tools": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Reflection": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0" + } + }, + "System.Xml.XmlDocument": { + "type": "Transitive", + "resolved": "4.3.0", + "contentHash": "lJ8AxvkX7GQxpC6GFCeBj8ThYVyQczx2+f/cWHJU8tjS7YfI6Cv6bon70jVEgs2CiFbmmM8b9j1oZVx0dSI2Ww==", + "dependencies": { + "System.Collections": "4.3.0", + "System.Diagnostics.Debug": "4.3.0", + "System.Globalization": "4.3.0", + "System.IO": "4.3.0", + "System.Resources.ResourceManager": "4.3.0", + "System.Runtime": "4.3.0", + "System.Runtime.Extensions": "4.3.0", + "System.Text.Encoding": "4.3.0", + "System.Threading": "4.3.0", + "System.Xml.ReaderWriter": "4.3.0" + } + }, + "System.Xml.XPath": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "UWd1H+1IJ9Wlq5nognZ/XJdyj8qPE4XufBUkAW59ijsCPjZkZe0MUzKKJFBr+ZWBe5Wq1u1d5f2CYgE93uH7DA==", + "dependencies": { + "System.Collections": "4.0.11", + "System.Diagnostics.Debug": "4.0.11", + "System.Globalization": "4.0.11", + "System.IO": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Threading": "4.0.11", + "System.Xml.ReaderWriter": "4.0.11" + } + }, + "System.Xml.XPath.XDocument": { + "type": "Transitive", + "resolved": "4.0.1", + "contentHash": "FLhdYJx4331oGovQypQ8JIw2kEmNzCsjVOVYY/16kZTUoquZG85oVn7yUhBE2OZt1yGPSXAL0HTEfzjlbNpM7Q==", + "dependencies": { + "System.Diagnostics.Debug": "4.0.11", + "System.Linq": "4.1.0", + "System.Resources.ResourceManager": "4.0.1", + "System.Runtime": "4.1.0", + "System.Runtime.Extensions": "4.1.0", + "System.Threading": "4.0.11", + "System.Xml.ReaderWriter": "4.0.11", + "System.Xml.XDocument": "4.0.11", + "System.Xml.XPath": "4.0.1" + } + }, + "xunit.abstractions": { + "type": "Transitive", + "resolved": "2.0.3", + "contentHash": "pot1I4YOxlWjIb5jmwvvQNbTrZ3lJQ+jUGkGjWE3hEFM0l5gOnBWS+H3qsex68s5cO52g+44vpGzhAt+42vwKg==" + }, + "xunit.analyzers": { + "type": "Transitive", + "resolved": "0.10.0", + "contentHash": "4/IDFCJfIeg6bix9apmUtIMwvOsiwqdEexeO/R2D4GReIGPLIRODTpId/l4LRSrAJk9lEO3Zx1H0Zx6uohJDNg==" + }, + "xunit.assert": { + "type": "Transitive", + "resolved": "2.4.1", + "contentHash": "O/Oe0BS5RmSsM+LQOb041TzuPo5MdH2Rov+qXGS37X+KFG1Hxz7kopYklM5+1Y+tRGeXrOx5+Xne1RuqLFQoyQ==", + "dependencies": { + "NETStandard.Library": "1.6.1" + } + }, + "xunit.core": { + "type": "Transitive", + "resolved": "2.4.1", + "contentHash": "Zsj5OMU6JasNGERXZy8s72+pcheG6Q15atS5XpZXqAtULuyQiQ6XNnUsp1gyfC6WgqScqMvySiEHmHcOG6Eg0Q==", + "dependencies": { + "xunit.extensibility.core": "[2.4.1]", + "xunit.extensibility.execution": "[2.4.1]" + } + }, + "xunit.extensibility.core": { + "type": "Transitive", + "resolved": "2.4.1", + "contentHash": "yKZKm/8QNZnBnGZFD9SewkllHBiK0DThybQD/G4PiAmQjKtEZyHi6ET70QPU9KtSMJGRYS6Syk7EyR2EVDU4Kg==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "xunit.abstractions": "2.0.3" + } + }, + "xunit.extensibility.execution": { + "type": "Transitive", + "resolved": "2.4.1", + "contentHash": "7e/1jqBpcb7frLkB6XDrHCGXAbKN4Rtdb88epYxCSRQuZDRW8UtTfdTEVpdTl8s4T56e07hOBVd4G0OdCxIY2A==", + "dependencies": { + "NETStandard.Library": "1.6.1", + "xunit.extensibility.core": "[2.4.1]" + } + }, + "timeline": { + "type": "Project", + "dependencies": { + "AutoMapper": "10.1.1", + "AutoMapper.Extensions.Microsoft.DependencyInjection": "8.1.0", + "Microsoft.AspNetCore.SpaServices.Extensions": "3.1.9", + "Microsoft.EntityFrameworkCore": "3.1.9", + "Microsoft.EntityFrameworkCore.Analyzers": "3.1.9", + "Microsoft.EntityFrameworkCore.Sqlite": "3.1.9", + "NSwag.AspNetCore": "13.8.2", + "SixLabors.ImageSharp": "1.0.1", + "System.IdentityModel.Tokens.Jwt": "6.8.0", + "Timeline.ErrorCodes": "1.0.0" + } + }, + "timeline.errorcodes": { + "type": "Project" + } + } + } +} \ No newline at end of file -- cgit v1.2.3