aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2019-04-21 00:08:59 +0800
committercrupest <crupest@outlook.com>2019-04-21 00:08:59 +0800
commit076e0131dff71a9f76fff13c92fffa0ef408935f (patch)
tree9fa853690866f452a571b45fb062ac692d298a2e
parent325d4c7dbfba45e9c5a7518279831f54c4690d20 (diff)
downloadtimeline-076e0131dff71a9f76fff13c92fffa0ef408935f.tar.gz
timeline-076e0131dff71a9f76fff13c92fffa0ef408935f.tar.bz2
timeline-076e0131dff71a9f76fff13c92fffa0ef408935f.zip
Reorgnize api. Add basic unit test.
-rw-r--r--Timeline.Tests/AuthorizationUnitTest.cs14
-rw-r--r--Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs (renamed from Timeline.Tests/Helpers/Authentication/AuthenticationHttpClientExtensions.cs)15
-rw-r--r--Timeline.Tests/Helpers/TestUsers.cs40
-rw-r--r--Timeline.Tests/Helpers/WebApplicationFactoryExtensions.cs21
-rw-r--r--Timeline.Tests/JwtTokenUnitTest.cs4
-rw-r--r--Timeline.Tests/Timeline.Tests.csproj2
-rw-r--r--Timeline.Tests/UserUnitTest.cs36
-rw-r--r--Timeline/Configs/DatabaseConfig.cs7
-rw-r--r--Timeline/Controllers/AdminUserController.cs83
-rw-r--r--Timeline/Controllers/TokenController.cs74
-rw-r--r--Timeline/Controllers/UserController.cs93
-rw-r--r--Timeline/Entities/AdminUser.cs14
-rw-r--r--Timeline/Entities/UserInfo.cs64
-rw-r--r--nuget.config3
14 files changed, 293 insertions, 177 deletions
diff --git a/Timeline.Tests/AuthorizationUnitTest.cs b/Timeline.Tests/AuthorizationUnitTest.cs
index e450af06..28715ada 100644
--- a/Timeline.Tests/AuthorizationUnitTest.cs
+++ b/Timeline.Tests/AuthorizationUnitTest.cs
@@ -26,7 +26,7 @@ namespace Timeline.Tests
{
using (var client = _factory.CreateDefaultClient())
{
- var response = await client.GetAsync(NeedAuthorizeUrl);
+ var response = await client.GetAsync(NeedAuthorizeUrl);
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
}
@@ -34,10 +34,9 @@ namespace Timeline.Tests
[Fact]
public async Task AuthenticationTest()
{
- using (var client = _factory.CreateDefaultClient())
+ using (var client = await _factory.CreateClientWithUser("user", "user"))
{
- var token = (await client.CreateUserTokenAsync("user", "user")).Token;
- var response = await client.SendWithAuthenticationAsync(token, NeedAuthorizeUrl);
+ var response = await client.GetAsync(NeedAuthorizeUrl);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
}
@@ -58,12 +57,11 @@ namespace Timeline.Tests
[Fact]
public async Task AdminAuthorizationTest()
{
- using (var client = _factory.CreateDefaultClient())
+ using (var client = await _factory.CreateClientWithUser("admin", "admin"))
{
- var token = (await client.CreateUserTokenAsync("admin", "admin")).Token;
- var response1 = await client.SendWithAuthenticationAsync(token, BothUserAndAdminUrl);
+ var response1 = await client.GetAsync(BothUserAndAdminUrl);
Assert.Equal(HttpStatusCode.OK, response1.StatusCode);
- var response2 = await client.SendWithAuthenticationAsync(token, OnlyAdminUrl);
+ var response2 = await client.GetAsync(OnlyAdminUrl);
Assert.Equal(HttpStatusCode.OK, response2.StatusCode);
}
}
diff --git a/Timeline.Tests/Helpers/Authentication/AuthenticationHttpClientExtensions.cs b/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs
index c0051c53..40191009 100644
--- a/Timeline.Tests/Helpers/Authentication/AuthenticationHttpClientExtensions.cs
+++ b/Timeline.Tests/Helpers/Authentication/AuthenticationExtensions.cs
@@ -1,4 +1,5 @@
-using Newtonsoft.Json;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Newtonsoft.Json;
using System;
using System.Net;
using System.Net.Http;
@@ -8,9 +9,9 @@ using Xunit;
namespace Timeline.Tests.Helpers.Authentication
{
- public static class AuthenticationHttpClientExtensions
+ public static class AuthenticationExtensions
{
- private const string CreateTokenUrl = "/User/CreateToken";
+ private const string CreateTokenUrl = "/token/create";
public static async Task<CreateTokenResponse> CreateUserTokenAsync(this HttpClient client, string username, string password, bool assertSuccess = true)
{
@@ -24,6 +25,14 @@ namespace Timeline.Tests.Helpers.Authentication
return result;
}
+ public static async Task<HttpClient> CreateClientWithUser<T>(this WebApplicationFactory<T> factory, string username, string password) where T : class
+ {
+ var client = factory.CreateDefaultClient();
+ var token = (await client.CreateUserTokenAsync(username, password)).Token;
+ client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token);
+ return client;
+ }
+
public static async Task<HttpResponseMessage> SendWithAuthenticationAsync(this HttpClient client, string token, string path, Action<HttpRequestMessage> requestBuilder = null)
{
var request = new HttpRequestMessage
diff --git a/Timeline.Tests/Helpers/TestUsers.cs b/Timeline.Tests/Helpers/TestUsers.cs
new file mode 100644
index 00000000..b7005d54
--- /dev/null
+++ b/Timeline.Tests/Helpers/TestUsers.cs
@@ -0,0 +1,40 @@
+using System.Collections.Generic;
+using System.Linq;
+using Timeline.Entities;
+using Timeline.Models;
+using Timeline.Services;
+
+namespace Timeline.Tests.Helpers
+{
+ public static class TestMockUsers
+ {
+ static TestMockUsers()
+ {
+ var mockUsers = new List<User>();
+ var passwordService = new PasswordService(null);
+
+ mockUsers.Add(new User
+ {
+ Name = "user",
+ EncryptedPassword = passwordService.HashPassword("user"),
+ RoleString = "user"
+ });
+ mockUsers.Add(new User
+ {
+ Name = "admin",
+ EncryptedPassword = passwordService.HashPassword("admin"),
+ RoleString = "user,admin"
+ });
+
+ MockUsers = mockUsers;
+
+ var mockUserInfos = mockUsers.Select(u => new UserInfo(u)).ToList();
+ mockUserInfos.Sort(UserInfo.Comparer);
+ MockUserInfos = mockUserInfos;
+ }
+
+ public static List<User> MockUsers { get; }
+
+ public static IReadOnlyList<UserInfo> MockUserInfos { get; }
+ }
+}
diff --git a/Timeline.Tests/Helpers/WebApplicationFactoryExtensions.cs b/Timeline.Tests/Helpers/WebApplicationFactoryExtensions.cs
index 4a7f87fb..a34217f4 100644
--- a/Timeline.Tests/Helpers/WebApplicationFactoryExtensions.cs
+++ b/Timeline.Tests/Helpers/WebApplicationFactoryExtensions.cs
@@ -4,7 +4,6 @@ using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Timeline.Models;
-using Timeline.Services;
using Xunit.Abstractions;
namespace Timeline.Tests.Helpers
@@ -42,28 +41,10 @@ namespace Timeline.Tests.Helpers
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<DatabaseContext>();
- var passwordService = new PasswordService(null);
-
// Ensure the database is created.
db.Database.EnsureCreated();
- db.Users.AddRange(new User[] {
- new User
- {
- Id = 0,
- Name = "user",
- EncryptedPassword = passwordService.HashPassword("user"),
- RoleString = "user"
- },
- new User
- {
- Id = 0,
- Name = "admin",
- EncryptedPassword = passwordService.HashPassword("admin"),
- RoleString = "user,admin"
- }
- });
-
+ db.Users.AddRange(TestMockUsers.MockUsers);
db.SaveChanges();
}
});
diff --git a/Timeline.Tests/JwtTokenUnitTest.cs b/Timeline.Tests/JwtTokenUnitTest.cs
index fa9c7628..39ffc928 100644
--- a/Timeline.Tests/JwtTokenUnitTest.cs
+++ b/Timeline.Tests/JwtTokenUnitTest.cs
@@ -12,8 +12,8 @@ namespace Timeline.Tests
{
public class JwtTokenUnitTest : IClassFixture<WebApplicationFactory<Startup>>
{
- private const string CreateTokenUrl = "User/CreateToken";
- private const string VerifyTokenUrl = "User/VerifyToken";
+ private const string CreateTokenUrl = "token/create";
+ private const string VerifyTokenUrl = "token/verify";
private readonly WebApplicationFactory<Startup> _factory;
diff --git a/Timeline.Tests/Timeline.Tests.csproj b/Timeline.Tests/Timeline.Tests.csproj
index 57e04fc0..820737cc 100644
--- a/Timeline.Tests/Timeline.Tests.csproj
+++ b/Timeline.Tests/Timeline.Tests.csproj
@@ -8,7 +8,7 @@
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Testing" Version="2.2.0-rtm-35646" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.0.1" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
<PrivateAssets>all</PrivateAssets>
diff --git a/Timeline.Tests/UserUnitTest.cs b/Timeline.Tests/UserUnitTest.cs
new file mode 100644
index 00000000..7d8cc824
--- /dev/null
+++ b/Timeline.Tests/UserUnitTest.cs
@@ -0,0 +1,36 @@
+using Microsoft.AspNetCore.Mvc.Testing;
+using Newtonsoft.Json;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+using Timeline.Entities;
+using Timeline.Tests.Helpers;
+using Timeline.Tests.Helpers.Authentication;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Timeline.Tests
+{
+ public class UserUnitTest : IClassFixture<WebApplicationFactory<Startup>>
+ {
+ private readonly WebApplicationFactory<Startup> _factory;
+
+ public UserUnitTest(WebApplicationFactory<Startup> factory, ITestOutputHelper outputHelper)
+ {
+ _factory = factory.WithTestConfig(outputHelper);
+ }
+
+ [Fact]
+ public async Task UserTest()
+ {
+ using (var client = await _factory.CreateClientWithUser("admin", "admin"))
+ {
+ var res1 = await client.GetAsync("users");
+ Assert.Equal(HttpStatusCode.OK, res1.StatusCode);
+ var users = JsonConvert.DeserializeObject<UserInfo[]>(await res1.Content.ReadAsStringAsync()).ToList();
+ users.Sort(UserInfo.Comparer);
+ Assert.Equal(TestMockUsers.MockUserInfos, users, UserInfo.EqualityComparer);
+ }
+ }
+ }
+}
diff --git a/Timeline/Configs/DatabaseConfig.cs b/Timeline/Configs/DatabaseConfig.cs
index 34e5e65f..05dc630e 100644
--- a/Timeline/Configs/DatabaseConfig.cs
+++ b/Timeline/Configs/DatabaseConfig.cs
@@ -1,9 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-
-namespace Timeline.Configs
+namespace Timeline.Configs
{
public class DatabaseConfig
{
diff --git a/Timeline/Controllers/AdminUserController.cs b/Timeline/Controllers/AdminUserController.cs
deleted file mode 100644
index 7cc8c150..00000000
--- a/Timeline/Controllers/AdminUserController.cs
+++ /dev/null
@@ -1,83 +0,0 @@
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Mvc;
-using System;
-using System.Threading.Tasks;
-using Timeline.Entities;
-using Timeline.Services;
-
-namespace Timeline.Controllers
-{
- [Route("admin")]
- [Authorize(Roles = "admin")]
- public class AdminUserController : Controller
- {
- private readonly IUserService _userService;
-
- public AdminUserController(IUserService userService)
- {
- _userService = userService;
- }
-
- [HttpGet("users")]
- public async Task<ActionResult<UserInfo[]>> List()
- {
- return Ok(await _userService.ListUsers());
- }
-
- [HttpGet("user/{username}")]
- public async Task<IActionResult> Get([FromRoute] string username)
- {
- var user = await _userService.GetUser(username);
- if (user == null)
- {
- return NotFound();
- }
- return Ok(user);
- }
-
- [HttpPut("user/{username}")]
- public async Task<IActionResult> Put([FromBody] AdminUserEntityRequest request, [FromRoute] string username)
- {
- var result = await _userService.PutUser(username, request.Password, request.Roles);
- switch (result)
- {
- case PutUserResult.Created:
- return CreatedAtAction("Get", new { username }, AdminUserPutResponse.Created);
- case PutUserResult.Modified:
- return Ok(AdminUserPutResponse.Modified);
- default:
- throw new Exception("Unreachable code.");
- }
- }
-
- [HttpPatch("user/{username}")]
- public async Task<IActionResult> Patch([FromBody] AdminUserEntityRequest request, [FromRoute] string username)
- {
- var result = await _userService.PatchUser(username, request.Password, request.Roles);
- switch (result)
- {
- case PatchUserResult.Success:
- return Ok();
- case PatchUserResult.NotExists:
- return NotFound();
- default:
- throw new Exception("Unreachable code.");
- }
- }
-
- [HttpDelete("user/{username}")]
- public async Task<ActionResult<AdminUserDeleteResponse>> Delete([FromRoute] string username)
- {
- var result = await _userService.DeleteUser(username);
- switch (result)
- {
- case DeleteUserResult.Success:
- return Ok(AdminUserDeleteResponse.Success);
- case DeleteUserResult.NotExists:
- return Ok(AdminUserDeleteResponse.NotExists);
- default:
- throw new Exception("Uncreachable code.");
- }
- }
- }
-}
diff --git a/Timeline/Controllers/TokenController.cs b/Timeline/Controllers/TokenController.cs
new file mode 100644
index 00000000..463fb83c
--- /dev/null
+++ b/Timeline/Controllers/TokenController.cs
@@ -0,0 +1,74 @@
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using System.Threading.Tasks;
+using Timeline.Entities;
+using Timeline.Services;
+
+namespace Timeline.Controllers
+{
+ [Route("token")]
+ public class TokenController : Controller
+ {
+ private static class LoggingEventIds
+ {
+ public const int LogInSucceeded = 4000;
+ public const int LogInFailed = 4001;
+ }
+
+ private readonly IUserService _userService;
+ private readonly ILogger<TokenController> _logger;
+
+ public TokenController(IUserService userService, ILogger<TokenController> logger)
+ {
+ _userService = userService;
+ _logger = logger;
+ }
+
+ [HttpPost("create")]
+ [AllowAnonymous]
+ public async Task<ActionResult<CreateTokenResponse>> Create([FromBody] CreateTokenRequest request)
+ {
+ var result = await _userService.CreateToken(request.Username, request.Password);
+
+ if (result == null)
+ {
+ _logger.LogInformation(LoggingEventIds.LogInFailed, "Attemp to login with username: {} and password: {} failed.", request.Username, request.Password);
+ return Ok(new CreateTokenResponse
+ {
+ Success = false
+ });
+ }
+
+ _logger.LogInformation(LoggingEventIds.LogInSucceeded, "Login with username: {} succeeded.", request.Username);
+
+ return Ok(new CreateTokenResponse
+ {
+ Success = true,
+ Token = result.Token,
+ UserInfo = result.UserInfo
+ });
+ }
+
+ [HttpPost("verify")]
+ [AllowAnonymous]
+ public async Task<ActionResult<VerifyTokenResponse>> Verify([FromBody] VerifyTokenRequest request)
+ {
+ var result = await _userService.VerifyToken(request.Token);
+
+ if (result == null)
+ {
+ return Ok(new VerifyTokenResponse
+ {
+ IsValid = false,
+ });
+ }
+
+ return Ok(new VerifyTokenResponse
+ {
+ IsValid = true,
+ UserInfo = result
+ });
+ }
+ }
+}
diff --git a/Timeline/Controllers/UserController.cs b/Timeline/Controllers/UserController.cs
index 285e0146..ab7e1b99 100644
--- a/Timeline/Controllers/UserController.cs
+++ b/Timeline/Controllers/UserController.cs
@@ -1,74 +1,81 @@
-using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Logging;
+using System;
using System.Threading.Tasks;
using Timeline.Entities;
using Timeline.Services;
namespace Timeline.Controllers
{
- [Route("[controller]")]
public class UserController : Controller
{
- private static class LoggingEventIds
- {
- public const int LogInSucceeded = 4000;
- public const int LogInFailed = 4001;
- }
-
private readonly IUserService _userService;
- private readonly ILogger<UserController> _logger;
- public UserController(IUserService userService, ILogger<UserController> logger)
+ public UserController(IUserService userService)
{
_userService = userService;
- _logger = logger;
}
- [HttpPost("[action]")]
- [AllowAnonymous]
- public async Task<ActionResult<CreateTokenResponse>> CreateToken([FromBody] CreateTokenRequest request)
+ [HttpGet("users"), Authorize(Roles = "admin")]
+ public async Task<ActionResult<UserInfo[]>> List()
{
- var result = await _userService.CreateToken(request.Username, request.Password);
+ return Ok(await _userService.ListUsers());
+ }
- if (result == null)
+ [HttpGet("user/{username}"), Authorize]
+ public async Task<IActionResult> Get([FromRoute] string username)
+ {
+ var user = await _userService.GetUser(username);
+ if (user == null)
{
- _logger.LogInformation(LoggingEventIds.LogInFailed, "Attemp to login with username: {} and password: {} failed.", request.Username, request.Password);
- return Ok(new CreateTokenResponse
- {
- Success = false
- });
+ return NotFound();
}
+ return Ok(user);
+ }
- _logger.LogInformation(LoggingEventIds.LogInSucceeded, "Login with username: {} succeeded.", request.Username);
-
- return Ok(new CreateTokenResponse
+ [HttpPut("user/{username}"), Authorize(Roles = "admin")]
+ public async Task<IActionResult> Put([FromBody] UserModifyRequest request, [FromRoute] string username)
+ {
+ var result = await _userService.PutUser(username, request.Password, request.Roles);
+ switch (result)
{
- Success = true,
- Token = result.Token,
- UserInfo = result.UserInfo
- });
+ case PutUserResult.Created:
+ return CreatedAtAction("Get", new { username }, UserPutResponse.Created);
+ case PutUserResult.Modified:
+ return Ok(UserPutResponse.Modified);
+ default:
+ throw new Exception("Unreachable code.");
+ }
}
- [HttpPost("[action]")]
- [AllowAnonymous]
- public async Task<ActionResult<VerifyTokenResponse>> VerifyToken([FromBody] VerifyTokenRequest request)
+ [HttpPatch("user/{username}"), Authorize(Roles = "admin")]
+ public async Task<IActionResult> Patch([FromBody] UserModifyRequest request, [FromRoute] string username)
{
- var result = await _userService.VerifyToken(request.Token);
-
- if (result == null)
+ var result = await _userService.PatchUser(username, request.Password, request.Roles);
+ switch (result)
{
- return Ok(new VerifyTokenResponse
- {
- IsValid = false,
- });
+ case PatchUserResult.Success:
+ return Ok();
+ case PatchUserResult.NotExists:
+ return NotFound();
+ default:
+ throw new Exception("Unreachable code.");
}
+ }
- return Ok(new VerifyTokenResponse
+ [HttpDelete("user/{username}"), Authorize(Roles = "admin")]
+ public async Task<ActionResult<UserDeleteResponse>> Delete([FromRoute] string username)
+ {
+ var result = await _userService.DeleteUser(username);
+ switch (result)
{
- IsValid = true,
- UserInfo = result
- });
+ case DeleteUserResult.Success:
+ return Ok(UserDeleteResponse.Success);
+ case DeleteUserResult.NotExists:
+ return Ok(UserDeleteResponse.NotExists);
+ default:
+ throw new Exception("Uncreachable code.");
+ }
}
}
}
diff --git a/Timeline/Entities/AdminUser.cs b/Timeline/Entities/AdminUser.cs
index 7b8b7fb7..eb126165 100644
--- a/Timeline/Entities/AdminUser.cs
+++ b/Timeline/Entities/AdminUser.cs
@@ -1,29 +1,29 @@
namespace Timeline.Entities
{
- public class AdminUserEntityRequest
+ public class UserModifyRequest
{
public string Password { get; set; }
public string[] Roles { get; set; }
}
- public class AdminUserPutResponse
+ public class UserPutResponse
{
public const int CreatedCode = 0;
public const int ModifiedCode = 1;
- public static AdminUserPutResponse Created { get; } = new AdminUserPutResponse { ReturnCode = CreatedCode };
- public static AdminUserPutResponse Modified { get; } = new AdminUserPutResponse { ReturnCode = ModifiedCode };
+ public static UserPutResponse Created { get; } = new UserPutResponse { ReturnCode = CreatedCode };
+ public static UserPutResponse Modified { get; } = new UserPutResponse { ReturnCode = ModifiedCode };
public int ReturnCode { get; set; }
}
- public class AdminUserDeleteResponse
+ public class UserDeleteResponse
{
public const int SuccessCode = 0;
public const int NotExistsCode = 1;
- public static AdminUserDeleteResponse Success { get; } = new AdminUserDeleteResponse { ReturnCode = SuccessCode };
- public static AdminUserDeleteResponse NotExists { get; } = new AdminUserDeleteResponse { ReturnCode = NotExistsCode };
+ public static UserDeleteResponse Success { get; } = new UserDeleteResponse { ReturnCode = SuccessCode };
+ public static UserDeleteResponse NotExists { get; } = new UserDeleteResponse { ReturnCode = NotExistsCode };
public int ReturnCode { get; set; }
}
diff --git a/Timeline/Entities/UserInfo.cs b/Timeline/Entities/UserInfo.cs
index d9c5acad..a1860552 100644
--- a/Timeline/Entities/UserInfo.cs
+++ b/Timeline/Entities/UserInfo.cs
@@ -1,14 +1,20 @@
using System;
+using System.Collections.Generic;
using System.Linq;
using Timeline.Models;
namespace Timeline.Entities
{
- public class UserInfo
+ public sealed class UserInfo
{
public UserInfo()
{
+ }
+ public UserInfo(string username, params string[] roles)
+ {
+ Username = username;
+ Roles = roles;
}
public UserInfo(User user)
@@ -17,10 +23,64 @@ namespace Timeline.Entities
throw new ArgumentNullException(nameof(user));
Username = user.Name;
- Roles = user.RoleString.Split(',').Select(s => s.Trim()).ToArray();
+
+ if (user.RoleString == null)
+ Roles = null;
+ else
+ Roles = user.RoleString.Split(',').Select(r => r.Trim()).ToArray();
}
public string Username { get; set; }
public string[] Roles { get; set; }
+
+ public static IEqualityComparer<UserInfo> EqualityComparer { get; } = new EqualityComparerImpl();
+ public static IComparer<UserInfo> Comparer { get; } = Comparer<UserInfo>.Create(Compare);
+
+ private class EqualityComparerImpl : IEqualityComparer<UserInfo>
+ {
+ bool IEqualityComparer<UserInfo>.Equals(UserInfo x, UserInfo y)
+ {
+ return Compare(x, y) == 0;
+ }
+
+ int IEqualityComparer<UserInfo>.GetHashCode(UserInfo obj)
+ {
+ return obj.Username.GetHashCode() ^ NormalizeRoles(obj.Roles).GetHashCode();
+ }
+ }
+
+ private static string NormalizeRoles(string[] rawRoles)
+ {
+ var roles = rawRoles.Where(r => !string.IsNullOrWhiteSpace(r)).Select(r => r.Trim()).ToList();
+ roles.Sort();
+ return string.Join(',', roles);
+ }
+
+ public static int Compare(UserInfo left, UserInfo right)
+ {
+ if (left == null)
+ {
+ if (right == null)
+ return 0;
+ return -1;
+ }
+
+ if (right == null)
+ return 1;
+
+ var uc = string.Compare(left.Username, right.Username);
+ if (uc != 0)
+ return uc;
+
+ var leftRoles = NormalizeRoles(left.Roles);
+ var rightRoles = NormalizeRoles(right.Roles);
+
+ return string.Compare(leftRoles, rightRoles);
+ }
+
+ public override string ToString()
+ {
+ return $"Username: {Username} ; Roles: {Roles}";
+ }
}
}
diff --git a/nuget.config b/nuget.config
index d4244526..194c0bbf 100644
--- a/nuget.config
+++ b/nuget.config
@@ -1,7 +1,6 @@
-<?xml version="1.0" encoding="utf-8" ?>
+<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
- <add key="nuget" value="https://api.nuget.org/v3/index.json" />
<add key="aspnetcore-dev" value="https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json" />
</packageSources>
</configuration>