File: VersioningTestsBase.cs
Web Access
Project: src\src\Mvc\test\Mvc.FunctionalTests\Microsoft.AspNetCore.Mvc.FunctionalTests.csproj (Microsoft.AspNetCore.Mvc.FunctionalTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Net;
using System.Net.Http;
using System.Reflection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.AspNetCore.Mvc.Testing;
using Newtonsoft.Json;
using Xunit.Abstractions;
 
namespace Microsoft.AspNetCore.Mvc.FunctionalTests;
 
public abstract class VersioningTestsBase<TStartup> : LoggedTest where TStartup : class
{
    private static void ConfigureWebHostBuilder(IWebHostBuilder builder) =>
        builder.UseStartup<TStartup>();
 
    protected override void Initialize(TestContext context, MethodInfo methodInfo, object[] testMethodArguments, ITestOutputHelper testOutputHelper)
    {
        base.Initialize(context, methodInfo, testMethodArguments, testOutputHelper);
        Factory = new MvcTestFixture<TStartup>(LoggerFactory).WithWebHostBuilder(ConfigureWebHostBuilder);
        Client = Factory.CreateDefaultClient();
    }
 
    public override void Dispose()
    {
        Factory.Dispose();
        base.Dispose();
    }
 
    public WebApplicationFactory<TStartup> Factory { get; private set; }
    public HttpClient Client { get; private set; }
 
    [Fact]
    public abstract Task HasEndpointMatch();
 
    [Theory]
    [InlineData("1")]
    [InlineData("2")]
    public async Task AttributeRoutedAction_WithVersionedRoutes_IsNotAmbiguous(string version)
    {
        // Arrange
        var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/api/Addresses?version=" + version);
 
        // Act
        var response = await Client.SendAsync(message);
 
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
 
        var body = await response.Content.ReadAsStringAsync();
        var result = JsonConvert.DeserializeObject<RoutingResult>(body);
 
        Assert.Contains("api/addresses", result.ExpectedUrls);
        Assert.Equal("Address", result.Controller);
        Assert.Equal("GetV" + version, result.Action);
    }
 
    [Theory]
    [InlineData("1")]
    [InlineData("2")]
    public async Task AttributeRoutedAction_WithAmbiguousVersionedRoutes_CanBeDisambiguatedUsingOrder(string version)
    {
        // Arrange
        var query = "?version=" + version;
        var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/api/Addresses/All" + query);
 
        // Act
 
        var response = await Client.SendAsync(message);
 
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
 
        var body = await response.Content.ReadAsStringAsync();
        var result = JsonConvert.DeserializeObject<RoutingResult>(body);
 
        Assert.Contains("/api/addresses/all?version=" + version, result.ExpectedUrls);
        Assert.Equal("Address", result.Controller);
        Assert.Equal("GetAllV" + version, result.Action);
    }
 
    [Fact]
    public async Task VersionedApi_CanReachV1Operations_OnTheSameController_WithNoVersionSpecified()
    {
        // Arrange
        var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Tickets");
 
        // Act
        var response = await Client.SendAsync(message);
 
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
 
        var body = await response.Content.ReadAsStringAsync();
        var result = JsonConvert.DeserializeObject<RoutingResult>(body);
 
        Assert.Equal("Tickets", result.Controller);
        Assert.Equal("Get", result.Action);
 
        Assert.DoesNotContain("id", result.RouteValues.Keys);
    }
 
    [Fact]
    public async Task VersionedApi_CanReachV1Operations_OnTheSameController_WithVersionSpecified()
    {
        // Arrange
        var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Tickets?version=2");
 
        // Act
        var response = await Client.SendAsync(message);
 
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
 
        var body = await response.Content.ReadAsStringAsync();
        var result = JsonConvert.DeserializeObject<RoutingResult>(body);
 
        Assert.Equal("Tickets", result.Controller);
        Assert.Equal("Get", result.Action);
    }
 
    [Fact]
    public async Task VersionedApi_CanReachV1OperationsWithParameters_OnTheSameController()
    {
        // Arrange
        var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Tickets/5");
 
        // Act
        var response = await Client.SendAsync(message);
 
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
 
        var body = await response.Content.ReadAsStringAsync();
        var result = JsonConvert.DeserializeObject<RoutingResult>(body);
 
        Assert.Equal("Tickets", result.Controller);
        Assert.Equal("GetById", result.Action);
    }
 
    [Fact]
    public async Task VersionedApi_CanReachV1OperationsWithParameters_OnTheSameController_WithVersionSpecified()
    {
        // Arrange
        var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Tickets/5?version=2");
 
        // Act
        var response = await Client.SendAsync(message);
 
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
 
        var body = await response.Content.ReadAsStringAsync();
        var result = JsonConvert.DeserializeObject<RoutingResult>(body);
 
        Assert.Equal("Tickets", result.Controller);
        Assert.Equal("GetById", result.Action);
        Assert.NotEmpty(result.RouteValues);
 
        Assert.Contains(
           new KeyValuePair<string, object>("id", "5"),
           result.RouteValues);
    }
 
    [Theory]
    [InlineData("2")]
    [InlineData("3")]
    [InlineData("4")]
    public async Task VersionedApi_CanReachOtherVersionOperations_OnTheSameController(string version)
    {
        // Arrange
        var message = new HttpRequestMessage(HttpMethod.Post, "http://localhost/Tickets?version=" + version);
 
        // Act
        var response = await Client.SendAsync(message);
 
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
 
        var body = await response.Content.ReadAsStringAsync();
        var result = JsonConvert.DeserializeObject<RoutingResult>(body);
 
        Assert.Equal("Tickets", result.Controller);
        Assert.Equal("Post", result.Action);
        Assert.NotEmpty(result.RouteValues);
 
        Assert.DoesNotContain(
           new KeyValuePair<string, object>("id", "5"),
           result.RouteValues);
    }
 
    [Fact]
    public async Task VersionedApi_CanNotReachOtherVersionOperations_OnTheSameController_WithNoVersionSpecified()
    {
        // Arrange
        var message = new HttpRequestMessage(HttpMethod.Post, "http://localhost/Tickets");
 
        // Act
        var response = await Client.SendAsync(message);
 
        // Assert
        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
 
        var body = await response.Content.ReadAsByteArrayAsync();
        Assert.Empty(body);
    }
 
    [Theory]
    [InlineData("PUT", "Put", "2")]
    [InlineData("PUT", "Put", "3")]
    [InlineData("PUT", "Put", "4")]
    [InlineData("DELETE", "Delete", "2")]
    [InlineData("DELETE", "Delete", "3")]
    [InlineData("DELETE", "Delete", "4")]
    public async Task VersionedApi_CanReachOtherVersionOperationsWithParameters_OnTheSameController(
        string method,
        string action,
        string version)
    {
        // Arrange
        var message = new HttpRequestMessage(new HttpMethod(method), "http://localhost/Tickets/5?version=" + version);
 
        // Act
        var response = await Client.SendAsync(message);
 
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
 
        var body = await response.Content.ReadAsStringAsync();
        var result = JsonConvert.DeserializeObject<RoutingResult>(body);
 
        Assert.Equal("Tickets", result.Controller);
        Assert.Equal(action, result.Action);
        Assert.NotEmpty(result.RouteValues);
 
        Assert.Contains(
           new KeyValuePair<string, object>("id", "5"),
           result.RouteValues);
    }
 
    [Theory]
    [InlineData("PUT")]
    [InlineData("DELETE")]
    public async Task VersionedApi_CanNotReachOtherVersionOperationsWithParameters_OnTheSameController_WithNoVersionSpecified(string method)
    {
        // Arrange
        var message = new HttpRequestMessage(new HttpMethod(method), "http://localhost/Tickets/5");
 
        // Act
        var response = await Client.SendAsync(message);
 
        // Assert
        Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
 
        var body = await response.Content.ReadAsByteArrayAsync();
        Assert.Empty(body);
    }
 
    [Theory]
    [InlineData("3")]
    [InlineData("4")]
    [InlineData("5")]
    public async Task VersionedApi_CanUseOrderToDisambiguate_OverlappingVersionRanges(string version)
    {
        // Arrange
        var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Books?version=" + version);
 
        // Act
        var response = await Client.SendAsync(message);
 
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
 
        var body = await response.Content.ReadAsStringAsync();
        var result = JsonConvert.DeserializeObject<RoutingResult>(body);
 
        Assert.Equal("Books", result.Controller);
        Assert.Equal("GetBreakingChange", result.Action);
    }
 
    [Theory]
    [InlineData("1")]
    [InlineData("2")]
    [InlineData("6")]
    public async Task VersionedApi_OverlappingVersionRanges_FallsBackToLowerOrderAction(string version)
    {
        // Arrange
        var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/Books?version=" + version);
 
        // Act
        var response = await Client.SendAsync(message);
 
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
 
        var body = await response.Content.ReadAsStringAsync();
        var result = JsonConvert.DeserializeObject<RoutingResult>(body);
 
        Assert.Equal("Books", result.Controller);
        Assert.Equal("Get", result.Action);
    }
 
    [Theory]
    [InlineData("GET", "Get")]
    [InlineData("POST", "Post")]
    public async Task VersionedApi_CanReachV1Operations_OnTheOriginalController_WithNoVersionSpecified(string method, string action)
    {
        // Arrange
        var message = new HttpRequestMessage(new HttpMethod(method), "http://localhost/Movies");
 
        // Act
        var response = await Client.SendAsync(message);
 
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
 
        var body = await response.Content.ReadAsStringAsync();
        var result = JsonConvert.DeserializeObject<RoutingResult>(body);
 
        Assert.Equal("Movies", result.Controller);
        Assert.Equal(action, result.Action);
    }
 
    [Theory]
    [InlineData("GET", "Get")]
    [InlineData("POST", "Post")]
    public async Task VersionedApi_CanReachV1Operations_OnTheOriginalController_WithVersionSpecified(string method, string action)
    {
        // Arrange
        var message = new HttpRequestMessage(new HttpMethod(method), "http://localhost/Movies?version=2");
 
        // Act
        var response = await Client.SendAsync(message);
 
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
 
        var body = await response.Content.ReadAsStringAsync();
        var result = JsonConvert.DeserializeObject<RoutingResult>(body);
 
        Assert.Equal("Movies", result.Controller);
        Assert.Equal(action, result.Action);
    }
 
    [Theory]
    [InlineData("GET", "GetById")]
    [InlineData("PUT", "Put")]
    [InlineData("DELETE", "Delete")]
    public async Task VersionedApi_CanReachV1OperationsWithParameters_OnTheOriginalController(string method, string action)
    {
        // Arrange
        var message = new HttpRequestMessage(new HttpMethod(method), "http://localhost/Movies/5");
 
        // Act
        var response = await Client.SendAsync(message);
 
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
 
        var body = await response.Content.ReadAsStringAsync();
        var result = JsonConvert.DeserializeObject<RoutingResult>(body);
 
        Assert.Equal("Movies", result.Controller);
        Assert.Equal(action, result.Action);
    }
 
    [Theory]
    [InlineData("GET", "GetById")]
    [InlineData("DELETE", "Delete")]
    public async Task VersionedApi_CanReachV1OperationsWithParameters_OnTheOriginalController_WithVersionSpecified(string method, string action)
    {
        // Arrange
        var message = new HttpRequestMessage(new HttpMethod(method), "http://localhost/Movies/5?version=2");
 
        // Act
        var response = await Client.SendAsync(message);
 
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
 
        var body = await response.Content.ReadAsStringAsync();
        var result = JsonConvert.DeserializeObject<RoutingResult>(body);
 
        Assert.Equal("Movies", result.Controller);
        Assert.Equal(action, result.Action);
    }
 
    [Fact]
    public async Task VersionedApi_CanReachOtherVersionOperationsWithParameters_OnTheV2Controller()
    {
        // Arrange
        var message = new HttpRequestMessage(HttpMethod.Put, "http://localhost/Movies/5?version=2");
 
        // Act
        var response = await Client.SendAsync(message);
 
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
 
        var body = await response.Content.ReadAsStringAsync();
        var result = JsonConvert.DeserializeObject<RoutingResult>(body);
 
        Assert.Equal("MoviesV2", result.Controller);
        Assert.Equal("Put", result.Action);
        Assert.NotEmpty(result.RouteValues);
    }
 
    [Theory]
    [InlineData("v1/Pets")]
    [InlineData("v2/Pets")]
    public async Task VersionedApi_CanHaveTwoRoutesWithVersionOnTheUrl_OnTheSameAction(string url)
    {
        // Arrange
        var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/" + url);
 
        // Act
        var response = await Client.SendAsync(message);
 
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
 
        var body = await response.Content.ReadAsStringAsync();
        var result = JsonConvert.DeserializeObject<RoutingResult>(body);
 
        Assert.Equal("Pets", result.Controller);
        Assert.Equal("Get", result.Action);
    }
 
    [Theory]
    [InlineData("v1/Pets/5", "V1")]
    [InlineData("v2/Pets/5", "V2")]
    public async Task VersionedApi_CanHaveTwoRoutesWithVersionOnTheUrl_OnDifferentActions(string url, string version)
    {
        // Arrange
        var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/" + url);
 
        // Act
        var response = await Client.SendAsync(message);
 
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
 
        var body = await response.Content.ReadAsStringAsync();
        var result = JsonConvert.DeserializeObject<RoutingResult>(body);
 
        Assert.Equal("Pets", result.Controller);
        Assert.Equal("Get" + version, result.Action);
    }
 
    [Theory]
    [InlineData("v1/Pets", "V1")]
    [InlineData("v2/Pets", "V2")]
    public async Task VersionedApi_CanHaveTwoRoutesWithVersionOnTheUrl_OnDifferentActions_WithInlineConstraint(string url, string version)
    {
        // Arrange
        var message = new HttpRequestMessage(HttpMethod.Post, "http://localhost/" + url);
 
        // Act
        var response = await Client.SendAsync(message);
 
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
 
        var body = await response.Content.ReadAsStringAsync();
        var result = JsonConvert.DeserializeObject<RoutingResult>(body);
 
        Assert.Equal("Pets", result.Controller);
        Assert.Equal("Post" + version, result.Action);
    }
 
    [Theory]
    [InlineData("Customers/5", "?version=1", "Get")]
    [InlineData("Customers/5", "?version=2", "Get")]
    [InlineData("Customers/5", "?version=3", "GetV3ToV5")]
    [InlineData("Customers/5", "?version=4", "GetV3ToV5")]
    [InlineData("Customers/5", "?version=5", "GetV3ToV5")]
    public async Task VersionedApi_CanProvideVersioningInformation_UsingPlainActionConstraint(string url, string query, string actionName)
    {
        // Arrange
        var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost/" + url + query);
 
        // Act
        var response = await Client.SendAsync(message);
 
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
 
        var body = await response.Content.ReadAsStringAsync();
        var result = JsonConvert.DeserializeObject<RoutingResult>(body);
 
        Assert.Equal("Customers", result.Controller);
        Assert.Equal(actionName, result.Action);
    }
 
    [Fact]
    public virtual async Task VersionedApi_ConstraintOrder_IsRespected()
    {
        // Arrange
        var message = new HttpRequestMessage(HttpMethod.Post, "http://localhost/" + "Customers?version=2");
 
        // Act
        var response = await Client.SendAsync(message);
 
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
 
        var body = await response.Content.ReadAsStringAsync();
        var result = JsonConvert.DeserializeObject<RoutingResult>(body);
 
        Assert.Equal("Customers", result.Controller);
        Assert.Equal("AnyV2OrHigher", result.Action);
    }
 
    [Fact]
    public virtual async Task VersionedApi_CanUseConstraintOrder_ToChangeSelectedAction()
    {
        // Arrange
        var message = new HttpRequestMessage(HttpMethod.Delete, "http://localhost/" + "Customers/5?version=2");
 
        // Act
        var response = await Client.SendAsync(message);
 
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
 
        var body = await response.Content.ReadAsStringAsync();
        var result = JsonConvert.DeserializeObject<RoutingResult>(body);
 
        Assert.Equal("Customers", result.Controller);
        Assert.Equal("Delete", result.Action);
    }
 
    [Theory]
    [InlineData("1")]
    [InlineData("2")]
    public async Task VersionedApi_MultipleVersionsUsingAttributeRouting_OnTheSameMethod(string version)
    {
        // Arrange
        var path = "/" + version + "/Vouchers?version=" + version;
        var message = new HttpRequestMessage(HttpMethod.Get, "http://localhost" + path);
 
        // Act
        var response = await Client.SendAsync(message);
 
        // Assert
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
 
        var body = await response.Content.ReadAsStringAsync();
        var result = JsonConvert.DeserializeObject<RoutingResult>(body);
 
        Assert.Equal("Vouchers", result.Controller);
        Assert.Equal("GetVouchersMultipleVersions", result.Action);
 
        var actualUrl = Assert.Single(result.ExpectedUrls);
        Assert.Equal(path, actualUrl);
    }
 
    protected class RoutingResult
    {
        public string[] ExpectedUrls { get; set; }
 
        public string ActualUrl { get; set; }
 
        public Dictionary<string, object> RouteValues { get; set; }
 
        public string RouteName { get; set; }
 
        public string Action { get; set; }
 
        public string Controller { get; set; }
 
        public string Link { get; set; }
    }
}