diff options
author | crupest <crupest@outlook.com> | 2020-10-27 19:21:35 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2020-10-27 19:21:35 +0800 |
commit | 05ccb4d8f1bbe3fb64e117136b4a89bcfb0b0b33 (patch) | |
tree | 929e514de85eb82a5acb96ecffc6e6d2d95f878f /BackEnd/Timeline.Tests | |
parent | 986c6f2e3b858d6332eba0b42acc6861cd4d0227 (diff) | |
download | timeline-05ccb4d8f1bbe3fb64e117136b4a89bcfb0b0b33.tar.gz timeline-05ccb4d8f1bbe3fb64e117136b4a89bcfb0b0b33.tar.bz2 timeline-05ccb4d8f1bbe3fb64e117136b4a89bcfb0b0b33.zip |
Split front and back end.
Diffstat (limited to 'BackEnd/Timeline.Tests')
28 files changed, 5868 insertions, 0 deletions
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<int, string>();
+
+ 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<AndConstraint<ObjectAssertions>> ThrowAsync(this AsyncFunctionAssertions assertions, Type exceptionType, string because = "", params object[] becauseArgs)
+ {
+ return (await assertions.ThrowAsync<Exception>(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<HttpResponseMessage> PatchAsJsonAsync<T>(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<HttpResponseMessage> PatchAsJsonAsync<T>(this HttpClient client, Uri url, T body)
+ {
+ return client.PatchAsync(url, new StringContent(
+ JsonConvert.SerializeObject(body), Encoding.UTF8, MediaTypeNames.Application.Json));
+ }
+
+ public static Task<HttpResponseMessage> 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<HttpResponseMessage> 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<HttpResponseMessage> 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<HttpResponseMessage> 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<T> ReadBodyAsJsonAsync<T>(this HttpResponseMessage response)
+ {
+ var stream = await response.Content.ReadAsStreamAsync();
+ return await JsonSerializer.DeserializeAsync<T>(stream, JsonSerializerOptions);
+ }
+
+ public static Task<CommonResponse> ReadBodyAsCommonResponseAsync(this HttpResponseMessage response)
+ {
+ return response.ReadBodyAsJsonAsync<CommonResponse>();
+ }
+ }
+}
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<Rgba32>(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<Rgba32>(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<ParameterInfo, ParameterInfoAssertions>
+ {
+ static ParameterInfoAssertions()
+ {
+ Formatter.AddFormatter(new ParameterInfoValueFormatter());
+ }
+
+ public ParameterInfoAssertions(ParameterInfo parameterInfo)
+ {
+ Subject = parameterInfo;
+ }
+
+ protected override string Identifier => "parameter";
+
+ public AndWhichConstraint<ParameterInfoAssertions, TAttribute> BeDecoratedWith<TAttribute>(string because = "", params object[] becauseArgs)
+ where TAttribute : Attribute
+ {
+ var attribute = Subject.GetCustomAttribute<TAttribute>(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<ParameterInfoAssertions, TAttribute>(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<HttpResponseMessage, HttpResponseMessageAssertions>
+ {
+ static HttpResponseMessageAssertions()
+ {
+ Formatter.AddFormatter(new HttpResponseMessageValueFormatter());
+ }
+
+ public HttpResponseMessageAssertions(HttpResponseMessage instance)
+ {
+ Subject = instance;
+ }
+
+ protected override string Identifier => "HttpResponseMessage";
+
+ public AndConstraint<HttpResponseMessageAssertions> HaveStatusCode(int expected, string because = "", params object[] becauseArgs)
+ {
+ return HaveStatusCode((HttpStatusCode)expected, because, becauseArgs);
+ }
+
+ public AndConstraint<HttpResponseMessageAssertions> 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<HttpResponseMessageAssertions>(this);
+ }
+
+ public AndWhichConstraint<HttpResponseMessageAssertions, T> HaveJsonBody<T>(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<HttpResponseMessageAssertions, T>(this, null);
+ }
+
+
+ try
+ {
+ var options = new JsonSerializerOptions
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase
+ };
+ options.Converters.Add(new JsonStringEnumConverter());
+ options.Converters.Add(new JsonDateTimeConverter());
+
+ var result = JsonSerializer.Deserialize<T>(body, options);
+
+ return new AndWhichConstraint<HttpResponseMessageAssertions, T>(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<HttpResponseMessageAssertions, T>(this, null);
+ }
+ }
+ }
+
+ public static class AssertionResponseExtensions
+ {
+ public static HttpResponseMessageAssertions Should(this HttpResponseMessage instance)
+ {
+ return new HttpResponseMessageAssertions(instance);
+ }
+
+ public static AndWhichConstraint<HttpResponseMessageAssertions, CommonResponse> HaveCommonBody(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs)
+ {
+ return assertions.HaveJsonBody<CommonResponse>(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<HttpResponseMessageAssertions, CommonDataResponse<TData>> HaveCommonDataBody<TData>(this HttpResponseMessageAssertions assertions, string because = "", params object[] becauseArgs)
+ {
+ return assertions.HaveJsonBody<CommonDataResponse<TData>>(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<CommonPutResponse>(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<CommonDeleteResponse>(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<string, string>
+ {
+ [ApplicationConfiguration.UseMockFrontEndKey] = "true",
+ ["WorkDir"] = WorkDir
+ });
+ })
+ .ConfigureServices(services =>
+ {
+ services.AddDbContext<DatabaseContext>(options =>
+ {
+ options.UseSqlite(Database.Connection);
+ });
+ })
+ .ConfigureWebHost(webBuilder =>
+ {
+ webBuilder
+ .UseTestServer()
+ .UseStartup<Startup>();
+ })
+ .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<UserService>.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<DatabaseContext>()
+ .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<UserInfo> 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<User>()
+ {
+ 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<UserInfo>();
+
+ var userService = scope.ServiceProvider.GetRequiredService<IUserService>();
+ 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<UserInfo>(s, options));
+ }
+
+ UserInfos = userInfoList;
+ }
+
+ await OnInitializeAsync();
+ }
+
+ public async Task DisposeAsync()
+ {
+ await OnDisposeAsync();
+ OnDispose();
+ await TestApp.DisposeAsync();
+ }
+
+ public Task<HttpClient> 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<HttpClient> 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<CreateTokenResponse>().Which.Token;
+ client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token);
+ return client;
+ }
+
+ public Task<HttpClient> 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<HttpClient> CreateClientAsAdministrator(bool setApiBase = true)
+ {
+ return CreateClientAs(0, setApiBase);
+ }
+
+ public Task<HttpClient> 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<TimelineInfo> _testTimelines;
+
+ private async Task CreateTestTimelines()
+ {
+ _testTimelines = new List<TimelineInfo>();
+ 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<TimelineInfo>().Which;
+ _testTimelines.Add(timelineInfo);
+ }
+ }
+
+ private static string CalculateUrlTail(string subpath, ICollection<KeyValuePair<string, string>> 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<KeyValuePair<string, string>> query = null)
+ {
+ return $"timelines/@{(id == 0 ? "admin" : ("user" + id))}{CalculateUrlTail(subpath, query)}";
+ }
+
+ private static string GenerateOrdinaryTimelineUrl(int id, string subpath = null, ICollection<KeyValuePair<string, string>> query = null)
+ {
+ return $"timelines/t{id}{CalculateUrlTail(subpath, query)}";
+ }
+
+ public delegate string TimelineUrlGenerator(int userId, string subpath = null, ICollection<KeyValuePair<string, string>> query = null);
+
+ public static IEnumerable<object[]> 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<object[]> TimelineUrlByNameGeneratorData()
+ {
+ yield return new[] { new Func<string, string, string>(GeneratePersonalTimelineUrlByName) };
+ yield return new[] { new Func<string, string, string>(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<TimelineInfo>().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<TimelineInfo>().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<TimelineInfo>().Which;
+ }
+
+ {
+ var testResult = new List<TimelineInfo>();
+ testResult.Add(user1Timeline);
+ testResult.AddRange(_testTimelines);
+
+ var res = await client.GetAsync("timelines");
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<List<TimelineInfo>>()
+ .Which.Should().BeEquivalentTo(testResult);
+ }
+ }
+
+ [Fact]
+ public async Task TimelineList_WithQuery()
+ {
+ var testResultRelate = new List<TimelineInfo>();
+ var testResultOwn = new List<TimelineInfo>();
+ var testResultJoin = new List<TimelineInfo>();
+ var testResultOwnPrivate = new List<TimelineInfo>();
+ var testResultRelatePublic = new List<TimelineInfo>();
+ var testResultRelateRegister = new List<TimelineInfo>();
+ var testResultJoinPrivate = new List<TimelineInfo>();
+ var testResultPublic = new List<TimelineInfo>();
+
+ {
+ 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<TimelineInfo>().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<TimelineInfo>().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<TimelineInfo>().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<TimelineInfo>().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<TimelineInfo>().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<TimelineInfo>().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<List<TimelineInfo>>()
+ .Which;
+ body.Should().BeEquivalentTo(testResultRelate);
+ }
+
+ {
+ var res = await client.GetAsync("timelines?relate=user3&relateType=own");
+ var body = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<List<TimelineInfo>>()
+ .Which;
+ body.Should().BeEquivalentTo(testResultOwn);
+ }
+
+ {
+ var res = await client.GetAsync("timelines?relate=user3&visibility=public");
+ var body = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<List<TimelineInfo>>()
+ .Which;
+ body.Should().BeEquivalentTo(testResultRelatePublic);
+ }
+
+ {
+ var res = await client.GetAsync("timelines?relate=user3&visibility=register");
+ var body = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<List<TimelineInfo>>()
+ .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<List<TimelineInfo>>()
+ .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<List<TimelineInfo>>()
+ .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<List<TimelineInfo>>()
+ .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<TimelineInfo>().Which;
+ }
+
+ {
+ var res = await client.GetAsync("timelines/aaa");
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>()
+ .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<string, string, string> 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<string, string, string> 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<TimelineInfo>()
+ .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<TimelineInfo>().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<TimelineInfo>().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<TimelineInfo>().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<UserInfo> members)
+ {
+ var res = await client.GetAsync(getUrl);
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>()
+ .Which.Members.Should().NotBeNull().And.BeEquivalentTo(members);
+ }
+
+ async Task AssertEmptyMembers()
+ {
+ var res = await client.GetAsync(getUrl);
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>()
+ .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<UserInfo> { 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<object[]> 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<long> 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<TimelinePostInfo>()
+ .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<TimelinePostInfo[]>()
+ .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<TimelinePostInfo>()
+ .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<TimelinePostInfo[]>()
+ .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<TimelinePostInfo>()
+ .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<TimelinePostInfo[]>()
+ .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<TimelinePostInfo[]>()
+ .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<long> CreatePost(DateTime time)
+ {
+ var res = await client.PostAsJsonAsync(generator(1, "posts"),
+ TimelineHelper.TextPostCreateRequest("aaa", time));
+ return res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelinePostInfo>()
+ .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<TimelinePostInfo[]>()
+ .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<TimelinePostInfo>().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<TimelinePostInfo[]>().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<TimelinePostInfo[]>()
+ .Which.Should().BeEmpty();
+ }
+
+ {
+ using var scope = TestApp.Host.Services.CreateScope();
+ var database = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
+ 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<TimelinePostInfo>()
+ .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<TimelineInfo>()
+ .Which.LastModified;
+ }
+
+ await Task.Delay(1000);
+
+ {
+ var res = await client.PatchAsJsonAsync(generator(1), new TimelinePatchRequest { Description = "123" });
+ lastModified = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>()
+ .Which.LastModified.Should().BeAfter(lastModified).And.Subject.Value;
+ }
+
+ {
+ var res = await client.GetAsync(generator(1));
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>()
+ .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<TimelineInfo>()
+ .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<string> { "a", "b", "c", "d" };
+ var posts = new List<TimelinePostInfo>();
+
+ 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<TimelinePostInfo>().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<string, string> { { "modifiedSince", posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture) } }));
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<List<TimelinePostInfo>>()
+ .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<string> { "a", "b", "c", "d" };
+ var posts = new List<TimelinePostInfo>();
+
+ 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<TimelinePostInfo>().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<string, string> { ["includeDeleted"] = "true" }));
+ posts = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<List<TimelinePostInfo>>()
+ .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<string> { "a", "b", "c", "d" };
+ var posts = new List<TimelinePostInfo>();
+
+ 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<TimelinePostInfo>().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<string, string> {
+ { "modifiedSince", posts[1].LastUpdated.ToString("s", CultureInfo.InvariantCulture) },
+ { "includeDeleted", "true" }
+ }));
+ posts = res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<List<TimelinePostInfo>>()
+ .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<TimelineInfo>().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<TimelineInfo>()
+ .Which.Should().BeEquivalentTo(timeline);
+ }
+
+ {
+ var res = await client.GetAsync(urlGenerator(1, null,
+ new Dictionary<string, string> { { "ifModifiedSince", lastModifiedTime.AddSeconds(1).ToString("s", CultureInfo.InvariantCulture) } }));
+ res.Should().HaveStatusCode(304);
+ }
+
+ {
+ var res = await client.GetAsync(urlGenerator(1, null,
+ new Dictionary<string, string> { { "ifModifiedSince", lastModifiedTime.AddSeconds(-1).ToString("s", CultureInfo.InvariantCulture) } }));
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>()
+ .Which.Should().BeEquivalentTo(timeline);
+ }
+
+ {
+ var res = await client.GetAsync(urlGenerator(1, null,
+ new Dictionary<string, string> { { "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<string, string> { { "ifModifiedSince", lastModifiedTime.AddSeconds(1).ToString("s", CultureInfo.InvariantCulture) },
+ {"checkUniqueId", testUniqueId } }));
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>()
+ .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<TimelineInfo>()
+ .Which;
+ timeline.Title.Should().Be(timeline.Name);
+ }
+
+ {
+ var res = await client.PatchAsJsonAsync(urlGenerator(1), new TimelinePatchRequest { Title = "atitle" });
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>()
+ .Which.Title.Should().Be("atitle");
+ }
+
+ {
+ var res = await client.GetAsync(urlGenerator(1));
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<TimelineInfo>()
+ .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<TimelineInfo>().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<TimelineInfo>().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<TimelinePostInfo>();
+ 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<CreateTokenResponse> 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<CreateTokenResponse>().Which;
+ }
+
+ public static IEnumerable<object[]> 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<object[]> 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<CreateTokenResponse>().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<IUserService>();
+ 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<IUserDeleteService>();
+ 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<VerifyTokenResponse>()
+ .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<IWebHostEnvironment>();
+ 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<UserInfo[]>()
+ .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<UserInfo[]>()
+ .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<UserInfo[]>()
+ .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<UserInfo>()
+ .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<UserInfo>()
+ .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<UserInfo>()
+ .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<UserInfo>()
+ .Which.Nickname.Should().Be("aaa");
+ }
+
+ {
+ var res = await client.GetAsync("users/user1");
+ res.Should().HaveStatusCode(200)
+ .And.HaveJsonBody<UserInfo>()
+ .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<UserInfo>()
+ .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<UserInfo>()
+ .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<object[]> 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<UserInfo>().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<UserInfo>().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<object[]> 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<object[]> 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<UserService>.Instance, _databaseContext, _passwordService, _clock);
+ _timelineService = new TimelineService(NullLogger<TimelineService>.Instance, _databaseContext, _dataManager, _userService, _imageValidator, _clock);
+ _userDeleteService = new UserDeleteService(NullLogger<UserDeleteService>.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<string> { "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<ArgumentException>();
+ await _timelineService.Awaiting(s => s.ChangeTimelineName("tl", "!!!")).Should().ThrowAsync<ArgumentException>();
+ await _timelineService.Awaiting(s => s.ChangeTimelineName("tl", "newtl")).Should().ThrowAsync<TimelineNotExistException>();
+
+ 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<EntityAlreadyExistException>();
+
+ 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 @@ +<Project Sdk="Microsoft.NET.Sdk.Web">
+
+ <PropertyGroup>
+ <TargetFramework>netcoreapp3.1</TargetFramework>
+
+ <LangVersion>8.0</LangVersion>
+ </PropertyGroup>
+
+ <ItemGroup>
+ <PackageReference Include="coverlet.collector" Version="1.3.0">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="FluentAssertions" Version="5.10.3" />
+ <PackageReference Include="JunitXml.TestLogger" Version="2.1.78" />
+ <PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
+ <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="3.1.9" />
+ <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.3.0">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
+ </PackageReference>
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
+ <PackageReference Include="Moq" Version="4.14.7" />
+ <PackageReference Include="xunit" Version="2.4.1" />
+ <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
+ </ItemGroup>
+
+ <ItemGroup>
+ <ProjectReference Include="..\Timeline\Timeline.csproj" />
+ </ItemGroup>
+</Project>
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<UsernameValidator>
+ {
+ 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 @@ +<?xml version="1.0" encoding="utf-8" ?>
+<RunSettings>
+ <DataCollectionRunSettings>
+ <DataCollectors>
+ <DataCollector friendlyName="XPlat code coverage">
+ <Configuration>
+ <!-- [Assembly-Filter]Type-Filter -->
+ <Exclude>[xunit.*]*,[Timeline]Timeline.Migrations.*</Exclude>
+ </Configuration>
+ </DataCollector>
+ </DataCollectors>
+ </DataCollectionRunSettings>
+</RunSettings>
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 |