From 54336e5634e52b1ad9a6e375afe2702311bb9a14 Mon Sep 17 00:00:00 2001 From: antoniovalentini Date: Sat, 11 Jun 2022 12:55:42 +0200 Subject: [PATCH 1/8] Migrate to net6 --- global.json | 2 +- src/Linker/Linker.csproj | 4 ++-- test/Linker.Tests/Linker.Tests.csproj | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/global.json b/global.json index 0b2cf8a..10b65be 100755 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "3.1.100", + "version": "6.0.100", "rollForward": "latestFeature" } } diff --git a/src/Linker/Linker.csproj b/src/Linker/Linker.csproj index f00e6b8..05102c4 100755 --- a/src/Linker/Linker.csproj +++ b/src/Linker/Linker.csproj @@ -5,9 +5,9 @@ - netcoreapp3.0 + net6.0 win-x64;linux-x64 - 8.0 + 10 portable 1701;NU1603 Exe diff --git a/test/Linker.Tests/Linker.Tests.csproj b/test/Linker.Tests/Linker.Tests.csproj index ce36062..be1efa6 100755 --- a/test/Linker.Tests/Linker.Tests.csproj +++ b/test/Linker.Tests/Linker.Tests.csproj @@ -1,8 +1,8 @@ - netcoreapp3.0 - 8.0 + net6.0 + 10 portable 1701;NU1603 Linker.Tests From e2e6b7c9ddaa5a806cb8c4bec7d672e83e4e37cd Mon Sep 17 00:00:00 2001 From: antoniovalentini Date: Sat, 11 Jun 2022 13:13:53 +0200 Subject: [PATCH 2/8] Merge program and startup into top level program --- src/Linker/Host/Program.cs | 22 ---------------------- src/Linker/Linker.csproj | 5 +---- src/Linker/Program.cs | 33 +++++++++++++++++++++++++++++++++ src/Linker/Web/Startup.cs | 27 --------------------------- 4 files changed, 34 insertions(+), 53 deletions(-) delete mode 100644 src/Linker/Host/Program.cs create mode 100644 src/Linker/Program.cs delete mode 100644 src/Linker/Web/Startup.cs diff --git a/src/Linker/Host/Program.cs b/src/Linker/Host/Program.cs deleted file mode 100644 index defb96f..0000000 --- a/src/Linker/Host/Program.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.IO; -using Linker.Web; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Logging; - -namespace Linker.Host -{ - public class Program - { - public static void Main(string[] args) - { - new WebHostBuilder() - .UseContentRoot(Directory.GetCurrentDirectory()) - .UseKestrel() - .UseIISIntegration() - .ConfigureLogging(log => log.AddConsole()) - .UseStartup() - .Build() - .Run(); - } - } -} diff --git a/src/Linker/Linker.csproj b/src/Linker/Linker.csproj index 05102c4..cb54573 100755 --- a/src/Linker/Linker.csproj +++ b/src/Linker/Linker.csproj @@ -27,10 +27,7 @@ - - - - + diff --git a/src/Linker/Program.cs b/src/Linker/Program.cs new file mode 100644 index 0000000..8bd9f2c --- /dev/null +++ b/src/Linker/Program.cs @@ -0,0 +1,33 @@ +using System.IO; +using Linker.Web.Configuration; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddRouting(); +builder.Services.AddControllers(); +builder.Services.AddLinkInMemoryStore(); + +builder.Host + .UseContentRoot(Directory.GetCurrentDirectory()) + .ConfigureLogging(log => log.AddConsole()); + +builder.WebHost + .UseKestrel() + .UseIISIntegration(); + +var app = builder.Build(); + +app.UseDefaultFiles(); +app.UseStaticFiles(); +app.UseRouting(); +app.UseEndpoints(endpoints => +{ + endpoints.MapControllers(); +}); + +app.Run(); diff --git a/src/Linker/Web/Startup.cs b/src/Linker/Web/Startup.cs deleted file mode 100644 index 2a52a60..0000000 --- a/src/Linker/Web/Startup.cs +++ /dev/null @@ -1,27 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyInjection; -using Linker.Web.Configuration; - -namespace Linker.Web -{ - public class Startup - { - public void ConfigureServices(IServiceCollection services) - { - services.AddRouting(); - services.AddControllers(); - services.AddLinkInMemoryStore(); - } - - public void Configure(IApplicationBuilder app) - { - app.UseDefaultFiles(); - app.UseStaticFiles(); - app.UseRouting(); - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } - } -} From 9021aefe38fbadeab49da4ae8b92196fe986589a Mon Sep 17 00:00:00 2001 From: antoniovalentini Date: Sat, 11 Jun 2022 14:54:24 +0200 Subject: [PATCH 3/8] Replace controller with minimal apis --- src/Linker/Program.cs | 47 +++++++++++++ src/Linker/Web/Controllers/LinkController.cs | 73 -------------------- 2 files changed, 47 insertions(+), 73 deletions(-) delete mode 100644 src/Linker/Web/Controllers/LinkController.cs diff --git a/src/Linker/Program.cs b/src/Linker/Program.cs index 8bd9f2c..2f27932 100644 --- a/src/Linker/Program.cs +++ b/src/Linker/Program.cs @@ -1,7 +1,10 @@ +using System; using System.IO; +using Linker.Model; using Linker.Web.Configuration; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -30,4 +33,48 @@ endpoints.MapControllers(); }); +app.MapGet("/{id}", (string id, IRetrieveLinks getLink) => +{ + var link = getLink.WithId(id); + + return link.HasValue + ? Results.Redirect(link.Value.Href.AbsoluteUri, true) + : Results.NotFound(); +}).WithName("Follow"); + +app.MapGet("/link/{id}", (string id, IRetrieveLinks getLink) => +{ + var link = getLink.WithId(id); + + return link.HasValue + ? Results.Ok(link) + : Results.NotFound(); +}).WithName("Metadata"); + +app.MapPut("/link/{id}", (string id, HttpContext ctx, ISaveLinks saveLink) + => Create(id, ctx.Request.Form["url"], saveLink)); + +app.MapPost("/link", (HttpContext ctx, ISaveLinks saveLink) => +{ + var url = ctx.Request.Form["url"]; + + return Create(UniqueId(), url, saveLink); + + string UniqueId() => Path.GetFileNameWithoutExtension(Path.GetRandomFileName()); +}); + +static IResult Create(string id, string url, ISaveLinks saveLink) +{ + if (IsNotAbsoluteUri(url)) + { + return Results.BadRequest("Invalid or missing URL"); + } + + saveLink.WithIdAndUrl(id, new Uri(url)); + + return Results.CreatedAtRoute("Follow", new { id }, url); + + bool IsNotAbsoluteUri(string uri) => !Uri.IsWellFormedUriString(uri, UriKind.Absolute); +} + app.Run(); diff --git a/src/Linker/Web/Controllers/LinkController.cs b/src/Linker/Web/Controllers/LinkController.cs deleted file mode 100644 index 458a1b4..0000000 --- a/src/Linker/Web/Controllers/LinkController.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using Microsoft.AspNetCore.Mvc; -using Linker.Model; -using System.IO; - -namespace Linker.Web.Controllers -{ - [Route("link")] - public class LinkController : Controller - { - private readonly IRetrieveLinks getLink; - private readonly ISaveLinks saveLink; - - public LinkController( - IRetrieveLinks getLink, - ISaveLinks saveLink) - { - this.getLink = getLink; - this.saveLink = saveLink; - } - - [HttpGet("/{id}", Name = "Follow")] - public IActionResult Follow(string id) - { - var link = getLink.WithId(id); - - if (link.HasValue) - { - return RedirectPermanent(link.Value.Href.AbsoluteUri); - } - - return NotFound(); - } - - [HttpGet("{id}", Name = "Metadata")] - public IActionResult Metadata(string id) - { - var link = getLink.WithId(id); - - if (link.HasValue) - { - return Ok(link); - } - - return NotFound(); - } - - [HttpPost] - public IActionResult Create(string url) - { - return Create(UniqueId(), url); - - string UniqueId() - => Path.GetFileNameWithoutExtension(Path.GetRandomFileName()); - } - - [HttpPut("{id}")] - public IActionResult Create(string id, string url) - { - if (IsNotAbsoluteUri(url)) - { - return BadRequest("Invalid or missing URL"); - } - - saveLink.WithIdAndUrl(id, new Uri(url)); - - return CreatedAtRoute("Follow", new { id }, url); - - bool IsNotAbsoluteUri(string uri) - => !Uri.IsWellFormedUriString(uri, UriKind.Absolute); - } - } -} From ef539813f1f35e67f72d47169c1c0d1db0f1caba Mon Sep 17 00:00:00 2001 From: antoniovalentini Date: Tue, 16 Aug 2022 15:41:11 +0200 Subject: [PATCH 4/8] Fix tests --- src/Linker/Model/Link.cs | 4 +- src/Linker/Program.cs | 10 +- .../CustomWebApplicationFactory.cs | 30 ++++++ .../Extensions/ActionResultExtensions.cs | 30 ------ .../LinkControllerActionResultExtensions.cs | 14 --- test/Linker.Tests/Linker.Tests.csproj | 1 + .../When_creating_a_new_link.cs | 72 ++++++-------- ...creating_a_new_link_with_an_invalid_url.cs | 41 ++++---- .../When_creating_a_new_link_without_id.cs | 94 +++++++++---------- .../When_creating_a_new_link_without_url.cs | 39 ++++---- .../When_following_a_known_link.cs | 40 ++++---- .../When_following_an_unknown_link.cs | 26 ++--- ...getting_the_metadata_about_a_known_link.cs | 52 ++++++---- ...ting_the_metadata_about_an_unknown_link.cs | 26 ++--- 14 files changed, 245 insertions(+), 234 deletions(-) create mode 100644 test/Linker.Tests/CustomWebApplicationFactory.cs delete mode 100644 test/Linker.Tests/Extensions/ActionResultExtensions.cs delete mode 100644 test/Linker.Tests/Extensions/LinkControllerActionResultExtensions.cs diff --git a/src/Linker/Model/Link.cs b/src/Linker/Model/Link.cs index 6324c7c..7b83d5d 100644 --- a/src/Linker/Model/Link.cs +++ b/src/Linker/Model/Link.cs @@ -15,8 +15,8 @@ public Link(object id, Uri href) Href = href; } - public object Id { get; } + public object Id { get; set; } - public Uri Href { get; } + public Uri Href { get; set; } } } diff --git a/src/Linker/Program.cs b/src/Linker/Program.cs index 2f27932..2bdcf6d 100644 --- a/src/Linker/Program.cs +++ b/src/Linker/Program.cs @@ -9,14 +9,18 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -var builder = WebApplication.CreateBuilder(args); +var webApplicationOptions = new WebApplicationOptions +{ + Args = args, + ContentRootPath = Directory.GetCurrentDirectory(), +}; +var builder = WebApplication.CreateBuilder(webApplicationOptions); builder.Services.AddRouting(); builder.Services.AddControllers(); builder.Services.AddLinkInMemoryStore(); builder.Host - .UseContentRoot(Directory.GetCurrentDirectory()) .ConfigureLogging(log => log.AddConsole()); builder.WebHost @@ -78,3 +82,5 @@ static IResult Create(string id, string url, ISaveLinks saveLink) } app.Run(); + +public partial class Program { } diff --git a/test/Linker.Tests/CustomWebApplicationFactory.cs b/test/Linker.Tests/CustomWebApplicationFactory.cs new file mode 100644 index 0000000..1b7cd62 --- /dev/null +++ b/test/Linker.Tests/CustomWebApplicationFactory.cs @@ -0,0 +1,30 @@ +using System.Linq; +using Linker.Model; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; + +namespace Linker.Tests; + +public class CustomWebApplicationFactory : WebApplicationFactory +{ + public readonly ISaveLinks SaveLinks = Substitute.For(); + public readonly IRetrieveLinks GetLinks = Substitute.For(); + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.ConfigureTestServices(services => + { + var saveLinks = services.Single(d => d.ServiceType == typeof(ISaveLinks)); + var retrieveLinks = services.Single(d => d.ServiceType == typeof(IRetrieveLinks)); + + services.Remove(saveLinks); + services.Remove(retrieveLinks); + + services.AddScoped(_ => SaveLinks); + services.AddScoped(_ => GetLinks); + }); + } +} diff --git a/test/Linker.Tests/Extensions/ActionResultExtensions.cs b/test/Linker.Tests/Extensions/ActionResultExtensions.cs deleted file mode 100644 index d2ce805..0000000 --- a/test/Linker.Tests/Extensions/ActionResultExtensions.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; -using Microsoft.AspNetCore.Mvc; - -namespace Linker.Tests -{ - internal static class ActionResultExtensions - { - internal static int? StatusCode(this IActionResult result) - { - return result.As().StatusCode; - } - - internal static T ContentAs(this IActionResult result) - { - return (T)result.As().Value; - } - - internal static T As(this IActionResult result) where T : class - { - if (!(result is T value)) - { - throw new ArgumentException( - $"The result should be of type {typeof(T)}, but is {result.GetType()}", - nameof(result)); - } - - return value; - } - } -} diff --git a/test/Linker.Tests/Extensions/LinkControllerActionResultExtensions.cs b/test/Linker.Tests/Extensions/LinkControllerActionResultExtensions.cs deleted file mode 100644 index 00240b3..0000000 --- a/test/Linker.Tests/Extensions/LinkControllerActionResultExtensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace Linker.Tests -{ - internal static class LinkControllerActionResultExtensions - { - internal static string CreatedLinkId(this IActionResult result) - { - return result - .As() - .RouteValues["id"] as string; - } - } -} diff --git a/test/Linker.Tests/Linker.Tests.csproj b/test/Linker.Tests/Linker.Tests.csproj index be1efa6..e167959 100755 --- a/test/Linker.Tests/Linker.Tests.csproj +++ b/test/Linker.Tests/Linker.Tests.csproj @@ -20,6 +20,7 @@ runtime; build; native; contentfiles; analyzers all + diff --git a/test/Linker.Tests/Web/Controllers/LinkControllerTests/When_creating_a_new_link.cs b/test/Linker.Tests/Web/Controllers/LinkControllerTests/When_creating_a_new_link.cs index d75073c..9ca6481 100644 --- a/test/Linker.Tests/Web/Controllers/LinkControllerTests/When_creating_a_new_link.cs +++ b/test/Linker.Tests/Web/Controllers/LinkControllerTests/When_creating_a_new_link.cs @@ -1,77 +1,63 @@ using System; -using Microsoft.AspNetCore.Mvc; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; using Xunit; using FluentAssertions; using NSubstitute; -using Linker.Web.Controllers; -using Linker.Model; namespace Linker.Tests { - public class When_creating_a_new_link + public class When_creating_a_new_link : IClassFixture { - [Fact] - public void Should_return_an_http_created_result() - { - var sut = new LinkController( - Substitute.For(), - Substitute.For()); + private readonly CustomWebApplicationFactory _factory; + private readonly HttpClient _client; + private readonly FormUrlEncodedContent _httpContent; - var result = sut.Create("id", "http://example.com"); + public When_creating_a_new_link(CustomWebApplicationFactory factory) + { + _factory = factory; + _client = factory.CreateClient(); - result.StatusCode().Should().Be(201); + _httpContent = new FormUrlEncodedContent( + new[] { new KeyValuePair("url", "http://example.com") }); } [Fact] - public void Should_return_the_route_name_for_the_created_link() + public async Task Should_return_an_http_created_result() { - var sut = new LinkController( - Substitute.For(), - Substitute.For()); + var result = await _client.PutAsync("/link/id", _httpContent); - var result = sut.Create("id", "http://example.com"); - - result.Should().BeOfType() - .Which.RouteName.Should().Be("Follow"); + result.StatusCode.Should().Be(HttpStatusCode.Created); } [Fact] - public void Should_return_the_route_id_for_the_created_link() + public async Task Should_return_the_redirect_location_for_the_created_link() { - var sut = new LinkController( - Substitute.For(), - Substitute.For()); - - var result = sut.Create("id", "http://example.com"); + var result = await _client.PutAsync("/link/id", _httpContent); - result.Should().BeOfType() - .Which.RouteValues.Should().ContainKey("id").And.NotBeNull(); + result.Should().BeOfType() + .Which.Headers.Should().ContainSingle(kv => kv.Key == "Location" && kv.Value.First() == "http://localhost/id"); } [Fact] - public void Should_return_the_url_of_the_created_link() + public async Task Should_return_the_url_of_the_created_link() { - var sut = new LinkController( - Substitute.For(), - Substitute.For()); + var result = await _client.PutAsync("/link/id", _httpContent); - var result = sut.Create("id", "http://example.com"); + var content = await result.Content.ReadAsStringAsync(); - result.Should().BeOfType() - .Which.Value.Should().Be("http://example.com"); + content.Should().Be("\"http://example.com\""); } [Fact] - public void Should_save_a_link_with_the_specified_id_and_url() + public async Task Should_save_a_link_with_the_specified_id_and_url() { - var saveLink = Substitute.For(); - var sut = new LinkController( - Substitute.For(), - saveLink); - - var result = sut.Create("id", "http://example.com"); + await _client.PutAsync("/link/id", _httpContent); - saveLink.Received().WithIdAndUrl("id", new Uri("http://example.com")); + _factory.SaveLinks.Received().WithIdAndUrl("id", new Uri("http://example.com")); } } } diff --git a/test/Linker.Tests/Web/Controllers/LinkControllerTests/When_creating_a_new_link_with_an_invalid_url.cs b/test/Linker.Tests/Web/Controllers/LinkControllerTests/When_creating_a_new_link_with_an_invalid_url.cs index eb31df3..fb4877e 100644 --- a/test/Linker.Tests/Web/Controllers/LinkControllerTests/When_creating_a_new_link_with_an_invalid_url.cs +++ b/test/Linker.Tests/Web/Controllers/LinkControllerTests/When_creating_a_new_link_with_an_invalid_url.cs @@ -1,42 +1,49 @@ +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; using FluentAssertions; -using Linker.Model; -using Linker.Web.Controllers; -using Microsoft.AspNetCore.Mvc; -using NSubstitute; using Xunit; namespace Linker.Tests { - public class When_creating_a_new_link_with_an_invalid_url + public class When_creating_a_new_link_with_an_invalid_url : IClassFixture { + private readonly CustomWebApplicationFactory _factory; + private readonly HttpClient _client; + + public When_creating_a_new_link_with_an_invalid_url(CustomWebApplicationFactory factory) + { + _factory = factory; + _client = factory.CreateClient(); + } + [Theory] [InlineData("example.com")] [InlineData("https://")] [InlineData("not an URL")] - public void Should_return_an_http_bad_request_result(string url) + public async Task Should_return_an_http_bad_request_result(string url) { - var sut = new LinkController( - Substitute.For(), - Substitute.For()); + var httpContent = new FormUrlEncodedContent( + new[] { new KeyValuePair("url", url) }); - var result = sut.Create(url); + var result = await _client.PutAsync("/link/id", httpContent); - result.Should().BeOfType(); + result.StatusCode.Should().Be(HttpStatusCode.BadRequest); } [Theory] [InlineData("example.com")] [InlineData("https://")] [InlineData("not an URL")] - public void Should_return_an_http_bad_request_result_even_with_a_provided_id(string url) + public async Task Should_return_an_http_bad_request_result_even_with_a_provided_id(string url) { - var sut = new LinkController( - Substitute.For(), - Substitute.For()); + var httpContent = new FormUrlEncodedContent( + new[] { new KeyValuePair("url", url) }); - var result = sut.Create(id: "example", url); + var result = await _client.PutAsync("/link/example", httpContent); - result.Should().BeOfType(); + result.StatusCode.Should().Be(HttpStatusCode.BadRequest); } } } diff --git a/test/Linker.Tests/Web/Controllers/LinkControllerTests/When_creating_a_new_link_without_id.cs b/test/Linker.Tests/Web/Controllers/LinkControllerTests/When_creating_a_new_link_without_id.cs index 40e3668..c57ec50 100644 --- a/test/Linker.Tests/Web/Controllers/LinkControllerTests/When_creating_a_new_link_without_id.cs +++ b/test/Linker.Tests/Web/Controllers/LinkControllerTests/When_creating_a_new_link_without_id.cs @@ -1,90 +1,88 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading.Tasks; using FluentAssertions; -using Linker.Model; -using Linker.Web.Controllers; -using Microsoft.AspNetCore.Mvc; using NSubstitute; using Xunit; namespace Linker.Tests { - public class When_creating_a_new_link_without_id + public class When_creating_a_new_link_without_id : IClassFixture { - [Fact] - public void Should_return_an_http_created_result() - { - var sut = new LinkController( - Substitute.For(), - Substitute.For()); + private readonly CustomWebApplicationFactory _factory; + private readonly HttpClient _client; + private readonly FormUrlEncodedContent _httpContent; - var result = sut.Create("http://example.com"); + public When_creating_a_new_link_without_id(CustomWebApplicationFactory factory) + { + _factory = factory; + _client = factory.CreateClient(); - result.StatusCode().Should().Be(201); + _httpContent = new FormUrlEncodedContent( + new[] { new KeyValuePair("url", "http://example.com") }); } [Fact] - public void Should_return_the_route_name_for_the_created_link() + public async Task Should_return_an_http_created_result() { - var sut = new LinkController( - Substitute.For(), - Substitute.For()); + var result = await _client.PostAsync("/link", _httpContent); - var result = sut.Create("http://example.com"); - - result.Should().BeOfType() - .Which.RouteName.Should().Be("Follow"); + result.StatusCode.Should().Be(HttpStatusCode.Created); } [Fact] - public void Should_return_the_generated_id_of_the_created_link() + public async Task Should_return_the_redirect_location_for_the_created_link() { - var sut = new LinkController( - Substitute.For(), - Substitute.For()); - - var result = sut.Create("http://example.com"); + var result = await _client.PostAsync("/link", _httpContent); - result.Should().BeOfType() - .Which.RouteValues.Should().ContainKey("id").And.NotBeNull(); + result.Should().BeOfType() + .Which.Headers.Should().ContainSingle(kv => kv.Key == "Location"); } [Fact] - public void Should_return_the_url_of_the_created_link() + public async Task Should_return_the_url_of_the_created_link() { - var sut = new LinkController( - Substitute.For(), - Substitute.For()); + var result = await _client.PostAsync("/link", _httpContent); - var result = sut.Create("http://example.com"); + var content = await result.Content.ReadAsStringAsync(); - result.Should().BeOfType() - .Which.Value.Should().Be("http://example.com"); + content.Should().Be("\"http://example.com\""); } [Fact] - public void Should_save_a_link_with_specified_url_and_the_returned_id() + public async Task Should_save_a_link_with_specified_url_and_the_returned_id() { - var saveLink = Substitute.For(); - var sut = new LinkController(Substitute.For(), saveLink); + var result = await _client.PostAsync("/link", _httpContent); - var id = sut.Create("http://example.com").CreatedLinkId(); + var id = GetLinkId(result.Headers); - saveLink.Received().WithIdAndUrl(id, new Uri("http://example.com")); + _factory.SaveLinks.Received().WithIdAndUrl(id, new Uri("http://example.com")); } [Fact] - public void Should_save_links_for_the_same_url_with_different_ids() + public async Task Should_save_links_for_the_same_url_with_different_ids() { - var saveLink = Substitute.For(); - var sut = new LinkController(Substitute.For(), saveLink); + var ids = new List(); + + var result = await _client.PostAsync("/link", _httpContent); - var ids = new[] - { - sut.Create("http://example.com").CreatedLinkId(), - sut.Create("http://example.com").CreatedLinkId() - }; + ids.Add(GetLinkId(result.Headers)); + + result = await _client.PostAsync("/link", _httpContent); + + ids.Add(GetLinkId(result.Headers)); ids.Should().OnlyHaveUniqueItems(); } + + private static string GetLinkId(HttpResponseHeaders headers) + { + var url = headers.First(h => h.Key == "Location").Value.First(); + return new Uri(url).AbsolutePath.TrimStart('/'); + } } } diff --git a/test/Linker.Tests/Web/Controllers/LinkControllerTests/When_creating_a_new_link_without_url.cs b/test/Linker.Tests/Web/Controllers/LinkControllerTests/When_creating_a_new_link_without_url.cs index 15f4b95..c9659bf 100644 --- a/test/Linker.Tests/Web/Controllers/LinkControllerTests/When_creating_a_new_link_without_url.cs +++ b/test/Linker.Tests/Web/Controllers/LinkControllerTests/When_creating_a_new_link_without_url.cs @@ -1,40 +1,45 @@ +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; using FluentAssertions; -using Linker.Model; -using Linker.Web.Controllers; -using Microsoft.AspNetCore.Mvc; -using NSubstitute; using Xunit; namespace Linker.Tests { - public class When_creating_a_new_link_without_url + public class When_creating_a_new_link_without_url : IClassFixture { + private readonly HttpClient _client; + + public When_creating_a_new_link_without_url(CustomWebApplicationFactory factory) + { + _client = factory.CreateClient(); + } + [Theory] [InlineData(null)] [InlineData("")] - public void Should_return_an_http_bad_request_result(string url) + public async Task Should_return_an_http_bad_request_result(string url) { - var sut = new LinkController( - Substitute.For(), - Substitute.For()); + var httpContent = new FormUrlEncodedContent( + new[] { new KeyValuePair("url", url) }); - var result = sut.Create(url); + var result = await _client.PostAsync("/link", httpContent); - result.Should().BeOfType(); + result.StatusCode.Should().Be(HttpStatusCode.BadRequest); } [Theory] [InlineData(null)] [InlineData("")] - public void Should_return_an_http_bad_request_result_even_with_a_provided_id(string url) + public async Task Should_return_an_http_bad_request_result_even_with_a_provided_id(string url) { - var sut = new LinkController( - Substitute.For(), - Substitute.For()); + var httpContent = new FormUrlEncodedContent( + new[] { new KeyValuePair("url", url) }); - var result = sut.Create(id: "example", url); + var result = await _client.PutAsync("/link/example", httpContent); - result.Should().BeOfType(); + result.StatusCode.Should().Be(HttpStatusCode.BadRequest); } } } diff --git a/test/Linker.Tests/Web/Controllers/LinkControllerTests/When_following_a_known_link.cs b/test/Linker.Tests/Web/Controllers/LinkControllerTests/When_following_a_known_link.cs index 6d66125..606618f 100644 --- a/test/Linker.Tests/Web/Controllers/LinkControllerTests/When_following_a_known_link.cs +++ b/test/Linker.Tests/Web/Controllers/LinkControllerTests/When_following_a_known_link.cs @@ -1,38 +1,44 @@ +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; using FluentAssertions; using Linker.Model; -using Linker.Web.Controllers; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Testing; using NSubstitute; using Xunit; namespace Linker.Tests { - public class When_following_a_known_link + public class When_following_a_known_link : IClassFixture { + private readonly CustomWebApplicationFactory _factory; + private readonly HttpClient _client; + + public When_following_a_known_link(CustomWebApplicationFactory factory) + { + _factory = factory; + _client = factory.CreateClient(new WebApplicationFactoryClientOptions {AllowAutoRedirect = false}); + } + [Fact] - public void Should_return_an_http_moved_permanently_result() + public async Task Should_return_an_http_moved_permanently_result() { - var getLink = Substitute.For(); - getLink.WithId("id").Returns(new Link("id,", "http://example.com")); - var sut = new LinkController(getLink, Substitute.For()); + _factory.GetLinks.WithId("id").Returns(new Link("id,", "http://example.com")); - var result = sut.Follow("id"); + var result = await _client.GetAsync("/id"); - result.Should().BeOfType() - .Which.Permanent.Should().BeTrue(); + result.StatusCode.Should().Be(HttpStatusCode.MovedPermanently); } [Fact] - public void Should_return_the_url_of_the_link() + public async Task Should_return_the_url_of_the_link() { - var getLink = Substitute.For(); - getLink.WithId("id").Returns(new Link("id,", "http://example.com")); - var sut = new LinkController(getLink, Substitute.For()); + _factory.GetLinks.WithId("id").Returns(new Link("id,", "http://example.com")); - var result = sut.Follow("id"); + var result = await _client.GetAsync("/id"); - result.Should().BeOfType() - .Which.Url.Should().Be("http://example.com/"); + result.Headers.Should().ContainSingle(kv => kv.Key == "Location" && kv.Value.First() == "http://example.com/"); } } } diff --git a/test/Linker.Tests/Web/Controllers/LinkControllerTests/When_following_an_unknown_link.cs b/test/Linker.Tests/Web/Controllers/LinkControllerTests/When_following_an_unknown_link.cs index 754448e..74e3122 100644 --- a/test/Linker.Tests/Web/Controllers/LinkControllerTests/When_following_an_unknown_link.cs +++ b/test/Linker.Tests/Web/Controllers/LinkControllerTests/When_following_an_unknown_link.cs @@ -1,24 +1,26 @@ +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; using FluentAssertions; -using Linker.Model; -using Linker.Web.Controllers; -using Microsoft.AspNetCore.Mvc; -using NSubstitute; using Xunit; namespace Linker.Tests { - public class When_following_an_unknown_link + public class When_following_an_unknown_link : IClassFixture { - [Fact] - public void Should_return_an_http_not_found_result() + private readonly HttpClient _client; + + public When_following_an_unknown_link(CustomWebApplicationFactory factory) { - var sut = new LinkController( - Substitute.For(), - Substitute.For()); + _client = factory.CreateClient(); + } - var result = sut.Follow("unknown-link-id"); + [Fact] + public async Task Should_return_an_http_not_found_result() + { + var result = await _client.GetAsync("/unknown-link-id"); - result.Should().BeOfType(); + result.StatusCode.Should().Be(HttpStatusCode.NotFound); } } } diff --git a/test/Linker.Tests/Web/Controllers/LinkControllerTests/When_getting_the_metadata_about_a_known_link.cs b/test/Linker.Tests/Web/Controllers/LinkControllerTests/When_getting_the_metadata_about_a_known_link.cs index 94c362c..4717976 100644 --- a/test/Linker.Tests/Web/Controllers/LinkControllerTests/When_getting_the_metadata_about_a_known_link.cs +++ b/test/Linker.Tests/Web/Controllers/LinkControllerTests/When_getting_the_metadata_about_a_known_link.cs @@ -1,47 +1,59 @@ +using System.Net; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; using Xunit; using FluentAssertions; using NSubstitute; -using Linker.Web.Controllers; using Linker.Model; namespace Linker.Tests { - public class When_getting_the_metadata_about_a_known_link + public class When_getting_the_metadata_about_a_known_link : IClassFixture { + private readonly CustomWebApplicationFactory _factory; + private readonly HttpClient _client; + private readonly JsonSerializerOptions _jsonSerializerOptions = new() {PropertyNamingPolicy = JsonNamingPolicy.CamelCase}; + + public When_getting_the_metadata_about_a_known_link(CustomWebApplicationFactory factory) + { + _factory = factory; + _client = factory.CreateClient(); + } + [Fact] - public void Should_return_an_http_ok_result() + public async Task Should_return_an_http_ok_result() { - var getLink = Substitute.For(); - getLink.WithId("id").Returns(new Link("id,", "http://example.com")); - var sut = new LinkController(getLink, Substitute.For()); + _factory.GetLinks.WithId("id").Returns(new Link("id,", "http://example.com")); - var result = sut.Metadata("id"); + var result = await _client.GetAsync("/link/id"); - result.StatusCode().Should().Be(200); + result.StatusCode.Should().Be(HttpStatusCode.OK); } [Fact] - public void Should_return_a_link_with_the_specified_id() + public async Task Should_return_a_link_with_the_specified_id() { - var getLink = Substitute.For(); - getLink.WithId("id").Returns(new Link("id", "http://example.com")); - var sut = new LinkController(getLink, Substitute.For()); + _factory.GetLinks.WithId("id").Returns(new Link("id", "http://example.com")); - var result = sut.Metadata("id"); + var result = await _client.GetAsync("/link/id"); + var content = await result.Content.ReadAsStringAsync(); - result.ContentAs().Id.Should().Be("id"); + var link = JsonSerializer.Deserialize(content, _jsonSerializerOptions); + link.Id.ToString().Should().Be("id"); } [Fact] - public void Should_return_the_href_of_the_link() + public async Task Should_return_the_href_of_the_link() { - var getLink = Substitute.For(); - getLink.WithId("id").Returns(new Link("id,", "http://example.com")); - var sut = new LinkController(getLink, Substitute.For()); + _factory.GetLinks.WithId("id").Returns(new Link("id", "http://example.com")); + + var result = await _client.GetAsync("/link/id"); + var content = await result.Content.ReadAsStringAsync(); - var result = sut.Metadata("id"); + var link = JsonSerializer.Deserialize(content, _jsonSerializerOptions); - result.ContentAs().Href.Should().Be("http://example.com"); + link.Href.Should().Be("http://example.com"); } } } diff --git a/test/Linker.Tests/Web/Controllers/LinkControllerTests/When_getting_the_metadata_about_an_unknown_link.cs b/test/Linker.Tests/Web/Controllers/LinkControllerTests/When_getting_the_metadata_about_an_unknown_link.cs index bce2106..6e8227f 100644 --- a/test/Linker.Tests/Web/Controllers/LinkControllerTests/When_getting_the_metadata_about_an_unknown_link.cs +++ b/test/Linker.Tests/Web/Controllers/LinkControllerTests/When_getting_the_metadata_about_an_unknown_link.cs @@ -1,24 +1,26 @@ -using Microsoft.AspNetCore.Mvc; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; using Xunit; using FluentAssertions; -using NSubstitute; -using Linker.Web.Controllers; -using Linker.Model; namespace Linker.Tests { - public class When_getting_the_metadata_about_an_unknown_link + public class When_getting_the_metadata_about_an_unknown_link : IClassFixture { - [Fact] - public void Should_return_an_http_not_found_result() + private readonly HttpClient _client; + + public When_getting_the_metadata_about_an_unknown_link(CustomWebApplicationFactory factory) { - var sut = new LinkController( - Substitute.For(), - Substitute.For()); + _client = factory.CreateClient(); + } - var result = sut.Metadata("unknown-link-id"); + [Fact] + public async Task Should_return_an_http_not_found_result() + { + var result = await _client.GetAsync("/link/unknown-link-id"); - result.Should().BeOfType(); + result.StatusCode.Should().Be(HttpStatusCode.NotFound); } } } From 2d847dee0359261df5e9bf658376160f1ad9459e Mon Sep 17 00:00:00 2001 From: antoniovalentini Date: Tue, 16 Aug 2022 16:12:18 +0200 Subject: [PATCH 5/8] Update appveyor.yml with visual studio 2022 image --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 5efadcc..3e18a9f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -image: Visual Studio 2019 +image: Visual Studio 2022 environment: DeploymentUser: linker-deployer DeploymentPassword: From c178f7e835eb5580c53792cb54a0b7f756658184 Mon Sep 17 00:00:00 2001 From: antoniovalentini Date: Tue, 16 Aug 2022 16:27:53 +0200 Subject: [PATCH 6/8] Update nugets --- test/Linker.Tests/Linker.Tests.csproj | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/test/Linker.Tests/Linker.Tests.csproj b/test/Linker.Tests/Linker.Tests.csproj index e167959..b16d856 100755 --- a/test/Linker.Tests/Linker.Tests.csproj +++ b/test/Linker.Tests/Linker.Tests.csproj @@ -16,14 +16,17 @@ - + runtime; build; native; contentfiles; analyzers all - - - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + From ddae8ea086ea2fcd3b5aca56adc8069ec71183ff Mon Sep 17 00:00:00 2001 From: antoniovalentini Date: Tue, 16 Aug 2022 16:57:12 +0200 Subject: [PATCH 7/8] Update docker image to support aspnet 6 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index fa85ccb..0e93796 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/core/aspnet:3.0 +FROM mcr.microsoft.com/dotnet/core/aspnet:6.0 WORKDIR /app COPY ./publish ./ ENTRYPOINT ["dotnet", "Linker.dll"] From 978516b4454e8d9886ae65ca95d3a7ce60abf768 Mon Sep 17 00:00:00 2001 From: antoniovalentini Date: Tue, 16 Aug 2022 16:57:59 +0200 Subject: [PATCH 8/8] Fix docker image --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 0e93796..71914c8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM mcr.microsoft.com/dotnet/core/aspnet:6.0 +FROM mcr.microsoft.com/dotnet/aspnet:6.0 WORKDIR /app COPY ./publish ./ ENTRYPOINT ["dotnet", "Linker.dll"]