mirror of
https://github.com/dotnetcore/AgileConfig.git
synced 2026-10-16 20:21:29 +08:00
test: raise unit test coverage to 60 percent
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RunSettings>
|
||||
<DataCollectionRunSettings>
|
||||
<DataCollectors>
|
||||
<DataCollector friendlyName="XPlat code coverage">
|
||||
<Configuration>
|
||||
<Format>cobertura</Format>
|
||||
<Exclude>[*.Tests]*,[Agile.Config.Protocol]*,[AgileConfig.Server.Event]*,[AgileConfig.Server.IService]*</Exclude>
|
||||
<ExcludeByAttribute>System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute,GeneratedCodeAttribute</ExcludeByAttribute>
|
||||
<ExcludeByFile>**/obj/**/*.cs,**/AgileConfig.Server.Data.Entity/App.cs,**/AgileConfig.Server.Data.Entity/AppInheritanced.cs,**/AgileConfig.Server.Data.Entity/Config.cs,**/AgileConfig.Server.Data.Entity/Function.cs,**/AgileConfig.Server.Data.Entity/PublishDetail.cs,**/AgileConfig.Server.Data.Entity/PublishTimeline.cs,**/AgileConfig.Server.Data.Entity/Role.cs,**/AgileConfig.Server.Data.Entity/RoleFunction.cs,**/AgileConfig.Server.Data.Entity/ServerNode.cs,**/AgileConfig.Server.Data.Entity/ServiceInfo.cs,**/AgileConfig.Server.Data.Entity/Setting.cs,**/AgileConfig.Server.Data.Entity/User.cs,**/AgileConfig.Server.Data.Entity/UserAppAuth.cs,**/AgileConfig.Server.Data.Entity/UserRole.cs,**/AppExportVM.cs,**/ApiPublishTimelineVM.cs,**/ApiServiceInfoVM.cs,**/HeartbeatParam.cs,**/HeartbeatResultVM.cs,**/RegisterResultVM.cs,**/ApplicationModels.cs,**/ConfigurationModels.cs,**/NodeModels.cs,**/ReleaseModels.cs,**/ApplicationManagement.cs,**/ConfigurationManagementContracts.cs,**/NodeManagementModels.cs,**/ReleaseManagementContracts.cs,**/ServiceInstanceManagementModels.cs,**/DbConfigInfo.cs,**/OidcSetting.cs,**/UIFileBag.cs,**/IWebsocketCollection.cs,**/Appsettings.cs,**/SystemSettings.cs,**/Global.cs</ExcludeByFile>
|
||||
</Configuration>
|
||||
</DataCollector>
|
||||
</DataCollectors>
|
||||
</DataCollectionRunSettings>
|
||||
</RunSettings>
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\AgileConfig.Server.Common\AgileConfig.Server.Common.csproj" />
|
||||
<ProjectReference Include="..\..\src\AgileConfig.Server.OIDC\AgileConfig.Server.OIDC.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Security.Claims;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AgileConfig.Server.Common.EventBus;
|
||||
using AgileConfig.Server.Common.RestClient;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace AgileConfig.Server.Common.Tests;
|
||||
|
||||
[TestClass]
|
||||
[DoNotParallelize]
|
||||
public class CommonInfrastructureTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void RetryHelpers_RetryUntilSuccessfulAndRethrowFinalFailure()
|
||||
{
|
||||
var attempts = 0;
|
||||
|
||||
var result = FunctionUtil.TRY(() =>
|
||||
{
|
||||
attempts++;
|
||||
if (attempts < 3) throw new InvalidOperationException();
|
||||
return "done";
|
||||
}, 3);
|
||||
|
||||
Assert.AreEqual("done", result);
|
||||
Assert.AreEqual(3, attempts);
|
||||
Assert.ThrowsExactly<InvalidOperationException>(() => FunctionUtil.TRY<int>(() => throw new InvalidOperationException(), 2));
|
||||
|
||||
attempts = 0;
|
||||
FunctionUtil.TRY(() =>
|
||||
{
|
||||
attempts++;
|
||||
if (attempts == 1) throw new InvalidOperationException();
|
||||
}, 2);
|
||||
Assert.AreEqual(2, attempts);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task AsyncRetryHelpers_RetryRethrowAndSwallowAsSpecified()
|
||||
{
|
||||
var attempts = 0;
|
||||
var result = await FunctionUtil.TRYAsync(async () =>
|
||||
{
|
||||
attempts++;
|
||||
await Task.Yield();
|
||||
if (attempts == 1) throw new InvalidOperationException();
|
||||
return 42;
|
||||
}, 2);
|
||||
|
||||
Assert.AreEqual(42, result);
|
||||
Assert.AreEqual(2, attempts);
|
||||
await Assert.ThrowsExactlyAsync<InvalidOperationException>(async () =>
|
||||
await FunctionUtil.TRYAsync<int>(() => Task.FromException<int>(new InvalidOperationException()), 1));
|
||||
|
||||
var swallowed = await FunctionUtil.EATAsync<int>(() => Task.FromException<int>(new InvalidOperationException()), 2);
|
||||
Assert.AreEqual(0, swallowed);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task RetryHelpers_RejectNullDelegates()
|
||||
{
|
||||
Assert.ThrowsExactly<ArgumentNullException>(() => FunctionUtil.TRY<string>(null, 1));
|
||||
Assert.ThrowsExactly<ArgumentNullException>(() => FunctionUtil.TRY(null, 1));
|
||||
await Assert.ThrowsExactlyAsync<ArgumentNullException>(() => FunctionUtil.TRYAsync<string>(null, 1));
|
||||
await Assert.ThrowsExactlyAsync<ArgumentNullException>(() => FunctionUtil.EATAsync<string>(null, 1));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void BasicAuthorizationExtensions_ParseValidCredentialsAndRejectInvalidHeaders()
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
const string encoded = "YXBwOnNlY3JldDpleHRyYQ==";
|
||||
context.Request.Headers.Authorization = "Basic " + encoded;
|
||||
|
||||
Assert.AreEqual(("app", "secret"), context.Request.GetUserNamePasswordFromBasicAuthorization());
|
||||
Assert.AreEqual(("app", "secret"), Encrypt.UnboxBasicAuth(context.Request));
|
||||
|
||||
context.Request.Headers.Authorization = "Bearer token";
|
||||
Assert.AreEqual(("", ""), context.Request.GetUserNamePasswordFromBasicAuthorization());
|
||||
Assert.AreEqual(("", ""), Encrypt.UnboxBasicAuth(context.Request));
|
||||
|
||||
context.Request.Headers.Authorization = "Basic not-base64";
|
||||
Assert.AreEqual(("", ""), context.Request.GetUserNamePasswordFromBasicAuthorization());
|
||||
Assert.AreEqual(("", ""), Encrypt.UnboxBasicAuth(context.Request));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void HttpAndEnvironmentExtensions_ReadClaimsAndQueryEnvironment()
|
||||
{
|
||||
var context = new DefaultHttpContext();
|
||||
context.Request.QueryString = new QueryString("?env=STAGING");
|
||||
context.User = new ClaimsPrincipal(new ClaimsIdentity(
|
||||
[
|
||||
new Claim("username", "alice"),
|
||||
new Claim("id", "user-1")
|
||||
]));
|
||||
|
||||
var accessor = new HttpContextAccessor { HttpContext = context };
|
||||
var envAccessor = new EnvAccessor(accessor);
|
||||
|
||||
Assert.AreEqual("STAGING", envAccessor.Env);
|
||||
Assert.AreEqual("alice", context.GetUserNameFromClaim());
|
||||
Assert.AreEqual("user-1", context.GetUserIdFromClaim());
|
||||
|
||||
var services = new ServiceCollection();
|
||||
Assert.AreSame(services, services.AddEnvAccessor());
|
||||
Assert.IsTrue(services.Any(x => x.ServiceType == typeof(IEnvAccessor) && x.Lifetime == ServiceLifetime.Singleton));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void EnumExtensions_UseDescriptionWhenPresent()
|
||||
{
|
||||
Assert.AreEqual("described", DescribedValue.Described.ToDesc());
|
||||
Assert.AreEqual(nameof(DescribedValue.Plain), DescribedValue.Plain.ToDesc());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task TinyEventBus_DispatchesRegisteredHandler()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
var bus = new TinyEventBus(services);
|
||||
var completion = new TaskCompletionSource<TestEvent>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
TestEventHandler.Completion = completion;
|
||||
|
||||
bus.Register<TestEventHandler>();
|
||||
var evt = new TestEvent();
|
||||
bus.Fire(evt);
|
||||
|
||||
var completed = await Task.WhenAny(completion.Task, Task.Delay(TimeSpan.FromSeconds(2)));
|
||||
Assert.AreSame(completion.Task, completed);
|
||||
Assert.AreSame(evt, await completion.Task);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task TinyEventBus_RegistersAdditionalHandlersAndIsolatesHandlerFailures()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddLogging();
|
||||
var bus = new TinyEventBus(services);
|
||||
var completion = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
ThrowingTestEventHandler.Completion = completion;
|
||||
|
||||
bus.Register<ThrowingTestEventHandler>();
|
||||
bus.Register<ThrowingTestEventHandler>();
|
||||
bus.Fire(new ThrowingTestEvent());
|
||||
|
||||
var completed = await Task.WhenAny(completion.Task, Task.Delay(TimeSpan.FromSeconds(2)));
|
||||
Assert.AreSame(completion.Task, completed);
|
||||
Assert.IsTrue(await completion.Task);
|
||||
await Task.Delay(50);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task DefaultRestClient_SendsHeadersAndSerializesResponses()
|
||||
{
|
||||
var handler = new RecordingHandler(_ => new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new StringContent("{\"value\":\"ok\"}", Encoding.UTF8, "application/json")
|
||||
});
|
||||
var client = new HttpClient(handler) { BaseAddress = new Uri("https://example.test") };
|
||||
var sut = new DefaultRestClient(new StaticHttpClientFactory(client));
|
||||
|
||||
var get = await sut.GetAsync<TestPayload>("/items", new() { ["X-Request-Id"] = "abc" });
|
||||
Assert.AreEqual("ok", get.Value);
|
||||
Assert.AreEqual(HttpMethod.Get, handler.Request.Method);
|
||||
Assert.AreEqual("abc", handler.Request.Headers.GetValues("X-Request-Id").Single());
|
||||
|
||||
var post = await sut.PostAsync<TestPayload>("/items", new { name = "agile" });
|
||||
Assert.AreEqual("ok", post.Value);
|
||||
Assert.AreEqual(HttpMethod.Post, handler.Request.Method);
|
||||
StringAssert.Contains(handler.Body, "agile");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ExceptionMiddleware_SetsResponseThenRethrows()
|
||||
{
|
||||
var loggerFactory = LoggerFactory.Create(_ => { });
|
||||
var middleware = new ExceptionHandlerMiddleware(_ => throw new InvalidOperationException("failure"), loggerFactory);
|
||||
var context = new DefaultHttpContext();
|
||||
context.Response.Body = new MemoryStream();
|
||||
|
||||
await Assert.ThrowsExactlyAsync<InvalidOperationException>(() => middleware.Invoke(context));
|
||||
|
||||
Assert.AreEqual(StatusCodes.Status500InternalServerError, context.Response.StatusCode);
|
||||
Assert.AreEqual("text/html", context.Response.ContentType);
|
||||
context.Response.Body.Position = 0;
|
||||
using var reader = new StreamReader(context.Response.Body, Encoding.UTF8, leaveOpen: true);
|
||||
StringAssert.Contains(await reader.ReadToEndAsync(), "500 InternalServerError");
|
||||
}
|
||||
|
||||
private enum DescribedValue
|
||||
{
|
||||
[System.ComponentModel.Description("described")]
|
||||
Described,
|
||||
Plain
|
||||
}
|
||||
|
||||
private sealed class TestEvent : IEvent;
|
||||
|
||||
private sealed class TestEventHandler : IEventHandler<TestEvent>
|
||||
{
|
||||
public static TaskCompletionSource<TestEvent> Completion { get; set; }
|
||||
|
||||
public Task Handle(IEvent evt)
|
||||
{
|
||||
Completion.TrySetResult((TestEvent)evt);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ThrowingTestEvent : IEvent;
|
||||
|
||||
private sealed class ThrowingTestEventHandler : IEventHandler<ThrowingTestEvent>
|
||||
{
|
||||
public static TaskCompletionSource<bool> Completion { get; set; }
|
||||
|
||||
public async Task Handle(IEvent evt)
|
||||
{
|
||||
await Task.Yield();
|
||||
Completion.TrySetResult(true);
|
||||
throw new InvalidOperationException("handler failure");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestPayload
|
||||
{
|
||||
public string Value { get; set; }
|
||||
}
|
||||
|
||||
private sealed class StaticHttpClientFactory(HttpClient client) : IHttpClientFactory
|
||||
{
|
||||
public HttpClient CreateClient(string name) => client;
|
||||
}
|
||||
|
||||
private sealed class RecordingHandler(Func<HttpRequestMessage, HttpResponseMessage> responseFactory) : HttpMessageHandler
|
||||
{
|
||||
public HttpRequestMessage Request { get; private set; }
|
||||
public string Body { get; private set; }
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
Request = request;
|
||||
Body = request.Content == null ? null : await request.Content.ReadAsStringAsync(cancellationToken);
|
||||
return responseFactory(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AgileConfig.Server.OIDC;
|
||||
using AgileConfig.Server.OIDC.SettingProvider;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace AgileConfig.Server.Common.Tests;
|
||||
|
||||
[TestClass]
|
||||
public class OidcClientTests
|
||||
{
|
||||
[TestMethod]
|
||||
public async Task Validate_UsesConfiguredAuthenticationMethodsAndReadsIdToken()
|
||||
{
|
||||
var setting = CreateSetting("client_secret_basic");
|
||||
var basicHandler = new RecordingHandler();
|
||||
var client = new OidcClient(new StaticSettingProvider(setting), new StaticHttpClientFactory(new HttpClient(basicHandler)));
|
||||
|
||||
var token = await client.Validate("code-1");
|
||||
|
||||
Assert.AreEqual("token-value", token.IdToken);
|
||||
Assert.AreEqual("Basic Y2xpZW50OnNlY3JldA==", basicHandler.Request.Headers.Authorization.ToString());
|
||||
StringAssert.Contains(basicHandler.Body, "code=code-1");
|
||||
Assert.DoesNotContain("client_secret", basicHandler.Body);
|
||||
|
||||
setting.TokenEndpointAuthMethod = "client_secret_post";
|
||||
var postHandler = new RecordingHandler();
|
||||
client = new OidcClient(new StaticSettingProvider(setting), new StaticHttpClientFactory(new HttpClient(postHandler)));
|
||||
await client.Validate("code-2");
|
||||
Assert.IsNull(postHandler.Request.Headers.Authorization);
|
||||
StringAssert.Contains(postHandler.Body, "client_id=client");
|
||||
StringAssert.Contains(postHandler.Body, "client_secret=secret");
|
||||
|
||||
setting.TokenEndpointAuthMethod = "none";
|
||||
var noneHandler = new RecordingHandler();
|
||||
client = new OidcClient(new StaticSettingProvider(setting), new StaticHttpClientFactory(new HttpClient(noneHandler)));
|
||||
await client.Validate("code-3");
|
||||
Assert.IsNull(noneHandler.Request.Headers.Authorization);
|
||||
Assert.DoesNotContain("client_id", noneHandler.Body);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GetAuthorizeUrl_UsesConfiguredClientParameters()
|
||||
{
|
||||
var setting = CreateSetting("none");
|
||||
var client = new OidcClient(new StaticSettingProvider(setting), new StaticHttpClientFactory(new HttpClient(new RecordingHandler())));
|
||||
|
||||
var authorizeUrl = client.GetAuthorizeUrl();
|
||||
StringAssert.StartsWith(authorizeUrl, "https://issuer.test/authorize?");
|
||||
StringAssert.Contains(authorizeUrl, "client_id=client");
|
||||
StringAssert.Contains(authorizeUrl, "redirect_uri=https://app.test/callback");
|
||||
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Validate_RejectsEmptyAndIncompleteResponses()
|
||||
{
|
||||
var setting = CreateSetting("none");
|
||||
var emptyClient = new OidcClient(new StaticSettingProvider(setting), new StaticHttpClientFactory(new HttpClient(new RecordingHandler(""))));
|
||||
await Assert.ThrowsExactlyAsync<Exception>(() => emptyClient.Validate("code"));
|
||||
|
||||
var missingTokenClient = new OidcClient(new StaticSettingProvider(setting), new StaticHttpClientFactory(new HttpClient(new RecordingHandler("{}"))));
|
||||
await Assert.ThrowsExactlyAsync<Exception>(() => missingTokenClient.Validate("code"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SettingsProviderAndServiceRegistration_ReadConfiguration()
|
||||
{
|
||||
Global.Config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string>
|
||||
{
|
||||
["SSO:OIDC:clientId"] = "configured-client",
|
||||
["SSO:OIDC:clientSecret"] = "configured-secret",
|
||||
["SSO:OIDC:redirectUri"] = "https://configured/callback",
|
||||
["SSO:OIDC:tokenEndpoint"] = "https://configured/token",
|
||||
["SSO:OIDC:authorizationEndpoint"] = "https://configured/authorize",
|
||||
["SSO:OIDC:userIdClaim"] = "id",
|
||||
["SSO:OIDC:userNameClaim"] = "username",
|
||||
["SSO:OIDC:scope"] = "openid profile",
|
||||
["SSO:OIDC:tokenEndpointAuthMethod"] = "none"
|
||||
}).Build();
|
||||
using var loggerFactory = LoggerFactory.Create(_ => { });
|
||||
var provider = new ConfigfileOidcSettingProvider(loggerFactory.CreateLogger<ConfigfileOidcSettingProvider>());
|
||||
|
||||
Assert.AreEqual("configured-client", provider.GetSetting().ClientId);
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddOIDC();
|
||||
Assert.IsTrue(services.Any(x => x.ServiceType == typeof(IOidcClient) && x.Lifetime == ServiceLifetime.Singleton));
|
||||
Assert.IsTrue(services.Any(x => x.ServiceType == typeof(IOidcSettingProvider) && x.Lifetime == ServiceLifetime.Singleton));
|
||||
}
|
||||
|
||||
private static OidcSetting CreateSetting(string method) => new(
|
||||
"client", "secret", "https://app.test/callback", "https://issuer.test/token", "https://issuer.test/authorize",
|
||||
"sub", "name", "openid profile", method);
|
||||
|
||||
private sealed class StaticSettingProvider(OidcSetting setting) : IOidcSettingProvider
|
||||
{
|
||||
public OidcSetting GetSetting() => setting;
|
||||
}
|
||||
|
||||
private sealed class StaticHttpClientFactory(HttpClient client) : IHttpClientFactory
|
||||
{
|
||||
public HttpClient CreateClient(string name) => client;
|
||||
}
|
||||
|
||||
private sealed class RecordingHandler(string response = "{\"id_token\":\"token-value\"}") : HttpMessageHandler
|
||||
{
|
||||
public HttpRequestMessage Request { get; private set; }
|
||||
public string Body { get; private set; }
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
Request = request;
|
||||
Body = await request.Content.ReadAsStringAsync(cancellationToken);
|
||||
return new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(response) };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using AgileConfig.Server.Apisite;
|
||||
using AgileConfig.Server.Apisite.Controllers;
|
||||
using AgileConfig.Server.Apisite.Models;
|
||||
using AgileConfig.Server.Common;
|
||||
using AgileConfig.Server.Common.EventBus;
|
||||
using AgileConfig.Server.Common.Resources;
|
||||
using AgileConfig.Server.Data.Entity;
|
||||
using AgileConfig.Server.Event;
|
||||
using AgileConfig.Server.IService;
|
||||
using AgileConfig.Server.OIDC;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Moq;
|
||||
|
||||
namespace ApiSiteTests;
|
||||
|
||||
[TestClass]
|
||||
public sealed class AdminControllerCoverageTests
|
||||
{
|
||||
[TestMethod]
|
||||
public async Task Login_HandlesEmptyInvalidAndSuccessfulCredentials()
|
||||
{
|
||||
SetConfig();
|
||||
var userService = new Mock<IUserService>();
|
||||
var permissionService = new Mock<IPermissionService>();
|
||||
var jwtService = new Mock<IJwtService>();
|
||||
var eventBus = new Mock<ITinyEventBus>();
|
||||
var controller = CreateController(userService, permissionService, jwtService, eventBus);
|
||||
|
||||
var empty = AsJson(await controller.Login4AntdPro(new LoginVM { userName = "admin", password = "" }));
|
||||
Assert.AreEqual("error", Read<string>(empty, "status"));
|
||||
Assert.AreEqual(Messages.PasswordCannotBeEmpty, Read<string>(empty, "message"));
|
||||
userService.Verify(x => x.ValidateUserPassword(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||
|
||||
userService.Setup(x => x.ValidateUserPassword("admin", "bad")).ReturnsAsync(false);
|
||||
var invalid = AsJson(await controller.Login4AntdPro(new LoginVM { userName = "admin", password = "bad" }));
|
||||
Assert.AreEqual("error", Read<string>(invalid, "status"));
|
||||
Assert.AreEqual(Messages.PasswordError, Read<string>(invalid, "message"));
|
||||
|
||||
var user = new User { Id = "admin-id", UserName = "admin", Status = UserStatus.Normal };
|
||||
userService.Setup(x => x.ValidateUserPassword("admin", "good")).ReturnsAsync(true);
|
||||
userService.Setup(x => x.GetUsersByNameAsync("admin")).ReturnsAsync(new List<User> { user });
|
||||
userService.Setup(x => x.GetUserRolesAsync("admin-id")).ReturnsAsync(new List<Role>
|
||||
{
|
||||
new() { Id = SystemRoleConstants.AdminId, Name = "Administrator" },
|
||||
new() { Id = SystemRoleConstants.OperatorId, Name = "Operator" }
|
||||
});
|
||||
permissionService.Setup(x => x.GetUserPermission("admin-id")).ReturnsAsync(new List<string> { Functions.User_Read });
|
||||
jwtService.Setup(x => x.GetToken("admin-id", "admin", true)).Returns("jwt-token");
|
||||
|
||||
var success = AsJson(await controller.Login4AntdPro(new LoginVM { userName = "admin", password = "good" }));
|
||||
Assert.AreEqual("ok", Read<string>(success, "status"));
|
||||
Assert.AreEqual("jwt-token", Read<string>(success, "token"));
|
||||
Assert.AreEqual("Bearer", Read<string>(success, "type"));
|
||||
CollectionAssert.AreEqual(new[] { "Administrator", "Operator" },
|
||||
((IEnumerable<string>)Read<object>(success, "currentAuthority")).ToArray());
|
||||
CollectionAssert.AreEqual(new[] { Functions.User_Read },
|
||||
((IEnumerable<string>)Read<object>(success, "currentFunctions")).ToArray());
|
||||
eventBus.Verify(x => x.Fire(It.Is<LoginEvent>(evt => evt.UserName == "admin")), Times.Once);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task OidcLogin_RejectsDisabledAndInvalidTokens()
|
||||
{
|
||||
var oidcClient = new Mock<IOidcClient>();
|
||||
var controller = CreateController(new Mock<IUserService>(), new Mock<IPermissionService>(),
|
||||
new Mock<IJwtService>(), new Mock<ITinyEventBus>(), oidcClient);
|
||||
|
||||
SetConfig(ssoEnabled: false);
|
||||
var disabled = await controller.OidcLoginByCode("code");
|
||||
Assert.IsInstanceOfType(disabled, typeof(BadRequestObjectResult));
|
||||
Assert.AreEqual("SSO not enabled", ((BadRequestObjectResult)disabled).Value);
|
||||
oidcClient.Verify(x => x.Validate(It.IsAny<string>()), Times.Never);
|
||||
|
||||
SetConfig(ssoEnabled: true);
|
||||
oidcClient.Setup(x => x.Validate("empty")).ReturnsAsync(("", "access"));
|
||||
var empty = AsJson(await controller.OidcLoginByCode("empty"));
|
||||
Assert.IsFalse(Read<bool>(empty, "success"));
|
||||
Assert.AreEqual("Code validate failed", Read<string>(empty, "message"));
|
||||
|
||||
oidcClient.Setup(x => x.Validate("invalid")).ReturnsAsync(("token", "access"));
|
||||
oidcClient.Setup(x => x.UnboxIdToken("token")).Returns(("", "user"));
|
||||
var invalidId = AsJson(await controller.OidcLoginByCode("invalid"));
|
||||
Assert.IsFalse(Read<bool>(invalidId, "success"));
|
||||
Assert.AreEqual("IdToken invalid", Read<string>(invalidId, "message"));
|
||||
|
||||
oidcClient.Setup(x => x.UnboxIdToken("token")).Returns(("id", ""));
|
||||
var invalidName = AsJson(await controller.OidcLoginByCode("invalid"));
|
||||
Assert.IsFalse(Read<bool>(invalidName, "success"));
|
||||
Assert.AreEqual("IdToken invalid", Read<string>(invalidName, "message"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task OidcLogin_CreatesFirstUserAndRejectsDeletedUser()
|
||||
{
|
||||
SetConfig(ssoEnabled: true);
|
||||
var oidcClient = new Mock<IOidcClient>();
|
||||
var userService = new Mock<IUserService>();
|
||||
var permissionService = new Mock<IPermissionService>();
|
||||
var jwtService = new Mock<IJwtService>();
|
||||
var eventBus = new Mock<ITinyEventBus>();
|
||||
var controller = CreateController(userService, permissionService, jwtService, eventBus, oidcClient);
|
||||
oidcClient.Setup(x => x.Validate("first")).ReturnsAsync(("id-token", "access"));
|
||||
oidcClient.Setup(x => x.UnboxIdToken("id-token")).Returns(("oidc-id", "oidc-user"));
|
||||
|
||||
User added = null;
|
||||
userService.SetupSequence(x => x.GetUsersByNameAsync("oidc-user"))
|
||||
.ReturnsAsync(new List<User>())
|
||||
.ReturnsAsync(() => new List<User> { added });
|
||||
userService.Setup(x => x.AddAsync(It.IsAny<User>()))
|
||||
.Callback<User>(user => added = user)
|
||||
.ReturnsAsync(true);
|
||||
userService.Setup(x => x.UpdateUserRolesAsync("oidc-id", It.IsAny<List<string>>())).ReturnsAsync(true);
|
||||
userService.Setup(x => x.GetUserRolesAsync("oidc-id")).ReturnsAsync(new List<Role>
|
||||
{
|
||||
new() { Id = SystemRoleConstants.OperatorId, Name = "Operator" }
|
||||
});
|
||||
permissionService.Setup(x => x.GetUserPermission("oidc-id")).ReturnsAsync(new List<string>());
|
||||
jwtService.Setup(x => x.GetToken("oidc-id", "oidc-user", false)).Returns("oidc-jwt");
|
||||
|
||||
var firstLogin = AsJson(await controller.OidcLoginByCode("first"));
|
||||
Assert.AreEqual("ok", Read<string>(firstLogin, "status"));
|
||||
Assert.AreEqual("oidc-jwt", Read<string>(firstLogin, "token"));
|
||||
Assert.IsNotNull(added);
|
||||
Assert.AreEqual("oidc-id", added.Id);
|
||||
Assert.AreEqual(UserSource.SSO, added.Source);
|
||||
Assert.AreEqual(UserStatus.Normal, added.Status);
|
||||
userService.Verify(x => x.UpdateUserRolesAsync("oidc-id",
|
||||
It.Is<List<string>>(roles => roles.SequenceEqual(new[] { SystemRoleConstants.OperatorId }))), Times.Once);
|
||||
eventBus.Verify(x => x.Fire(It.Is<LoginEvent>(evt => evt.UserName == "oidc-user")), Times.Once);
|
||||
|
||||
var deleted = new User { Id = "deleted-id", UserName = "deleted", Status = UserStatus.Deleted };
|
||||
oidcClient.Setup(x => x.Validate("deleted")).ReturnsAsync(("deleted-token", "access"));
|
||||
oidcClient.Setup(x => x.UnboxIdToken("deleted-token")).Returns(("deleted-id", "deleted"));
|
||||
userService.Setup(x => x.GetUsersByNameAsync("deleted")).ReturnsAsync(new List<User> { deleted });
|
||||
|
||||
var deletedResult = AsJson(await controller.OidcLoginByCode("deleted"));
|
||||
Assert.AreEqual("error", Read<string>(deletedResult, "status"));
|
||||
Assert.AreEqual(Messages.UserDeleted, Read<string>(deletedResult, "message"));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task PasswordInitialization_ReportsStateAndValidationOutcomes()
|
||||
{
|
||||
SetConfig();
|
||||
var system = new Mock<ISystemInitializationService>();
|
||||
var eventBus = new Mock<ITinyEventBus>();
|
||||
var controller = CreateController(new Mock<IUserService>(), new Mock<IPermissionService>(),
|
||||
new Mock<IJwtService>(), eventBus, system: system);
|
||||
|
||||
system.SetupSequence(x => x.HasSa()).Returns(false).Returns(true);
|
||||
var state = AsJson(controller.PasswordInited());
|
||||
Assert.IsTrue(Read<bool>(state, "success"));
|
||||
Assert.IsFalse(Read<bool>(state, "data"));
|
||||
|
||||
var empty = AsJson(controller.InitPassword(new InitPasswordVM { password = "", confirmPassword = "" }));
|
||||
Assert.IsFalse(Read<bool>(empty, "success"));
|
||||
Assert.AreEqual(Messages.PasswordCannotBeEmpty, Read<string>(empty, "message"));
|
||||
|
||||
var tooLong = AsJson(controller.InitPassword(new InitPasswordVM
|
||||
{
|
||||
password = new string('p', 51),
|
||||
confirmPassword = new string('p', 51)
|
||||
}));
|
||||
Assert.IsFalse(Read<bool>(tooLong, "success"));
|
||||
Assert.AreEqual(Messages.PasswordMaxLength50, Read<string>(tooLong, "message"));
|
||||
|
||||
var mismatch = AsJson(controller.InitPassword(new InitPasswordVM { password = "one", confirmPassword = "two" }));
|
||||
Assert.IsFalse(Read<bool>(mismatch, "success"));
|
||||
Assert.AreEqual(Messages.PasswordMismatch, Read<string>(mismatch, "message"));
|
||||
|
||||
var alreadySet = AsJson(controller.InitPassword(new InitPasswordVM { password = "one", confirmPassword = "one" }));
|
||||
Assert.IsFalse(Read<bool>(alreadySet, "success"));
|
||||
Assert.AreEqual(Messages.PasswordAlreadySet, Read<string>(alreadySet, "message"));
|
||||
|
||||
system.Setup(x => x.HasSa()).Returns(false);
|
||||
system.SetupSequence(x => x.TryInitSaPassword("good-password")).Returns(false).Returns(true);
|
||||
var failed = AsJson(controller.InitPassword(new InitPasswordVM
|
||||
{
|
||||
password = "good-password",
|
||||
confirmPassword = "good-password"
|
||||
}));
|
||||
Assert.IsFalse(Read<bool>(failed, "success"));
|
||||
Assert.AreEqual(Messages.InitPasswordFailed, Read<string>(failed, "message"));
|
||||
|
||||
var succeeded = AsJson(controller.InitPassword(new InitPasswordVM
|
||||
{
|
||||
password = "good-password",
|
||||
confirmPassword = "good-password"
|
||||
}));
|
||||
Assert.IsTrue(Read<bool>(succeeded, "success"));
|
||||
eventBus.Verify(x => x.Fire(It.IsAny<InitSaPasswordSuccessful>()), Times.Once);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ChangePassword_HandlesPreviewValidationAndPersistenceOutcomes()
|
||||
{
|
||||
var userService = new Mock<IUserService>();
|
||||
var eventBus = new Mock<ITinyEventBus>();
|
||||
var system = new Mock<ISystemInitializationService>();
|
||||
var controller = CreateController(userService, new Mock<IPermissionService>(), new Mock<IJwtService>(),
|
||||
eventBus, system: system);
|
||||
|
||||
SetConfig(previewMode: true);
|
||||
var preview = AsJson(await controller.ChangePassword(new ChangePasswordVM
|
||||
{
|
||||
oldPassword = "old",
|
||||
password = "new",
|
||||
confirmPassword = "new"
|
||||
}));
|
||||
Assert.IsFalse(Read<bool>(preview, "success"));
|
||||
Assert.AreEqual(Messages.DemoModeNoPasswordChange, Read<string>(preview, "message"));
|
||||
userService.Verify(x => x.ValidateUserPassword(It.IsAny<string>(), It.IsAny<string>()), Times.Never);
|
||||
|
||||
SetConfig();
|
||||
var emptyOld = AsJson(await controller.ChangePassword(new ChangePasswordVM { oldPassword = "" }));
|
||||
Assert.AreEqual("err_resetpassword_01", Read<string>(emptyOld, "err_code"));
|
||||
|
||||
userService.Setup(x => x.ValidateUserPassword("current-user", "bad-old")).ReturnsAsync(false);
|
||||
var badOld = AsJson(await controller.ChangePassword(new ChangePasswordVM { oldPassword = "bad-old" }));
|
||||
Assert.AreEqual(Messages.OriginalPasswordError, Read<string>(badOld, "message"));
|
||||
Assert.AreEqual("err_resetpassword_02", Read<string>(badOld, "err_code"));
|
||||
|
||||
userService.Setup(x => x.ValidateUserPassword("current-user", "old")).ReturnsAsync(true);
|
||||
var emptyNew = AsJson(await controller.ChangePassword(new ChangePasswordVM
|
||||
{
|
||||
oldPassword = "old",
|
||||
password = "",
|
||||
confirmPassword = ""
|
||||
}));
|
||||
Assert.AreEqual(Messages.NewPasswordCannotBeEmpty, Read<string>(emptyNew, "message"));
|
||||
|
||||
var tooLong = AsJson(await controller.ChangePassword(new ChangePasswordVM
|
||||
{
|
||||
oldPassword = "old",
|
||||
password = new string('n', 51),
|
||||
confirmPassword = new string('n', 51)
|
||||
}));
|
||||
Assert.AreEqual(Messages.NewPasswordMaxLength50, Read<string>(tooLong, "message"));
|
||||
|
||||
var mismatch = AsJson(await controller.ChangePassword(new ChangePasswordVM
|
||||
{
|
||||
oldPassword = "old",
|
||||
password = "new-one",
|
||||
confirmPassword = "new-two"
|
||||
}));
|
||||
Assert.AreEqual(Messages.NewPasswordMismatch, Read<string>(mismatch, "message"));
|
||||
|
||||
userService.Setup(x => x.GetUsersByNameAsync("current-user"))
|
||||
.ReturnsAsync(new List<User> { new() { Id = "deleted", UserName = "current-user", Status = UserStatus.Deleted } });
|
||||
var missing = AsJson(await controller.ChangePassword(new ChangePasswordVM
|
||||
{
|
||||
oldPassword = "old",
|
||||
password = "new-one",
|
||||
confirmPassword = "new-one"
|
||||
}));
|
||||
Assert.AreEqual(Messages.UserNotFound, Read<string>(missing, "message"));
|
||||
Assert.AreEqual("err_resetpassword_06", Read<string>(missing, "err_code"));
|
||||
|
||||
var user = new User { Id = "current-id", UserName = "current-user", Status = UserStatus.Normal, Salt = "salt" };
|
||||
userService.Setup(x => x.GetUsersByNameAsync("current-user")).ReturnsAsync(new List<User> { user });
|
||||
userService.SetupSequence(x => x.UpdateAsync(It.IsAny<User>())).ReturnsAsync(false).ReturnsAsync(true);
|
||||
var failed = AsJson(await controller.ChangePassword(new ChangePasswordVM
|
||||
{
|
||||
oldPassword = "old",
|
||||
password = "new-one",
|
||||
confirmPassword = "new-one"
|
||||
}));
|
||||
Assert.AreEqual(Messages.ChangePasswordFailed, Read<string>(failed, "message"));
|
||||
|
||||
var succeeded = AsJson(await controller.ChangePassword(new ChangePasswordVM
|
||||
{
|
||||
oldPassword = "old",
|
||||
password = "new-two",
|
||||
confirmPassword = "new-two"
|
||||
}));
|
||||
Assert.IsTrue(Read<bool>(succeeded, "success"));
|
||||
Assert.AreEqual(Encrypt.Md5("new-two" + user.Salt), user.Password);
|
||||
eventBus.Verify(x => x.Fire(It.Is<ChangeUserPasswordSuccessful>(evt => evt.UserName == "current-user")), Times.Once);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Logoff_SignsOutAndRedirectsToLogin()
|
||||
{
|
||||
SetConfig();
|
||||
var auth = new Mock<IAuthenticationService>();
|
||||
auth.Setup(x => x.SignOutAsync(It.IsAny<HttpContext>(), It.IsAny<string>(), It.IsAny<AuthenticationProperties>()))
|
||||
.Returns(Task.CompletedTask);
|
||||
var controller = CreateController(new Mock<IUserService>(), new Mock<IPermissionService>(),
|
||||
new Mock<IJwtService>(), new Mock<ITinyEventBus>());
|
||||
controller.HttpContext.RequestServices = new ServiceCollection()
|
||||
.AddSingleton(auth.Object)
|
||||
.BuildServiceProvider();
|
||||
|
||||
var result = await controller.Logoff();
|
||||
Assert.IsInstanceOfType(result, typeof(RedirectResult));
|
||||
Assert.AreEqual("Login", ((RedirectResult)result).Url);
|
||||
auth.Verify(x => x.SignOutAsync(It.IsAny<HttpContext>(), null, null), Times.Once);
|
||||
}
|
||||
|
||||
private static AdminController CreateController(
|
||||
Mock<IUserService> userService,
|
||||
Mock<IPermissionService> permissionService,
|
||||
Mock<IJwtService> jwtService,
|
||||
Mock<ITinyEventBus> eventBus,
|
||||
Mock<IOidcClient> oidcClient = null,
|
||||
Mock<ISystemInitializationService> system = null)
|
||||
{
|
||||
var context = new DefaultHttpContext
|
||||
{
|
||||
User = new ClaimsPrincipal(new ClaimsIdentity(
|
||||
new[] { new Claim("username", "current-user") }, "test"))
|
||||
};
|
||||
var controller = new AdminController(
|
||||
new Mock<ISettingService>().Object,
|
||||
userService.Object,
|
||||
permissionService.Object,
|
||||
jwtService.Object,
|
||||
(oidcClient ?? new Mock<IOidcClient>()).Object,
|
||||
eventBus.Object,
|
||||
(system ?? new Mock<ISystemInitializationService>()).Object);
|
||||
controller.ControllerContext = new ControllerContext { HttpContext = context };
|
||||
return controller;
|
||||
}
|
||||
|
||||
private static void SetConfig(bool ssoEnabled = false, bool previewMode = false)
|
||||
{
|
||||
Global.Config = new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string>
|
||||
{
|
||||
["SSO:enabled"] = ssoEnabled.ToString(),
|
||||
["preview_mode"] = previewMode.ToString()
|
||||
}).Build();
|
||||
}
|
||||
|
||||
private static JsonResult AsJson(IActionResult result)
|
||||
{
|
||||
Assert.IsInstanceOfType(result, typeof(JsonResult));
|
||||
return (JsonResult)result;
|
||||
}
|
||||
|
||||
private static T Read<T>(JsonResult result, string propertyName)
|
||||
{
|
||||
Assert.IsNotNull(result.Value);
|
||||
var property = result.Value.GetType().GetProperty(propertyName);
|
||||
Assert.IsNotNull(property, $"Missing JSON property '{propertyName}'.");
|
||||
return (T)property.GetValue(result.Value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using AgileConfig.Server.Application;
|
||||
using AgileConfig.Server.Application.Configurations;
|
||||
using AgileConfig.Server.Application.Releases;
|
||||
using AgileConfig.Server.Apisite.Controllers;
|
||||
using AgileConfig.Server.Apisite.Models;
|
||||
using AgileConfig.Server.Common.EventBus;
|
||||
using AgileConfig.Server.Data.Entity;
|
||||
using AgileConfig.Server.IService;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Moq;
|
||||
|
||||
namespace ApiSiteTests;
|
||||
|
||||
[TestClass]
|
||||
public class LegacyConfigControllerTests
|
||||
{
|
||||
[TestMethod]
|
||||
public async Task Mutations_MapApplicationOutcomesToLegacyResponses()
|
||||
{
|
||||
var dependencies = new Dependencies();
|
||||
var controller = dependencies.CreateController();
|
||||
var env = new EnvString { Value = "DEV" };
|
||||
var model = new ConfigVM { Id = "config-1", AppId = "app-1", Key = "key", Value = "value" };
|
||||
|
||||
dependencies.ConfigurationManagement
|
||||
.Setup(x => x.CreateAsync(It.IsAny<CreateConfigurationCommand>()))
|
||||
.ReturnsAsync(ApplicationResult<Config>.Failure(ApplicationError.Conflict));
|
||||
Assert.IsNotNull(await controller.Add(model, env) as JsonResult);
|
||||
|
||||
dependencies.ConfigurationManagement
|
||||
.Setup(x => x.CreateAsync(It.IsAny<CreateConfigurationCommand>()))
|
||||
.ReturnsAsync(ApplicationResult<Config>.Success(new Config { Id = model.Id }));
|
||||
Assert.IsNotNull(await controller.Add(model, env) as JsonResult);
|
||||
|
||||
dependencies.ConfigurationManagement
|
||||
.Setup(x => x.UpdateAsync(It.IsAny<UpdateConfigurationCommand>()))
|
||||
.ReturnsAsync(ApplicationResult<Config>.Failure(ApplicationError.NotFound));
|
||||
dependencies.ConfigService.Setup(x => x.GetAsync(model.Id, env.Value)).ReturnsAsync((Config)null);
|
||||
Assert.IsNotNull(await controller.Edit(model, env) as JsonResult);
|
||||
|
||||
dependencies.ConfigurationManagement
|
||||
.Setup(x => x.UpdateAsync(It.IsAny<UpdateConfigurationCommand>()))
|
||||
.ReturnsAsync(ApplicationResult<Config>.Failure(ApplicationError.Conflict));
|
||||
Assert.IsNotNull(await controller.Edit(model, env) as JsonResult);
|
||||
|
||||
dependencies.ConfigurationManagement
|
||||
.Setup(x => x.UpdateAsync(It.IsAny<UpdateConfigurationCommand>()))
|
||||
.ReturnsAsync(ApplicationResult<Config>.Success(new Config { Id = model.Id }));
|
||||
Assert.IsNotNull(await controller.Edit(model, env) as JsonResult);
|
||||
|
||||
dependencies.ConfigurationManagement
|
||||
.Setup(x => x.DeleteAsync(It.IsAny<DeleteConfigurationCommand>()))
|
||||
.ReturnsAsync(ApplicationResult<Config>.Failure(ApplicationError.NotFound));
|
||||
Assert.IsNotNull(await controller.Delete(model.Id, env) as JsonResult);
|
||||
|
||||
dependencies.ConfigurationManagement
|
||||
.Setup(x => x.DeleteAsync(It.IsAny<DeleteConfigurationCommand>()))
|
||||
.ReturnsAsync(ApplicationResult<Config>.Success(new Config { Id = model.Id }));
|
||||
Assert.IsNotNull(await controller.Delete(model.Id, env) as JsonResult);
|
||||
|
||||
dependencies.ReleaseManagement
|
||||
.Setup(x => x.PublishAsync(It.IsAny<PublishConfigurationsCommand>()))
|
||||
.ReturnsAsync(ApplicationResult<PublishTimeline>.Success(new PublishTimeline()));
|
||||
dependencies.ReleaseManagement
|
||||
.Setup(x => x.RollbackAsync(It.IsAny<RollbackConfigurationCommand>()))
|
||||
.ReturnsAsync(ApplicationResult<PublishTimeline>.Failure(ApplicationError.OperationFailed));
|
||||
Assert.IsNotNull(await controller.Publish(new PublishLogVM { AppId = "app-1", Ids = [], Log = "publish" }, env) as JsonResult);
|
||||
Assert.IsNotNull(await controller.Rollback("timeline-1", env) as JsonResult);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task SearchAndReadEndpoints_FilterSortAndReturnLegacyPayloads()
|
||||
{
|
||||
var dependencies = new Dependencies();
|
||||
var controller = dependencies.CreateController();
|
||||
var env = new EnvString { Value = "DEV" };
|
||||
var configs = new List<Config>
|
||||
{
|
||||
new() { Id = "disabled", Key = "disabled", Status = ConfigStatus.Deleted, CreateTime = DateTime.UtcNow },
|
||||
new() { Id = "old", Key = "old", Group = "z", Status = ConfigStatus.Enabled, OnlineStatus = OnlineStatus.WaitPublish, CreateTime = DateTime.UtcNow.AddMinutes(-1), EditStatus = EditStatus.Add },
|
||||
new() { Id = "new", Key = "new", Group = "a", Status = ConfigStatus.Enabled, OnlineStatus = OnlineStatus.Online, CreateTime = DateTime.UtcNow, EditStatus = EditStatus.Edit }
|
||||
};
|
||||
dependencies.ConfigService.Setup(x => x.Search("app-1", It.IsAny<string>(), It.IsAny<string>(), env.Value))
|
||||
.ReturnsAsync(configs);
|
||||
dependencies.ConfigService.Setup(x => x.GetAllConfigsAsync(env.Value)).ReturnsAsync(configs);
|
||||
dependencies.ConfigService.Setup(x => x.GetAsync("new", env.Value)).ReturnsAsync(configs.Last());
|
||||
dependencies.ConfigService.Setup(x => x.GetAsync("missing", env.Value)).ReturnsAsync((Config)null);
|
||||
|
||||
Assert.IsNotNull(await controller.All(env.Value) as JsonResult);
|
||||
Assert.IsNotNull(await controller.Search("app-1", null, null, null, "group", "ascend", env, 10, 1) as JsonResult);
|
||||
Assert.IsNotNull(await controller.Search("app-1", null, null, OnlineStatus.Online, "createTime", "descend", env, 10, 1) as JsonResult);
|
||||
Assert.IsNotNull(await controller.Get("new", env) as JsonResult);
|
||||
Assert.IsNotNull(await controller.Get("missing", env) as JsonResult);
|
||||
Assert.IsNotNull(await controller.WaitPublishStatus("app-1", env) as JsonResult);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task BatchAndContentEndpoints_UseConfigurationServiceResults()
|
||||
{
|
||||
var dependencies = new Dependencies();
|
||||
var controller = dependencies.CreateController();
|
||||
var env = new EnvString { Value = "DEV" };
|
||||
var existing = new List<Config>
|
||||
{
|
||||
new() { Id = "old", AppId = "app-1", Key = "old", Value = "old", EditStatus = EditStatus.Commit }
|
||||
};
|
||||
dependencies.ConfigService.Setup(x => x.GetByAppIdAsync("app-1", env.Value)).ReturnsAsync(existing);
|
||||
dependencies.ConfigService.Setup(x => x.GenerateKey(It.IsAny<Config>())).Returns<Config>(config =>
|
||||
string.IsNullOrEmpty(config.Group) ? config.Key : $"{config.Group}:{config.Key}");
|
||||
dependencies.ConfigService.Setup(x => x.AddRangeAsync(It.IsAny<List<Config>>(), env.Value)).ReturnsAsync(true);
|
||||
dependencies.ConfigService.Setup(x => x.UpdateAsync(It.IsAny<List<Config>>(), env.Value)).ReturnsAsync(true);
|
||||
dependencies.ConfigService.Setup(x => x.IsPublishedAsync(It.IsAny<string>(), env.Value)).ReturnsAsync(false);
|
||||
dependencies.ConfigService.Setup(x => x.GetAsync("old", env.Value)).ReturnsAsync(existing.Single());
|
||||
dependencies.ConfigService.Setup(x => x.CancelEdit(It.IsAny<List<string>>(), env.Value)).ReturnsAsync(true);
|
||||
dependencies.ConfigService.Setup(x => x.SaveJsonAsync(It.IsAny<string>(), "app-1", env.Value, It.IsAny<bool>())).ReturnsAsync(true);
|
||||
dependencies.ConfigService.Setup(x => x.ValidateKvString("key=value")).Returns((true, ""));
|
||||
dependencies.ConfigService.Setup(x => x.SaveKvListAsync("key=value", "app-1", env.Value, It.IsAny<bool>())).ReturnsAsync(true);
|
||||
|
||||
var add = await controller.AddRange(new List<ConfigVM>
|
||||
{
|
||||
new() { AppId = "app-1", Key = "new", Value = "value" }
|
||||
}, env) as JsonResult;
|
||||
Assert.IsNotNull(add);
|
||||
|
||||
var duplicate = await controller.AddRange(new List<ConfigVM>
|
||||
{
|
||||
new() { AppId = "app-1", Key = "old", Value = "value" }
|
||||
}, env) as JsonResult;
|
||||
Assert.IsNotNull(duplicate);
|
||||
|
||||
Assert.IsNotNull(await controller.DeleteSome(new List<string> { "old" }, env) as JsonResult);
|
||||
Assert.IsNotNull(await controller.CancelEdit("old", env) as JsonResult);
|
||||
Assert.IsNotNull(await controller.CancelSomeEdit(new List<string> { "old" }, env) as JsonResult);
|
||||
Assert.IsNotNull(await controller.GetKvList("app-1", env) as JsonResult);
|
||||
Assert.IsNotNull(await controller.GetJson("app-1", env) as JsonResult);
|
||||
Assert.IsNotNull(await controller.SaveJson(new SaveJsonVM { json = "{}", isPatch = true }, "app-1", env) as JsonResult);
|
||||
Assert.IsNotNull(await controller.SaveKvList(new SaveKVListVM { str = "key=value", isPatch = false }, "app-1", env) as JsonResult);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task SynchronizationAndHistoryEndpoints_HandleFoundAndMissingApps()
|
||||
{
|
||||
var dependencies = new Dependencies();
|
||||
var controller = dependencies.CreateController();
|
||||
var env = new EnvString { Value = "DEV" };
|
||||
dependencies.AppService.Setup(x => x.GetAsync("missing")).ReturnsAsync((App)null);
|
||||
dependencies.AppService.Setup(x => x.GetAsync("app-1")).ReturnsAsync(new App { Id = "app-1" });
|
||||
dependencies.ConfigService.Setup(x => x.EnvSync("app-1", "DEV", It.IsAny<List<string>>())).ReturnsAsync(true);
|
||||
dependencies.ConfigService.Setup(x => x.GetPublishDetailListAsync("app-1", env.Value)).ReturnsAsync(new List<PublishDetail>
|
||||
{
|
||||
new() { Version = 2, PublishTimelineId = "t2" },
|
||||
new() { Version = 1, PublishTimelineId = "t1" }
|
||||
});
|
||||
dependencies.ConfigService.Setup(x => x.GetPublishTimeLineNodeAsync(It.IsAny<string>(), env.Value))
|
||||
.ReturnsAsync(new PublishTimeline());
|
||||
dependencies.ConfigService.Setup(x => x.GetConfigPublishedHistory("config-1", env.Value))
|
||||
.ReturnsAsync(new List<PublishDetail> { new() { Version = 1, PublishTimelineId = "t1" } });
|
||||
|
||||
Assert.IsNotNull(await controller.SyncEnv(new List<string> { "TEST" }, "missing", "DEV") as JsonResult);
|
||||
Assert.IsNotNull(await controller.SyncEnv(new List<string> { "TEST" }, "app-1", "DEV") as JsonResult);
|
||||
Assert.IsNotNull(await controller.PublishHistory("app-1", env) as JsonResult);
|
||||
Assert.IsNotNull(await controller.ConfigPublishedHistory("config-1", env) as JsonResult);
|
||||
}
|
||||
|
||||
private sealed class Dependencies
|
||||
{
|
||||
public Mock<IConfigService> ConfigService { get; } = new();
|
||||
public Mock<IAppService> AppService { get; } = new();
|
||||
public Mock<ITinyEventBus> EventBus { get; } = new();
|
||||
public Mock<IConfigurationManagementService> ConfigurationManagement { get; } = new();
|
||||
public Mock<IReleaseManagementService> ReleaseManagement { get; } = new();
|
||||
|
||||
public ConfigController CreateController()
|
||||
{
|
||||
var controller = new ConfigController(
|
||||
ConfigService.Object,
|
||||
AppService.Object,
|
||||
EventBus.Object,
|
||||
ConfigurationManagement.Object,
|
||||
ReleaseManagement.Object);
|
||||
controller.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() };
|
||||
return controller;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
||||
using AgileConfig.Server.Apisite.Controllers;
|
||||
using AgileConfig.Server.Apisite.Models;
|
||||
using AgileConfig.Server.Application;
|
||||
using AgileConfig.Server.Common;
|
||||
using AgileConfig.Server.Data.Entity;
|
||||
using AgileConfig.Server.IService;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -213,4 +214,82 @@ public class TestAppController
|
||||
Assert.IsTrue(addedConfigs.All(x => x.OnlineStatus == OnlineStatus.WaitPublish));
|
||||
Assert.IsTrue(addedConfigs.All(x => x.Status == ConfigStatus.Enabled));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task SearchAndReadEndpoints_ReturnAppsForAdministrators()
|
||||
{
|
||||
var appService = new Mock<IAppService>();
|
||||
var userService = new Mock<IUserService>();
|
||||
var configService = new Mock<IConfigService>();
|
||||
var settingService = new Mock<ISettingService>();
|
||||
var controller = BuildController(appService, configService, settingService, userService);
|
||||
controller.ControllerContext.HttpContext.User = new System.Security.Claims.ClaimsPrincipal(
|
||||
new System.Security.Claims.ClaimsIdentity(
|
||||
[new System.Security.Claims.Claim("id", "admin-user")], "test"));
|
||||
|
||||
var parent = new App { Id = "parent", Name = "Parent", Enabled = true };
|
||||
var child = new App { Id = "child", Name = "Child", Enabled = true };
|
||||
appService.Setup(x => x.SearchAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
|
||||
It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>(), "admin-user", true))
|
||||
.ReturnsAsync((new List<App> { parent }, 1L));
|
||||
appService.Setup(x => x.SearchGroupedAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
|
||||
It.IsAny<string>(), It.IsAny<int>(), It.IsAny<int>(), "admin-user", true))
|
||||
.ReturnsAsync((new List<GroupedApp> { new() { App = parent, Children = [new GroupedApp { App = child }] } }, 1L));
|
||||
appService.Setup(x => x.GetInheritancedAppsAsync(It.IsAny<string>())).ReturnsAsync(new List<App>());
|
||||
appService.Setup(x => x.GetAsync("parent")).ReturnsAsync(parent);
|
||||
userService.Setup(x => x.GetUserRolesAsync("admin-user"))
|
||||
.ReturnsAsync(new List<Role> { new() { Id = SystemRoleConstants.AdminId } });
|
||||
|
||||
Assert.IsInstanceOfType(await controller.Search(null, null, null, "name", "ascend", false), typeof(JsonResult));
|
||||
Assert.IsInstanceOfType(await controller.Search(null, null, null, "name", "ascend", true), typeof(JsonResult));
|
||||
Assert.IsInstanceOfType(await controller.Get("parent"), typeof(JsonResult));
|
||||
Assert.IsInstanceOfType(await controller.Get("missing"), typeof(NotFoundObjectResult));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task AppManagementAndExportEndpoints_ExecuteExpectedServiceOperations()
|
||||
{
|
||||
var appService = new Mock<IAppService>();
|
||||
var userService = new Mock<IUserService>();
|
||||
var configService = new Mock<IConfigService>();
|
||||
var settingService = new Mock<ISettingService>();
|
||||
var applicationManagement = new Mock<IApplicationManagementService>();
|
||||
var controller = new AppController(appService.Object, userService.Object, configService.Object, settingService.Object,
|
||||
applicationManagement.Object);
|
||||
controller.ControllerContext.HttpContext = new DefaultHttpContext();
|
||||
controller.ControllerContext.HttpContext.User = new System.Security.Claims.ClaimsPrincipal(
|
||||
new System.Security.Claims.ClaimsIdentity(
|
||||
[new System.Security.Claims.Claim("id", "admin-user")], "test"));
|
||||
|
||||
var app = new App { Id = "app-1", Name = "App", Enabled = true, Type = AppType.PRIVATE };
|
||||
appService.Setup(x => x.GetAsync("app-1")).ReturnsAsync(app);
|
||||
appService.Setup(x => x.GetInheritancedAppsAsync("app-1")).ReturnsAsync(new List<App>());
|
||||
appService.Setup(x => x.UpdateAsync(app)).ReturnsAsync(true);
|
||||
appService.Setup(x => x.SearchAsync("app-1", null, null, nameof(App.Id), "ascend", 1, 1, "admin-user", false))
|
||||
.ReturnsAsync((new List<App> { app }, 1L));
|
||||
appService.Setup(x => x.SaveUserAppAuth("app-1", It.IsAny<List<string>>())).ReturnsAsync(true);
|
||||
appService.Setup(x => x.GetUserAppAuth("app-1")).ReturnsAsync(new List<User> { new() { Id = "user-1" } });
|
||||
appService.Setup(x => x.GetAllInheritancedAppsAsync()).ReturnsAsync(new List<App> { app, new() { Id = "hidden", Enabled = false } });
|
||||
appService.Setup(x => x.GetAppGroups()).ReturnsAsync(new List<string> { "z", "a" });
|
||||
userService.Setup(x => x.GetUserRolesAsync("admin-user"))
|
||||
.ReturnsAsync(new List<Role> { new() { Id = SystemRoleConstants.AdminId } });
|
||||
settingService.Setup(x => x.GetEnvironmentList()).ReturnsAsync(["DEV", "DEV", ""]);
|
||||
configService.Setup(x => x.GetByAppIdAsync("app-1", "DEV")).ReturnsAsync(new List<Config>
|
||||
{
|
||||
new() { Key = "key", Value = "value", Group = "group" }
|
||||
});
|
||||
applicationManagement.Setup(x => x.UpdateAsync(It.IsAny<UpdateApplicationCommand>()))
|
||||
.ReturnsAsync(ApplicationResult<App>.Failure(ApplicationError.ValidationFailed));
|
||||
applicationManagement.Setup(x => x.DeleteAsync(It.IsAny<DeleteApplicationCommand>()))
|
||||
.ReturnsAsync(ApplicationResult<App>.Success(app));
|
||||
|
||||
Assert.IsInstanceOfType(await controller.DisableOrEnable("app-1"), typeof(JsonResult));
|
||||
Assert.IsInstanceOfType(await controller.Edit(new AppVM { Id = "app-1" }), typeof(JsonResult));
|
||||
Assert.IsInstanceOfType(await controller.Delete("app-1"), typeof(JsonResult));
|
||||
Assert.IsInstanceOfType(await controller.Export(new AppExportRequest { AppIds = ["app-1", "APP-1"] }), typeof(FileContentResult));
|
||||
Assert.IsInstanceOfType(await controller.InheritancedApps("app-1"), typeof(JsonResult));
|
||||
Assert.IsInstanceOfType(await controller.SaveAppAuth(new AppAuthVM { AppId = "app-1", AuthorizedUsers = ["user-1"] }), typeof(JsonResult));
|
||||
Assert.IsInstanceOfType(await controller.GetUserAppAuth("app-1"), typeof(JsonResult));
|
||||
Assert.IsInstanceOfType(await controller.GetAppGroups(), typeof(JsonResult));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using AgileConfig.Server.Apisite.Controllers;
|
||||
using AgileConfig.Server.Apisite.Models;
|
||||
using AgileConfig.Server.Common;
|
||||
using AgileConfig.Server.Common.EventBus;
|
||||
using AgileConfig.Server.Common.Resources;
|
||||
using AgileConfig.Server.Data.Entity;
|
||||
using AgileConfig.Server.Event;
|
||||
using AgileConfig.Server.IService;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Moq;
|
||||
|
||||
namespace ApiSiteTests;
|
||||
|
||||
[TestClass]
|
||||
public sealed class UserControllerCoverageTests
|
||||
{
|
||||
[TestMethod]
|
||||
public async Task Search_FiltersSortsPagesAndMapsRoles()
|
||||
{
|
||||
var userService = new Mock<IUserService>();
|
||||
var eventBus = new Mock<ITinyEventBus>();
|
||||
var baseTime = new DateTime(2026, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||
var users = new List<User>
|
||||
{
|
||||
new() { Id = "old", UserName = "alice", Team = "zeta", Status = UserStatus.Normal, CreateTime = baseTime.AddMinutes(-10) },
|
||||
new() { Id = "new", UserName = "alice", Team = "alpha", Status = UserStatus.Normal, CreateTime = baseTime },
|
||||
new() { Id = "null-name", UserName = null, Team = "alpha", Status = UserStatus.Normal, CreateTime = baseTime.AddMinutes(-1) },
|
||||
new() { Id = "null-team", UserName = "alice", Team = null, Status = UserStatus.Normal, CreateTime = baseTime.AddMinutes(-2) },
|
||||
new() { Id = "deleted", UserName = "alice", Team = "alpha", Status = UserStatus.Deleted, CreateTime = baseTime.AddMinutes(1) },
|
||||
new() { Id = SystemSettings.SuperAdminId, UserName = "admin", Team = "root", Status = UserStatus.Normal, CreateTime = baseTime.AddMinutes(2) }
|
||||
};
|
||||
userService.Setup(x => x.GetAll()).ReturnsAsync(users);
|
||||
userService.Setup(x => x.GetUserRolesAsync(It.IsAny<string>()))
|
||||
.ReturnsAsync((string id) => new List<Role>
|
||||
{
|
||||
new() { Id = id + "-role", Name = id + " role" }
|
||||
});
|
||||
var controller = CreateController(userService, eventBus);
|
||||
|
||||
var filtered = AsJson(await controller.Search("alice", "alpha", 1, 10));
|
||||
var filteredUsers = Read<IEnumerable<UserVM>>(filtered, "data").ToList();
|
||||
|
||||
Assert.AreEqual(1, Read<int>(filtered, "total"));
|
||||
Assert.AreEqual("new", filteredUsers.Single().Id);
|
||||
CollectionAssert.AreEqual(new[] { "new-role" }, filteredUsers.Single().UserRoleIds);
|
||||
CollectionAssert.AreEqual(new[] { "new role" }, filteredUsers.Single().UserRoleNames);
|
||||
|
||||
var paged = AsJson(await controller.Search(null, null, 2, 2));
|
||||
var pagedUsers = Read<IEnumerable<UserVM>>(paged, "data").ToList();
|
||||
|
||||
Assert.AreEqual(2, Read<int>(paged, "current"));
|
||||
Assert.AreEqual(2, Read<int>(paged, "pageSize"));
|
||||
Assert.AreEqual(4, Read<int>(paged, "total"));
|
||||
CollectionAssert.AreEqual(new[] { "null-team", "old" }, pagedUsers.Select(x => x.Id).ToArray());
|
||||
userService.Verify(x => x.GetUserRolesAsync(It.IsAny<string>()), Times.Exactly(3));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Search_RejectsInvalidPaging()
|
||||
{
|
||||
var userService = new Mock<IUserService>();
|
||||
var controller = CreateController(userService, new Mock<ITinyEventBus>());
|
||||
|
||||
Assert.ThrowsExactly<ArgumentException>(() => controller.Search(null, null, 0, 20).GetAwaiter().GetResult());
|
||||
Assert.ThrowsExactly<ArgumentException>(() => controller.Search(null, null, 1, 0).GetAwaiter().GetResult());
|
||||
userService.Verify(x => x.GetAll(), Times.Never);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Add_RejectsNullAndExistingNormalUsers()
|
||||
{
|
||||
var userService = new Mock<IUserService>();
|
||||
var controller = CreateController(userService, new Mock<ITinyEventBus>());
|
||||
|
||||
Assert.ThrowsExactly<ArgumentNullException>(() => controller.Add(null).GetAwaiter().GetResult());
|
||||
|
||||
userService.Setup(x => x.GetUsersByNameAsync("already"))
|
||||
.ReturnsAsync(new List<User>
|
||||
{
|
||||
new() { Id = "existing", UserName = "already", Status = UserStatus.Normal }
|
||||
});
|
||||
var result = AsJson(await controller.Add(new UserVM { UserName = "already", Password = "password" }));
|
||||
|
||||
Assert.IsFalse(Read<bool>(result, "success"));
|
||||
StringAssert.Contains(Read<string>(result, "message"), "already");
|
||||
userService.Verify(x => x.AddAsync(It.IsAny<User>()), Times.Never);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Add_CreatesUserWithDefaultRoleAndRaisesEvent()
|
||||
{
|
||||
var userService = new Mock<IUserService>();
|
||||
var eventBus = new Mock<ITinyEventBus>();
|
||||
User addedUser = null;
|
||||
List<string> addedRoleIds = null;
|
||||
userService.Setup(x => x.GetUsersByNameAsync("new-user"))
|
||||
.ReturnsAsync(new List<User>
|
||||
{
|
||||
new() { Id = "deleted-user", UserName = "new-user", Status = UserStatus.Deleted }
|
||||
});
|
||||
userService.Setup(x => x.AddAsync(It.IsAny<User>()))
|
||||
.Callback<User>(user => addedUser = user)
|
||||
.ReturnsAsync(true);
|
||||
userService.Setup(x => x.UpdateUserRolesAsync(It.IsAny<string>(), It.IsAny<List<string>>()))
|
||||
.Callback<string, List<string>>((_, roleIds) => addedRoleIds = roleIds)
|
||||
.ReturnsAsync(true);
|
||||
var controller = CreateController(userService, eventBus, "admin-user");
|
||||
|
||||
var result = AsJson(await controller.Add(new UserVM
|
||||
{
|
||||
UserName = "new-user",
|
||||
Password = "new-password",
|
||||
Team = "platform",
|
||||
UserRoleIds = null
|
||||
}));
|
||||
|
||||
Assert.IsTrue(Read<bool>(result, "success"));
|
||||
Assert.IsNotNull(addedUser);
|
||||
Assert.AreEqual("new-user", addedUser.UserName);
|
||||
Assert.AreEqual("platform", addedUser.Team);
|
||||
Assert.AreEqual(UserStatus.Normal, addedUser.Status);
|
||||
Assert.IsFalse(string.IsNullOrEmpty(addedUser.Id));
|
||||
Assert.AreEqual(Encrypt.Md5("new-password" + addedUser.Salt), addedUser.Password);
|
||||
CollectionAssert.AreEqual(new[] { SystemRoleConstants.OperatorId }, addedRoleIds);
|
||||
eventBus.Verify(x => x.Fire(It.Is<AddUserSuccessful>(evt =>
|
||||
ReferenceEquals(evt.User, addedUser) && evt.UserName == "admin-user")), Times.Once);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Add_ReportsAddAndRoleFailures()
|
||||
{
|
||||
var userService = new Mock<IUserService>();
|
||||
var eventBus = new Mock<ITinyEventBus>();
|
||||
userService.Setup(x => x.GetUsersByNameAsync(It.IsAny<string>())).ReturnsAsync(new List<User>());
|
||||
userService.Setup(x => x.AddAsync(It.IsAny<User>())).ReturnsAsync(false);
|
||||
userService.Setup(x => x.UpdateUserRolesAsync(It.IsAny<string>(), It.IsAny<List<string>>())).ReturnsAsync(true);
|
||||
var controller = CreateController(userService, eventBus);
|
||||
|
||||
var addFailure = AsJson(await controller.Add(new UserVM
|
||||
{
|
||||
UserName = "failed-add",
|
||||
Password = "password",
|
||||
UserRoleIds = new List<string> { "role-1", "", "role-1", " " }
|
||||
}));
|
||||
Assert.IsFalse(Read<bool>(addFailure, "success"));
|
||||
Assert.AreEqual(Messages.AddUserFailed, Read<string>(addFailure, "message"));
|
||||
eventBus.Verify(x => x.Fire(It.IsAny<AddUserSuccessful>()), Times.Never);
|
||||
|
||||
userService.Setup(x => x.AddAsync(It.IsAny<User>())).ReturnsAsync(true);
|
||||
userService.Setup(x => x.UpdateUserRolesAsync(It.IsAny<string>(), It.IsAny<List<string>>())).ReturnsAsync(false);
|
||||
var roleFailure = AsJson(await controller.Add(new UserVM
|
||||
{
|
||||
UserName = "failed-roles",
|
||||
Password = "password",
|
||||
UserRoleIds = new List<string> { "role-1", "", "role-1", " " }
|
||||
}));
|
||||
Assert.IsFalse(Read<bool>(roleFailure, "success"));
|
||||
Assert.AreEqual(Messages.AddUserFailed, Read<string>(roleFailure, "message"));
|
||||
eventBus.Verify(x => x.Fire(It.IsAny<AddUserSuccessful>()), Times.Once);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Edit_HandlesNullMissingAndServiceResults()
|
||||
{
|
||||
var userService = new Mock<IUserService>();
|
||||
var eventBus = new Mock<ITinyEventBus>();
|
||||
var existingUser = new User { Id = "user-1", UserName = "user", Team = "old-team", Salt = "salt" };
|
||||
userService.Setup(x => x.GetUserAsync("missing")).ReturnsAsync((User)null);
|
||||
userService.Setup(x => x.GetUserAsync("user-1")).ReturnsAsync(existingUser);
|
||||
userService.SetupSequence(x => x.UpdateAsync(It.IsAny<User>()))
|
||||
.ReturnsAsync(true)
|
||||
.ReturnsAsync(false)
|
||||
.ReturnsAsync(true);
|
||||
userService.SetupSequence(x => x.UpdateUserRolesAsync(It.IsAny<string>(), It.IsAny<List<string>>()))
|
||||
.ReturnsAsync(true)
|
||||
.ReturnsAsync(true)
|
||||
.ReturnsAsync(false);
|
||||
var controller = CreateController(userService, eventBus, "editor");
|
||||
|
||||
Assert.ThrowsExactly<ArgumentNullException>(() => controller.Edit(null).GetAwaiter().GetResult());
|
||||
var missing = AsJson(await controller.Edit(new UserVM { Id = "missing", Team = "new-team" }));
|
||||
Assert.IsFalse(Read<bool>(missing, "success"));
|
||||
Assert.AreEqual(Messages.UserNotFoundForOperation, Read<string>(missing, "message"));
|
||||
|
||||
var success = AsJson(await controller.Edit(new UserVM
|
||||
{
|
||||
Id = "user-1",
|
||||
Team = "new-team",
|
||||
UserRoleIds = null
|
||||
}));
|
||||
Assert.IsTrue(Read<bool>(success, "success"));
|
||||
Assert.AreEqual("new-team", existingUser.Team);
|
||||
Assert.IsNotNull(existingUser.UpdateTime);
|
||||
|
||||
var updateFailure = AsJson(await controller.Edit(new UserVM
|
||||
{
|
||||
Id = "user-1",
|
||||
Team = "another-team",
|
||||
UserRoleIds = new List<string> { "role-1", "", "role-1" }
|
||||
}));
|
||||
Assert.IsFalse(Read<bool>(updateFailure, "success"));
|
||||
Assert.AreEqual(Messages.UpdateUserFailed, Read<string>(updateFailure, "message"));
|
||||
|
||||
var roleFailure = AsJson(await controller.Edit(new UserVM
|
||||
{
|
||||
Id = "user-1",
|
||||
Team = "final-team",
|
||||
UserRoleIds = new List<string> { "role-1", "", "role-1" }
|
||||
}));
|
||||
Assert.IsFalse(Read<bool>(roleFailure, "success"));
|
||||
eventBus.Verify(x => x.Fire(It.Is<EditUserSuccessful>(evt =>
|
||||
ReferenceEquals(evt.User, existingUser) && evt.UserName == "editor")), Times.Exactly(2));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task ResetPassword_HandlesMissingAndUpdateResults()
|
||||
{
|
||||
var userService = new Mock<IUserService>();
|
||||
var eventBus = new Mock<ITinyEventBus>();
|
||||
var user = new User { Id = "user-1", UserName = "target", Salt = "user-salt", Password = "old" };
|
||||
userService.Setup(x => x.GetUserAsync("missing")).ReturnsAsync((User)null);
|
||||
userService.Setup(x => x.GetUserAsync("user-1")).ReturnsAsync(user);
|
||||
userService.SetupSequence(x => x.UpdateAsync(It.IsAny<User>())).ReturnsAsync(false).ReturnsAsync(true);
|
||||
var controller = CreateController(userService, eventBus, "operator");
|
||||
|
||||
Assert.ThrowsExactly<ArgumentNullException>(() => controller.ResetPassword(null).GetAwaiter().GetResult());
|
||||
var missing = AsJson(await controller.ResetPassword("missing"));
|
||||
Assert.IsFalse(Read<bool>(missing, "success"));
|
||||
Assert.AreEqual(Messages.UserNotFoundForOperation, Read<string>(missing, "message"));
|
||||
|
||||
var failed = AsJson(await controller.ResetPassword("user-1"));
|
||||
Assert.IsFalse(Read<bool>(failed, "success"));
|
||||
Assert.AreEqual(Messages.ResetUserPasswordFailed, Read<string>(failed, "message"));
|
||||
Assert.AreEqual(Encrypt.Md5("123456" + user.Salt), user.Password);
|
||||
|
||||
var succeeded = AsJson(await controller.ResetPassword("user-1"));
|
||||
Assert.IsTrue(Read<bool>(succeeded, "success"));
|
||||
Assert.AreEqual("", Read<string>(succeeded, "message"));
|
||||
eventBus.Verify(x => x.Fire(It.Is<ResetUserPasswordSuccessful>(evt =>
|
||||
evt.OpUser == "operator" && evt.UserName == "target")), Times.Once);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task Delete_HandlesMissingAndUpdateResults()
|
||||
{
|
||||
var userService = new Mock<IUserService>();
|
||||
var eventBus = new Mock<ITinyEventBus>();
|
||||
var user = new User { Id = "user-1", UserName = "target", Status = UserStatus.Normal };
|
||||
userService.Setup(x => x.GetUserAsync("missing")).ReturnsAsync((User)null);
|
||||
userService.Setup(x => x.GetUserAsync("user-1")).ReturnsAsync(user);
|
||||
userService.SetupSequence(x => x.UpdateAsync(It.IsAny<User>())).ReturnsAsync(false).ReturnsAsync(true);
|
||||
var controller = CreateController(userService, eventBus, "operator");
|
||||
|
||||
Assert.ThrowsExactly<ArgumentNullException>(() => controller.Delete(null).GetAwaiter().GetResult());
|
||||
var missing = AsJson(await controller.Delete("missing"));
|
||||
Assert.IsFalse(Read<bool>(missing, "success"));
|
||||
Assert.AreEqual(Messages.UserNotFoundForOperation, Read<string>(missing, "message"));
|
||||
|
||||
var failed = AsJson(await controller.Delete("user-1"));
|
||||
Assert.IsFalse(Read<bool>(failed, "success"));
|
||||
Assert.AreEqual(Messages.DeleteUserFailed, Read<string>(failed, "message"));
|
||||
Assert.AreEqual(UserStatus.Deleted, user.Status);
|
||||
|
||||
var succeeded = AsJson(await controller.Delete("user-1"));
|
||||
Assert.IsTrue(Read<bool>(succeeded, "success"));
|
||||
Assert.AreEqual("", Read<string>(succeeded, "message"));
|
||||
eventBus.Verify(x => x.Fire(It.Is<DeleteUserSuccessful>(evt =>
|
||||
ReferenceEquals(evt.User, user) && evt.UserName == "operator")), Times.Once);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task AdminUsers_FiltersAndSortsNormalAdministrators()
|
||||
{
|
||||
var userService = new Mock<IUserService>();
|
||||
userService.Setup(x => x.GetUsersByRoleAsync(SystemRoleConstants.AdminId))
|
||||
.ReturnsAsync(new List<User>
|
||||
{
|
||||
new() { Id = "z", UserName = "z-user", Team = "z-team", Status = UserStatus.Normal },
|
||||
new() { Id = "a2", UserName = "z-user", Team = "a-team", Status = UserStatus.Normal },
|
||||
new() { Id = "a1", UserName = "a-user", Team = "a-team", Status = UserStatus.Normal },
|
||||
new() { Id = "deleted", UserName = "deleted", Team = "", Status = UserStatus.Deleted }
|
||||
});
|
||||
var controller = CreateController(userService, new Mock<ITinyEventBus>());
|
||||
|
||||
var result = AsJson(await controller.AdminUsers());
|
||||
var users = Read<IEnumerable<UserVM>>(result, "data").ToList();
|
||||
|
||||
Assert.IsTrue(Read<bool>(result, "success"));
|
||||
CollectionAssert.AreEqual(new[] { "a1", "a2", "z" }, users.Select(x => x.Id).ToArray());
|
||||
userService.Verify(x => x.GetUsersByRoleAsync(SystemRoleConstants.AdminId), Times.Once);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task AllUsers_FiltersAndSortsNormalNonSuperAdministrators()
|
||||
{
|
||||
var userService = new Mock<IUserService>();
|
||||
userService.Setup(x => x.GetAll()).ReturnsAsync(new List<User>
|
||||
{
|
||||
new() { Id = SystemSettings.SuperAdminId, UserName = "admin", Team = "root", Status = UserStatus.Normal },
|
||||
new() { Id = "deleted", UserName = "deleted", Team = "a-team", Status = UserStatus.Deleted },
|
||||
new() { Id = "z", UserName = "z-user", Team = "z-team", Status = UserStatus.Normal },
|
||||
new() { Id = "a2", UserName = "z-user", Team = "a-team", Status = UserStatus.Normal },
|
||||
new() { Id = "a1", UserName = "a-user", Team = "a-team", Status = UserStatus.Normal }
|
||||
});
|
||||
var controller = CreateController(userService, new Mock<ITinyEventBus>());
|
||||
|
||||
var result = AsJson(await controller.AllUsers());
|
||||
var users = Read<IEnumerable<UserVM>>(result, "data").ToList();
|
||||
|
||||
Assert.IsTrue(Read<bool>(result, "success"));
|
||||
CollectionAssert.AreEqual(new[] { "a1", "a2", "z" }, users.Select(x => x.Id).ToArray());
|
||||
userService.Verify(x => x.GetAll(), Times.Once);
|
||||
}
|
||||
|
||||
private static UserController CreateController(Mock<IUserService> userService,
|
||||
Mock<ITinyEventBus> eventBus, string currentUser = "test-user")
|
||||
{
|
||||
var context = new DefaultHttpContext
|
||||
{
|
||||
User = new ClaimsPrincipal(new ClaimsIdentity(
|
||||
new[] { new Claim("username", currentUser) }, "test"))
|
||||
};
|
||||
var controller = new UserController(userService.Object, eventBus.Object);
|
||||
controller.ControllerContext = new ControllerContext { HttpContext = context };
|
||||
return controller;
|
||||
}
|
||||
|
||||
private static JsonResult AsJson(IActionResult result)
|
||||
{
|
||||
Assert.IsInstanceOfType(result, typeof(JsonResult));
|
||||
return (JsonResult)result;
|
||||
}
|
||||
|
||||
private static T Read<T>(JsonResult result, string propertyName)
|
||||
{
|
||||
Assert.IsNotNull(result.Value);
|
||||
var property = result.Value.GetType().GetProperty(propertyName);
|
||||
Assert.IsNotNull(property);
|
||||
return (T)property.GetValue(result.Value);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user