aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author杨宇千 <crupest@outlook.com>2019-10-28 23:35:00 +0800
committer杨宇千 <crupest@outlook.com>2019-10-28 23:35:00 +0800
commit006d799d2fe5f081c188f95a8590c4b75a93caae (patch)
tree97ed1fa767e1492cd3df292247d8fd3e47252882
parentc11a1b7be5d41bb1825a7190c708fdb04923a4fd (diff)
downloadtimeline-006d799d2fe5f081c188f95a8590c4b75a93caae.tar.gz
timeline-006d799d2fe5f081c188f95a8590c4b75a93caae.tar.bz2
timeline-006d799d2fe5f081c188f95a8590c4b75a93caae.zip
Add UserDetailController unit tests.
-rw-r--r--Timeline.Tests/Controllers/UserDetailControllerTest.cs93
-rw-r--r--Timeline.Tests/Helpers/ParameterInfoAssertions.cs63
-rw-r--r--Timeline.Tests/Helpers/ReflectionHelper.cs13
-rw-r--r--Timeline.Tests/Helpers/ResponseAssertions.cs (renamed from Timeline.Tests/Helpers/AssertionResponseExtensions.cs)0
-rw-r--r--Timeline/Controllers/UserDetailController.cs44
-rw-r--r--Timeline/Filters/User.cs42
-rw-r--r--Timeline/Resources/Filters.Designer.cs9
-rw-r--r--Timeline/Resources/Filters.resx3
-rw-r--r--Timeline/Resources/Filters.zh.resx3
9 files changed, 270 insertions, 0 deletions
diff --git a/Timeline.Tests/Controllers/UserDetailControllerTest.cs b/Timeline.Tests/Controllers/UserDetailControllerTest.cs
new file mode 100644
index 00000000..99341c40
--- /dev/null
+++ b/Timeline.Tests/Controllers/UserDetailControllerTest.cs
@@ -0,0 +1,93 @@
+using FluentAssertions;
+using Microsoft.AspNetCore.Mvc;
+using Moq;
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.Threading.Tasks;
+using Timeline.Controllers;
+using Timeline.Filters;
+using Timeline.Models.Validation;
+using Timeline.Services;
+using Timeline.Tests.Helpers;
+using Xunit;
+
+namespace Timeline.Tests.Controllers
+{
+ public class UserDetailControllerTest : IDisposable
+ {
+ private readonly Mock<IUserDetailService> _mockUserDetailService;
+ private readonly UserDetailController _controller;
+
+ public UserDetailControllerTest()
+ {
+ _mockUserDetailService = new Mock<IUserDetailService>();
+ _controller = new UserDetailController(_mockUserDetailService.Object);
+ }
+
+ public void Dispose()
+ {
+ _controller.Dispose();
+ }
+
+ [Fact]
+ public void AttributeTest()
+ {
+ typeof(UserDetailController).Should().BeDecoratedWith<ApiControllerAttribute>();
+
+ var getNickname = typeof(UserDetailController).GetMethod(nameof(UserDetailController.GetNickname));
+ getNickname.Should().BeDecoratedWith<HttpGetAttribute>()
+ .And.BeDecoratedWith<CatchUserNotExistExceptionAttribute>();
+ getNickname.GetParameter("username").Should().BeDecoratedWith<UsernameAttribute>()
+ .And.BeDecoratedWith<FromRouteAttribute>();
+
+ var putNickname = typeof(UserDetailController).GetMethod(nameof(UserDetailController.PutNickname));
+ putNickname.Should().BeDecoratedWith<HttpPutAttribute>()
+ .And.BeDecoratedWith<CatchUserNotExistExceptionAttribute>();
+ putNickname.GetParameter("username").Should().BeDecoratedWith<UsernameAttribute>()
+ .And.BeDecoratedWith<FromRouteAttribute>();
+ var stringLengthAttributeOnPutBody = putNickname.GetParameter("body").Should().BeDecoratedWith<FromBodyAttribute>()
+ .And.BeDecoratedWith<StringLengthAttribute>()
+ .Which;
+ stringLengthAttributeOnPutBody.MinimumLength.Should().Be(1);
+ stringLengthAttributeOnPutBody.MaximumLength.Should().Be(10);
+
+ var deleteNickname = typeof(UserDetailController).GetMethod(nameof(UserDetailController.DeleteNickname));
+ deleteNickname.Should().BeDecoratedWith<HttpDeleteAttribute>()
+ .And.BeDecoratedWith<CatchUserNotExistExceptionAttribute>();
+ deleteNickname.GetParameter("username").Should().BeDecoratedWith<UsernameAttribute>()
+ .And.BeDecoratedWith<FromRouteAttribute>();
+ }
+
+ [Fact]
+ public async Task GetNickname_ShouldWork()
+ {
+ const string username = "uuu";
+ const string nickname = "nnn";
+ _mockUserDetailService.Setup(s => s.GetNickname(username)).ReturnsAsync(nickname);
+ var actionResult = await _controller.GetNickname(username);
+ actionResult.Result.Should().BeAssignableTo<OkObjectResult>(nickname);
+ _mockUserDetailService.VerifyAll();
+ }
+
+ [Fact]
+ public async Task PutNickname_ShouldWork()
+ {
+ const string username = "uuu";
+ const string nickname = "nnn";
+ _mockUserDetailService.Setup(s => s.SetNickname(username, nickname)).Returns(Task.CompletedTask);
+ var actionResult = await _controller.PutNickname(username, nickname);
+ actionResult.Should().BeAssignableTo<OkResult>();
+ _mockUserDetailService.VerifyAll();
+ }
+
+ [Fact]
+ public async Task DeleteNickname_ShouldWork()
+ {
+ const string username = "uuu";
+ _mockUserDetailService.Setup(s => s.SetNickname(username, null)).Returns(Task.CompletedTask);
+ var actionResult = await _controller.DeleteNickname(username);
+ actionResult.Should().BeAssignableTo<OkResult>();
+ _mockUserDetailService.VerifyAll();
+ }
+ }
+}
diff --git a/Timeline.Tests/Helpers/ParameterInfoAssertions.cs b/Timeline.Tests/Helpers/ParameterInfoAssertions.cs
new file mode 100644
index 00000000..e3becee1
--- /dev/null
+++ b/Timeline.Tests/Helpers/ParameterInfoAssertions.cs
@@ -0,0 +1,63 @@
+using FluentAssertions;
+using FluentAssertions.Execution;
+using FluentAssertions.Formatting;
+using FluentAssertions.Primitives;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Threading.Tasks;
+
+namespace Timeline.Tests.Helpers
+{
+ public class ParameterInfoValueFormatter : IValueFormatter
+ {
+ public bool CanHandle(object value)
+ {
+ return value is ParameterInfo;
+ }
+
+ public string Format(object value, FormattingContext context, FormatChild formatChild)
+ {
+ var param = (ParameterInfo)value;
+ return $"{param.Member.DeclaringType.FullName}.{param.Member.Name}#{param.Name}";
+ }
+ }
+
+ public class ParameterInfoAssertions : ReferenceTypeAssertions<ParameterInfo, ParameterInfoAssertions>
+ {
+ static ParameterInfoAssertions()
+ {
+ Formatter.AddFormatter(new ParameterInfoValueFormatter());
+ }
+
+ public ParameterInfoAssertions(ParameterInfo parameterInfo)
+ {
+ Subject = parameterInfo;
+ }
+
+ protected override string Identifier => "parameter";
+
+ public AndWhichConstraint<ParameterInfoAssertions, TAttribute> BeDecoratedWith<TAttribute>(string because = "", params object[] becauseArgs)
+ where TAttribute : Attribute
+ {
+ var attribute = Subject.GetCustomAttribute<TAttribute>(false);
+
+ Execute.Assertion
+ .BecauseOf(because, becauseArgs)
+ .ForCondition(attribute != null)
+ .FailWith("Expected {0} {1} to be decorated with {2}{reason}, but that attribute was not found.",
+ Identifier, Subject, typeof(TAttribute).FullName);
+
+ return new AndWhichConstraint<ParameterInfoAssertions, TAttribute>(this, attribute);
+ }
+ }
+
+ public static class ParameterInfoAssertionExtensions
+ {
+ public static ParameterInfoAssertions Should(this ParameterInfo parameterInfo)
+ {
+ return new ParameterInfoAssertions(parameterInfo);
+ }
+ }
+}
diff --git a/Timeline.Tests/Helpers/ReflectionHelper.cs b/Timeline.Tests/Helpers/ReflectionHelper.cs
new file mode 100644
index 00000000..3f6036e3
--- /dev/null
+++ b/Timeline.Tests/Helpers/ReflectionHelper.cs
@@ -0,0 +1,13 @@
+using System.Linq;
+using System.Reflection;
+
+namespace Timeline.Tests.Helpers
+{
+ public static class ReflectionHelper
+ {
+ public static ParameterInfo GetParameter(this MethodInfo methodInfo, string name)
+ {
+ return methodInfo.GetParameters().Where(p => p.Name == name).Single();
+ }
+ }
+}
diff --git a/Timeline.Tests/Helpers/AssertionResponseExtensions.cs b/Timeline.Tests/Helpers/ResponseAssertions.cs
index 08f10b2b..08f10b2b 100644
--- a/Timeline.Tests/Helpers/AssertionResponseExtensions.cs
+++ b/Timeline.Tests/Helpers/ResponseAssertions.cs
diff --git a/Timeline/Controllers/UserDetailController.cs b/Timeline/Controllers/UserDetailController.cs
new file mode 100644
index 00000000..ef13b462
--- /dev/null
+++ b/Timeline/Controllers/UserDetailController.cs
@@ -0,0 +1,44 @@
+using Microsoft.AspNetCore.Mvc;
+using System.Threading.Tasks;
+using Timeline.Filters;
+using Timeline.Models.Validation;
+using Timeline.Services;
+using System.ComponentModel.DataAnnotations;
+
+namespace Timeline.Controllers
+{
+ [ApiController]
+ public class UserDetailController : Controller
+ {
+ private readonly IUserDetailService _service;
+
+ public UserDetailController(IUserDetailService service)
+ {
+ _service = service;
+ }
+
+ [HttpGet("users/{username}/nickname")]
+ [CatchUserNotExistException]
+ public async Task<ActionResult<string>> GetNickname([FromRoute][Username] string username)
+ {
+ return Ok(await _service.GetNickname(username));
+ }
+
+ [HttpPut("users/{username}/nickname")]
+ [CatchUserNotExistException]
+ public async Task<ActionResult> PutNickname([FromRoute][Username] string username,
+ [FromBody][StringLength(10, MinimumLength = 1)] string body)
+ {
+ await _service.SetNickname(username, body);
+ return Ok();
+ }
+
+ [HttpDelete("users/{username}/nickname")]
+ [CatchUserNotExistException]
+ public async Task<ActionResult> DeleteNickname([FromRoute][Username] string username)
+ {
+ await _service.SetNickname(username, null);
+ return Ok();
+ }
+ }
+}
diff --git a/Timeline/Filters/User.cs b/Timeline/Filters/User.cs
new file mode 100644
index 00000000..22fae938
--- /dev/null
+++ b/Timeline/Filters/User.cs
@@ -0,0 +1,42 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
+using System;
+using Timeline.Models.Http;
+
+namespace Timeline
+{
+ public static partial class ErrorCodes
+ {
+ public static partial class Http
+ {
+ public static partial class Filter // bxx = 1xx
+ {
+ public static class User // bbb = 101
+ {
+ public const int NotExist = 11010001;
+ }
+
+ }
+ }
+ }
+}
+
+namespace Timeline.Filters
+{
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)]
+ public class CatchUserNotExistExceptionAttribute : ExceptionFilterAttribute
+ {
+ [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062:Validate arguments of public methods", Justification = "ASP.Net already checked.")]
+ public override void OnException(ExceptionContext context)
+ {
+ var body = new CommonResponse(
+ ErrorCodes.Http.Filter.User.NotExist,
+ Resources.Filters.MessageUserNotExist);
+
+ if (context.HttpContext.Request.Method == "GET")
+ context.Result = new NotFoundObjectResult(body);
+ else
+ context.Result = new BadRequestObjectResult(body);
+ }
+ }
+}
diff --git a/Timeline/Resources/Filters.Designer.cs b/Timeline/Resources/Filters.Designer.cs
index ae3565f7..e3c8be41 100644
--- a/Timeline/Resources/Filters.Designer.cs
+++ b/Timeline/Resources/Filters.Designer.cs
@@ -86,5 +86,14 @@ namespace Timeline.Resources {
return ResourceManager.GetString("MessageHeaderContentTypeMissing", resourceCulture);
}
}
+
+ /// <summary>
+ /// Looks up a localized string similar to The user does not exist..
+ /// </summary>
+ internal static string MessageUserNotExist {
+ get {
+ return ResourceManager.GetString("MessageUserNotExist", resourceCulture);
+ }
+ }
}
}
diff --git a/Timeline/Resources/Filters.resx b/Timeline/Resources/Filters.resx
index d2b7e68a..ba1fcee8 100644
--- a/Timeline/Resources/Filters.resx
+++ b/Timeline/Resources/Filters.resx
@@ -126,4 +126,7 @@
<data name="MessageHeaderContentTypeMissing" xml:space="preserve">
<value>Header Content-Type is required.</value>
</data>
+ <data name="MessageUserNotExist" xml:space="preserve">
+ <value>The user does not exist.</value>
+ </data>
</root> \ No newline at end of file
diff --git a/Timeline/Resources/Filters.zh.resx b/Timeline/Resources/Filters.zh.resx
index 90e97e49..690a3e39 100644
--- a/Timeline/Resources/Filters.zh.resx
+++ b/Timeline/Resources/Filters.zh.resx
@@ -126,4 +126,7 @@
<data name="MessageHeaderContentTypeMissing" xml:space="preserve">
<value>缺少必需的请求头Content-Type。</value>
</data>
+ <data name="MessageUserNotExist" xml:space="preserve">
+ <value>用户不存在。</value>
+ </data>
</root> \ No newline at end of file