/src/Nancy.Tests/Unit/ModelBinding/DefaultBinderFixture.cs

https://github.com/factormystic/Nancy · C# · 483 lines · 325 code · 97 blank · 61 comment · 0 complexity · 5dd8f3aba585d124848643b7403d252d MD5 · raw file

  1. namespace Nancy.Tests.Unit.ModelBinding
  2. {
  3. using System;
  4. using System.Collections.Generic;
  5. using FakeItEasy;
  6. using Nancy.ModelBinding;
  7. using Fakes;
  8. using Nancy.ModelBinding.DefaultConverters;
  9. using Xunit;
  10. public class DefaultBinderFixture
  11. {
  12. private readonly IFieldNameConverter passthroughNameConverter;
  13. private readonly BindingDefaults emptyDefaults;
  14. public DefaultBinderFixture()
  15. {
  16. this.passthroughNameConverter = A.Fake<IFieldNameConverter>();
  17. A.CallTo(() => this.passthroughNameConverter.Convert(null)).WithAnyArguments()
  18. .ReturnsLazily(f => (string)f.Arguments[0]);
  19. this.emptyDefaults = A.Fake<BindingDefaults>();
  20. A.CallTo(() => this.emptyDefaults.DefaultBodyDeserializers).Returns(new IBodyDeserializer[] { });
  21. A.CallTo(() => this.emptyDefaults.DefaultTypeConverters).Returns(new ITypeConverter[] { });
  22. }
  23. [Fact]
  24. public void Should_throw_if_type_converters_is_null()
  25. {
  26. // Given, When
  27. var result = Record.Exception(() => new DefaultBinder(null, new IBodyDeserializer[] { }, A.Fake<IFieldNameConverter>(), new BindingDefaults()));
  28. // Then
  29. result.ShouldBeOfType(typeof(ArgumentNullException));
  30. }
  31. [Fact]
  32. public void Should_throw_if_body_deserializers_is_null()
  33. {
  34. // Given, When
  35. var result = Record.Exception(() => new DefaultBinder(new ITypeConverter[] { }, null, A.Fake<IFieldNameConverter>(), new BindingDefaults()));
  36. // Then
  37. result.ShouldBeOfType(typeof(ArgumentNullException));
  38. }
  39. [Fact]
  40. public void Should_throw_if_field_name_converter_is_null()
  41. {
  42. // Given, When
  43. var result = Record.Exception(() => new DefaultBinder(new ITypeConverter[] { }, new IBodyDeserializer[] { }, null, new BindingDefaults()));
  44. // Then
  45. result.ShouldBeOfType(typeof(ArgumentNullException));
  46. }
  47. [Fact]
  48. public void Should_throw_if_defaults_is_null()
  49. {
  50. // Given, When
  51. var result = Record.Exception(() => new DefaultBinder(new ITypeConverter[] { }, new IBodyDeserializer[] { }, A.Fake<IFieldNameConverter>(), null));
  52. // Then
  53. result.ShouldBeOfType(typeof(ArgumentNullException));
  54. }
  55. [Fact]
  56. public void Should_call_body_deserializer_if_one_matches()
  57. {
  58. // Given
  59. var deserializer = A.Fake<IBodyDeserializer>();
  60. A.CallTo(() => deserializer.CanDeserialize(null)).WithAnyArguments().Returns(true);
  61. var binder = this.GetBinder(bodyDeserializers: new[] { deserializer });
  62. var context = CreateContextWithHeader("Content-Type", new[] { "application/xml" });
  63. // When
  64. binder.Bind(context, this.GetType());
  65. // Then
  66. A.CallTo(() => deserializer.Deserialize(null, null, null)).WithAnyArguments()
  67. .MustHaveHappened(Repeated.Exactly.Once);
  68. }
  69. [Fact]
  70. public void Should_not_call_body_deserializer_if_doesnt_match()
  71. {
  72. // Given
  73. var deserializer = A.Fake<IBodyDeserializer>();
  74. A.CallTo(() => deserializer.CanDeserialize(null)).WithAnyArguments().Returns(false);
  75. var binder = this.GetBinder(bodyDeserializers: new[] { deserializer });
  76. var context = CreateContextWithHeader("Content-Type", new[] { "application/xml" });
  77. // When
  78. binder.Bind(context, this.GetType());
  79. // Then
  80. A.CallTo(() => deserializer.Deserialize(null, null, null)).WithAnyArguments()
  81. .MustNotHaveHappened();
  82. }
  83. [Fact]
  84. public void Should_pass_request_content_type_to_can_deserialize()
  85. {
  86. // Then
  87. var deserializer = A.Fake<IBodyDeserializer>();
  88. var binder = this.GetBinder(bodyDeserializers: new[] { deserializer });
  89. var context = CreateContextWithHeader("Content-Type", new[] { "application/xml" });
  90. // When
  91. binder.Bind(context, this.GetType());
  92. // Then
  93. A.CallTo(() => deserializer.CanDeserialize("application/xml"))
  94. .MustHaveHappened(Repeated.Exactly.Once);
  95. }
  96. [Fact]
  97. public void Should_return_object_from_deserializer_if_one_returned()
  98. {
  99. // Given
  100. var modelObject = new object();
  101. var deserializer = A.Fake<IBodyDeserializer>();
  102. A.CallTo(() => deserializer.CanDeserialize(null)).WithAnyArguments().Returns(true);
  103. A.CallTo(() => deserializer.Deserialize(null, null, null)).WithAnyArguments().Returns(modelObject);
  104. var binder = this.GetBinder(bodyDeserializers: new[] { deserializer });
  105. var context = CreateContextWithHeader("Content-Type", new[] { "application/xml" });
  106. // When
  107. var result = binder.Bind(context, this.GetType());
  108. // Then
  109. result.ShouldBeSameAs(modelObject);
  110. }
  111. [Fact]
  112. public void Should_see_if_a_type_converter_is_available_for_each_property_on_the_model_where_incoming_value_exists()
  113. {
  114. // Given
  115. var typeConverter = A.Fake<ITypeConverter>();
  116. A.CallTo(() => typeConverter.CanConvertTo(null)).WithAnyArguments().Returns(false);
  117. var binder = this.GetBinder(typeConverters: new[] { typeConverter });
  118. var context = new NancyContext { Request = new FakeRequest("GET", "/") };
  119. context.Request.Form["StringProperty"] = "Test";
  120. context.Request.Form["IntProperty"] = "12";
  121. // When
  122. binder.Bind(context, typeof(TestModel));
  123. // Then
  124. A.CallTo(() => typeConverter.CanConvertTo(null)).WithAnyArguments()
  125. .MustHaveHappened(Repeated.Exactly.Times(2));
  126. }
  127. [Fact]
  128. public void Should_call_convert_on_type_converter_if_available()
  129. {
  130. // Given
  131. var typeConverter = A.Fake<ITypeConverter>();
  132. A.CallTo(() => typeConverter.CanConvertTo(typeof(string))).WithAnyArguments().Returns(true);
  133. A.CallTo(() => typeConverter.Convert(null, null, null)).WithAnyArguments().Returns(null);
  134. var binder = this.GetBinder(typeConverters: new[] { typeConverter });
  135. var context = new NancyContext { Request = new FakeRequest("GET", "/") };
  136. context.Request.Form["StringProperty"] = "Test";
  137. // When
  138. binder.Bind(context, typeof(TestModel));
  139. // Then
  140. A.CallTo(() => typeConverter.Convert(null, null, null)).WithAnyArguments()
  141. .MustHaveHappened(Repeated.Exactly.Once);
  142. }
  143. [Fact]
  144. public void Should_ignore_properties_that_cannot_be_converted()
  145. {
  146. // Given
  147. var binder = this.GetBinder(typeConverters: new[] { new FallbackConverter() });
  148. var context = new NancyContext { Request = new FakeRequest("GET", "/") };
  149. context.Request.Form["StringProperty"] = "Test";
  150. context.Request.Form["IntProperty"] = "12";
  151. context.Request.Form["DateProperty"] = "Broken";
  152. // When
  153. var result = (TestModel)binder.Bind(context, typeof(TestModel));
  154. // Then
  155. result.StringProperty.ShouldEqual("Test");
  156. result.IntProperty.ShouldEqual(12);
  157. result.DateProperty.ShouldEqual(default(DateTime));
  158. }
  159. [Fact]
  160. public void Should_use_field_name_converter_for_each_field()
  161. {
  162. // Given
  163. var binder = this.GetBinder();
  164. var context = new NancyContext { Request = new FakeRequest("GET", "/") };
  165. context.Request.Form["StringProperty"] = "Test";
  166. context.Request.Form["IntProperty"] = "12";
  167. // When
  168. binder.Bind(context, typeof(TestModel));
  169. // Then
  170. A.CallTo(() => this.passthroughNameConverter.Convert(null)).WithAnyArguments()
  171. .MustHaveHappened(Repeated.Exactly.Times(2));
  172. }
  173. [Fact]
  174. public void Should_not_bind_anything_on_blacklist()
  175. {
  176. // Given
  177. var binder = this.GetBinder(typeConverters: new[] { new FallbackConverter() });
  178. var context = new NancyContext { Request = new FakeRequest("GET", "/") };
  179. context.Request.Form["StringProperty"] = "Test";
  180. context.Request.Form["IntProperty"] = "12";
  181. // When
  182. var result = (TestModel)binder.Bind(context, typeof(TestModel), "IntProperty");
  183. // Then
  184. result.StringProperty.ShouldEqual("Test");
  185. result.IntProperty.ShouldEqual(0);
  186. }
  187. [Fact]
  188. public void Should_use_default_body_deserializer_if_one_found()
  189. {
  190. // Given
  191. var deserializer = A.Fake<IBodyDeserializer>();
  192. A.CallTo(() => deserializer.CanDeserialize(null)).WithAnyArguments().Returns(true);
  193. A.CallTo(() => this.emptyDefaults.DefaultBodyDeserializers).Returns(new[] { deserializer });
  194. var binder = this.GetBinder();
  195. var context = CreateContextWithHeader("Content-Type", new[] { "application/xml" });
  196. // When
  197. binder.Bind(context, this.GetType());
  198. // Then
  199. A.CallTo(() => deserializer.Deserialize(null, null, null)).WithAnyArguments()
  200. .MustHaveHappened(Repeated.Exactly.Once);
  201. }
  202. [Fact]
  203. public void Should_use_default_type_converter_if_one_found()
  204. {
  205. // Given
  206. var typeConverter = A.Fake<ITypeConverter>();
  207. A.CallTo(() => typeConverter.CanConvertTo(typeof(string))).WithAnyArguments().Returns(true);
  208. A.CallTo(() => typeConverter.Convert(null, null, null)).WithAnyArguments().Returns(null);
  209. A.CallTo(() => this.emptyDefaults.DefaultTypeConverters).Returns(new[] { typeConverter });
  210. var binder = this.GetBinder();
  211. var context = new NancyContext { Request = new FakeRequest("GET", "/") };
  212. context.Request.Form["StringProperty"] = "Test";
  213. // When
  214. binder.Bind(context, typeof(TestModel));
  215. // Then
  216. A.CallTo(() => typeConverter.Convert(null, null, null)).WithAnyArguments()
  217. .MustHaveHappened(Repeated.Exactly.Once);
  218. }
  219. [Fact]
  220. public void User_body_serializer_should_take_precedence_over_default_one()
  221. {
  222. // Given
  223. var userDeserializer = A.Fake<IBodyDeserializer>();
  224. A.CallTo(() => userDeserializer.CanDeserialize(null)).WithAnyArguments().Returns(true);
  225. var defaultDeserializer = A.Fake<IBodyDeserializer>();
  226. A.CallTo(() => defaultDeserializer.CanDeserialize(null)).WithAnyArguments().Returns(true);
  227. A.CallTo(() => this.emptyDefaults.DefaultBodyDeserializers).Returns(new[] { defaultDeserializer });
  228. var binder = this.GetBinder(bodyDeserializers: new[] { userDeserializer });
  229. var context = CreateContextWithHeader("Content-Type", new[] { "application/xml" });
  230. // When
  231. binder.Bind(context, this.GetType());
  232. // Then
  233. A.CallTo(() => userDeserializer.Deserialize(null, null, null)).WithAnyArguments()
  234. .MustHaveHappened(Repeated.Exactly.Once);
  235. A.CallTo(() => defaultDeserializer.Deserialize(null, null, null)).WithAnyArguments()
  236. .MustNotHaveHappened();
  237. }
  238. [Fact]
  239. public void User_type_converter_should_take_precedence_over_default_one()
  240. {
  241. // Given
  242. var userTypeConverter = A.Fake<ITypeConverter>();
  243. A.CallTo(() => userTypeConverter.CanConvertTo(typeof(string))).WithAnyArguments().Returns(true);
  244. A.CallTo(() => userTypeConverter.Convert(null, null, null)).WithAnyArguments().Returns(null);
  245. var defaultTypeConverter = A.Fake<ITypeConverter>();
  246. A.CallTo(() => defaultTypeConverter.CanConvertTo(typeof(string))).WithAnyArguments().Returns(true);
  247. A.CallTo(() => defaultTypeConverter.Convert(null, null, null)).WithAnyArguments().Returns(null);
  248. A.CallTo(() => this.emptyDefaults.DefaultTypeConverters).Returns(new[] { defaultTypeConverter });
  249. var binder = this.GetBinder(new[] { userTypeConverter });
  250. var context = new NancyContext { Request = new FakeRequest("GET", "/") };
  251. context.Request.Form["StringProperty"] = "Test";
  252. // When
  253. binder.Bind(context, typeof(TestModel));
  254. // Then
  255. A.CallTo(() => userTypeConverter.Convert(null, null, null)).WithAnyArguments()
  256. .MustHaveHappened(Repeated.Exactly.Once);
  257. A.CallTo(() => defaultTypeConverter.Convert(null, null, null)).WithAnyArguments()
  258. .MustNotHaveHappened();
  259. }
  260. [Fact]
  261. public void Should_bind_model_from_request()
  262. {
  263. var binder = this.GetBinder();
  264. var context = CreateContextWithHeader("Content-Type", new[] { "application/xml" });
  265. context.Request.Query["StringProperty"] = "Test";
  266. context.Request.Query["IntProperty"] = "0";
  267. // When
  268. var result = (TestModel)binder.Bind(context, typeof(TestModel));
  269. // Then
  270. result.StringProperty.ShouldEqual("Test");
  271. result.IntProperty.ShouldEqual(0);
  272. }
  273. [Fact]
  274. public void Should_bind_model_from_context_parameters()
  275. {
  276. var binder = this.GetBinder();
  277. var context = CreateContextWithHeader("Content-Type", new[] { "application/xml" });
  278. context.Parameters["StringProperty"] = "Test";
  279. context.Parameters["IntProperty"] = "0";
  280. // When
  281. var result = (TestModel)binder.Bind(context, typeof(TestModel));
  282. // Then
  283. result.StringProperty.ShouldEqual("Test");
  284. result.IntProperty.ShouldEqual(0);
  285. }
  286. [Fact]
  287. public void Form_properties_should_take_precendence_over_request_properties()
  288. {
  289. var binder = this.GetBinder();
  290. var context = CreateContextWithHeader("Content-Type", new[] { "application/xml" });
  291. context.Request.Form["StringProperty"] = "Test";
  292. context.Request.Form["IntProperty"] = "0";
  293. context.Request.Query["StringProperty"] = "Test2";
  294. context.Request.Query["IntProperty"] = "1";
  295. // When
  296. var result = (TestModel)binder.Bind(context, typeof(TestModel));
  297. // Then
  298. result.StringProperty.ShouldEqual("Test");
  299. result.IntProperty.ShouldEqual(0);
  300. }
  301. [Fact]
  302. public void Form_properties_should_take_precendence_over_request_properties_and_context_properties()
  303. {
  304. var binder = this.GetBinder();
  305. var context = CreateContextWithHeader("Content-Type", new[] { "application/xml" });
  306. context.Request.Form["StringProperty"] = "Test";
  307. context.Request.Form["IntProperty"] = "0";
  308. context.Request.Query["StringProperty"] = "Test2";
  309. context.Request.Query["IntProperty"] = "1";
  310. context.Parameters["StringProperty"] = "Test3";
  311. context.Parameters["IntProperty"] = "2";
  312. // When
  313. var result = (TestModel)binder.Bind(context, typeof(TestModel));
  314. // Then
  315. result.StringProperty.ShouldEqual("Test");
  316. result.IntProperty.ShouldEqual(0);
  317. }
  318. [Fact]
  319. public void Request_properties_should_take_precendence_over_context_properties()
  320. {
  321. var binder = this.GetBinder();
  322. var context = CreateContextWithHeader("Content-Type", new[] { "application/xml" });
  323. context.Request.Query["StringProperty"] = "Test";
  324. context.Request.Query["IntProperty"] = "0";
  325. context.Parameters["StringProperty"] = "Test2";
  326. context.Parameters["IntProperty"] = "1";
  327. // When
  328. var result = (TestModel)binder.Bind(context, typeof(TestModel));
  329. // Then
  330. result.StringProperty.ShouldEqual("Test");
  331. result.IntProperty.ShouldEqual(0);
  332. }
  333. [Fact]
  334. public void Should_be_able_to_bind_from_form_and_request_simultaneously()
  335. {
  336. var binder = this.GetBinder();
  337. var context = CreateContextWithHeader("Content-Type", new[] { "application/xml" });
  338. context.Request.Form["StringProperty"] = "Test";
  339. context.Request.Query["IntProperty"] = "0";
  340. // When
  341. var result = (TestModel)binder.Bind(context, typeof(TestModel));
  342. // Then
  343. result.StringProperty.ShouldEqual("Test");
  344. result.IntProperty.ShouldEqual(0);
  345. }
  346. [Fact]
  347. public void Should_be_able_to_bind_from_request_and_context_simultaneously()
  348. {
  349. var binder = this.GetBinder();
  350. var context = CreateContextWithHeader("Content-Type", new[] { "application/xml" });
  351. context.Request.Query["StringProperty"] = "Test";
  352. context.Parameters["IntProperty"] = "0";
  353. // When
  354. var result = (TestModel)binder.Bind(context, typeof(TestModel));
  355. // Then
  356. result.StringProperty.ShouldEqual("Test");
  357. result.IntProperty.ShouldEqual(0);
  358. }
  359. private IBinder GetBinder(IEnumerable<ITypeConverter> typeConverters = null, IEnumerable<IBodyDeserializer> bodyDeserializers = null, IFieldNameConverter nameConverter = null, BindingDefaults bindingDefaults = null)
  360. {
  361. var converters = typeConverters ?? new ITypeConverter[] { };
  362. var deserializers = bodyDeserializers ?? new IBodyDeserializer[] { };
  363. var converter = nameConverter ?? this.passthroughNameConverter;
  364. var defaults = bindingDefaults ?? this.emptyDefaults;
  365. return new DefaultBinder(converters, deserializers, converter, defaults);
  366. }
  367. private static NancyContext CreateContextWithHeader(string name, IEnumerable<string> values)
  368. {
  369. var header = new Dictionary<string, IEnumerable<string>>
  370. {
  371. { name, values }
  372. };
  373. return new NancyContext()
  374. {
  375. Request = new FakeRequest("GET", "/", header),
  376. Parameters = DynamicDictionary.Empty
  377. };
  378. }
  379. public class TestModel
  380. {
  381. public string StringProperty { get; set; }
  382. public int IntProperty { get; set; }
  383. public DateTime DateProperty { get; set; }
  384. }
  385. }
  386. }